diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..6275a346 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,14 @@ + + +## Summary + + + +## Test plan + + diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml new file mode 100644 index 00000000..004cbf5f --- /dev/null +++ b/.github/workflows/maven.yml @@ -0,0 +1,248 @@ +name: Maven CI + +on: + push: + branches: ["main", "develop", "feat/**", "fix/**", "ci/**"] + paths-ignore: + - "**/*.md" + - "doc/**" + pull_request: + branches: ["main", "develop", "feat/**", "fix/**"] + paths-ignore: + - "**/*.md" + - "doc/**" + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + # ── Linux ──────────────────────────────────────────────────────────────────── + linux: + name: Build and test β€” Linux (Java 21 / Spark 3.5) + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Java 21 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "21" + cache: maven + + - name: Install MEOS build dependencies (Linux) + run: | + sudo apt-get update -qq + sudo apt-get install -y \ + cmake ninja-build \ + libjson-c-dev libgeos-dev libproj-dev libgsl-dev libh3-dev + + - name: Checkout MobilityDB source (for MEOS build) + uses: actions/checkout@v4 + with: + # Ecosystem pin: the MobilityDB commit the bundled JMEOS jar is + # regenerated against. Published as an immutable tag on the integrator + # fork (not yet upstream). + repository: estebanzimanyi/MobilityDB + ref: ecosystem-pin-2026-06-11p + path: MobilityDB-src + + - name: Build and install libmeos.so + run: | + # The build dir MUST live inside MobilityDB-src: pgtypes/postgres.h + # includes "../../meos/include/meos_error.h", which the compiler + # resolves via the generated -isystem /pgtypes path + # (/pgtypes/../../meos/include). That only reaches the source + # tree's meos/include when is inside the source tree. + cmake -S MobilityDB-src -B MobilityDB-src/meos-build \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DMEOS=ON \ + -DCBUFFER=ON -DNPOINT=ON -DPOSE=ON -DRGEO=ON \ + -DH3=ON \ + -DH3_LIBRARY=/usr/lib/x86_64-linux-gnu/libh3.so \ + -DH3_INCLUDE_DIR=/usr/include/h3 + cmake --build MobilityDB-src/meos-build -j + sudo cmake --install MobilityDB-src/meos-build + echo "LD_LIBRARY_PATH=/usr/local/lib" >> "$GITHUB_ENV" + + - name: License header check + run: bash tools/scripts/check_license.sh + + - name: Compile + run: mvn -B compile + + - name: Unit tests + run: mvn -B test + + - name: Package (fat jar) + run: mvn -B package -DskipTests + + - name: Upload fat jar + uses: actions/upload-artifact@v4 + with: + name: mobilityspark-spark.jar + path: target/*-spark.jar + + # ── macOS ──────────────────────────────────────────────────────────────────── + macos: + name: Build and test β€” macOS (Java 21 / Spark 3.5) + runs-on: macos-latest + # Non-blocking best-effort: Linux is the authoritative green. macOS builds + # libmeos from the same ecosystem pin (with H3 via Homebrew); kept + # non-blocking so a platform-specific toolchain hiccup never gates the PR. + continue-on-error: true + + steps: + - uses: actions/checkout@v4 + + - name: Set up Java 21 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "21" + cache: maven + + - name: Install MEOS dependencies (Homebrew) + run: | + brew install json-c geos proj gsl cmake ninja h3 + + - name: Checkout MobilityDB source (for MEOS build) + uses: actions/checkout@v4 + with: + # Ecosystem pin: the MobilityDB commit the bundled JMEOS jar is + # regenerated against. Published as an immutable tag on the integrator + # fork (not yet upstream). + repository: estebanzimanyi/MobilityDB + ref: ecosystem-pin-2026-06-11p + path: MobilityDB-src + + - name: Build and install libmeos.dylib + run: | + # Homebrew on macOS-ARM installs to /opt/homebrew; on Intel to /usr/local. + BREW_PREFIX="$(brew --prefix)" + cmake -S MobilityDB-src -B MobilityDB-src/meos-build \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_PREFIX_PATH="$BREW_PREFIX" \ + -DCMAKE_INSTALL_RPATH="$BREW_PREFIX/lib" \ + -DMEOS=ON \ + -DCBUFFER=ON -DNPOINT=ON -DPOSE=ON -DRGEO=ON \ + -DH3=ON \ + -DH3_LIBRARY="$BREW_PREFIX/lib/libh3.dylib" \ + -DH3_INCLUDE_DIR="$BREW_PREFIX/include/h3" + cmake --build MobilityDB-src/meos-build -j + sudo cmake --install MobilityDB-src/meos-build + # Ad-hoc codesign so that the JVM's hardened-runtime library validation + # accepts the unsigned CMake-built dylib. + sudo codesign --force --sign - /usr/local/lib/libmeos.dylib + # JarLibraryLoader reads LD_LIBRARY_PATH first in CI mode (GITHUB_WORKFLOW + # set), then falls back to DYLD_LIBRARY_PATH. Set both env vars so the + # correct /usr/local/lib path reaches JNR-FFI regardless of which one + # the JVM process sees after the hardened runtime strips DYLD_* vars. + echo "LD_LIBRARY_PATH=/usr/local/lib" >> "$GITHUB_ENV" + echo "DYLD_LIBRARY_PATH=/usr/local/lib:$BREW_PREFIX/lib" >> "$GITHUB_ENV" + + - name: Compile + run: mvn -B compile + + - name: Unit tests + run: mvn -B test + + - name: Package (fat jar) + run: mvn -B package -DskipTests + + # ── Windows ────────────────────────────────────────────────────────────────── + windows: + name: Build and test β€” Windows (Java 21 / Spark 3.5) + runs-on: windows-latest + # Non-blocking best-effort. Windows now builds MEOS from the same ecosystem + # pin as Linux/macOS (the pin carries the MEOS_TZDATA_DIR cmake option), but + # is kept non-blocking until a green Windows run confirms the MSYS2 toolchain + # path end-to-end. (th3index/H3 is off here pending an MSYS2 h3 package; the + # th3index UDFs are bound lazily by JNR-FFI so the rest of the suite is + # unaffected.) Drop this flag once Windows is observed green in CI. + continue-on-error: true + defaults: + run: + shell: msys2 {0} + + steps: + - uses: actions/checkout@v4 + + - name: Setup MSYS2 (UCRT64) + uses: msys2/setup-msys2@v2 + with: + msystem: UCRT64 + update: true + install: >- + mingw-w64-ucrt-x86_64-gcc + mingw-w64-ucrt-x86_64-cmake + mingw-w64-ucrt-x86_64-ninja + mingw-w64-ucrt-x86_64-json-c + mingw-w64-ucrt-x86_64-geos + mingw-w64-ucrt-x86_64-proj + mingw-w64-ucrt-x86_64-gsl + mingw-w64-ucrt-x86_64-tzdata + + - name: Set up Java 21 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "21" + cache: maven + + - name: Checkout MobilityDB source (for MEOS build) + uses: actions/checkout@v4 + with: + # Same ecosystem pin as Linux/macOS β€” the pin now carries the + # MEOS_TZDATA_DIR cmake option, so Windows no longer needs the + # divergent meos-windows-bootstrap branch. + repository: estebanzimanyi/MobilityDB + ref: ecosystem-pin-2026-06-11p + path: MobilityDB-src + + - name: Resolve native timezone data path + run: | + # cygpath -m converts the MSYS2 POSIX prefix to a Windows-native path + # (e.g. C:/msys64/ucrt64/share/zoneinfo) that fopen() inside the DLL + # can open at runtime. + TZDATA_WIN=$(cygpath -m "$MSYSTEM_PREFIX/share/zoneinfo") + echo "MEOS_TZDATA_WIN=$TZDATA_WIN" >> "$GITHUB_ENV" + + - name: Build and install libmeos.dll + run: | + cmake -S MobilityDB-src -B MobilityDB-src/meos-build \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DMEOS=ON \ + -DCBUFFER=ON -DNPOINT=ON -DPOSE=ON -DRGEO=ON \ + -DMEOS_TZDATA_DIR="$MEOS_TZDATA_WIN" + cmake --build MobilityDB-src/meos-build -j + cmake --install MobilityDB-src/meos-build --prefix "$PWD/meos-install" + # Save the DLL directory as a separate env var rather than stomping + # PATH: the MSYS2 PATH is POSIX-style and would overwrite the Windows + # PATH (losing Maven) in subsequent PowerShell steps. + echo "MEOS_DLL_DIR=$(cygpath -w "$PWD/meos-install/bin")" >> "$GITHUB_ENV" + # JarLibraryLoader (JMEOS) in CI mode reads LD_LIBRARY_PATH and passes + # it to jnr.ffi.LibraryLoader.search() to locate libmeos.dll; PATH is + # not checked. Use the Windows-native path so JNR-FFI's File lookup works. + echo "LD_LIBRARY_PATH=$(cygpath -w "$PWD/meos-install/bin")" >> "$GITHUB_ENV" + # MSYS2 runtime DLLs (libgeos, libproj, libjson-c, libgsl) are needed + # when the JVM loads libmeos.dll's transitive dependencies at runtime. + echo "UCRT64_BIN=$(cygpath -w "$MSYSTEM_PREFIX/bin")" >> "$GITHUB_ENV" + + - name: Compile + shell: pwsh + run: | + $env:PATH = "$env:MEOS_DLL_DIR;$env:PATH" + mvn -B compile + + - name: Unit tests + shell: pwsh + run: | + $env:PATH = "$env:MEOS_DLL_DIR;$env:UCRT64_BIN;$env:PATH" + mvn -B test diff --git a/.gitignore b/.gitignore index fc368f31..0fbf0af9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,14 +3,21 @@ .project .settings/ -# Intellij +# IntelliJ IDEA .idea/ *.iml *.iws +*.ipr -# Mac +# macOS .DS_Store +**/.DS_Store # Maven log/ target/ + +# Large BerlinMOD benchmark data (generated locally β€” too large for GitHub) +berlinmod/data/trips.csv +dependency-reduced-pom.xml +hs_err_pid*.log diff --git a/INTEGRATION_NOTES.md b/INTEGRATION_NOTES.md new file mode 100644 index 00000000..f0760e00 --- /dev/null +++ b/INTEGRATION_NOTES.md @@ -0,0 +1,19 @@ +# INTEGRATION BRANCH NOTE β€” MEOS / JMEOS pin + +`integration/berlinmod-bench` builds against ecosystem pin +**`ecosystem-pin-2026-06-11p`**. + +- **`libs/JMEOS-1.4.jar`** is the canonical JMEOS regen at that pin + (JMEOS PR #19): a single generated `functions.GeneratedFunctions` + surface. The legacy hand-rolled `functions.functions` facade is retired, + and every UDF binds the generated surface directly. +- **`lib/libmeos.so`** is built from the pin with `-DH3=ON` and the + CBUFFER / NPOINT / POSE / RGEO families, so the th3index family is backed + with no build-time special-casing. +- **CI** (`.github/workflows/maven.yml`) builds `libmeos` from the pin on + Linux and macOS (with H3). The Windows job is non-blocking until the + `MEOS_TZDATA_DIR` cmake option lands in the pin (it currently lives only on + the `meos-windows-bootstrap` branch); once folded, Windows repoints to the + pin like the other platforms. + +The full unit suite is green (907/907). diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..b9640c8f --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +------------------------------------------------------------------------------- +This MobilityDB code is provided under The PostgreSQL License. + +Copyright (c) 2020-2025, UniversitΓ© libre de Bruxelles and MobilityDB +contributors + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose, without fee, and without a written agreement is +hereby granted, provided that the above copyright notice and this paragraph and +the following two paragraphs appear in all copies. + +IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR +DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING +LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, +EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. + +UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND +UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, +SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. diff --git a/README.md b/README.md index 0e2d118c..498cde01 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,14 @@ The MobilityDB project is developed by the Computer & Decision Engineering Depar More information about MobilityDB, including publications, presentations, etc., can be found in the MobilityDB [website](https://mobilitydb.com). +### For contributors and reviewers + +- Reviewing a pull request? See the + [PR Reviewer Guide](doc/contributing/reviewer-guide.md) β€” tier ranking, + dependency chains and the standards checklist. Reviewers landing in + any of the three platform repos (MobilityDB / MobilityDuck / + MobilitySpark) find the same canonical structure at the same path. + ## Table of Contents @@ -40,7 +48,7 @@ More information about MobilityDB, including publications, presentations, etc., - πŸš€ MobilityDB installed with MEOS - πŸ”§ JMEOS working version -- ⚑ Spark 3.4.0 +- ⚑ Apache Spark 3.5.x (LTS); see [`doc/spark-version.md`](doc/spark-version.md) for the Spark-version target and the rationale for not yet supporting Spark 4 - πŸ“ Maven 4 - β˜•Β Java 17 (recommended) diff --git a/doc/contributing/reviewer-guide.md b/doc/contributing/reviewer-guide.md new file mode 100644 index 00000000..1dd319d3 --- /dev/null +++ b/doc/contributing/reviewer-guide.md @@ -0,0 +1,129 @@ + + +# MobilitySpark PR Reviewer Guide + +Quick reference for anyone reviewing open pull requests in **MobilitySpark** and its JMEOS dependency. +Updated in the same commit as any PR that changes PR state or adds new branches. +**Last updated: 2026-05-11 β€” PR #9 extended to 86 UDFs (100% `meos_h3.h` parity) and the portable q02.sql polygon-side prefilter via MobilityDB PR #938 (`geo_to_h3index_set`). All three coordinated PRs source-complete (MobilityDB #938, MobilityDB-BerlinMOD #24, this PR #9); awaiting upstream th3index merge + JMEOS regen.** + +--- + +## How to find this guide + +- **In the repo:** `doc/contributing/reviewer-guide.md` +- **Rule:** every commit that opens, closes, or restructures a PR must update this file in the same commit. A one-liner status change is enough; a fuller rewrite is needed when the dependency graph changes. + +--- + +## CI legend + +| Symbol | Meaning | +|--------|---------| +| βœ… | All checks green | +| ❌ | Real failure β€” needs investigation before review | +| ⏳ | CI running | +| ❓ | No CI result yet (doc-only, draft, or external PR) | +| ⚠️ | Non-blocking failure (e.g. macOS/Windows `continue-on-error`, Codacy ACTION_REQUIRED β€” maintainer overrides in UI) | + +--- + +## Dependency chains β€” land in this order + +``` +MobilityDB/MobilityDB + PR #807 / #866 / #893 th3index temporal H3-cell index type + └─► JMEOS regen against MEOS 1.4 master (parallel-session work) + └─► MobilityDB/MobilitySpark + PR #9 perf/spark-mt-and-binary (th3index spatial prefilter β€” Stage-2 of perf) + +MobilityDB/JMEOS + PR #9 JashanReel:fix-tests-using-docker (multi-module Maven layout; needs cleanup review) + └─► PR #12 estebanzimanyi:fix/multimodule-with-split-interface (split JNR-FFI β†’ ARM64/macOS fix) + └─► MobilityDB/MobilitySpark + PR #7 fix/license-main-java (multi-platform CI bootstrap β€” stacks on JMEOS above) + └─► PR #5 feat/jmeos-1.3-berlinmod-poc (JMEOS 1.4 + BerlinMOD + 99.3% parity) + └─► PR #8 doc/reviewer-guide (this guide β€” independent, mergeable any time) +``` + +**PR #11** (`estebanzimanyi:fix/split-meos-library-interface`) was superseded by +`fix/multimodule-with-split-interface` (#12) and **closed 2026-05-10** as part of the +MobilitySpark consolidation pass. + +**PR #8** (`SachaDelsaux:JMEOS_v1.3`) is subsumed by PR #9 β€” external author; recommended for +closure but left in place pending author signal. + +--- + +## Tier 1 β€” Merge immediately (visibility / trivially reviewable) + +| Repo | PR | Branch | Description | CI | +|------|----|--------|-------------|----| +| MobilitySpark | #8 | `doc/reviewer-guide` | **This guide** β€” visibility wiring (PR template + README banner) β€” single commit | βœ… | +| JMEOS | #8 | `SachaDelsaux:JMEOS_v1.3` | Subsumed by #9 β€” recommended for closure (external author; comment posted, awaiting signal) | ❓ | + +--- + +## Tier 0 β€” Performance (cross-platform th3index unification) + +Cross-platform unification: all three benchmarked platforms (MobilityDB, MobilityDuck, MobilitySpark) execute identical portable SQL with the th3index spatial prefilter. PostgreSQL/MobilityDB additionally gets GiST + SP-GiST indexes β€” the native machinery that the columnar prefilter simulates on the other two platforms. **Land MobilityDB #807 / #866 / #893 / #938 first (foundation + polygon-side public API); then MobilityDB-BerlinMOD #24 (data side); then this PR #9 (consumer side).** + +| Repo | PR | Branch | Description | CI | Notes | +|------|----|--------|-------------|----|-------| +| MobilityDB | **#938** | `estebanzimanyi:feat/h3-static-geo-coverage` | Static-geometry β†’ H3 cell set public API: `h3_gs_point_to_cell` (promoted to public) + `geo_to_h3index_set` (POINT/LINESTRING/POLYGON/MULTI*/GeometryCollection in one walker) + `ever_eq_anyof_h3indexset_th3index`. PG SQL wrappers. pg_regress fixtures verified locally on PG 17 build (15/19 pass; 2 intentional ERROR; 2 wait on a pre-existing th3index branch SRID-resolver gap). | ❓ | Stacks on MobilityDB #866. Unblocks the polygon-side BerlinMOD prefilter consumed by this PR's q02.sql. | +| MobilityDB-BerlinMOD | **#24** | `estebanzimanyi:feat/portability-export-th3index` | Extend `berlinmod_portability_export()` to write a 4-column `trips.csv` including `trip_h3` (th3index hex-WKB at H3 resolution 7). Single SQL function change. | ❓ | Data-side, depends on `tgeompoint_to_th3index` from MobilityDB #807 / #866. | +| MobilitySpark | **#9** | `estebanzimanyi:perf/spark-mt-and-binary` | Cross-platform th3index unification: 86 UDFs (Th3IndexUDFs at 100% parity to `meos_h3.h`); load-time `trip_h3` materialisation; portable BerlinMOD SQL (q02/q04/q05/q06/q10) adopts the prefilter directly β€” including the polygon-side Q2 path via MobilityDB #938's `geoToH3IndexSet` + `everIntersectsH3IndexSet_Th3Index`; load_mbdb.sql adds GiST + SP-GiST indexes; load_mduck.sql + setup/generate_data.sh updated. `preprocessForSpark`'s th3index injection rules removed (now redundant with portable SQL). | ❌ | Source-complete; CI builds will fail until MobilityDB #807 / #866 / #893 / #938 land + JMEOS regen exposes the new symbols (parallel-session `feat/regen-against-meos-1.4`) + MobilityDuck th3index port lands. | + +--- + +## Tier 2 β€” Cross-repo chain (stack in order: JMEOS β†’ MobilitySpark) + +| Repo | PR | Branch | Description | CI | Notes | +|------|----|--------|-------------|----|-------| +| JMEOS | #9 | `JashanReel:fix-tests-using-docker` | Multi-module Maven layout (`jmeos-core/`); MEOS 1.3 API; test fixes | ❓ | **Land first** β€” needs cleanup (IDE files, binary blobs, *.class, squash 78 commits) | +| JMEOS | #12 | `estebanzimanyi:fix/multimodule-with-split-interface` | Split `MeosLibrary` 1685-method interface into 4 `public static` sub-interfaces for JNR-FFI; removes binary blobs + `.class` + debug files; updates `.gitignore` | ❓ | Stacks on #9 | +| MobilitySpark | #7 | `estebanzimanyi:fix/license-main-java` | Multi-platform CI bootstrap: license check, compile, 51 unit tests on Linux; macOS/Windows non-blocking | βœ…βš οΈ | Stacks on JMEOS #12 | +| MobilitySpark | #5 | `estebanzimanyi:feat/jmeos-1.3-berlinmod-poc` | JMEOS 1.3 + BerlinMOD Q1–Q17 + TemporalParquet edge-to-cloud pipeline; 37/37 tests | βœ… | Stacks on #7 | + +--- + +## Tier 3 β€” Fork staging (awaiting upstream merge) + +These PRs exist on the fork and are awaiting upstream review after the Tier 2 chain merges. + +| Repo | PR | Branch | Description | CI | Notes | +|------|----|--------|-------------|----|-------| +| MobilitySpark fork (`estebanzimanyi/MobilitySpark`) | #1 | `feat/udf-parity-phase2` | Expand UDF surface: 141 new UDFs in 7 classes + JMEOS-1.4 sub-interface fix; 203/203 tests | βœ… | Depends on JMEOS #11 being merged upstream | + +### UDF breakdown for fork PR #1 + +| Class | UDFs | Summary | +|-------|------|---------| +| `ConstructorUDFs` | 18 | Text-literal constructors for temporal/span/set/box types | +| `AccessorUDFs` | 25 | start/end/min/max value, shift, scale, atSpan, atSpanset | +| `SpanAlgebraUDFs` | 23 | Span+set topology predicates + algebraic operations | +| `AnalyticsUDFs` | 12 | tfloat math, tnumber integral/twavg, tpoint length/speed/azimuth/direction | +| `PredicateUDFs` | 36 | Temporal order comparisons + ever/always lifting | +| `OutputUDFs` | 15 | Text serialisation, metadata, instant/sequence navigation | +| `TemporalLiftingUDFs` | 30 | Lifted comparisons (tbool), arithmetic, deltaValue, tprecision, tsample | + +Total: 40 pre-existing + 141 new = **181 UDFs**. + +--- + +## Review checklist + +For every MobilitySpark / JMEOS PR, verify: + +- [ ] PostgreSQL License header on every `.java` file +- [ ] `meos_initialize()` in `@BeforeAll`; **no** `meos_finalize()` in `@AfterAll` +- [ ] Surefire `forkCount=1 reuseForks=false` preserved in `pom.xml` +- [ ] New UDF: `null` input β†’ `null` output (STRICT semantics) +- [ ] New UDF: `DBL_MAX` MEOS return β†’ `null` Java return (NAD sentinel) +- [ ] No large binary data files (> 10 MB) in the commit +- [ ] CI green on Linux before requesting merge diff --git a/doc/images/OGC_Associate_Member_3DR.png b/doc/images/OGC_Associate_Member_3DR.png new file mode 100644 index 00000000..4982719b Binary files /dev/null and b/doc/images/OGC_Associate_Member_3DR.png differ diff --git a/doc/images/mobilitydb-logo.png b/doc/images/mobilitydb-logo.png new file mode 100644 index 00000000..83d97dcb Binary files /dev/null and b/doc/images/mobilitydb-logo.png differ diff --git a/doc/images/mobilitydb-logo.svg b/doc/images/mobilitydb-logo.svg new file mode 100644 index 00000000..6c84040e --- /dev/null +++ b/doc/images/mobilitydb-logo.svg @@ -0,0 +1,158 @@ + + + +image/svg+xmlMobilityDB + \ No newline at end of file diff --git a/doc/spark-version.md b/doc/spark-version.md new file mode 100644 index 00000000..aea20679 --- /dev/null +++ b/doc/spark-version.md @@ -0,0 +1,68 @@ +# Spark version target + +MobilitySpark targets **Apache Spark 3.5.x** (the current LTS line). One artefact, one supported Spark major. + +## Position table + +| Axis | Choice | Why | +|---|---|---| +| Spark engine version | **3.5.x** only | LTS line through 2026; the production-line default in every managed Spark distribution (Databricks DBR 15, AWS EMR 7.0-7.4, Google Dataproc image 2.2, Azure Synapse pool default). One MobilitySpark jar runs on the whole 3.x line because the Java/Scala API surface of `spark-core` is stable across 3.0 β†’ 3.5. | +| MEOS C-library version | follows JMEOS | The two existing MEOS lines (v1.3 on MobilityDB master, v1.4 as the in-flight 15-PR bump) flow through JMEOS as separate jar builds. MobilitySpark pins one JMEOS jar; JMEOS pins one MEOS SHA. Version discipline lives in JMEOS, not here. | +| Scala version | 2.12 / 2.13 (as Spark 3.5 supports) | The Maven profile selects against `spark-core_2.13` by default; `_2.12` is available for adopters on the older Scala line. | + +## Why not Spark 4.0 today + +Spark 4.0 (May 2025 GA) is **early-adoption**, not production-default. Concrete signals: + +| Distribution / ecosystem | Default Spark version | Spark 4 status | +|---|---|---| +| Databricks Runtime | DBR 15 = Spark 3.5 | DBR 16 (Spark 4) available, not the UI default | +| AWS EMR | EMR 7.0-7.4 = Spark 3.5 | EMR 7.5+ has Spark 4, not the default image | +| Google Dataproc | Image 2.2 = Spark 3.5 LTS | Image 2.3 (Spark 4) in preview | +| Azure Synapse / HDInsight | Spark 3.5 pool | Spark 4 not yet GA in the managed runtime | +| Cloudera CDP | Spark 3.5 | Spark 4 not yet shipped | +| Delta Lake | Delta 3.x β†’ Spark 3.4 / 3.5 | Delta 4.0 β†’ Spark 4 (separate release line) | +| Apache Iceberg | `iceberg-spark-runtime-3.5` stable | `iceberg-spark-runtime-4.0` in Iceberg 1.7+, recently-stabilised | +| Apache Hudi | Spark 3.x supported across recent releases | Spark 4 support rolling out in the 1.x line | +| Apache Sedona (spatial, closest analogue) | Spark 3.4 / 3.5 first-class | Spark 4 added in Sedona 1.7 (late 2025), some modules still flagged experimental | + +Spark 4 also introduces behavioural breakers (ANSI-by-default, type widening, parameterised SQL) that **break existing 3.x SQL**, so enterprise data teams typically wait 12-18 months before migrating because every SQL workload needs revalidation. + +## Trigger signals that switch this position + +Commit to Spark 4 support when **two or more** of these fire: + +| Signal | Why it matters | +|---|---| +| Databricks DBR 16 becomes the default runtime in the Databricks UI | Mainstream tipping point β€” production workloads start moving | +| AWS EMR 7.5+ becomes the default in the EMR console | Same, for AWS | +| Iceberg makes Spark 4 the **recommended** runtime (not just supported) | Data-lake stack consensus | +| Apache Sedona drops the "experimental" qualifier on Spark 4 modules | Direct spatial-analogue signal | +| A MobilityDB user opens a Spark-4 issue against MobilitySpark | Concrete demand | + +None have fired as of this writing. Track them. + +## What the version axis looks like if/when Spark 4 is added + +The pattern would mirror MobilityDuck's multi-DuckDB-version foundation (`MobilityDuck/doc/multi-duckdb-version.md`): + +- Maven profile per Spark version (`mvn -Pspark-3.5` / `mvn -Pspark-4.0`); each profile pins the matching `spark-core` dependency. +- `if (sparkVersionAtLeast("4.0.0")) { … } else { … }` preprocessor-style branches for the small set of API divergences (`udf.register` shape, `DataType.fromName`, deprecated `ScalaUDF` constructors). +- CI matrix dimension `spark_version: [3.5.4, 4.0.0]`; each row builds a separate jar. +- Per-Spark-version jar in the release artefacts (`mobilityspark-1.0.0-spark-3.5.jar` / `mobilityspark-1.0.0-spark-4.0.jar`). + +This is **future work**, not current. The estimated effort is small-to-medium because the Spark Java API divergences between 3.5 and 4.0 are narrow compared to the DuckDB v1.4-vs-v1.5 ABI break. + +## Relationship to MEOS-version churn + +The MEOS-version axis is **orthogonal** to the Spark-version axis. MEOS versions (v1.3, v1.4) flow through JMEOS regenerations; MobilitySpark depends on one JMEOS jar at a time. Bumping MEOS means swapping the JMEOS dependency, not touching anything Spark-version-specific. + +## Conclusion + +| Question | Answer | +|---|---| +| What Spark version does MobilitySpark target? | 3.5.x (current LTS) | +| Does MobilitySpark support Spark 4 today? | No | +| Is the lack of Spark 4 support a parity gap with the standard stack? | No β€” Spark 4 is early-adoption; the production stack is 3.5 | +| When will Spark 4 support be added? | When the trigger signals fire (see table above) | +| Is there a MEOS-version-axis problem MobilitySpark needs to solve? | No β€” JMEOS owns it; MobilitySpark pins one JMEOS jar at a time | diff --git a/docs/parity-100.md b/docs/parity-100.md new file mode 100644 index 00000000..4d0c3c16 --- /dev/null +++ b/docs/parity-100.md @@ -0,0 +1,113 @@ +# MobilitySpark β€” portable dialect parity & MobilityDB SQL surface + +Two parity axes, both audited from the repo, both with **all six type +families in scope** β€” `temporal`, `geo`, `cbuffer`, `npoint`, `pose`, +`rgeo`. No family is deferred or excluded from any headline. + +| Axis | State | Gate | +|---|---|---| +| **Portable bare-name dialect** (RFC #920 β€” the cross-engine contract) | **29/29 canonical bare names registered, 0 unbacked, all six families β€” 100%** | [`scripts/portable_parity.py`](../scripts/portable_parity.py) | +| **MobilityDB SQL surface** (every `CREATE FUNCTION`, snakeβ†’camel match) | **1571/1577 active addressable covered (99.6%)** β€” every section 100% except the `v_clip` ABI gap below; all six families counted | [`scripts/parity-audit.py`](../scripts/parity-audit.py) β†’ [`parity-status.md`](parity-status.md) | + +--- + +## Portable bare-name dialect (this is 100%) + +Single source of truth: `MobilityDB/MEOS-API meta/portable-aliases.json` +(vendored read-only at [`meta/portable-aliases.json`](../meta/portable-aliases.json), +RFC #920, discussion MobilityDB#861, native in MobilityDB#1075). It maps +**29 SQL operator symbols to 29 portable bare function names**, +type-agnostically. + +[`PortableOperatorAliasUDFs`](../src/main/java/org/mobilitydb/spark/portable/PortableOperatorAliasUDFs.java) +registers all 29 bare names as Spark-SQL UDFs, each **reusing the +operator's own existing backing field verbatim** (equivalence by +construction β€” the alias *is* the operator's backing, it cannot drift). +The backings chosen are the MEOS superclass entrypoints +(`*_temporal_temporal`, `*_tspatial_tspatial`, `t*_temporal_temporal`, +`tdistance_tgeo_tgeo`, `nad_tgeo_*`), which libmeos dispatches internally +for any temporal value carried in the type-erased hex-WKB string β€” so +`tcbuffer` / `tnpoint` / `tpose` / `trgeometry` are covered by +construction alongside `temporal` and `geo`. The type-qualified +operator spellings they supersede 1:1 (`temporalBefore`, `tnumberLeft`, +`teqTemporal`, …) are dropped β€” the bare name is the portable contract. + +`python3 scripts/portable_parity.py` gates this: **29/29 backed, 0 +unbacked, 100%** (same prefix logic as MobilityDB/MEOS-API +`portable_parity.py`). + +--- + +## MobilityDB SQL surface (99.6%) + +The full MobilityDB SQL surface is partitioned by `scripts/parity-audit.py` +into: + +| Bucket | This release | +|--------|---| +| **Active addressable** (any function a Spark UDF can semantically reproduce β€” **all six families**) | **1571 / 1577 covered (99.6%)** | +| **Out of scope** (PG plumbing: `*_in/_out/_recv/_send`, `_transfn/_combinefn/_finalfn`, GiST/SPGiST/GIN index opclasses, `_cmp/_eq/.../_hash`, PG range types, PG-extension-only entry points) | excluded by mechanism, not by family | + +PostGIS `BOX2D`/`BOX3D` are *not* out of scope β€” PostGIS is embedded in +MEOS, so `box2d`/`box3d` UDFs are real. + +The `cbuffer` / `npoint` / `pose` / `rgeo` typed per-family UDF surface is +implemented (`CbufferUDFs`, `NpointUDFs`, `PoseUDFs`, `RgeoUDFs`), reusing +each function's own MEOS C symbol via the `MeosNative` raw-FFI / generic +`functions.*` pattern β€” no reimplementation. Every active section is at +100% with two structural exceptions: + +- **Index-plumbing exception (documented, uniform with temporal/geo).** + The GiST/GIN opclass-support callbacks of + `cbuffer/166_tcbuffer_indexes`, `npoint/092_tnpoint_gin`, + `npoint/098_tnpoint_indexes`, `pose/114_tpose_indexes`, + `rgeo/134_trgeo_indexes` are out of scope β€” the same PG-only index + access-method class already excluded for `temporal/043_temporal_gist`, + `geo/073_tgeo_gist`, etc. Index methods are PG-internal; Spark has no + equivalent. + +- **`rgeo/133_trgeo_vclip` β€” MEOS-library ABI gap (6 functions).** + The `v_clip_*` user surface is implemented only in the `mobilitydb` + PostgreSQL extension (`VClip_*` `MODULE_PATHNAME` wrappers), not in the + MEOS C library MobilitySpark links. `lib/libmeos.so` exports only the + low-level kernels `v_clip_tpoly_point` / `v_clip_tpoly_tpoly`, which + take raw PostGIS `LWPOLY*`/`LWPOINT*`/`Pose*` structs plus + out-parameters β€” there is no `GSERIALIZED`/`Temporal` entry point + reachable from the hex-WKB string convention β€” and the other four + (`v_clip_poly_point`, `v_clip_poly_poly`, `v_clip_tpoly_poly`, + `v_clip_tpoly_tpoint`) are not exported at all. These six are recorded + as a documented gap and intentionally **not stubbed**: binding them + requires an `extern "C"` GSERIALIZED/Temporal-level v-clip entry point + to be added to MEOS upstream (proposed fix: export + `v_clip_trgeo_geo` / `v_clip_trgeo_trgeo` wrappers that accept + `GSERIALIZED`/`Temporal` and a `TimestampTz`, returning the scalar + clip distance β€” mirroring the existing `tdistance_trgeo_*` exports). + +Re-run `python3 scripts/parity-audit.py --mdb ../MobilityDB --mspark .` +to regenerate [`parity-status.md`](parity-status.md); `DEFERRED_FAMILIES` +is empty by invariant (cbuffer/npoint/pose/rgeo never deferred). + +--- + +## Why this matters for the ecosystem + +- **One reference, every engine.** A user learns the 29 bare names once; + MobilityDB (native, #1075), MobilityDuck, and MobilitySpark expose the + identical dialect. The BerlinMOD Q1–Q17 portable SQL runs unchanged + across all three. +- **Equivalence by construction.** Every alias reuses the operator's own + backing C symbol β€” no reimplementation, no second code path, no drift. +- **Reusable audit.** `scripts/parity-audit.py` (surface) and + `scripts/portable_parity.py` (dialect) regenerate from the repo and run + in CI; both keep all six families in scope. + +--- + +## How to verify + +```bash +mvn test # full unit suite (CI: Linux green) +python3 scripts/portable_parity.py # 29/29, 0 unbacked (exit 0) +python3 scripts/parity-audit.py \ + --mdb ../MobilityDB --mspark . # regenerates docs/parity-status.md +``` diff --git a/docs/parity-status.md b/docs/parity-status.md new file mode 100644 index 00000000..74615bc1 --- /dev/null +++ b/docs/parity-status.md @@ -0,0 +1,193 @@ +# MobilitySpark parity status β€” surface-level audit + +Generated 2026-05-18. **Active addressable scope** (temporal + geo, excluding PG-only helpers): 1571/1577 names covered (99.6%). + +**Out of scope** (PG-only β€” no Spark equivalent exists): 601 names skipped β€” 91 from PG-only sections (GiST/SPGiST opclasses, set/span/spanset index files, `019_geo_constructors.in.sql` PG geometric types, `999_oid_cache.in.sql`) plus 510 PG helper functions inside active sections (`*_in/_out/_recv/_send`, `*_transfn/_combinefn/_finalfn/_serialize/_deserialize`, `*_sel/_joinsel/_supportfn/_analyze`, `*_typmod_in/_typmod_out`). Listed in appendix B; not counted in the headline. + +**All six type families in scope** (temporal, geo, cbuffer, npoint, pose, rgeo). None is deferred or excluded from the headline β€” they are full user-facing temporal types covered like every other family (RFC #920; MobilityDB#1075). + +**Methodology**: parsed `CREATE FUNCTION` from `mobilitydb/sql/**/*.in.sql` and `spark.udf().register("name", ...)` (scalar + UDAF) from `MobilitySpark/src/main/java/**/*.java`. Match is by **function name only**, case-insensitive; MobilityDB snake_case is converted to camelCase before comparison so e.g. `tdistance_tgeo_geo` matches `tdistanceTgeoGeo`. A name registered in MobilitySpark is treated as covering all its overloads; per-overload signature parity is not verified at this granularity. + +**Caveats**: +- A name match doesn't prove signature parity. e.g. `before(temporal, temporal)` registered in MobilitySpark does not necessarily cover MobilityDB's `before(tstzspan, temporal)`. +- Spark SQL has no infix-operator extension API; equivalent named functions are registered. The `MDB operators` column lists how many `CREATE OPERATOR` statements exist in the section, all of which collapse to named-function form in MobilitySpark. + +Regenerate with `python3 scripts/parity-audit.py --mdb ../MobilityDB --mspark . --out docs/parity-status.md`. The OUT_OF_SCOPE_SECTIONS / OUT_OF_SCOPE_NAME_SUFFIXES / DEFERRED_FAMILIES sets at the top of that script control bucketing. + +## Active-scope coverage summary (addressable surface) + +| Section | Addressable | Covered | Missing | Coverage | OOS | MDB operators | +|---|---:|---:|---:|---:|---:|---:| +| `cbuffer/150_cbuffer.in.sql` | 18 | 18 | 0 | 100% | 13 | 7 | +| `cbuffer/151_cbufferset.in.sql` | 27 | 27 | 0 | 100% | 15 | 23 | +| `cbuffer/152_tcbuffer.in.sql` | 71 | 71 | 0 | 100% | 13 | 6 | +| `cbuffer/154_tcbuffer_compops.in.sql` | 2 | 2 | 0 | 100% | 4 | 18 | +| `cbuffer/155_tcbuffer_spatialfuncs.in.sql` | 11 | 11 | 0 | 100% | 0 | 0 | +| `cbuffer/158_tcbuffer_topops.in.sql` | 7 | 7 | 0 | 100% | 0 | 25 | +| `cbuffer/159_tcbuffer_posops.in.sql` | 12 | 12 | 0 | 100% | 0 | 44 | +| `cbuffer/160_tcbuffer_distance.in.sql` | 5 | 5 | 0 | 100% | 0 | 17 | +| `cbuffer/161_tcbuffer_aggfuncs.in.sql` | 0 | 0 | 0 | 0% | 7 | 0 | +| `cbuffer/162_tcbuffer_spatialrels.in.sql` | 13 | 13 | 0 | 100% | 0 | 0 | +| `cbuffer/164_tcbuffer_tempspatialrels.in.sql` | 6 | 6 | 0 | 100% | 0 | 0 | +| `cbuffer/167_portable_aliases.in.sql` | 19 | 19 | 0 | 100% | 0 | 0 | +| `geo/050_geoset.in.sql` | 34 | 34 | 0 | 100% | 22 | 46 | +| `geo/051_stbox.in.sql` | 66 | 66 | 0 | 100% | 17 | 29 | +| `geo/052_tgeo.in.sql` | 62 | 62 | 0 | 100% | 18 | 12 | +| `geo/052_tpoint.in.sql` | 62 | 62 | 0 | 100% | 16 | 12 | +| `geo/053_tgeo_inout.in.sql` | 18 | 18 | 0 | 100% | 0 | 0 | +| `geo/053_tpoint_inout.in.sql` | 18 | 18 | 0 | 100% | 0 | 0 | +| `geo/054_tgeo_compops.in.sql` | 2 | 2 | 0 | 100% | 5 | 36 | +| `geo/054_tpoint_compops.in.sql` | 2 | 2 | 0 | 100% | 4 | 36 | +| `geo/056_tgeo_spatialfuncs.in.sql` | 17 | 17 | 0 | 100% | 0 | 0 | +| `geo/056_tpoint_spatialfuncs.in.sql` | 29 | 29 | 0 | 100% | 1 | 0 | +| `geo/058_tgeo_tile.in.sql` | 5 | 5 | 0 | 100% | 0 | 0 | +| `geo/058_tpoint_tile.in.sql` | 11 | 11 | 0 | 100% | 0 | 0 | +| `geo/060_tgeo_boxops.in.sql` | 13 | 13 | 0 | 100% | 0 | 50 | +| `geo/060_tpoint_boxops.in.sql` | 13 | 13 | 0 | 100% | 0 | 50 | +| `geo/062_tgeo_posops.in.sql` | 16 | 16 | 0 | 100% | 0 | 76 | +| `geo/062_tpoint_posops.in.sql` | 16 | 16 | 0 | 100% | 0 | 76 | +| `geo/064_tgeo_distance.in.sql` | 4 | 4 | 0 | 100% | 0 | 16 | +| `geo/064_tpoint_distance.in.sql` | 4 | 4 | 0 | 100% | 0 | 21 | +| `geo/066_tpoint_similarity.in.sql` | 5 | 5 | 0 | 100% | 0 | 0 | +| `geo/068_tgeo_aggfuncs.in.sql` | 0 | 0 | 0 | 0% | 9 | 0 | +| `geo/068_tpoint_aggfuncs.in.sql` | 0 | 0 | 0 | 0% | 12 | 0 | +| `geo/070_tgeo_spatialrels.in.sql` | 14 | 14 | 0 | 100% | 0 | 0 | +| `geo/070_tpoint_spatialrels.in.sql` | 12 | 12 | 0 | 100% | 0 | 0 | +| `geo/072_tgeo_tempspatialrels.in.sql` | 6 | 6 | 0 | 100% | 0 | 0 | +| `geo/072_tpoint_tempspatialrels.in.sql` | 5 | 5 | 0 | 100% | 0 | 0 | +| `geo/076_tgeo_analytics.in.sql` | 13 | 13 | 0 | 100% | 0 | 0 | +| `geo/076_tpoint_analytics.in.sql` | 18 | 18 | 0 | 100% | 0 | 0 | +| `geo/078_tpoint_datagen.in.sql` | 0 | 0 | 0 | 0% | 1 | 0 | +| `geo/079_portable_aliases.in.sql` | 23 | 23 | 0 | 100% | 0 | 0 | +| `npoint/081_npoint.in.sql` | 19 | 19 | 0 | 100% | 22 | 12 | +| `npoint/082_npointset.in.sql` | 28 | 28 | 0 | 100% | 15 | 23 | +| `npoint/083_tnpoint.in.sql` | 65 | 65 | 0 | 100% | 12 | 6 | +| `npoint/085_tnpoint_compops.in.sql` | 2 | 2 | 0 | 100% | 4 | 18 | +| `npoint/087_tnpoint_spatialfuncs.in.sql` | 12 | 12 | 0 | 100% | 0 | 0 | +| `npoint/089_tnpoint_topops.in.sql` | 7 | 7 | 0 | 100% | 0 | 25 | +| `npoint/090_tnpoint_posops.in.sql` | 12 | 12 | 0 | 100% | 0 | 44 | +| `npoint/091_tnpoint_routeops.in.sql` | 4 | 4 | 0 | 100% | 0 | 20 | +| `npoint/093_tnpoint_distance.in.sql` | 4 | 4 | 0 | 100% | 0 | 12 | +| `npoint/095_tnpoint_aggfuncs.in.sql` | 0 | 0 | 0 | 0% | 8 | 0 | +| `npoint/099_portable_aliases.in.sql` | 19 | 19 | 0 | 100% | 0 | 0 | +| `pose/100_pose.in.sql` | 21 | 21 | 0 | 100% | 13 | 7 | +| `pose/101_poseset.in.sql` | 31 | 31 | 0 | 100% | 15 | 23 | +| `pose/102_tpose.in.sql` | 72 | 72 | 0 | 100% | 13 | 6 | +| `pose/104_tpose_compops.in.sql` | 2 | 2 | 0 | 100% | 4 | 18 | +| `pose/105_tpose_spatialfuncs.in.sql` | 8 | 8 | 0 | 100% | 0 | 0 | +| `pose/108_tpose_topops.in.sql` | 7 | 7 | 0 | 100% | 0 | 25 | +| `pose/109_tpose_posops.in.sql` | 16 | 16 | 0 | 100% | 0 | 56 | +| `pose/111_tpose_aggfuncs.in.sql` | 0 | 0 | 0 | 0% | 7 | 0 | +| `pose/113_tpose_distance.in.sql` | 4 | 4 | 0 | 100% | 0 | 12 | +| `pose/115_portable_aliases.in.sql` | 23 | 23 | 0 | 100% | 0 | 0 | +| `rgeo/122_trgeo.in.sql` | 74 | 74 | 0 | 100% | 13 | 6 | +| `rgeo/124_trgeo_compops.in.sql` | 2 | 2 | 0 | 100% | 4 | 18 | +| `rgeo/125_trgeo_spatialfuncs.in.sql` | 8 | 8 | 0 | 100% | 0 | 0 | +| `rgeo/128_trgeo_topops.in.sql` | 5 | 5 | 0 | 100% | 0 | 25 | +| `rgeo/129_trgeo_posops.in.sql` | 12 | 12 | 0 | 100% | 0 | 44 | +| `rgeo/131_trgeo_aggfuncs.in.sql` | 0 | 0 | 0 | 0% | 7 | 0 | +| `rgeo/133_trgeo_distance.in.sql` | 4 | 4 | 0 | 100% | 0 | 12 | +| `rgeo/133_trgeo_vclip.in.sql` | 6 | 0 | 6 | 0% | 0 | 0 | +| `rgeo/135_portable_aliases.in.sql` | 19 | 19 | 0 | 100% | 0 | 0 | +| `temporal/001_set.in.sql` | 39 | 39 | 0 | 100% | 43 | 38 | +| `temporal/002_set_ops.in.sql` | 11 | 11 | 0 | 100% | 0 | 176 | +| `temporal/003_span.in.sql` | 36 | 36 | 0 | 100% | 32 | 30 | +| `temporal/005_span_ops.in.sql` | 12 | 12 | 0 | 100% | 0 | 160 | +| `temporal/007_spanset.in.sql` | 51 | 51 | 0 | 100% | 30 | 30 | +| `temporal/009_spanset_ops.in.sql` | 14 | 14 | 0 | 100% | 0 | 280 | +| `temporal/015_span_aggfuncs.in.sql` | 0 | 0 | 0 | 0% | 10 | 0 | +| `temporal/021_tbox.in.sql` | 43 | 43 | 0 | 100% | 17 | 21 | +| `temporal/022_temporal.in.sql` | 94 | 94 | 0 | 100% | 23 | 24 | +| `temporal/023_temporal_inout.in.sql` | 16 | 16 | 0 | 100% | 0 | 0 | +| `temporal/025_temporal_tile.in.sql` | 16 | 16 | 0 | 100% | 0 | 0 | +| `temporal/026_tnumber_mathfuncs.in.sql` | 17 | 17 | 0 | 100% | 0 | 24 | +| `temporal/028_tbool_boolops.in.sql` | 4 | 4 | 0 | 100% | 0 | 7 | +| `temporal/029_ttext_textfuncs.in.sql` | 4 | 4 | 0 | 100% | 0 | 3 | +| `temporal/030_temporal_compops.in.sql` | 6 | 6 | 0 | 100% | 13 | 180 | +| `temporal/032_temporal_boxops.in.sql` | 11 | 11 | 0 | 100% | 0 | 100 | +| `temporal/034_temporal_posops.in.sql` | 8 | 8 | 0 | 100% | 0 | 112 | +| `temporal/036_tnumber_distance.in.sql` | 2 | 2 | 0 | 100% | 0 | 17 | +| `temporal/038_temporal_similarity.in.sql` | 5 | 5 | 0 | 100% | 0 | 0 | +| `temporal/040_temporal_aggfuncs.in.sql` | 0 | 0 | 0 | 0% | 40 | 0 | +| `temporal/042_temporal_waggfuncs.in.sql` | 0 | 0 | 0 | 0% | 8 | 0 | +| `temporal/046_temporal_analytics.in.sql` | 4 | 4 | 0 | 100% | 0 | 0 | +| `temporal/048_portable_aliases.in.sql` | 19 | 19 | 0 | 100% | 0 | 0 | +| **TOTAL (active)** | **1577** | **1571** | **6** | **100%** | **510** | β€” | + +## Missing function names per active section + +### `rgeo/133_trgeo_vclip.in.sql` β€” 6 missing of 6 addressable (0% covered) + +- `v_clip_poly_point` β†’ `vClipPolyPoint` +- `v_clip_poly_poly` β†’ `vClipPolyPoly` +- `v_clip_tpoly_point` β†’ `vClipTpolyPoint` +- `v_clip_tpoly_poly` β†’ `vClipTpolyPoly` +- `v_clip_tpoly_tpoint` β†’ `vClipTpolyTpoint` +- `v_clip_tpoly_tpoly` β†’ `vClipTpolyTpoly` + +## Appendix B β€” Out of scope (PG-only, no Spark equivalent) + +These entries are PG-specific helpers β€” index opclasses, aggregate transition/combine/final/serialize callbacks, planner hooks (`_sel`, `_joinsel`, `_supportfn`, `_analyze`), text/binary I/O helpers (`_in`, `_out`, `_recv`, `_send`), type modifier helpers, the `999_oid_cache` PG catalog hook, PG geometric type constructors (`019_geo_constructors`), and the sibling-family GiST/GIN index opclass-support callbacks (`cbuffer/166_tcbuffer_indexes`, `npoint/092_tnpoint_gin`, `npoint/098_tnpoint_indexes`, `pose/114_tpose_indexes`, `rgeo/134_trgeo_indexes` β€” the same index-plumbing class already excluded for temporal/geo). None of them have Spark equivalents and they should not be implemented; listed here only for completeness. + +### Whole sections excluded + +| Section | Names | +|---|---:| +| `cbuffer/166_tcbuffer_indexes.in.sql` | 1 | +| `geo/073_tgeo_gist.in.sql` | 8 | +| `geo/073_tpoint_gist.in.sql` | 3 | +| `geo/074_tgeo_spgist.in.sql` | 9 | +| `npoint/092_tnpoint_gin.in.sql` | 3 | +| `npoint/098_tnpoint_indexes.in.sql` | 1 | +| `pose/114_tpose_indexes.in.sql` | 1 | +| `rgeo/134_trgeo_indexes.in.sql` | 1 | +| `temporal/011_span_indexes.in.sql` | 19 | +| `temporal/012_spanset_indexes.in.sql` | 3 | +| `temporal/013_set_indexes.in.sql` | 10 | +| `temporal/019_geo_constructors.in.sql` | 7 | +| `temporal/043_temporal_gist.in.sql` | 14 | +| `temporal/044_temporal_spgist.in.sql` | 10 | +| `temporal/999_oid_cache.in.sql` | 1 | + +### PG helpers inside active sections + +| Section | PG helpers | +|---|---:| +| `cbuffer/150_cbuffer.in.sql` | 13 | +| `cbuffer/151_cbufferset.in.sql` | 15 | +| `cbuffer/152_tcbuffer.in.sql` | 13 | +| `cbuffer/154_tcbuffer_compops.in.sql` | 4 | +| `cbuffer/161_tcbuffer_aggfuncs.in.sql` | 7 | +| `geo/050_geoset.in.sql` | 22 | +| `geo/051_stbox.in.sql` | 17 | +| `geo/052_tgeo.in.sql` | 18 | +| `geo/052_tpoint.in.sql` | 16 | +| `geo/054_tgeo_compops.in.sql` | 5 | +| `geo/054_tpoint_compops.in.sql` | 4 | +| `geo/056_tpoint_spatialfuncs.in.sql` | 1 | +| `geo/068_tgeo_aggfuncs.in.sql` | 9 | +| `geo/068_tpoint_aggfuncs.in.sql` | 12 | +| `geo/078_tpoint_datagen.in.sql` | 1 | +| `npoint/081_npoint.in.sql` | 22 | +| `npoint/082_npointset.in.sql` | 15 | +| `npoint/083_tnpoint.in.sql` | 12 | +| `npoint/085_tnpoint_compops.in.sql` | 4 | +| `npoint/095_tnpoint_aggfuncs.in.sql` | 8 | +| `pose/100_pose.in.sql` | 13 | +| `pose/101_poseset.in.sql` | 15 | +| `pose/102_tpose.in.sql` | 13 | +| `pose/104_tpose_compops.in.sql` | 4 | +| `pose/111_tpose_aggfuncs.in.sql` | 7 | +| `rgeo/122_trgeo.in.sql` | 13 | +| `rgeo/124_trgeo_compops.in.sql` | 4 | +| `rgeo/131_trgeo_aggfuncs.in.sql` | 7 | +| `temporal/001_set.in.sql` | 43 | +| `temporal/003_span.in.sql` | 32 | +| `temporal/007_spanset.in.sql` | 30 | +| `temporal/015_span_aggfuncs.in.sql` | 10 | +| `temporal/021_tbox.in.sql` | 17 | +| `temporal/022_temporal.in.sql` | 23 | +| `temporal/030_temporal_compops.in.sql` | 13 | +| `temporal/040_temporal_aggfuncs.in.sql` | 40 | +| `temporal/042_temporal_waggfuncs.in.sql` | 8 | + diff --git a/lib/libmeos.so b/lib/libmeos.so new file mode 100644 index 00000000..d4ad17e1 Binary files /dev/null and b/lib/libmeos.so differ diff --git a/libs/JMEOS-1.4.jar b/libs/JMEOS-1.4.jar new file mode 100644 index 00000000..8555e228 Binary files /dev/null and b/libs/JMEOS-1.4.jar differ diff --git a/libs/MobilityDB-JMEOS-Linux.jar b/libs/MobilityDB-JMEOS-Linux.jar deleted file mode 100644 index b19aa037..00000000 Binary files a/libs/MobilityDB-JMEOS-Linux.jar and /dev/null differ diff --git a/libs/MobilityDB-JMEOS.jar b/libs/MobilityDB-JMEOS.jar deleted file mode 100644 index a2488c2c..00000000 Binary files a/libs/MobilityDB-JMEOS.jar and /dev/null differ diff --git a/meta/portable-aliases.json b/meta/portable-aliases.json new file mode 100644 index 00000000..1cabac15 --- /dev/null +++ b/meta/portable-aliases.json @@ -0,0 +1,60 @@ +{ + "_comment": "Canonical portable bare-name dialect β€” the single codegen source of truth (RFC #920). Every binding/engine generates the SAME bare names from this mapping so users learn one reference and assume the rest. Operators are SQL operator symbols; bareName is the portable function name. The mapping is type-agnostic: it applies to EVERY temporal type family.", + "provenance": { + "discussion": "MobilityDB#861", + "rfc": "MobilityDB RFC #920 (doc/rfc/sql-portability/README.md, branch rfc/sql-portability)", + "nativePR": "MobilityDB#1075 (1303 operator-overload aliases, each reusing the operator's own C symbol β€” identical by construction; CI-gated by tools/portable_aliases/generate.py --check)", + "manualChapter": "MobilityDB#1078" + }, + "families": { + "topology": [{"operator": "&&", "bareName": "overlaps"}, + {"operator": "@>", "bareName": "contains"}, + {"operator": "<@", "bareName": "contained"}, + {"operator": "-|-", "bareName": "adjacent"}], + "timePosition": [{"operator": "<<#", "bareName": "before"}, + {"operator": "#>>", "bareName": "after"}, + {"operator": "&<#", "bareName": "overbefore"}, + {"operator": "#&>", "bareName": "overafter"}], + "spaceX": [{"operator": "<<", "bareName": "left"}, + {"operator": ">>", "bareName": "right"}, + {"operator": "&<", "bareName": "overleft"}, + {"operator": "&>", "bareName": "overright"}], + "spaceY": [{"operator": "<<|", "bareName": "below"}, + {"operator": "|>>", "bareName": "above"}, + {"operator": "&<|", "bareName": "overbelow"}, + {"operator": "|&>", "bareName": "overabove"}], + "spaceZ": [{"operator": "<>", "bareName": "back"}, + {"operator": "&", "bareName": "overback"}], + "temporalComparison": [{"operator": "#=", "bareName": "teq"}, + {"operator": "#<>", "bareName": "tne"}, + {"operator": "#<", "bareName": "tlt"}, + {"operator": "#<=", "bareName": "tle"}, + {"operator": "#>", "bareName": "tgt"}, + {"operator": "#>=", "bareName": "tge"}], + "distance": [{"operator": "<->", "bareName": "tdistance"}, + {"operator": "|=|", "bareName": "nearestApproachDistance"}], + "same": [{"operator": "~=", "bareName": "same"}] + }, + "alreadyCanonical": [ + {"family": "ever", "operators": ["?="], "pattern": "ever_*"}, + {"family": "always", "operators": ["%="], "pattern": "always_*"}, + {"functions": ["eIntersects", "atTime", "restriction functions", + "spatial-relationship functions"]} + ], + "_explicitBackingComment": "Bare names whose MEOS C family prefix differs from the bare name itself. Verified against the catalog (not guessed): `nearestApproachDistance` is backed by the `nad_*` family (35 functions). Lets the parity audit resolve 100% honestly instead of false-flagging a real, present family.", + "explicitBacking": { + "nearestApproachDistance": ["nad"] + }, + "scope": { + "inScopeTypeFamilies": ["temporal", "geo", "cbuffer", "npoint", "pose", + "rgeo"], + "note": "cbuffer / npoint / pose / rgeo are FULL user-facing temporal types and ARE in scope β€” covered like every other type. PR #1075 already aliases all six families (1303 aliases). They must NOT be excluded from any parity headline; an upstream/audit note that 'defers' or 'jointly excludes' them is a known error being corrected β€” where another engine defers them, that is incomplete work to close (a gap with a plan), never an accepted exclusion." + }, + "notes": [ + "Generate aliases by reusing each operator's own backing C function (equivalence by construction), never by reimplementing; mirror MobilityDB tools/portable_aliases/generate.py + its 100%-coverage audit.", + "User-facing API uses the full name `trgeometry`; internal functions keep the `trgeo_` prefix β€” do NOT normalize the internal prefix.", + "Goal: 100% parity ecosystem-wide β€” every operator has its bare name on every engine, no gaps, no headline exclusions." + ] +} diff --git a/pom.xml b/pom.xml index 417d4287..a6af81a7 100644 --- a/pom.xml +++ b/pom.xml @@ -4,38 +4,140 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - org.mobiltydb - SparkMeos - 1.0-SNAPSHOT + org.mobilitydb + mobilityspark + 0.1.0-SNAPSHOT - 17 - 17 + 21 + 21 UTF-8 + 3.5.1 + 2.13 + ${project.basedir}/libs/JMEOS-1.4.jar + org.apache.spark - spark-core_2.13 - 3.4.0 + spark-core_${scala.binary.version} + ${spark.version} + provided org.apache.spark - spark-sql_2.13 - 3.4.0 - compile + spark-sql_${scala.binary.version} + ${spark.version} + provided + org.jmeos - meos - 1.0 + jmeos + 1.4 + system + ${jmeos.jar} + + com.github.jnr jnr-ffi - 2.2.11 + 2.2.17 + + + + + org.junit.jupiter + junit-jupiter + 5.10.0 + test - \ No newline at end of file + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + + org/mobiltydb/**/*.java + utils/**/*.java + + + UDF/**/*.java + UDT/**/*.java + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.1 + + + package + shade + + true + spark + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.1.2 + + false + + 1 + false + + + --add-opens=java.base/java.lang=ALL-UNNAMED + --add-opens=java.base/java.lang.invoke=ALL-UNNAMED + --add-opens=java.base/java.lang.reflect=ALL-UNNAMED + --add-opens=java.base/java.io=ALL-UNNAMED + --add-opens=java.base/java.net=ALL-UNNAMED + --add-opens=java.base/java.nio=ALL-UNNAMED + --add-opens=java.base/java.util=ALL-UNNAMED + --add-opens=java.base/java.util.concurrent=ALL-UNNAMED + --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED + --add-opens=java.base/java.util.concurrent.locks=ALL-UNNAMED + --add-opens=java.base/java.util.zip=ALL-UNNAMED + --add-opens=java.base/sun.nio.ch=ALL-UNNAMED + --add-opens=java.base/sun.nio.cs=ALL-UNNAMED + --add-opens=java.base/sun.security.action=ALL-UNNAMED + --add-opens=java.base/sun.util.calendar=ALL-UNNAMED + + + + + + diff --git a/scripts/parity-audit.py b/scripts/parity-audit.py new file mode 100644 index 00000000..30929401 --- /dev/null +++ b/scripts/parity-audit.py @@ -0,0 +1,543 @@ +#!/usr/bin/env python3 +"""Compare MobilityDB SQL surface against MobilitySpark registered surface. + +Adapted from MobilityDuck/scripts/parity-audit.py. Same OUT_OF_SCOPE / +DEFERRED bucketing model; differences: + +- MobilitySpark Java sources (`*.java`) instead of C++; matches Spark UDF + registration: `spark.udf().register("name", ...)` and the UDAF variant + `spark.udf().register("name", org.apache.spark.sql.functions.udaf(...)`. +- MobilityDB SQL is snake_case; MobilitySpark UDFs are camelCase. Match + converts snake_case β†’ camelCase before comparison. + +Usage: + python3 scripts/parity-audit.py \\ + --mdb /path/to/MobilityDB \\ + --mspark /path/to/MobilitySpark \\ + --out docs/parity-status.md +""" + +import argparse +import collections +import glob +import os +import re +import sys +from datetime import date + + +# Type families deferred from the active parity sweep. +# +# EMPTY BY INVARIANT. cbuffer / npoint / pose / rgeo are FULL user-facing +# temporal types and ARE in scope β€” covered like every other family (RFC +# #920; MobilityDB#1075 aliases all six). They must never be excluded from +# the parity headline. Re-deferring any of them is incomplete work, not an +# accepted end state. Keep this set empty. +DEFERRED_FAMILIES = set() + + +# Whole SQL sections that are PG-only (no Spark equivalent exists). +# Match by tail of the relpath under mobilitydb/sql/. +OUT_OF_SCOPE_SECTIONS = { + "temporal/011_span_indexes.in.sql", # GiST/SPGiST opclasses + "temporal/012_spanset_indexes.in.sql", # GiST/SPGiST opclasses + "temporal/013_set_indexes.in.sql", # GiST/SPGiST opclasses + "temporal/019_geo_constructors.in.sql", # PG geometric types + "temporal/043_temporal_gist.in.sql", # GiST support + "temporal/044_temporal_spgist.in.sql", # SPGiST support + "temporal/999_oid_cache.in.sql", # PG catalog hook + "geo/073_tgeo_gist.in.sql", # GiST support + "geo/073_tpoint_gist.in.sql", # GiST support + "geo/074_tgeo_spgist.in.sql", # SPGiST support + "geo/074_tpoint_spgist.in.sql", # SPGiST support + # Sibling-family index plumbing β€” GiST/GIN opclass-support callbacks + # (consistent / extract_query / extract_value / triconsistent), the + # exact same PG-only class already excluded for temporal/geo above. + # No Spark equivalent: index access methods are PG-internal. + "cbuffer/166_tcbuffer_indexes.in.sql", # GiST support + "npoint/092_tnpoint_gin.in.sql", # GIN support + "npoint/098_tnpoint_indexes.in.sql", # GiST support + "pose/114_tpose_indexes.in.sql", # GiST support + "rgeo/134_trgeo_indexes.in.sql", # GiST support +} + + +# Function-name suffixes that mark PG-only helpers. +OUT_OF_SCOPE_NAME_SUFFIXES = ( + "_transfn", + "_combinefn", + "_finalfn", + "_serialize", + "_deserialize", + "_sel", + "_joinsel", + "_supportfn", + "_analyze", + "_typmod_in", + "_typmod_out", + "_in", + "_out", + "_recv", + "_send", + # PG btree opclass support β€” user-callable but only meaningful inside + # PG operator classes for sorting and hash. Spark uses Dataset.distinct + # / orderBy which works on hex-WKB string equality natively. + "_cmp", + "_eq", + "_ne", + "_lt", + "_le", + "_gt", + "_ge", + "_hash", + "_hash_extended", +) + + +# PG-specific / PG-extension-specific entry points with no portable equivalent. +# Excluded from the audit; do not register as UDFs. +# +# NOTE: box2d / box3d ARE addressable here even though they're PostGIS types, +# because PostGIS is embedded in MEOS β€” the BOX3D / GBOX structs and their +# I/O functions are exported by libmeos.so. The ranges (range/multirange) +# are PG built-ins NOT in MEOS, so they remain OOS. +OUT_OF_SCOPE_NAMES = frozenset({ + # PostgreSQL built-in range types β€” NOT in MEOS, no portable equivalent + "range", # span β†’ PG range type + "multirange", # spanset β†’ PG multirange type + # PG-extension-specific data generators + "create_trip", # BerlinMOD synthetic-trajectory generator (PG-only, depends + # on BerlinMOD road-network schema + PG random functions) + # Platform-bridge functions + "transform_gk", # Gauss-KrΓΌger projection used to interoperate with the + # SECONDO research platform; not portable to Spark/DuckDB +}) + + +def is_out_of_scope_name(fname): + lower = fname.lower() + if lower in OUT_OF_SCOPE_NAMES: + return True + for suf in OUT_OF_SCOPE_NAME_SUFFIXES: + if lower.endswith(suf) and len(lower) > len(suf): + return True + return False + + +def snake_to_camel(name): + """Convert MobilityDB snake_case to MobilitySpark camelCase. + + Examples: + tgeompoint_in β†’ tgeompointIn + eintersects_tgeo_geo β†’ eintersectsTgeoGeo + nad_tgeo_tgeo β†’ nadTgeoTgeo + tdistance_tgeo_geo β†’ tdistanceTgeoGeo + """ + parts = name.split("_") + if not parts: + return name + return parts[0] + "".join(p.capitalize() for p in parts[1:]) + + +CREATE_FUNC_RE = re.compile( + r"CREATE\s+(?:OR\s+REPLACE\s+)?FUNCTION\s+(\w+)\s*\(([^)]*)\)", + re.IGNORECASE | re.DOTALL, +) +CREATE_OP_RE = re.compile(r"CREATE\s+OPERATOR\s+(\S+)\s*\(", re.IGNORECASE) + +# Spark UDF registration patterns. +REGISTER_RE = re.compile(r'spark\.udf\(\)\.register\s*\(\s*"([^"]+)"') + + +def collect_mobilitydb(mdb_root): + sql_root = os.path.join(mdb_root, "mobilitydb", "sql") + if not os.path.isdir(sql_root): + sys.exit(f"MobilityDB SQL dir not found: {sql_root}") + + section_funcs = collections.OrderedDict() + section_op_count = collections.OrderedDict() + all_funcs = set() + + for section in sorted(os.listdir(sql_root)): + full = os.path.join(sql_root, section) + if not os.path.isdir(full): + continue + for sql in sorted(glob.glob(f"{full}/*.in.sql")): + rel = os.path.relpath(sql, sql_root) + with open(sql) as f: + text = f.read() + funcs = collections.Counter() + for m in CREATE_FUNC_RE.finditer(text): + funcs[m.group(1)] += 1 + all_funcs.add(m.group(1)) + section_funcs[rel] = funcs + section_op_count[rel] = len(CREATE_OP_RE.findall(text)) + + return section_funcs, section_op_count, all_funcs + + +def collect_mobilityspark(mspark_root): + src_root = os.path.join(mspark_root, "src", "main", "java") + if not os.path.isdir(src_root): + sys.exit(f"MobilitySpark src dir not found: {src_root}") + + funcs = collections.Counter() + files_for_func = collections.defaultdict(set) + for java in glob.glob(f"{src_root}/**/*.java", recursive=True): + with open(java, errors="replace") as f: + text = f.read() + rel = os.path.relpath(java, src_root) + for m in REGISTER_RE.finditer(text): + funcs[m.group(1)] += 1 + files_for_func[m.group(1)].add(rel) + return funcs, files_for_func + + +def is_deferred(section_relpath): + family = section_relpath.split("/", 1)[0] + return family in DEFERRED_FAMILIES + + +def is_out_of_scope_section(section_relpath): + return section_relpath in OUT_OF_SCOPE_SECTIONS + + +# Known MobilitySpark type-prefixes used by registered UDFs. A SQL function +# `abs(tnumber)` is covered if any Spark UDF named e.g. `tnumberAbs`, +# `tintAbs`, `tfloatAbs` exists. The match strips one of these prefixes +# and compares the remainder (case-insensitive). +TYPE_PREFIXES = ( + "tnumber", "tpoint", "tgeompoint", "tgeogpoint", + "tgeo", "tgeometry", "tgeography", + "tfloat", "tint", "tbool", "ttext", + "tbox", "stbox", + "span", "spanset", "set", + "tcbuffer", "tnpoint", "tpose", "trgeo", + "temporal", "geo", "geom", "geog", +) + + +def write_report(out_path, mdb_section_funcs, mdb_section_op_count, + all_mdb_funcs, mspark_funcs): + # Build case-insensitive lookup of registered Spark UDF names. + mspark_funcs_lower = {k.lower(): k for k in mspark_funcs} + + # Build loose-match index: for each Spark UDF, also index its name + # with known type-prefixes stripped. So `tnumberAbs` becomes + # discoverable by the bare name `abs`, `tpointSRID` by `srid`, etc. + loose_index = set() + for k in mspark_funcs: + kl = k.lower() + loose_index.add(kl) + for pfx in TYPE_PREFIXES: + if kl.startswith(pfx) and len(kl) > len(pfx): + loose_index.add(kl[len(pfx):]) + + # Build a list of Spark UDF names sorted by length (longest first) for + # efficient prefix scanning. + mspark_lower_list = sorted(mspark_funcs_lower.keys(), key=len, reverse=True) + + # Known MobilitySpark type-SUFFIX tokens that may be appended to a + # MobilityDB camel-case name to disambiguate overloads. Match is + # case-insensitive on the camelCase form. + TYPE_SUFFIXES = ( + "tintint", "tfloatfloat", "tboolbool", "ttexttext", + "tnumbertnumber", "ttempt", "temporaltemporal", + "tgeotgeo", "tgeogeo", "tgeotgeompoint", + "tpointtpoint", "tpointgeo", "tpointpoint", + "tcbuffertcbuffer", "tcbuffergeo", + "tnpointtnpoint", "tnpointgeo", + "tposetpose", "tposegeo", + "trgeotrgeo", "trgeogeo", + "intint", "floatfloat", "textstring", "boolbool", + "tboxtbox", "stboxstbox", + "tinttbox", "tboxtint", "tfloattbox", "tboxtfloat", + "tnumbertbox", "tboxtnumber", + "tspatialstbox", "stboxtspatial", + "spanspan", "spansetspanset", "spansetspan", "spanspanset", + "setset", + "tint", "tfloat", "tbool", "ttext", "ttempt", "temporal", + "tgeo", "tpoint", "tcbuffer", "tnpoint", "tpose", "trgeo", + "tbox", "stbox", + "span", "spanset", "set", + "geo", "geom", "geog", "point", + "int", "float", "text", "bool", + ) + + # MobilityDB SQL "wrapper" prefixes that span multiple type combos. + # A name like `temporal_above` is a dispatcher over + # {above_stbox_tspatial, above_tspatial_stbox, above_tspatial_tspatial}, + # all of which appear in MobilitySpark with type-suffixed names like + # `stboxAboveTpoint`, `tpointAboveStbox`, `tpointAbove`. To recognise + # this, strip the wrapper prefix and check whether the remainder + # appears as a substring (case-insensitive) of any Spark UDF name. + WRAPPER_PREFIXES = ( + "temporal_", "tnumber_", "tspatial_", "tgeo_", "tpoint_", + ) + + def is_covered(mdb_fname): + """A MobilityDB SQL name is covered by MobilitySpark if any of: + 1. exact match (case-insensitive) on snake or camel form + 2. loose: name appears after stripping a known type-PREFIX from a + Spark UDF name (e.g. abs ↔ tnumberAbs) + 3. suffix: any Spark UDF starts with the camel-case form and + ends with a known type-SUFFIX (e.g. always_eq ↔ alwaysEqTintInt, + alwaysEqTfloatFloat, alwaysEqTemporal) + 4. wrapper: MobilityDB dispatcher name `_` is + considered covered if any Spark UDF contains `` (PascalCase) + β€” e.g. temporal_above ↔ stboxAboveTpoint / tpointAboveStbox.""" + camel = snake_to_camel(mdb_fname).lower() + bare = mdb_fname.lower() + if bare in mspark_funcs_lower or camel in mspark_funcs_lower: + return True + if bare in loose_index or camel in loose_index: + return True + for udf in mspark_lower_list: + if udf.startswith(camel) and len(udf) > len(camel): + tail = udf[len(camel):] + for suf in TYPE_SUFFIXES: + if tail == suf: + return True + # Wrapper match + for wpfx in WRAPPER_PREFIXES: + if bare.startswith(wpfx) and len(bare) > len(wpfx): + verb = bare[len(wpfx):] + # Avoid 1-letter false positives. + if len(verb) >= 3: + for udf in mspark_lower_list: + if verb in udf: + return True + return False + + active_results = [] + deferred_results = [] + out_of_scope_results = [] + + for sec, funcs in mdb_section_funcs.items(): + if not funcs: + continue + section_oos = is_out_of_scope_section(sec) + section_deferred = is_deferred(sec) + covered, missing, oos_names = [], [], [] + for fname, count in sorted(funcs.items()): + if section_oos: + oos_names.append((fname, count)) + continue + if not section_deferred and is_out_of_scope_name(fname): + oos_names.append((fname, count)) + continue + if is_covered(fname): + covered.append((fname, count)) + else: + missing.append((fname, count)) + addressable = len(covered) + len(missing) + pct = (len(covered) / addressable * 100) if addressable else 0 + row = (sec, len(funcs), len(covered), len(missing), pct, + missing, covered, mdb_section_op_count[sec], + oos_names, addressable) + if section_oos: + out_of_scope_results.append(row) + elif section_deferred: + deferred_results.append(row) + else: + active_results.append(row) + + def totals(results): + cov = sum(r[2] for r in results) + miss = sum(r[3] for r in results) + n = cov + miss + pct = (cov / n * 100) if n else 0 + return n, cov, miss, pct + + def oos_total(results): + return sum(len(r[8]) for r in results) + + a_total, a_cov, a_miss, a_pct = totals(active_results) + d_total, d_cov, d_miss, d_pct = totals(deferred_results) + a_oos_inside = oos_total(active_results) + section_oos_total = sum(len(r[8]) for r in out_of_scope_results) + total_oos = a_oos_inside + section_oos_total + + lines = [] + lines.append("# MobilitySpark parity status β€” surface-level audit") + lines.append("") + lines.append( + f"Generated {date.today().isoformat()}. **Active addressable scope** " + f"(temporal + geo, excluding PG-only helpers): " + f"{a_cov}/{a_total} names covered ({a_pct:.1f}%)." + ) + lines.append("") + lines.append( + f"**Out of scope** (PG-only β€” no Spark equivalent exists): " + f"{total_oos} names skipped β€” {section_oos_total} from PG-only " + f"sections (GiST/SPGiST opclasses, set/span/spanset index files, " + f"`019_geo_constructors.in.sql` PG geometric types, " + f"`999_oid_cache.in.sql`) plus {a_oos_inside} PG helper functions " + f"inside active sections (`*_in/_out/_recv/_send`, `*_transfn/" + f"_combinefn/_finalfn/_serialize/_deserialize`, `*_sel/_joinsel/" + f"_supportfn/_analyze`, `*_typmod_in/_typmod_out`). Listed in " + f"appendix B; not counted in the headline." + ) + lines.append("") + if DEFERRED_FAMILIES: + lines.append( + f"**Deferred families** ({', '.join(sorted(DEFERRED_FAMILIES))}) " + "appear in appendix C and are also excluded from the headline." + ) + else: + lines.append( + "**All six type families in scope** (temporal, geo, cbuffer, " + "npoint, pose, rgeo). None is deferred or excluded from the " + "headline β€” they are full user-facing temporal types covered " + "like every other family (RFC #920; MobilityDB#1075)." + ) + lines.append("") + lines.append( + "**Methodology**: parsed `CREATE FUNCTION` from " + "`mobilitydb/sql/**/*.in.sql` and `spark.udf().register(\"name\", " + "...)` (scalar + UDAF) from `MobilitySpark/src/main/java/**/*.java`. " + "Match is by **function name only**, case-insensitive; MobilityDB " + "snake_case is converted to camelCase before comparison so e.g. " + "`tdistance_tgeo_geo` matches `tdistanceTgeoGeo`. A name registered " + "in MobilitySpark is treated as covering all its overloads; " + "per-overload signature parity is not verified at this granularity." + ) + lines.append("") + lines.append("**Caveats**:") + lines.append( + "- A name match doesn't prove signature parity. e.g. " + "`before(temporal, temporal)` registered in MobilitySpark does not " + "necessarily cover MobilityDB's `before(tstzspan, temporal)`." + ) + lines.append( + "- Spark SQL has no infix-operator extension API; equivalent named " + "functions are registered. The `MDB operators` column lists how " + "many `CREATE OPERATOR` statements exist in the section, all of " + "which collapse to named-function form in MobilitySpark." + ) + lines.append("") + lines.append( + "Regenerate with `python3 scripts/parity-audit.py --mdb " + "../MobilityDB --mspark . --out docs/parity-status.md`. The " + "OUT_OF_SCOPE_SECTIONS / OUT_OF_SCOPE_NAME_SUFFIXES / " + "DEFERRED_FAMILIES sets at the top of that script control bucketing." + ) + lines.append("") + + lines.append("## Active-scope coverage summary (addressable surface)") + lines.append("") + lines.append("| Section | Addressable | Covered | Missing | Coverage | OOS | MDB operators |") + lines.append("|---|---:|---:|---:|---:|---:|---:|") + for sec, total, cov, miss, pct, _, _, ops, oos_names, addressable in active_results: + lines.append( + f"| `{sec}` | {addressable} | {cov} | {miss} | {pct:.0f}% | " + f"{len(oos_names)} | {ops} |" + ) + lines.append( + f"| **TOTAL (active)** | **{a_total}** | **{a_cov}** | " + f"**{a_miss}** | **{a_pct:.0f}%** | **{a_oos_inside}** | β€” |" + ) + lines.append("") + + lines.append("## Missing function names per active section") + lines.append("") + for sec, total, cov, miss, pct, missing, _, _, _, addressable in active_results: + if not missing: + continue + lines.append(f"### `{sec}` β€” {miss} missing of {addressable} addressable ({pct:.0f}% covered)") + lines.append("") + for fname, count in missing: + tag = f" ({count} overloads)" if count > 1 else "" + lines.append(f"- `{fname}` β†’ `{snake_to_camel(fname)}`{tag}") + lines.append("") + + # ----- Appendix B: out-of-scope (PG-only) ----- + lines.append("## Appendix B β€” Out of scope (PG-only, no Spark equivalent)") + lines.append("") + lines.append( + "These entries are PG-specific helpers β€” index opclasses, " + "aggregate transition/combine/final/serialize callbacks, planner " + "hooks (`_sel`, `_joinsel`, `_supportfn`, `_analyze`), text/binary " + "I/O helpers (`_in`, `_out`, `_recv`, `_send`), type modifier " + "helpers, the `999_oid_cache` PG catalog hook, PG geometric " + "type constructors (`019_geo_constructors`), and the sibling-family " + "GiST/GIN index opclass-support callbacks " + "(`cbuffer/166_tcbuffer_indexes`, `npoint/092_tnpoint_gin`, " + "`npoint/098_tnpoint_indexes`, `pose/114_tpose_indexes`, " + "`rgeo/134_trgeo_indexes` β€” the same index-plumbing class already " + "excluded for temporal/geo). None of them have Spark equivalents " + "and they should not be implemented; listed here only for " + "completeness." + ) + lines.append("") + if out_of_scope_results: + lines.append("### Whole sections excluded") + lines.append("") + lines.append("| Section | Names |") + lines.append("|---|---:|") + for sec, total, _, _, _, _, _, _, oos_names, _ in out_of_scope_results: + lines.append(f"| `{sec}` | {len(oos_names)} |") + lines.append("") + if a_oos_inside: + lines.append("### PG helpers inside active sections") + lines.append("") + lines.append("| Section | PG helpers |") + lines.append("|---|---:|") + for sec, _, _, _, _, _, _, _, oos_names, _ in active_results: + if oos_names: + lines.append(f"| `{sec}` | {len(oos_names)} |") + lines.append("") + + # ----- Appendix C: deferred families ----- + if deferred_results: + lines.append("## Appendix C β€” Deferred families") + lines.append("") + lines.append( + f"These families ({', '.join(sorted(DEFERRED_FAMILIES))}) are " + "deferred until the active temporal + geo surface stabilises. " + "Re-include by editing `DEFERRED_FAMILIES` at the top of " + "`scripts/parity-audit.py`. Listed here so the picture stays " + "complete; not counted in headline coverage." + ) + lines.append("") + lines.append("| Section | Addressable | Covered | Missing | Coverage |") + lines.append("|---|---:|---:|---:|---:|") + for sec, total, cov, miss, pct, _, _, _, _, addressable in deferred_results: + lines.append( + f"| `{sec}` | {addressable} | {cov} | {miss} | {pct:.0f}% |" + ) + lines.append( + f"| **TOTAL (deferred)** | **{d_total}** | **{d_cov}** | " + f"**{d_miss}** | **{d_pct:.0f}%** |" + ) + lines.append("") + + with open(out_path, "w") as f: + f.write("\n".join(lines) + "\n") + + return a_total, a_cov, a_pct + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--mdb", default="../MobilityDB", + help="Path to MobilityDB checkout (default ../MobilityDB)") + ap.add_argument("--mspark", default=".", + help="Path to MobilitySpark checkout (default .)") + ap.add_argument("--out", default="docs/parity-status.md", + help="Output path (default docs/parity-status.md)") + args = ap.parse_args() + + mdb_section_funcs, mdb_section_op_count, all_mdb_funcs = collect_mobilitydb(args.mdb) + mspark_funcs, _files = collect_mobilityspark(args.mspark) + + a_total, a_cov, a_pct = write_report( + args.out, mdb_section_funcs, mdb_section_op_count, + all_mdb_funcs, mspark_funcs, + ) + print(f"Wrote {args.out}") + print(f"Active addressable coverage: {a_cov}/{a_total} ({a_pct:.1f}%)") + + +if __name__ == "__main__": + main() diff --git a/scripts/portable_parity.py b/scripts/portable_parity.py new file mode 100644 index 00000000..ed27c0cb --- /dev/null +++ b/scripts/portable_parity.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +"""Portable bare-name parity gate for MobilitySpark. + +The MobilitySpark analogue of MobilityDB/MEOS-API `portable_parity.py` +(and of MobilityDB `tools/portable_aliases/generate.py --check`). Same +prefix logic, applied to the set of Spark-SQL UDF names MobilitySpark +registers. + +Single source of truth: the 29 operator -> bareName pairs in +`meta/portable-aliases.json` (vendored read-only from +MobilityDB/MEOS-API PR #8 / RFC #920). For every canonical bare name a +registered UDF *backs* it iff some registered name equals the bare name +or starts with `bareName + "_"`; failing that, the verified +`explicitBacking` C-family prefixes are tried (e.g. +`nearestApproachDistance` -> `nad`). A bare name with no match is +`needs-explicit-backing` (unbacked) β€” the precise worklist, never a +fabricated verdict. + +Done = 0 unbacked = 29/29 = 100%, across all six type families +(temporal, geo, cbuffer, npoint, pose, rgeo) β€” none excluded. + + python3 scripts/portable_parity.py # gate (exit 1 if unbacked) + python3 scripts/portable_parity.py --out FILE # also write JSON report + +Usage: + python3 scripts/portable_parity.py \\ + --mspark . --contract meta/portable-aliases.json +""" + +import argparse +import glob +import json +import os +import re +import sys + + +REGISTER_RE = re.compile(r'\.udf\(\)\.register\(\s*"([A-Za-z0-9_]+)"') + + +def registered_udf_names(mspark_root): + """Every name passed to spark.udf().register("name", ...) in src/main.""" + names = set() + pat = os.path.join(mspark_root, "src", "main", "java", "**", "*.java") + for path in glob.glob(pat, recursive=True): + with open(path, encoding="utf-8") as fh: + names.update(REGISTER_RE.findall(fh.read())) + return names + + +def build_parity(contract, names): + fam_of = {p["bareName"]: (fam, p["operator"]) + for fam, lst in contract["families"].items() for p in lst} + explicit = contract.get("explicitBacking", {}) + + def matches(prefix): + return sorted(n for n in names + if n == prefix or n.startswith(prefix + "_")) + + by_bare = {} + for bare, (fam, op) in sorted(fam_of.items()): + hits, via = matches(bare), "prefix" + if not hits: + for pref in explicit.get(bare, []): + hits += matches(pref) + via = "explicit" if hits else None + by_bare[bare] = { + "operator": op, "family": fam, "via": via, + "backedBy": len(hits), "sample": hits[:3], + "status": "backed" if hits else "needs-explicit-backing", + } + backed = [b for b, v in by_bare.items() if v["status"] == "backed"] + unbacked = sorted(b for b, v in by_bare.items() + if v["status"] == "needs-explicit-backing") + total = len(by_bare) + return { + "total": total, + "backed": len(backed), + "needsExplicitBacking": len(unbacked), + "parityPct": round(len(backed) * 100 / total, 1) if total else 0, + "unbacked": unbacked, + "byBareName": by_bare, + } + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--mspark", default=".", + help="MobilitySpark repo root (default: .)") + ap.add_argument("--contract", default="meta/portable-aliases.json", + help="path to vendored portable-aliases.json") + ap.add_argument("--out", default=None, + help="optional JSON report path") + args = ap.parse_args() + + contract_path = (args.contract if os.path.isabs(args.contract) + else os.path.join(args.mspark, args.contract)) + with open(contract_path, encoding="utf-8") as fh: + contract = json.load(fh) + + names = registered_udf_names(args.mspark) + rep = build_parity(contract, names) + + if args.out: + os.makedirs(os.path.dirname(os.path.abspath(args.out)), exist_ok=True) + with open(args.out, "w", encoding="utf-8") as fh: + json.dump(rep, fh, indent=2) + + print(f"[portable-parity] {rep['backed']}/{rep['total']} canonical bare " + f"names backed by a registered UDF ({rep['parityPct']}%); " + f"{rep['needsExplicitBacking']} unbacked", file=sys.stderr) + for b in rep["unbacked"]: + v = rep["byBareName"][b] + print(f" UNBACKED: {b!r} ({v['operator']}, {v['family']})", + file=sys.stderr) + + if rep["needsExplicitBacking"]: + print("CHECK: FAIL β€” portable bare-name parity < 100%", + file=sys.stderr) + return 1 + print(f"CHECK: PASS β€” {rep['total']}/{rep['total']} bare names backed, " + "0 unbacked, all six families", file=sys.stderr) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/main/java/org/mobilitydb/spark/MeosMemory.java b/src/main/java/org/mobilitydb/spark/MeosMemory.java new file mode 100644 index 00000000..ef62f8c5 --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/MeosMemory.java @@ -0,0 +1,84 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby retained provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS + * DOCUMENTATION, EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS + * ON AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS + * TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark; + +import jnr.ffi.Pointer; +import sun.misc.Unsafe; +import java.lang.reflect.Field; + +/** + * Native memory management for MEOS objects returned by JNR-FFI calls. + * + * MEOS standalone mode allocates temporal objects with the system malloc + * (palloc/pfree map to malloc/free when not running inside PostgreSQL). + * JNR-FFI Pointer values returned from MEOS functions are raw native + * addresses β€” they are NOT tracked by the Java GC. Callers must free + * each Pointer explicitly after use, otherwise the native heap grows + * without bound (one leaked Temporal* per UDF call Γ— millions of rows + * in cross-join queries like Q2/Q4/Q5/Q6). + * + * Implementation uses sun.misc.Unsafe.freeMemory() which calls the system + * free() underneath β€” safe for MEOS pointers since MEOS standalone mode + * uses the system allocator. This avoids JNR-FFI classloader boundary + * issues that arise when loading libc via LibraryLoader inside Spark. + * + * Usage: + *
+ *   Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(hex);
+ *   try {
+ *       // ... use tptr ...
+ *   } finally {
+ *       MeosMemory.free(tptr);
+ *   }
+ * 
+ */ +public final class MeosMemory { + + private static final Unsafe UNSAFE; + static { + try { + Field f = Unsafe.class.getDeclaredField("theUnsafe"); + f.setAccessible(true); + UNSAFE = (Unsafe) f.get(null); + } catch (ReflectiveOperationException e) { + throw new ExceptionInInitializerError(e); + } + } + + private MeosMemory() {} + + /** Free a native pointer allocated by MEOS. Null-safe. */ + public static void free(Pointer ptr) { + if (ptr != null) UNSAFE.freeMemory(ptr.address()); + } + + /** Free multiple native pointers in one call. Null-safe. */ + public static void free(Pointer... ptrs) { + for (Pointer p : ptrs) { + if (p != null) UNSAFE.freeMemory(p.address()); + } + } +} diff --git a/src/main/java/org/mobilitydb/spark/MeosNative.java b/src/main/java/org/mobilitydb/spark/MeosNative.java new file mode 100644 index 00000000..282a69b9 --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/MeosNative.java @@ -0,0 +1,444 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark; + +import jnr.ffi.LibraryLoader; +import jnr.ffi.Pointer; + +/** + * Supplementary JNR-FFI interface for libmeos symbols not yet declared in + * JMEOS-1.4. JMEOS-1.4 was generated from an older API snapshot and still + * uses {@code _tpoint_} naming for functions that MEOS 1.4 has renamed to + * {@code _tspatial_} or {@code _tgeo_}. + * + * Loading the same "meos" library twice is safe: JNR-FFI caches native + * library handles by name so the OS shared-library is opened only once. + */ +public final class MeosNative { + + private MeosNative() {} + + public interface Lib { + + // ---------------------------------------------------------------- + // Nearest approach distance (NAD) β€” returns double, DBL_MAX on fail + // ---------------------------------------------------------------- + + double nad_tgeo_geo(Pointer temporal, Pointer geo); + double nad_tgeo_stbox(Pointer temporal, Pointer stbox); + double nad_tgeo_tgeo(Pointer temporal1, Pointer temporal2); + + // ---------------------------------------------------------------- + // Nearest approach instant (NAI) β€” returns TInstant * + // ---------------------------------------------------------------- + + Pointer nai_tgeo_geo(Pointer temporal, Pointer geo); + Pointer nai_tgeo_tgeo(Pointer temporal1, Pointer temporal2); + + // ---------------------------------------------------------------- + // Shortest line β€” returns GSERIALIZED * + // ---------------------------------------------------------------- + + Pointer shortestline_tgeo_geo(Pointer temporal, Pointer geo); + + // ---------------------------------------------------------------- + // Scalar value-to-bin (renamed from float_bucket / int_bucket) + // ---------------------------------------------------------------- + + double float_get_bin(double value, double size, double origin); + int int_get_bin(int value, int size, int origin); + + // ---------------------------------------------------------------- + // TBox expand (renamed from tbox_expand_float / tbox_expand_int) + // ---------------------------------------------------------------- + + Pointer tfloatbox_expand(Pointer tbox, double value); + Pointer tintbox_expand(Pointer tbox, int value); + + // ---------------------------------------------------------------- + // tgeometry / tgeography MFJSON constructors (not in JMEOS-1.4) + // ---------------------------------------------------------------- + + Pointer tgeometry_from_mfjson(String mfjson); + Pointer tgeography_from_mfjson(String mfjson); + + // ---------------------------------------------------------------- + // tgeometry / tgeography text constructors (not in JMEOS-1.4) + // ---------------------------------------------------------------- + + Pointer tgeometry_in(String wkt); + Pointer tgeography_in(String wkt); + + // ---------------------------------------------------------------- + // Temporal accessor (not in JMEOS-1.4) + // ---------------------------------------------------------------- + + int temporal_mem_size(Pointer temporal); + Pointer tgeompoint_to_tgeometry(Pointer p); + Pointer tgeogpoint_to_tgeography(Pointer p); + Pointer tgeometry_to_tgeompoint(Pointer p); + Pointer tgeography_to_tgeogpoint(Pointer p); + Pointer tgeometry_to_tgeography(Pointer p); + Pointer tgeography_to_tgeometry(Pointer p); + + // Time-restriction (TimestampTz = int64 microseconds since J2000) + Pointer temporal_before_timestamptz(Pointer temporal, long pgEpochMicros); + Pointer temporal_after_timestamptz(Pointer temporal, long pgEpochMicros); + + // ttext concatenation (not in JMEOS-1.4) + Pointer textcat_ttext_text(Pointer ttext, Pointer text); + Pointer textcat_text_ttext(Pointer text, Pointer ttext); + Pointer textcat_ttext_ttext(Pointer ttext1, Pointer ttext2); + + // MobilityDB extension introspection + String mobilitydb_version(); + String mobilitydb_full_version(); + + // Typed set element accessors (return bool, fill out-param) + boolean intset_value_n(Pointer set, int n, Pointer result); + boolean bigintset_value_n(Pointer set, int n, Pointer result); + boolean floatset_value_n(Pointer set, int n, Pointer result); + + // Aggregate-as-scalar + double tnumber_avg_value(Pointer temporal); + + // tgeometry/tgeography instant constructor (geometry-typed, timestamp via long) + Pointer tgeoinst_make(Pointer geo, long pgEpochMicros); + + // Array-returning bbox accessors (count via out-param, returned + // pointer is contiguous TBox[]/STBox[] respectively) + Pointer tnumber_tboxes(Pointer temporal, Pointer count); + Pointer tgeo_stboxes(Pointer temporal, Pointer count); + + // Similarity paths (Match[] = pairs of {int i, int j}, 8 bytes each) + Pointer temporal_dyntimewarp_path(Pointer p1, Pointer p2, Pointer count); + Pointer temporal_frechet_path(Pointer p1, Pointer p2, Pointer count); + + // Affine transformation (AFFINE = 12 doubles, 96 bytes) + Pointer tgeo_affine(Pointer temporal, Pointer affine); + + // Span tiling β€” returns Span[] with count via out-param + Pointer intspan_bins(Pointer span, int vsize, int vorigin, Pointer count); + Pointer bigintspan_bins(Pointer span, long vsize, long vorigin, Pointer count); + Pointer floatspan_bins(Pointer span, double vsize, double vorigin, Pointer count); + + // Span expand (returns expanded Span*) + Pointer intspan_expand(Pointer span, int value); + Pointer bigintspan_expand(Pointer span, long value); + Pointer floatspan_expand(Pointer span, double value); + + // tpoint minus geometry, direction (instantaneous bearing in radians) + Pointer tpoint_minus_geom(Pointer temporal, Pointer geo); + boolean tpoint_direction(Pointer temporal, Pointer result); + + // Time-tiling: Span[] of consecutive time bins + Pointer temporal_time_bins(Pointer temporal, Pointer interval, long origin, Pointer count); + Pointer tstzspan_bins(Pointer span, Pointer interval, long origin, Pointer count); + + // Value-tiling: Span[] of consecutive value bins for tnumber + Pointer tint_value_bins(Pointer temporal, int vsize, int vorigin, Pointer count); + Pointer tfloat_value_bins(Pointer temporal, double vsize, double vorigin, Pointer count); + + // STBox quad-split: returns STBox[] of 4 quadrants (or 8 if Z, 16 if T) + Pointer stbox_quad_split(Pointer stbox, Pointer count); + + // Scalar timestamptz_get_bin: TimestampTz value, Interval, TimestampTz origin β†’ TimestampTz + long timestamptz_get_bin(long ts, Pointer interval, long origin); + + // Single-point space tile: STBox of the cell containing point + Pointer stbox_get_space_tile(Pointer point, double xsize, double ysize, double zsize, Pointer sorigin); + Pointer stbox_get_time_tile(long t, Pointer duration, long torigin); + Pointer stbox_get_space_time_tile(Pointer point, long t, double xsize, double ysize, double zsize, Pointer duration, Pointer sorigin, long torigin); + + // Space + space-time bounding boxes (multi-tile, contiguous STBox[]) + Pointer tgeo_space_boxes(Pointer temporal, double xsize, double ysize, double zsize, Pointer sorigin, boolean bitmatrix, boolean border_inc, Pointer count); + Pointer tgeo_space_time_boxes(Pointer temporal, double xsize, double ysize, double zsize, Pointer duration, Pointer sorigin, long torigin, boolean bitmatrix, boolean border_inc, Pointer count); + + // tnumber value-time boxes (Datum vsize/vorigin passed as long) + Pointer tnumber_value_time_boxes(Pointer temporal, long vsize, Pointer duration, long vorigin, long torigin, Pointer count); + + // Splits β€” return Temporal** (array of pointers) + various out-bin arrays + Pointer temporal_time_split(Pointer temporal, Pointer duration, long torigin, Pointer time_bins_out, Pointer count); + Pointer tgeo_space_split(Pointer temporal, double xsize, double ysize, double zsize, Pointer sorigin, boolean bitmatrix, boolean border_inc, Pointer space_bins_out, Pointer count); + Pointer tgeo_space_time_split(Pointer temporal, double xsize, double ysize, double zsize, Pointer duration, Pointer sorigin, long torigin, boolean bitmatrix, boolean border_inc, Pointer space_bins_out, Pointer time_bins_out, Pointer count); + + // valueSet support: temporal_values_p returns Datum*, set_make_free + // packs them into a typed Set; temptype_basetype maps temporal type + // (T_TINT/T_TFLOAT/etc.) to its base value type (T_INT4/T_FLOAT8/etc.). + Pointer temporal_values_p(Pointer temporal, Pointer count); + int temptype_basetype(int temptype); + Pointer set_make_free(Pointer values, int count, int basetype, boolean order); + + // segmentMin/MaxDuration β€” temporal_segm_duration with atleast flag + Pointer temporal_segm_duration(Pointer temporal, Pointer duration, boolean atleast, boolean strict); + + // STBox β†’ BOX3D / GBOX + text serialization (PostGIS embedded in MEOS) + Pointer stbox_to_box3d(Pointer stbox); + String box3d_out(Pointer box3d, int maxdd); + Pointer stbox_to_gbox(Pointer stbox); + String gbox_out(Pointer gbox, int maxdd); + + // Tile-set generators (return arrays of bounding boxes) + Pointer stbox_space_tiles(Pointer bounds, double xsize, double ysize, double zsize, Pointer sorigin, boolean border_inc, Pointer count); + Pointer stbox_time_tiles(Pointer bounds, Pointer duration, long torigin, boolean border_inc, Pointer count); + Pointer stbox_space_time_tiles(Pointer bounds, double xsize, double ysize, double zsize, Pointer duration, Pointer sorigin, long torigin, boolean border_inc, Pointer count); + Pointer tintbox_time_tiles(Pointer box, Pointer duration, long torigin, Pointer count); + Pointer tfloatbox_time_tiles(Pointer box, Pointer duration, long torigin, Pointer count); + Pointer tintbox_value_time_tiles(Pointer box, int xsize, Pointer duration, int xorigin, long torigin, Pointer count); + Pointer tfloatbox_value_time_tiles(Pointer box, double vsize, Pointer duration, double vorigin, long torigin, Pointer count); + + // tpoint β†’ array of simple sub-tpoints (no self-intersections) + Pointer tpoint_make_simple(Pointer temporal, Pointer count); + + // Type-converters for timeBoxes intermediate step + Pointer tnumber_to_tbox(Pointer temporal); + + // Value-only tile generators (TBox[]) + Pointer tintbox_value_tiles(Pointer box, int xsize, int xorigin, Pointer count); + Pointer tfloatbox_value_tiles(Pointer box, double vsize, double vorigin, Pointer count); + + // Value/value-time splits returning Temporal** + bin out-arrays (Datum vsize/vorigin) + Pointer tnumber_value_split(Pointer temporal, long vsize, long vorigin, Pointer bins_out, Pointer count); + Pointer tnumber_value_time_split(Pointer temporal, long size, Pointer duration, long vorigin, long torigin, Pointer value_bins_out, Pointer time_bins_out, Pointer count); + + // Single-tile lookup: takes Datum value/origin + MeosType basetype/spantype + Pointer tbox_get_value_time_tile(long value, long t, long vsize, Pointer duration, long vorigin, long torigin, int basetype, int spantype); + + // tpoint analytics + boolean tpoint_tfloat_to_geomeas(Pointer tpoint, Pointer measure, boolean segmentize, Pointer result_out); + boolean tpoint_as_mvtgeom(Pointer temporal, Pointer bounds, int extent, int buffer, boolean clip_geom, Pointer gsarr_out, Pointer timesarr_out, Pointer count_out); + + // Split-by-N functions (count via out-param, returned pointer is contiguous array) + Pointer temporal_split_n_spans(Pointer temporal, int n, Pointer count); + Pointer temporal_split_each_n_spans(Pointer temporal, int n, Pointer count); + Pointer tnumber_split_n_tboxes(Pointer temporal, int n, Pointer count); + Pointer tnumber_split_each_n_tboxes(Pointer temporal, int n, Pointer count); + Pointer tgeo_split_n_stboxes(Pointer temporal, int n, Pointer count); + Pointer tgeo_split_each_n_stboxes(Pointer temporal, int n, Pointer count); + + // ---------------------------------------------------------------- + // Cross-type: STBox Γ— TSpatial (spatial direction) + // ---------------------------------------------------------------- + + boolean left_stbox_tspatial(Pointer stbox, Pointer tspatial); + boolean right_stbox_tspatial(Pointer stbox, Pointer tspatial); + boolean overleft_stbox_tspatial(Pointer stbox, Pointer tspatial); + boolean overright_stbox_tspatial(Pointer stbox, Pointer tspatial); + boolean above_stbox_tspatial(Pointer stbox, Pointer tspatial); + boolean below_stbox_tspatial(Pointer stbox, Pointer tspatial); + boolean overabove_stbox_tspatial(Pointer stbox, Pointer tspatial); + boolean overbelow_stbox_tspatial(Pointer stbox, Pointer tspatial); + boolean front_stbox_tspatial(Pointer stbox, Pointer tspatial); + boolean back_stbox_tspatial(Pointer stbox, Pointer tspatial); + boolean overfront_stbox_tspatial(Pointer stbox, Pointer tspatial); + boolean overback_stbox_tspatial(Pointer stbox, Pointer tspatial); + + // ---------------------------------------------------------------- + // Cross-type: STBox Γ— TSpatial (temporal direction) + // ---------------------------------------------------------------- + + boolean before_stbox_tspatial(Pointer stbox, Pointer tspatial); + boolean after_stbox_tspatial(Pointer stbox, Pointer tspatial); + boolean overbefore_stbox_tspatial(Pointer stbox, Pointer tspatial); + boolean overafter_stbox_tspatial(Pointer stbox, Pointer tspatial); + + // ---------------------------------------------------------------- + // Cross-type: STBox Γ— TSpatial (topological) + // ---------------------------------------------------------------- + + boolean adjacent_stbox_tspatial(Pointer stbox, Pointer tspatial); + boolean contains_stbox_tspatial(Pointer stbox, Pointer tspatial); + boolean contained_stbox_tspatial(Pointer stbox, Pointer tspatial); + boolean overlaps_stbox_tspatial(Pointer stbox, Pointer tspatial); + boolean same_stbox_tspatial(Pointer stbox, Pointer tspatial); + + // ---------------------------------------------------------------- + // Cross-type: TSpatial Γ— STBox (spatial direction) + // ---------------------------------------------------------------- + + boolean left_tspatial_stbox(Pointer tspatial, Pointer stbox); + boolean right_tspatial_stbox(Pointer tspatial, Pointer stbox); + boolean overleft_tspatial_stbox(Pointer tspatial, Pointer stbox); + boolean overright_tspatial_stbox(Pointer tspatial, Pointer stbox); + boolean above_tspatial_stbox(Pointer tspatial, Pointer stbox); + boolean below_tspatial_stbox(Pointer tspatial, Pointer stbox); + boolean overabove_tspatial_stbox(Pointer tspatial, Pointer stbox); + boolean overbelow_tspatial_stbox(Pointer tspatial, Pointer stbox); + boolean front_tspatial_stbox(Pointer tspatial, Pointer stbox); + boolean back_tspatial_stbox(Pointer tspatial, Pointer stbox); + boolean overfront_tspatial_stbox(Pointer tspatial, Pointer stbox); + boolean overback_tspatial_stbox(Pointer tspatial, Pointer stbox); + + // ---------------------------------------------------------------- + // Cross-type: TSpatial Γ— STBox (temporal direction) + // ---------------------------------------------------------------- + + boolean before_tspatial_stbox(Pointer tspatial, Pointer stbox); + boolean after_tspatial_stbox(Pointer tspatial, Pointer stbox); + boolean overbefore_tspatial_stbox(Pointer tspatial, Pointer stbox); + boolean overafter_tspatial_stbox(Pointer tspatial, Pointer stbox); + + // ---------------------------------------------------------------- + // Cross-type: TSpatial Γ— STBox (topological) + // ---------------------------------------------------------------- + + boolean adjacent_tspatial_stbox(Pointer tspatial, Pointer stbox); + boolean contains_tspatial_stbox(Pointer tspatial, Pointer stbox); + boolean contained_tspatial_stbox(Pointer tspatial, Pointer stbox); + boolean overlaps_tspatial_stbox(Pointer tspatial, Pointer stbox); + boolean same_tspatial_stbox(Pointer tspatial, Pointer stbox); + + // ---------------------------------------------------------------- + // Exact spatial-minimum distance (MobilityDB PR #1007) β€” returns + // double, DBL_MAX on failure. Threshold-aware: caller passes the + // running min (DBL_MAX for an unbounded first call) and the + // kernel short-circuits any pair whose lower bound already meets + // or exceeds it. Distinct from `nad_tgeo_tgeo` which is the + // time-synchronous (NAD) variant. + // ---------------------------------------------------------------- + + double mindistance_tgeo_tgeo(Pointer temporal1, Pointer temporal2, + double threshold); + + // Array form: minimum spatial distance over all pairs from two + // temporal-geo arrays. Pairs are visited in bbox-distance order + // and iteration short-circuits once the running min provably + // dominates every remaining pair's lower bound. + double tgeoarr_tgeoarr_mindist(Pointer arr1, int count1, + Pointer arr2, int count2); + + // ================================================================ + // cbuffer family (meos_cbuffer.h) β€” not in JMEOS-1.4. + // All symbols verified present in lib/libmeos.so via `nm -D`. + // ================================================================ + + // Base type: ctor, hex-WKB I/O, accessors + Pointer cbuffer_make(Pointer point, double radius); // Cbuffer* + Pointer cbuffer_from_hexwkb(String hexwkb); // Cbuffer* + String cbuffer_as_hexwkb(Pointer cb, byte variant, Pointer size); + Pointer cbuffer_in(String str); // Cbuffer* + Pointer cbuffer_point(Pointer cb); // GSERIALIZED* + double cbuffer_radius(Pointer cb); + // Spatial relationships (return int: -1 err, 0 false, 1 true) + int cbuffer_contains(Pointer cb1, Pointer cb2); + int cbuffer_covers(Pointer cb1, Pointer cb2); + int cbuffer_disjoint(Pointer cb1, Pointer cb2); + int cbuffer_intersects(Pointer cb1, Pointer cb2); + int cbuffer_touches(Pointer cb1, Pointer cb2); + int cbuffer_dwithin(Pointer cb1, Pointer cb2, double dist); + boolean cbuffer_same(Pointer cb1, Pointer cb2); + // Temporal cbuffer: ctor, text I/O, accessors + Pointer tcbuffer_make(Pointer tpoint, Pointer tfloat); // Temporal* + Pointer tcbuffer_in(String str); // Temporal* + Pointer tcbuffer_points(Pointer temp); // Set* + Pointer tcbuffer_radius(Pointer temp); // Set* + + // ================================================================ + // npoint family (meos_npoint.h, npoint/tnpoint_routeops.h) + // β€” not in JMEOS-1.4. All verified present via `nm -D`. + // ================================================================ + + // Base type network point / segment + Pointer npoint_make(long rid, double pos); // Npoint* + Pointer nsegment_make(long rid, double pos1, double pos2); // Nsegment* + Pointer npoint_from_hexwkb(String hexwkb); // Npoint* + String npoint_as_hexwkb(Pointer np, byte variant, Pointer size); + Pointer npoint_in(String str); // Npoint* + Pointer nsegment_in(String str); // Nsegment* + String nsegment_out(Pointer ns, int maxdd); + long npoint_route(Pointer np); + double npoint_position(Pointer np); + double nsegment_start_position(Pointer ns); + double nsegment_end_position(Pointer ns); + Pointer npointset_in(String str); // Set* + Pointer npointset_routes(Pointer s); // Set* + // Temporal network point + Pointer tnpointinst_make(Pointer np, long pgEpochMicros); // TInstant* + long tnpoint_route(Pointer temp); + Pointer tnpoint_routes(Pointer temp); // Set* + Pointer tnpoint_positions(Pointer temp, Pointer count); // Nsegment** + // Route-set operators (091). `invert` flag = false (temp first). + boolean contains_rid_tnpoint_bigint(Pointer temp, long rid, boolean invert); + boolean contained_rid_tnpoint_bigint(Pointer temp, long rid, boolean invert); + boolean same_rid_tnpoint_bigint(Pointer temp, long rid, boolean invert); + boolean contains_rid_tnpoint_bigintset(Pointer temp, Pointer s, boolean invert); + boolean contained_rid_tnpoint_bigintset(Pointer temp, Pointer s, boolean invert); + boolean same_rid_tnpoint_bigintset(Pointer temp, Pointer s, boolean invert); + boolean overlaps_rid_tnpoint_bigintset(Pointer temp, Pointer s, boolean invert); + boolean contains_rid_tnpoint_tnpoint(Pointer temp1, Pointer temp2); + boolean contained_rid_tnpoint_tnpoint(Pointer temp1, Pointer temp2); + boolean same_rid_tnpoint_tnpoint(Pointer temp1, Pointer temp2); + boolean overlaps_rid_tnpoint_tnpoint(Pointer temp1, Pointer temp2); + + // ================================================================ + // pose family (meos_pose.h) β€” not in JMEOS-1.4. + // All verified present via `nm -D`. + // ================================================================ + + Pointer pose_make_2d(double x, double y, double theta, int srid); // Pose* + Pointer pose_from_hexwkb(String hexwkb); // Pose* + String pose_as_hexwkb(Pointer pose, byte variant, Pointer size); + Pointer pose_in(String str); // Pose* + Pointer pose_to_point(Pointer pose); // GSERIALIZED* + double pose_rotation(Pointer pose); + Pointer pose_orientation(Pointer pose); // double* (quat) + boolean pose_same(Pointer pose1, Pointer pose2); + Pointer poseset_in(String str); // Set* + Pointer tpose_make(Pointer tpoint, Pointer tradius); // Temporal* + Pointer tpose_in(String str); // Temporal* + Pointer tpose_from_mfjson(String mfjson); // Temporal* + Pointer tpose_points(Pointer temp); // Set* + Pointer tpose_rotation(Pointer temp); // Temporal* + + // ================================================================ + // rgeo family (meos_rgeo.h) β€” not in JMEOS-1.4. + // All verified present via `nm -D`. + // + // NOTE: the v_clip_* kernels of 133_trgeo_vclip.in.sql are + // intentionally NOT declared here. libmeos.so exports only the + // low-level v_clip_tpoly_point / v_clip_tpoly_tpoly, which take + // raw PostGIS LWPOLY*/LWPOINT*/Pose* + out-params (no GSERIALIZED/ + // Temporal entry point reachable from the hex-WKB convention); the + // other four are not exported at all. Documented ABI gap, not + // stubbed. + // ================================================================ + + Pointer geo_tpose_to_trgeo(Pointer gs, Pointer temp); // Temporal* + Pointer trgeo_to_tpose(Pointer temp); // Temporal* + } + + public static final Lib INSTANCE; + static { + LibraryLoader loader = LibraryLoader.create(Lib.class); + String libPath = System.getProperty("java.library.path"); + if (libPath != null) { + for (String p : libPath.split(":")) { + if (!p.isEmpty()) loader.search(p); + } + } + INSTANCE = loader.load("meos"); + } +} diff --git a/src/main/java/org/mobilitydb/spark/MeosThread.java b/src/main/java/org/mobilitydb/spark/MeosThread.java new file mode 100644 index 00000000..35790413 --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/MeosThread.java @@ -0,0 +1,95 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark; + +import functions.GeneratedFunctions; +import functions.error_handler_fn; +import org.apache.spark.sql.api.java.*; + +/** + * Per-thread MEOS initialisation for Spark executor threads. + * + * In Spark's multi-threaded executor model every task thread initialises + * MEOS independently. meos_initialize() sets up the per-thread MEOS state + * (session_timezone, timezone cache, GEOS context, PROJ context, GSL RNGs, + * errno). The ThreadLocal in MEOS_READY runs initialisation exactly once + * per native thread. + * + * Usage β€” two patterns: + * + * 1. Wrap lambdas at registration time (preferred β€” no boilerplate in the + * lambda body and impossible to forget): + * spark.udf().register("foo", MeosThread.wrap((String s) -> ...), Type); + * + * 2. Call ensureReady() explicitly at the top of a lambda where wrapping is + * not convenient. + */ +public final class MeosThread { + + private MeosThread() {} + + /** + * No-exit MEOS error handler. MEOS's default handler calls + * exit(EXIT_FAILURE) on an ERROR, which would tear down the whole JVM if a + * MEOS error fired inside a Spark task. This handler returns instead of + * exiting; the error still surfaces because MEOS sets meos_errno, which the + * generated wrappers check (MeosErrorHandler.checkError) and rethrow as a + * Java exception. Held as a static field so JNR keeps the native callback + * alive for the process lifetime. + */ + public static final error_handler_fn NOEXIT_ERROR_HANDLER = + (errorLevel, errorCode, errorMessage) -> { /* do not exit the JVM */ }; + + private static final ThreadLocal MEOS_READY = ThreadLocal.withInitial(() -> { + GeneratedFunctions.meos_initialize(); + GeneratedFunctions.meos_initialize_timezone("UTC"); + GeneratedFunctions.meos_initialize_error_handler(NOEXIT_ERROR_HANDLER); + return Boolean.TRUE; + }); + + /** Ensure MEOS is initialised for the calling thread. */ + public static void ensureReady() { + MEOS_READY.get(); + } + + // ------------------------------------------------------------------ + // UDF wrappers β€” call ensureReady() before delegating to the lambda. + // Use these in registerAll() instead of scattering ensureReady() calls + // inside every individual UDF method body. + // ------------------------------------------------------------------ + + public static UDF1 wrap(UDF1 udf) { + return s -> { ensureReady(); return udf.call(s); }; + } + + public static UDF2 wrap(UDF2 udf) { + return (s, a) -> { ensureReady(); return udf.call(s, a); }; + } + + public static UDF3 wrap(UDF3 udf) { + return (s, a, b) -> { ensureReady(); return udf.call(s, a, b); }; + } +} diff --git a/src/main/java/org/mobilitydb/spark/MobilitySparkSession.java b/src/main/java/org/mobilitydb/spark/MobilitySparkSession.java new file mode 100644 index 00000000..532b4703 --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/MobilitySparkSession.java @@ -0,0 +1,169 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark; + +import org.apache.spark.sql.SparkSession; +import org.mobilitydb.spark.geo.DistanceUDFs; +import org.mobilitydb.spark.geo.GeoAnalyticsUDFs; +import org.mobilitydb.spark.geo.GeoUDFs; +import org.mobilitydb.spark.geo.STBoxUDFs; +import org.mobilitydb.spark.geo.StaticGeoUDFs; +import org.mobilitydb.spark.geo.AlwaysSpatialRelsUDFs; +import org.mobilitydb.spark.geo.GeoAffineUDFs; +import org.mobilitydb.spark.geo.TempSpatialRelsUDFs; +import org.mobilitydb.spark.geo.TPointSTBoxOpsUDFs; +import org.mobilitydb.spark.temporal.AccessorAliasUDFs; +import org.mobilitydb.spark.temporal.AccessorUDFs; +import org.mobilitydb.spark.temporal.TileUDFs; +import org.mobilitydb.spark.temporal.SeqSetGapsUDFs; +import org.mobilitydb.spark.temporal.AnalyticsUDFs; +import org.mobilitydb.spark.temporal.BoolOpsUDFs; +import org.mobilitydb.spark.temporal.BucketUDFs; +import org.mobilitydb.spark.temporal.ConstructorUDFs; +import org.mobilitydb.spark.temporal.MathUDFs; +import org.mobilitydb.spark.temporal.PosOpsUDFs; +import org.mobilitydb.spark.temporal.PredicateUDFs; +import org.mobilitydb.spark.temporal.SimilarityUDFs; +import org.mobilitydb.spark.temporal.SpanAccessorUDFs; +import org.mobilitydb.spark.temporal.SpanAlgebraUDFs; +import org.mobilitydb.spark.temporal.SpanUDFs; +import org.mobilitydb.spark.temporal.IOAliasUDFs; +import org.mobilitydb.spark.temporal.SpansetOpsUDFs; +import org.mobilitydb.spark.temporal.SetOpsUDFs; +import org.mobilitydb.spark.temporal.SubtypeConstructorUDFs; +import org.mobilitydb.spark.temporal.AggregateUDAFs; +import org.mobilitydb.spark.temporal.MoreAccessorUDFs; +import org.mobilitydb.spark.temporal.RestrictionUDFs; +import org.mobilitydb.spark.temporal.TBoxOpsUDFs; +import org.mobilitydb.spark.temporal.TBoxUDFs; +import org.mobilitydb.spark.temporal.TemporalBoxOpsUDFs; +import org.mobilitydb.spark.temporal.TemporalCompUDFs; +import org.mobilitydb.spark.temporal.TTextUDFs; +import org.mobilitydb.spark.temporal.TemporalUDFs; +import org.mobilitydb.spark.temporal.TransformUDFs; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.concurrent.atomic.AtomicBoolean; + +import static functions.GeneratedFunctions.*; + +/** + * Entry point for MobilitySpark. + * + * Initialises MEOS and registers all UDFs with the given SparkSession. + * Call {@link #create(SparkSession)} before running any temporal SQL, + * and {@link #close()} after (or use try-with-resources). + * + *
{@code
+ *   SparkSession spark = SparkSession.builder().master("local[2]").getOrCreate();
+ *   try (MobilitySparkSession ms = MobilitySparkSession.create(spark)) {
+ *       spark.sql("SELECT atTime(trip, instant) FROM trips").show();
+ *   }
+ * }
+ */ +public final class MobilitySparkSession implements AutoCloseable { + + private static final AtomicBoolean SRS_CSV_REGISTERED = new AtomicBoolean(false); + + private MobilitySparkSession() {} + + public static MobilitySparkSession create(SparkSession spark) { + meos_initialize(); + meos_initialize_timezone("UTC"); + meos_initialize_error_handler(MeosThread.NOEXIT_ERROR_HANDLER); + registerSpatialRefSys(); + TemporalUDFs.registerAll(spark); + SpanUDFs.registerAll(spark); + GeoUDFs.registerAll(spark); + GeoAnalyticsUDFs.registerAll(spark); + StaticGeoUDFs.registerAll(spark); + DistanceUDFs.registerAll(spark); + ConstructorUDFs.registerAll(spark); + AccessorUDFs.registerAll(spark); + SpanAccessorUDFs.registerAll(spark); + SpanAlgebraUDFs.registerAll(spark); + SpansetOpsUDFs.registerAll(spark); + IOAliasUDFs.registerAll(spark); + SubtypeConstructorUDFs.registerAll(spark); + AccessorAliasUDFs.registerAll(spark); + TileUDFs.registerAll(spark); + SeqSetGapsUDFs.registerAll(spark); + SetOpsUDFs.registerAll(spark); + AnalyticsUDFs.registerAll(spark); + PredicateUDFs.registerAll(spark); + TBoxUDFs.registerAll(spark); + TBoxOpsUDFs.registerAll(spark); + TemporalCompUDFs.registerAll(spark); + TemporalBoxOpsUDFs.registerAll(spark); + TTextUDFs.registerAll(spark); + STBoxUDFs.registerAll(spark); + PosOpsUDFs.registerAll(spark); + MathUDFs.registerAll(spark); + BoolOpsUDFs.registerAll(spark); + BucketUDFs.registerAll(spark); + SimilarityUDFs.registerAll(spark); + TempSpatialRelsUDFs.registerAll(spark); + AlwaysSpatialRelsUDFs.registerAll(spark); + GeoAffineUDFs.registerAll(spark); + TPointSTBoxOpsUDFs.registerAll(spark); + MoreAccessorUDFs.registerAll(spark); + RestrictionUDFs.registerAll(spark); + TransformUDFs.registerAll(spark); + AggregateUDAFs.registerAll(spark); + org.mobilitydb.spark.h3.Th3IndexUDFs.registerAll(spark); + return new MobilitySparkSession(); + } + + /** + * Extracts the bundled spatial_ref_sys.csv to a temp file and registers + * it with MEOS so that geodetic coordinate operations (e.g. length on + * tgeogpoint) can look up SRID definitions without a PostGIS installation. + * Only runs once per JVM; subsequent calls are no-ops. + */ + private static void registerSpatialRefSys() { + if (!SRS_CSV_REGISTERED.compareAndSet(false, true)) return; + try (InputStream in = MobilitySparkSession.class + .getResourceAsStream("/spatial_ref_sys.csv")) { + if (in == null) return; + File tmp = File.createTempFile("meos_spatial_ref_sys", ".csv"); + tmp.deleteOnExit(); + try (OutputStream out = new FileOutputStream(tmp)) { + byte[] buf = new byte[65536]; + int n; + while ((n = in.read(buf)) > 0) out.write(buf, 0, n); + } + meos_set_spatial_ref_sys_csv(tmp.getAbsolutePath()); + } catch (Exception ignored) {} + } + + @Override + public void close() { + meos_finalize(); + } +} diff --git a/src/main/java/org/mobilitydb/spark/examples/N01HelloWorld.java b/src/main/java/org/mobilitydb/spark/examples/N01HelloWorld.java new file mode 100644 index 00000000..eb4f7d48 --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/examples/N01HelloWorld.java @@ -0,0 +1,70 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.examples; + +import org.apache.spark.sql.SparkSession; +import org.mobilitydb.spark.MobilitySparkSession; + +import static functions.GeneratedFunctions.*; + +/** + * N01 Hello World β€” the minimal MobilitySpark program. + * + * Creates a single tgeompoint value inside Spark SQL, rounds it back to + * text, and prints it. Mirrors meos/examples/01_hello_world.c. + * + * Run with: + * spark-submit --class org.mobilitydb.spark.examples.N01HelloWorld \ + * target/mobilityspark-0.1.0-SNAPSHOT-spark.jar + */ +public final class N01HelloWorld { + + public static void main(String[] args) { + SparkSession spark = SparkSession.builder() + .master("local[2]") + .appName("MobilitySpark N01 Hello World") + .getOrCreate(); + spark.sparkContext().setLogLevel("WARN"); + + try (MobilitySparkSession ms = MobilitySparkSession.create(spark)) { + + // Build a tgeompoint sequence and round-trip through hex-WKB. + String wkt = "[POINT(1 1)@2020-01-01 00:00:00+00, " + + "POINT(2 2)@2020-01-01 01:00:00+00]"; + String hex = temporal_as_hexwkb(tgeompoint_in(wkt), (byte) 0); + + // Register a simple view so we can use Spark SQL. + spark.sql("CREATE OR REPLACE TEMPORARY VIEW trips AS " + + "SELECT '" + hex + "' AS trip"); + + System.out.println("=== Hello World: tgeompoint round-trip ==="); + spark.sql("SELECT trip FROM trips").show(false); + + } finally { + spark.stop(); + } + } +} diff --git a/src/main/java/org/mobilitydb/spark/geo/AlwaysSpatialRelsUDFs.java b/src/main/java/org/mobilitydb/spark/geo/AlwaysSpatialRelsUDFs.java new file mode 100644 index 00000000..da3f2e71 --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/geo/AlwaysSpatialRelsUDFs.java @@ -0,0 +1,151 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.geo; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosThread; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.api.java.UDF2; +import org.apache.spark.sql.api.java.UDF3; +import org.apache.spark.sql.types.DataTypes; + +import java.util.function.BiFunction; + +/** + * Spark SQL UDFs for "always" spatial relationship predicates: do all + * times in the temporal value satisfy the relation? Counterpart to the + * "ever" family in TempSpatialRelsUDFs. + * + * MEOS function authority: meos/include/meos_geo.h + * + * Native functions return int (-1 = error, 0 = false, 1 = true). The + * UDF wrappers convert to Boolean (null on error). + */ +public final class AlwaysSpatialRelsUDFs { + + private AlwaysSpatialRelsUDFs() {} + + @FunctionalInterface + private interface IntBiFn { int apply(Pointer a, Pointer b); } + + @FunctionalInterface + private interface IntTriFn { int apply(Pointer a, Pointer b, double dist); } + + private static Boolean tri(int v) { + return v == -1 ? null : v == 1; + } + + private static UDF2 tgeoGeo(IntBiFn fn) { + return (trip, geomWkt) -> { + if (trip == null || geomWkt == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(trip); if (t == null) return null; + Pointer g = GeneratedFunctions.geo_from_text(geomWkt, 0); + if (g == null) { MeosMemory.free(t); return null; } + try { return tri(fn.apply(t, g)); } + finally { MeosMemory.free(t, g); } + }; + } + + private static UDF2 tgeoTgeo(IntBiFn fn) { + return (trip1, trip2) -> { + if (trip1 == null || trip2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(trip1); if (p1 == null) return null; + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(trip2); + if (p2 == null) { MeosMemory.free(p1); return null; } + try { return tri(fn.apply(p1, p2)); } + finally { MeosMemory.free(p1, p2); } + }; + } + + private static UDF2 geoTgeo(IntBiFn fn) { + return (geomWkt, trip) -> { + if (geomWkt == null || trip == null) return null; + MeosThread.ensureReady(); + Pointer g = GeneratedFunctions.geo_from_text(geomWkt, 0); if (g == null) return null; + Pointer t = GeneratedFunctions.temporal_from_hexwkb(trip); + if (t == null) { MeosMemory.free(g); return null; } + try { return tri(fn.apply(g, t)); } + finally { MeosMemory.free(g, t); } + }; + } + + private static UDF3 tgeoGeoDist(IntTriFn fn) { + return (trip, geomWkt, dist) -> { + if (trip == null || geomWkt == null || dist == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(trip); if (t == null) return null; + Pointer g = GeneratedFunctions.geo_from_text(geomWkt, 0); + if (g == null) { MeosMemory.free(t); return null; } + try { return tri(fn.apply(t, g, dist)); } + finally { MeosMemory.free(t, g); } + }; + } + + private static UDF3 tgeoTgeoDist(IntTriFn fn) { + return (trip1, trip2, dist) -> { + if (trip1 == null || trip2 == null || dist == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(trip1); if (p1 == null) return null; + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(trip2); + if (p2 == null) { MeosMemory.free(p1); return null; } + try { return tri(fn.apply(p1, p2, dist)); } + finally { MeosMemory.free(p1, p2); } + }; + } + + public static final UDF2 aDisjointTgeoGeo = tgeoGeo(GeneratedFunctions::adisjoint_tgeo_geo); + public static final UDF2 aDisjointTgeoTgeo = tgeoTgeo(GeneratedFunctions::adisjoint_tgeo_tgeo); + public static final UDF2 aIntersectsTgeoGeo = tgeoGeo(GeneratedFunctions::aintersects_tgeo_geo); + public static final UDF2 aIntersectsTgeoTgeo = tgeoTgeo(GeneratedFunctions::aintersects_tgeo_tgeo); + public static final UDF2 aTouchesTgeoGeo = tgeoGeo(GeneratedFunctions::atouches_tgeo_geo); + public static final UDF2 aTouchesTgeoTgeo = tgeoTgeo(GeneratedFunctions::atouches_tgeo_tgeo); + public static final UDF2 aContainsTgeoGeo = tgeoGeo(GeneratedFunctions::acontains_tgeo_geo); + public static final UDF2 aContainsTgeoTgeo = tgeoTgeo(GeneratedFunctions::acontains_tgeo_tgeo); + public static final UDF2 aContainsGeoTgeo = geoTgeo(GeneratedFunctions::acontains_geo_tgeo); + public static final UDF2 aCoversTgeoGeo = tgeoGeo(GeneratedFunctions::acovers_tgeo_geo); + + public static final UDF3 aDwithinTgeoGeo = tgeoGeoDist(GeneratedFunctions::adwithin_tgeo_geo); + public static final UDF3 aDwithinTgeoTgeo = tgeoTgeoDist(GeneratedFunctions::adwithin_tgeo_tgeo); + + public static void registerAll(SparkSession spark) { + spark.udf().register("aDisjointTgeoGeo", aDisjointTgeoGeo, DataTypes.BooleanType); + spark.udf().register("aDisjointTgeoTgeo", aDisjointTgeoTgeo, DataTypes.BooleanType); + spark.udf().register("aIntersectsTgeoGeo", aIntersectsTgeoGeo, DataTypes.BooleanType); + spark.udf().register("aIntersectsTgeoTgeo", aIntersectsTgeoTgeo, DataTypes.BooleanType); + spark.udf().register("aTouchesTgeoGeo", aTouchesTgeoGeo, DataTypes.BooleanType); + spark.udf().register("aTouchesTgeoTgeo", aTouchesTgeoTgeo, DataTypes.BooleanType); + spark.udf().register("aContainsTgeoGeo", aContainsTgeoGeo, DataTypes.BooleanType); + spark.udf().register("aContainsTgeoTgeo", aContainsTgeoTgeo, DataTypes.BooleanType); + spark.udf().register("aContainsGeoTgeo", aContainsGeoTgeo, DataTypes.BooleanType); + spark.udf().register("aCoversTgeoGeo", aCoversTgeoGeo, DataTypes.BooleanType); + spark.udf().register("aDwithinTgeoGeo", aDwithinTgeoGeo, DataTypes.BooleanType); + spark.udf().register("aDwithinTgeoTgeo", aDwithinTgeoTgeo, DataTypes.BooleanType); + } +} diff --git a/src/main/java/org/mobilitydb/spark/geo/DistanceUDFs.java b/src/main/java/org/mobilitydb/spark/geo/DistanceUDFs.java new file mode 100644 index 00000000..fd28e7c7 --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/geo/DistanceUDFs.java @@ -0,0 +1,427 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.geo; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosNative; +import org.mobilitydb.spark.MeosThread; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.api.java.UDF2; +import org.apache.spark.sql.types.DataTypes; + +/** + * Spark SQL UDFs for temporal distance operations between tgeo/tnumber types. + * + * All functions return a hex-WKB tfloat (the distance evolving over time). + * Input geometry is accepted as WKT strings. + * + * MEOS function authority: meos/include/meos.h, meos/include/meos_geo.h + */ +public final class DistanceUDFs { + + private DistanceUDFs() {} + + // ------------------------------------------------------------------ + // Spatial distance β€” tgeo Γ— geometry + // ------------------------------------------------------------------ + + // tdistanceTgeoGeo(trip STRING, geomWkt STRING) β†’ STRING (tfloat hex-WKB) + // MEOS: tdistance_tgeo_geo(const Temporal *, const GSERIALIZED *) β†’ Temporal * + public static final UDF2 tdistanceTgeoGeo = + (trip, geomWkt) -> { + if (trip == null || geomWkt == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + Pointer gsptr = GeneratedFunctions.geo_from_text(geomWkt, 0); + if (gsptr == null) { MeosMemory.free(tptr); return null; } + try { + Pointer r = GeneratedFunctions.tdistance_tgeo_geo(tptr, gsptr); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(tptr); + MeosMemory.free(gsptr); + } + }; + + // ------------------------------------------------------------------ + // Spatial distance β€” tgeo Γ— tgeo + // ------------------------------------------------------------------ + + // tdistanceTgeoTgeo(trip1 STRING, trip2 STRING) β†’ STRING (tfloat hex-WKB) + // MEOS: tdistance_tgeo_tgeo(const Temporal *, const Temporal *) β†’ Temporal * + public static final UDF2 tdistanceTgeoTgeo = + (trip1, trip2) -> { + if (trip1 == null || trip2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(trip1); + if (p1 == null) return null; + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(trip2); + if (p2 == null) { MeosMemory.free(p1); return null; } + try { + Pointer r = GeneratedFunctions.tdistance_tgeo_tgeo(p1, p2); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(p1); + MeosMemory.free(p2); + } + }; + + // ------------------------------------------------------------------ + // Number distance β€” tfloat Γ— float + // ------------------------------------------------------------------ + + // tdistanceTfloatFloat(tfloat STRING, d DOUBLE) β†’ STRING (tfloat hex-WKB) + // MEOS: tdistance_tfloat_float(const Temporal *, double) β†’ Temporal * + public static final UDF2 tdistanceTfloatFloat = + (hex, d) -> { + if (hex == null || d == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(hex); + if (ptr == null) return null; + try { + Pointer r = GeneratedFunctions.tdistance_tfloat_float(ptr, d); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // ------------------------------------------------------------------ + // Number distance β€” tint Γ— int + // ------------------------------------------------------------------ + + // tdistanceTintInt(tint STRING, i INT) β†’ STRING (tint hex-WKB) + // MEOS: tdistance_tint_int(const Temporal *, int) β†’ Temporal * + public static final UDF2 tdistanceTintInt = + (hex, i) -> { + if (hex == null || i == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(hex); + if (ptr == null) return null; + try { + Pointer r = GeneratedFunctions.tdistance_tint_int(ptr, i); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // ------------------------------------------------------------------ + // Number distance β€” tnumber Γ— tnumber + // ------------------------------------------------------------------ + + // tdistanceTnumberTnumber(t1 STRING, t2 STRING) β†’ STRING (tfloat hex-WKB) + // MEOS: tdistance_tnumber_tnumber(const Temporal *, const Temporal *) β†’ Temporal * + public static final UDF2 tdistanceTnumberTnumber = + (hex1, hex2) -> { + if (hex1 == null || hex2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(hex1); + if (p1 == null) return null; + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(hex2); + if (p2 == null) { MeosMemory.free(p1); return null; } + try { + Pointer r = GeneratedFunctions.tdistance_tnumber_tnumber(p1, p2); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(p1); + MeosMemory.free(p2); + } + }; + + // ------------------------------------------------------------------ + // Nearest approach distance (NAD) β€” returns Double (null on failure) + // MEOS returns DBL_MAX when the inputs never approach; map to null. + // ------------------------------------------------------------------ + + // nadTgeoGeo(trip STRING, geomWkt STRING) β†’ DOUBLE + // MEOS: nad_tgeo_geo(const Temporal *, const GSERIALIZED *) β†’ double + public static final UDF2 nadTgeoGeo = + (trip, geomWkt) -> { + if (trip == null || geomWkt == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + Pointer gsptr = GeneratedFunctions.geo_from_text(geomWkt, 0); + if (gsptr == null) { MeosMemory.free(tptr); return null; } + try { + double d = MeosNative.INSTANCE.nad_tgeo_geo(tptr, gsptr); + return d == Double.MAX_VALUE ? null : d; + } finally { + MeosMemory.free(tptr); + MeosMemory.free(gsptr); + } + }; + + // nadTgeoStbox(trip STRING, stboxHex STRING) β†’ DOUBLE + // MEOS: nad_tgeo_stbox(const Temporal *, const STBox *) β†’ double + public static final UDF2 nadTgeoStbox = + (trip, stboxHex) -> { + if (trip == null || stboxHex == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + Pointer sptr = GeneratedFunctions.stbox_from_hexwkb(stboxHex); + if (sptr == null) { MeosMemory.free(tptr); return null; } + try { + double d = MeosNative.INSTANCE.nad_tgeo_stbox(tptr, sptr); + return d == Double.MAX_VALUE ? null : d; + } finally { + MeosMemory.free(tptr); + MeosMemory.free(sptr); + } + }; + + // nadTgeoTgeo(trip1 STRING, trip2 STRING) β†’ DOUBLE + // MEOS: nad_tgeo_tgeo(const Temporal *, const Temporal *) β†’ double + public static final UDF2 nadTgeoTgeo = + (trip1, trip2) -> { + if (trip1 == null || trip2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(trip1); + if (p1 == null) return null; + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(trip2); + if (p2 == null) { MeosMemory.free(p1); return null; } + try { + double d = MeosNative.INSTANCE.nad_tgeo_tgeo(p1, p2); + return d == Double.MAX_VALUE ? null : d; + } finally { + MeosMemory.free(p1); + MeosMemory.free(p2); + } + }; + + // ------------------------------------------------------------------ + // Nearest approach instant (NAI) β€” returns hex-WKB TInstant (STRING) + // ------------------------------------------------------------------ + + // naiTgeoGeo(trip STRING, geomWkt STRING) β†’ STRING (TInstant hex-WKB) + // MEOS: nai_tgeo_geo(const Temporal *, const GSERIALIZED *) β†’ TInstant * + public static final UDF2 naiTgeoGeo = + (trip, geomWkt) -> { + if (trip == null || geomWkt == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + Pointer gsptr = GeneratedFunctions.geo_from_text(geomWkt, 0); + if (gsptr == null) { MeosMemory.free(tptr); return null; } + try { + Pointer r = MeosNative.INSTANCE.nai_tgeo_geo(tptr, gsptr); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(tptr); + MeosMemory.free(gsptr); + } + }; + + // ------------------------------------------------------------------ + // Shortest line β€” returns geometry (WKT) of the closest-approach segment + // ------------------------------------------------------------------ + + // shortestLineTgeoGeo(trip STRING, geomWkt STRING) β†’ STRING (WKT geometry) + // MEOS: shortestline_tgeo_geo(const Temporal *, const GSERIALIZED *) β†’ GSERIALIZED * + public static final UDF2 shortestLineTgeoGeo = + (trip, geomWkt) -> { + if (trip == null || geomWkt == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + Pointer gsptr = GeneratedFunctions.geo_from_text(geomWkt, 0); + if (gsptr == null) { MeosMemory.free(tptr); return null; } + try { + Pointer r = MeosNative.INSTANCE.shortestline_tgeo_geo(tptr, gsptr); + if (r == null) return null; + try { + return GeneratedFunctions.geo_as_text(r, 6); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(tptr); + MeosMemory.free(gsptr); + } + }; + + // shortestLineTgeoTgeo(trip1 STRING, trip2 STRING) β†’ STRING (WKT geometry) + // MEOS: shortestline_tgeo_tgeo(const Temporal *, const Temporal *) β†’ GSERIALIZED * + public static final UDF2 shortestLineTgeoTgeo = + (trip1, trip2) -> { + if (trip1 == null || trip2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(trip1); + if (p1 == null) return null; + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(trip2); + if (p2 == null) { MeosMemory.free(p1); return null; } + try { + Pointer r = GeneratedFunctions.shortestline_tgeo_tgeo(p1, p2); + if (r == null) return null; + try { + return GeneratedFunctions.geo_as_text(r, 6); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(p1); + MeosMemory.free(p2); + } + }; + + // naiTgeoTgeo(trip1 STRING, trip2 STRING) β†’ STRING (TInstant hex-WKB) + // MEOS: nai_tgeo_tgeo(const Temporal *, const Temporal *) β†’ TInstant * + public static final UDF2 naiTgeoTgeo = + (trip1, trip2) -> { + if (trip1 == null || trip2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(trip1); + if (p1 == null) return null; + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(trip2); + if (p2 == null) { MeosMemory.free(p1); return null; } + try { + Pointer r = MeosNative.INSTANCE.nai_tgeo_tgeo(p1, p2); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(p1); + MeosMemory.free(p2); + } + }; + + // ------------------------------------------------------------------ + // Minimum spatial distance (MobilityDB PR #1007) + // minDistance({tgeo,geo},{tgeo,geo}) β†’ double + // Per-pair scalar. For the GROUP-BY-over-cross-join shape that the + // canonical Q5 expresses, wrap with the built-in MIN aggregate: + // + // SELECT MIN(minDistance(t1.trip, t2.trip)) FROM ... GROUP BY ... + // + // The (tgeo, geo) overload reuses the NAD kernel β€” NAD reduces to + // spatial-min when one argument has no time dimension. The (tgeo, + // tgeo) overload calls the threshold-aware kernel with DBL_MAX so + // every call computes the exact per-pair minimum; the kernel still + // benefits from the outer STBox lower-bound prune. + // ------------------------------------------------------------------ + + // minDistance(trip STRING, geomWkt STRING) β†’ DOUBLE + public static final UDF2 minDistanceTgeoGeo = + (trip, geomWkt) -> { + if (trip == null || geomWkt == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + Pointer gsptr = GeneratedFunctions.geo_from_text(geomWkt, 0); + if (gsptr == null) { MeosMemory.free(tptr); return null; } + try { + double d = MeosNative.INSTANCE.nad_tgeo_geo(tptr, gsptr); + return d == Double.MAX_VALUE ? null : d; + } finally { + MeosMemory.free(tptr); + MeosMemory.free(gsptr); + } + }; + + // minDistance(trip1 STRING, trip2 STRING) β†’ DOUBLE + public static final UDF2 minDistanceTgeoTgeo = + (trip1, trip2) -> { + if (trip1 == null || trip2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(trip1); + if (p1 == null) return null; + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(trip2); + if (p2 == null) { MeosMemory.free(p1); return null; } + try { + double d = MeosNative.INSTANCE.mindistance_tgeo_tgeo( + p1, p2, Double.MAX_VALUE); + return d == Double.MAX_VALUE ? null : d; + } finally { + MeosMemory.free(p1); + MeosMemory.free(p2); + } + }; + + public static void registerAll(SparkSession spark) { + spark.udf().register("tdistanceTgeoGeo", tdistanceTgeoGeo, DataTypes.StringType); + spark.udf().register("tdistanceTgeoTgeo", tdistanceTgeoTgeo, DataTypes.StringType); + spark.udf().register("tdistanceTfloatFloat", tdistanceTfloatFloat, DataTypes.StringType); + spark.udf().register("tdistanceTintInt", tdistanceTintInt, DataTypes.StringType); + spark.udf().register("tdistanceTnumberTnumber", tdistanceTnumberTnumber, DataTypes.StringType); + + spark.udf().register("nadTgeoGeo", nadTgeoGeo, DataTypes.DoubleType); + spark.udf().register("nadTgeoStbox", nadTgeoStbox, DataTypes.DoubleType); + spark.udf().register("nadTgeoTgeo", nadTgeoTgeo, DataTypes.DoubleType); + spark.udf().register("naiTgeoGeo", naiTgeoGeo, DataTypes.StringType); + spark.udf().register("naiTgeoTgeo", naiTgeoTgeo, DataTypes.StringType); + spark.udf().register("shortestLineTgeoGeo", shortestLineTgeoGeo, DataTypes.StringType); + spark.udf().register("shortestLineTgeoTgeo", shortestLineTgeoTgeo, DataTypes.StringType); + + spark.udf().register("minDistanceTgeoGeo", minDistanceTgeoGeo, DataTypes.DoubleType); + spark.udf().register("minDistanceTgeoTgeo", minDistanceTgeoTgeo, DataTypes.DoubleType); + + // MobilityDB SQL bare-name aliases. + // The portable operator alias `nearestApproachDistance` (|=|) is + // registered by org.mobilitydb.spark.portable.PortableOperatorAliasUDFs, + // reusing this same nadTgeoGeo backing field. + spark.udf().register("nearestApproachInstant", naiTgeoGeo, DataTypes.StringType); + spark.udf().register("shortestLine", shortestLineTgeoGeo, DataTypes.StringType); + spark.udf().register("minDistance", minDistanceTgeoTgeo, DataTypes.DoubleType); + } +} diff --git a/src/main/java/org/mobilitydb/spark/geo/GeoAffineUDFs.java b/src/main/java/org/mobilitydb/spark/geo/GeoAffineUDFs.java new file mode 100644 index 00000000..b273cdc9 --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/geo/GeoAffineUDFs.java @@ -0,0 +1,176 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.geo; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import jnr.ffi.Runtime; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosNative; +import org.mobilitydb.spark.MeosThread; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.api.java.UDF2; +import org.apache.spark.sql.api.java.UDF3; +import org.apache.spark.sql.api.java.UDF4; +import org.apache.spark.sql.api.java.UDF5; +import org.apache.spark.sql.api.java.UDF13; +import org.apache.spark.sql.types.DataTypes; + +/** + * Spark SQL UDFs for affine transformations on tgeo: translate, rotate + * (2D), rotateX/Y/Z (3D), transscale. + * + * MobilityDB SQL composes these from MEOS's single tgeo_affine(Temporal*, + * AFFINE*) primitive. We build the AFFINE struct in direct memory and call + * tgeo_affine via MeosNative. + * + * AFFINE layout (96 bytes, all doubles): + * afac bfac cfac β€” row 0 spatial coefficients + * dfac efac ffac β€” row 1 + * gfac hfac ifac β€” row 2 + * xoff yoff zoff β€” translation offsets + * + * Identity matrix: afac=efac=ifac=1, all other coefficients 0. + */ +public final class GeoAffineUDFs { + + private GeoAffineUDFs() {} + + private static Pointer makeAffine(double afac, double bfac, double cfac, + double dfac, double efac, double ffac, + double gfac, double hfac, double ifac, + double xoff, double yoff, double zoff) { + Pointer aff = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(96); + aff.putDouble(0, afac); + aff.putDouble(8, bfac); + aff.putDouble(16, cfac); + aff.putDouble(24, dfac); + aff.putDouble(32, efac); + aff.putDouble(40, ffac); + aff.putDouble(48, gfac); + aff.putDouble(56, hfac); + aff.putDouble(64, ifac); + aff.putDouble(72, xoff); + aff.putDouble(80, yoff); + aff.putDouble(88, zoff); + return aff; + } + + private static String applyAffine(String hex, Pointer affine) { + Pointer t = GeneratedFunctions.temporal_from_hexwkb(hex); + if (t == null) return null; + try { + Pointer r = MeosNative.INSTANCE.tgeo_affine(t, affine); + if (r == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(t); } + } + + // translate(temporal, dx, dy) β€” 2D translation (z unchanged) + public static final UDF3 translate2 = + (hex, dx, dy) -> { + if (hex == null || dx == null || dy == null) return null; + MeosThread.ensureReady(); + return applyAffine(hex, makeAffine(1,0,0, 0,1,0, 0,0,1, dx, dy, 0)); + }; + + // translate(temporal, dx, dy, dz) β€” 3D translation + public static final UDF4 translate3 = + (hex, dx, dy, dz) -> { + if (hex == null || dx == null || dy == null || dz == null) return null; + MeosThread.ensureReady(); + return applyAffine(hex, makeAffine(1,0,0, 0,1,0, 0,0,1, dx, dy, dz)); + }; + + // rotate(temporal, angleRadians) β€” 2D rotation around origin (z unchanged) + public static final UDF2 rotate = + (hex, angle) -> { + if (hex == null || angle == null) return null; + MeosThread.ensureReady(); + double c = Math.cos(angle), s = Math.sin(angle); + return applyAffine(hex, makeAffine(c,-s,0, s,c,0, 0,0,1, 0, 0, 0)); + }; + + // rotateX(temporal, angleRadians) β€” 3D rotation about x-axis + public static final UDF2 rotateX = + (hex, angle) -> { + if (hex == null || angle == null) return null; + MeosThread.ensureReady(); + double c = Math.cos(angle), s = Math.sin(angle); + return applyAffine(hex, makeAffine(1,0,0, 0,c,-s, 0,s,c, 0, 0, 0)); + }; + + // rotateY(temporal, angleRadians) β€” 3D rotation about y-axis + public static final UDF2 rotateY = + (hex, angle) -> { + if (hex == null || angle == null) return null; + MeosThread.ensureReady(); + double c = Math.cos(angle), s = Math.sin(angle); + return applyAffine(hex, makeAffine(c,0,s, 0,1,0, -s,0,c, 0, 0, 0)); + }; + + // rotateZ(temporal, angleRadians) β€” 3D rotation about z-axis + public static final UDF2 rotateZ = + (hex, angle) -> { + if (hex == null || angle == null) return null; + MeosThread.ensureReady(); + double c = Math.cos(angle), s = Math.sin(angle); + return applyAffine(hex, makeAffine(c,-s,0, s,c,0, 0,0,1, 0, 0, 0)); + }; + + // transscale(temporal, dx, dy, sx, sy) β€” translate then scale + // Equivalent affine: ((px+dx)*sx, (py+dy)*sy) = sx*px + sx*dx, sy*py + sy*dy + public static final UDF5 transscale = + (hex, dx, dy, sx, sy) -> { + if (hex == null || dx == null || dy == null || sx == null || sy == null) return null; + MeosThread.ensureReady(); + return applyAffine(hex, makeAffine(sx,0,0, 0,sy,0, 0,0,1, sx*dx, sy*dy, 0)); + }; + + // affine(temporal, afac, bfac, cfac, dfac, efac, ffac, gfac, hfac, ifac, xoff, yoff, zoff) + public static final UDF13 affine = + (hex, afac, bfac, cfac, dfac, efac, ffac, gfac, hfac, ifac, xoff, yoff, zoff) -> { + if (hex == null || afac == null || bfac == null || cfac == null || dfac == null + || efac == null || ffac == null || gfac == null || hfac == null || ifac == null + || xoff == null || yoff == null || zoff == null) return null; + MeosThread.ensureReady(); + return applyAffine(hex, makeAffine(afac, bfac, cfac, dfac, efac, ffac, + gfac, hfac, ifac, xoff, yoff, zoff)); + }; + + public static void registerAll(SparkSession spark) { + spark.udf().register("translate", translate2, DataTypes.StringType); + spark.udf().register("translate3", translate3, DataTypes.StringType); + spark.udf().register("rotate", rotate, DataTypes.StringType); + spark.udf().register("rotateX", rotateX, DataTypes.StringType); + spark.udf().register("rotateY", rotateY, DataTypes.StringType); + spark.udf().register("rotateZ", rotateZ, DataTypes.StringType); + spark.udf().register("transscale", transscale, DataTypes.StringType); + spark.udf().register("affine", affine, DataTypes.StringType); + } +} diff --git a/src/main/java/org/mobilitydb/spark/geo/GeoAnalyticsUDFs.java b/src/main/java/org/mobilitydb/spark/geo/GeoAnalyticsUDFs.java new file mode 100644 index 00000000..a8ab1da7 --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/geo/GeoAnalyticsUDFs.java @@ -0,0 +1,494 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.geo; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import jnr.ffi.Runtime; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.api.java.*; +import org.apache.spark.sql.types.DataTypes; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosThread; + +/** + * Spark SQL UDFs for high-value spatial analytics on temporal geometry types. + * + * Coverage: + * - Plain geometry operations: geomBuffer, geomConvexHull, geomIntersection + * - SRID / CRS management: setSRID, transform + * - Text output: asText, asEWKT + * - Elevation restriction: atElevation (3-D trips) + * - Temporal spatial predicates (TBool): tContains, tCovers, tDwithin + * - Bearing analytics: bearingToPoint, bearing + * - Spatial aggregates: twCentroid + * + * Storage convention: + * tgeompoint β†’ hex-WKB STRING (temporal_as_hexwkb) + * geometry β†’ WKT STRING (parsed via geo_from_text) + * TBool result β†’ hex-WKB STRING + * TFloat result β†’ hex-WKB STRING + * + * MEOS function authority: meos/include/meos_geo.h + */ +public final class GeoAnalyticsUDFs { + + private GeoAnalyticsUDFs() {} + + // Convenience: decode a trip and extract its SRID from its bounding box + private static int tripSrid(Pointer tptr) { + Pointer bbox = GeneratedFunctions.tspatial_to_stbox(tptr); + return (bbox != null) ? GeneratedFunctions.stbox_srid(bbox) : 0; + } + + // ------------------------------------------------------------------ + // GEOMETRY OPERATIONS (plain geometry in/out, WKT strings) + // + // MEOS: geom_buffer(gs, size, params) meos_geo.h:401 + // geom_convex_hull(gs) meos_geo.h:403 + // geom_intersection2d(gs1, gs2) meos_geo.h:405 + // ------------------------------------------------------------------ + + // geomBuffer("POLYGON((...))", 100.0) β†’ WKT of buffered polygon + public static final UDF2 geomBuffer = + (geomWkt, radius) -> { + if (geomWkt == null || radius == null) return null; + MeosThread.ensureReady(); + Pointer g = GeneratedFunctions.geo_from_text(geomWkt, 0); + if (g == null) return null; + Pointer r = GeneratedFunctions.geom_buffer(g, radius, ""); + if (r == null) return null; + return GeneratedFunctions.geo_as_text(r, 15); + }; + + // geomConvexHull("MULTIPOINT((0 0),(1 1),(0 1))") β†’ "POLYGON((0 0,0 1,1 1,0 0))" + public static final UDF1 geomConvexHull = + (geomWkt) -> { + if (geomWkt == null) return null; + MeosThread.ensureReady(); + Pointer g = GeneratedFunctions.geo_from_text(geomWkt, 0); + if (g == null) return null; + Pointer r = GeneratedFunctions.geom_convex_hull(g); + if (r == null) return null; + return GeneratedFunctions.geo_as_text(r, 15); + }; + + // geomIntersection("POLYGON(...)", "POLYGON(...)") β†’ WKT of intersection + public static final UDF2 geomIntersection = + (geomWkt1, geomWkt2) -> { + if (geomWkt1 == null || geomWkt2 == null) return null; + MeosThread.ensureReady(); + Pointer g1 = GeneratedFunctions.geo_from_text(geomWkt1, 0); + Pointer g2 = GeneratedFunctions.geo_from_text(geomWkt2, 0); + if (g1 == null || g2 == null) return null; + Pointer r = GeneratedFunctions.geom_intersection2d(g1, g2); + if (r == null) return null; + return GeneratedFunctions.geo_as_text(r, 15); + }; + + // ------------------------------------------------------------------ + // SRID / CRS MANAGEMENT (temporal in/out, hex-WKB strings) + // + // MEOS: tspatial_set_srid(temp, srid) meos_geo.h:687 + // tspatial_transform(temp, srid) meos_geo.h:688 + // ------------------------------------------------------------------ + + // setSRID(trip, 4326) β†’ same trip with SRID label changed (no reprojection) + public static final UDF2 setSRID = + (trip, srid) -> { + if (trip == null || srid == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + Pointer r = GeneratedFunctions.tspatial_set_srid(tptr, srid); + if (r == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + }; + + // transform(trip, 4326) β†’ trip reprojected to SRID 4326 + public static final UDF2 transform = + (trip, srid) -> { + if (trip == null || srid == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + Pointer r = GeneratedFunctions.tspatial_transform(tptr, srid); + if (r == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + }; + + // ------------------------------------------------------------------ + // TEXT OUTPUT (temporal in, WKT/EWKT string out) + // + // MEOS: tspatial_as_text(temp, maxdd) meos_geo.h:620 + // tspatial_as_ewkt(temp, maxdd) meos_geo.h:619 + // ------------------------------------------------------------------ + + // asText(trip, 6) β†’ "[POINT(1.234567 2.345678)@2020-01-01, ...]" + public static final UDF2 asText = + (trip, maxdd) -> { + if (trip == null || maxdd == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + return GeneratedFunctions.tspatial_as_text(tptr, maxdd); + }; + + // asEWKT(trip, 6) β†’ "[SRID=4326;POINT(...)@2020-01-01, ...]" + public static final UDF2 asEWKT = + (trip, maxdd) -> { + if (trip == null || maxdd == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + return GeneratedFunctions.tspatial_as_ewkt(tptr, maxdd); + }; + + // ------------------------------------------------------------------ + // ELEVATION RESTRICTION (3-D tgeompoint, hex-WKB out) + // + // MEOS: tpoint_at_elevation(temp, zspan) meos_geo.h:699 + // floatspan_make(lower, upper, lower_inc, upper_inc) + // + // zmin/zmax define the closed [zmin, zmax] elevation band. + // Returns null when no portion of the trip falls in the band. + // ------------------------------------------------------------------ + + // atElevation(trip, 0.0, 100.0) β†’ sub-trip within elevation band [0,100] + public static final UDF3 atElevation = + (trip, zmin, zmax) -> { + if (trip == null || zmin == null || zmax == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + Pointer zspan = GeneratedFunctions.floatspan_make(zmin, zmax, true, true); + if (zspan == null) return null; + Pointer r = GeneratedFunctions.tpoint_at_elevation(tptr, zspan); + if (r == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + }; + + // ------------------------------------------------------------------ + // TEMPORAL SPATIAL PREDICATES (TBool hex-WKB out) + // + // MEOS: tcontains_geo_tgeo(gs, temp) meos_geo.h:837 + // tcovers_tgeo_tgeo(temp1, temp2) meos_geo.h:842 + // tdwithin_tgeo_tgeo(temp1, temp2, dist) meos_geo.h:848 + // + // These return temporal booleans: at each instant, whether the + // predicate holds. Result is encoded as hex-WKB for downstream UDFs. + // ------------------------------------------------------------------ + + // tContains("POLYGON(...)", trip) β†’ tbool hex-WKB: true at instants inside polygon + public static final UDF2 tContains = + (geomWkt, trip) -> { + if (geomWkt == null || trip == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + int srid = tripSrid(tptr); + Pointer g = GeneratedFunctions.geo_from_text(geomWkt, srid); + if (g == null) return null; + Pointer r = GeneratedFunctions.tcontains_geo_tgeo(g, tptr); + if (r == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + }; + + // tCovers(trip1, trip2) β†’ tbool hex-WKB: true at instants where trip1 covers trip2 + public static final UDF2 tCovers = + (trip1, trip2) -> { + if (trip1 == null || trip2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(trip1); + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(trip2); + if (p1 == null || p2 == null) return null; + Pointer r = GeneratedFunctions.tcovers_tgeo_tgeo(p1, p2); + if (r == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + }; + + // tDwithin(trip1, trip2, 100.0) β†’ tbool hex-WKB: true at instants within 100 units + public static final UDF3 tDwithin = + (trip1, trip2, dist) -> { + if (trip1 == null || trip2 == null || dist == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(trip1); + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(trip2); + if (p1 == null || p2 == null) return null; + Pointer r = GeneratedFunctions.tdwithin_tgeo_tgeo(p1, p2, dist.doubleValue()); + if (r == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + }; + + // ------------------------------------------------------------------ + // BEARING ANALYTICS (TFloat hex-WKB out) + // + // MEOS: bearing_tpoint_point(temp, gs, invert) meos_geo.h + // bearing_tpoint_tpoint(temp1, temp2) meos_geo.h + // + // bearing_tpoint_point: invert=false β†’ bearing FROM trip TO point. + // ------------------------------------------------------------------ + + // bearingToPoint(trip, "POINT(lon lat)") β†’ tfloat hex-WKB, bearing in radians + public static final UDF2 bearingToPoint = + (trip, geomWkt) -> { + if (trip == null || geomWkt == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + int srid = tripSrid(tptr); + Pointer g = GeneratedFunctions.geo_from_text(geomWkt, srid); + if (g == null) return null; + Pointer r = GeneratedFunctions.bearing_tpoint_point(tptr, g, false); + if (r == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + }; + + // bearing(trip1, trip2) β†’ tfloat hex-WKB, instantaneous bearing between trips + public static final UDF2 bearing = + (trip1, trip2) -> { + if (trip1 == null || trip2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(trip1); + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(trip2); + if (p1 == null || p2 == null) return null; + Pointer r = GeneratedFunctions.bearing_tpoint_tpoint(p1, p2); + if (r == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + }; + + // ------------------------------------------------------------------ + // SPATIAL AGGREGATES (geometry WKT out) + // + // MEOS: tpoint_twcentroid(temp) meos_geo.h + // + // Returns the time-weighted centroid of the trip as a WKT POINT string. + // ------------------------------------------------------------------ + + // twCentroid(trip) β†’ WKT POINT of the time-weighted centroid + public static final UDF1 twCentroid = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + Pointer g = GeneratedFunctions.tpoint_twcentroid(tptr); + if (g == null) return null; + return GeneratedFunctions.geo_as_text(g, 15); + }; + + // ------------------------------------------------------------------ + // geoSame(wkt1 STRING, wkt2 STRING) β†’ BOOLEAN + // + // Returns true if two geometries are exactly equal (same type, coordinates, + // and SRID). + // + // MEOS: geo_same(const GSERIALIZED *, const GSERIALIZED *) β†’ bool + // ------------------------------------------------------------------ + public static final UDF2 geoSame = + (wkt1, wkt2) -> { + if (wkt1 == null || wkt2 == null) return null; + MeosThread.ensureReady(); + Pointer g1 = GeneratedFunctions.geo_from_text(wkt1, 0); + if (g1 == null) return null; + try { + Pointer g2 = GeneratedFunctions.geo_from_text(wkt2, 0); + if (g2 == null) return null; + try { + return GeneratedFunctions.geo_same(g1, g2); + } finally { + MeosMemory.free(g2); + } + } finally { + MeosMemory.free(g1); + } + }; + + // ------------------------------------------------------------------ + // tpointConvexHull(trip STRING) β†’ STRING (hex-EWKB geometry) + // + // Returns the convex hull of the trajectory of the temporal point. + // + // MEOS: tgeo_convex_hull(const Temporal *) β†’ GSERIALIZED * + // geo_as_hexewkb(const GSERIALIZED *, const char *endian) + // ------------------------------------------------------------------ + public static final UDF1 tpointConvexHull = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + try { + Pointer gptr = GeneratedFunctions.tgeo_convex_hull(tptr); + if (gptr == null) return null; + try { + return GeneratedFunctions.geo_as_hexewkb(gptr, null); + } finally { + MeosMemory.free(gptr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // ------------------------------------------------------------------ + // tpointExpandSpace(trip STRING, distance DOUBLE) β†’ STRING (hex-WKB STBOX) + // + // Returns the spatiotemporal bounding box of the temporal point expanded + // by distance in each spatial dimension. + // + // MEOS: tspatial_to_stbox(const Temporal *) β†’ STBox * + // stbox_expand_space(const STBox *, double d) β†’ STBox * + // stbox_as_hexwkb(const STBox *, uint8_t variant, size_t *size) + // ------------------------------------------------------------------ + public static final UDF2 tpointExpandSpace = + (trip, distance) -> { + if (trip == null || distance == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + try { + Pointer stbox = GeneratedFunctions.tspatial_to_stbox(tptr); + if (stbox == null) return null; + try { + Pointer expanded = GeneratedFunctions.stbox_expand_space(stbox, distance); + if (expanded == null) return null; + try { + Pointer sizeOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + return GeneratedFunctions.stbox_as_hexwkb(expanded, (byte) 0, sizeOut); + } finally { + MeosMemory.free(expanded); + } + } finally { + MeosMemory.free(stbox); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // ------------------------------------------------------------------ + // REGISTRATION + // ------------------------------------------------------------------ + + public static void registerAll(SparkSession spark) { + // geometry operations + spark.udf().register("geomBuffer", geomBuffer, DataTypes.StringType); + spark.udf().register("geomConvexHull", geomConvexHull, DataTypes.StringType); + spark.udf().register("geomIntersection", geomIntersection, DataTypes.StringType); + // SRID management + spark.udf().register("setSRID", setSRID, DataTypes.StringType); + spark.udf().register("transform", transform, DataTypes.StringType); + // text output + spark.udf().register("asText", asText, DataTypes.StringType); + spark.udf().register("asEWKT", asEWKT, DataTypes.StringType); + // elevation restriction + spark.udf().register("atElevation", atElevation, DataTypes.StringType); + // temporal predicates + spark.udf().register("tContains", tContains, DataTypes.StringType); + spark.udf().register("tCovers", tCovers, DataTypes.StringType); + spark.udf().register("tDwithin", tDwithin, DataTypes.StringType); + // bearing + spark.udf().register("bearingToPoint", bearingToPoint, DataTypes.StringType); + spark.udf().register("bearing", bearing, DataTypes.StringType); + // spatial aggregate + spark.udf().register("twCentroid", twCentroid, DataTypes.StringType); + // geometry equality + spark.udf().register("geoSame", geoSame, DataTypes.BooleanType); + // tpoint analytics + spark.udf().register("tpointConvexHull", tpointConvexHull, DataTypes.StringType); + spark.udf().register("tpointExpandSpace", tpointExpandSpace, DataTypes.StringType); + // tpoint minus geom + bearing direction + spark.udf().register("minusGeometry", minusGeometry, DataTypes.StringType); + spark.udf().register("tdirection", tdirection, DataTypes.DoubleType); + // transformPipeline (PROJ pipeline string) + spark.udf().register("transformPipeline", tpointTransformPipeline, DataTypes.StringType); + spark.udf().register("stboxTransformPipeline", stboxTransformPipeline, DataTypes.StringType); + } + + public static final org.apache.spark.sql.api.java.UDF4 + tpointTransformPipeline = (trip, pipeline, srid, isForward) -> { + if (trip == null || pipeline == null) return null; + org.mobilitydb.spark.MeosThread.ensureReady(); + jnr.ffi.Pointer t = GeneratedFunctions.temporal_from_hexwkb(trip); + if (t == null) return null; + try { + jnr.ffi.Pointer r = GeneratedFunctions.tspatial_transform_pipeline(t, pipeline, + srid == null ? 0 : srid, isForward == null ? true : isForward); + if (r == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); } + finally { org.mobilitydb.spark.MeosMemory.free(r); } + } finally { org.mobilitydb.spark.MeosMemory.free(t); } + }; + + public static final org.apache.spark.sql.api.java.UDF4 + stboxTransformPipeline = (stboxHex, pipeline, srid, isForward) -> { + if (stboxHex == null || pipeline == null) return null; + org.mobilitydb.spark.MeosThread.ensureReady(); + jnr.ffi.Pointer s = GeneratedFunctions.stbox_from_hexwkb(stboxHex); + if (s == null) return null; + try { + jnr.ffi.Pointer r = GeneratedFunctions.stbox_transform_pipeline(s, pipeline, + srid == null ? 0 : srid, isForward == null ? true : isForward); + if (r == null) return null; + try { + jnr.ffi.Pointer sizeOut = jnr.ffi.Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + return GeneratedFunctions.stbox_as_hexwkb(r, (byte) 0, sizeOut); + } finally { org.mobilitydb.spark.MeosMemory.free(r); } + } finally { org.mobilitydb.spark.MeosMemory.free(s); } + }; + + // minusGeometry(tpoint, geomWkt) β†’ tpoint with geometry subtracted (or null if total) + public static final org.apache.spark.sql.api.java.UDF2 minusGeometry = + (trip, geomWkt) -> { + if (trip == null || geomWkt == null) return null; + org.mobilitydb.spark.MeosThread.ensureReady(); + jnr.ffi.Pointer t = GeneratedFunctions.temporal_from_hexwkb(trip); + if (t == null) return null; + jnr.ffi.Pointer g = GeneratedFunctions.geo_from_text(geomWkt, 0); + if (g == null) { org.mobilitydb.spark.MeosMemory.free(t); return null; } + try { + jnr.ffi.Pointer r = org.mobilitydb.spark.MeosNative.INSTANCE.tpoint_minus_geom(t, g); + if (r == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); } + finally { org.mobilitydb.spark.MeosMemory.free(r); } + } finally { org.mobilitydb.spark.MeosMemory.free(t, g); } + }; + + // tdirection(tpoint) β†’ bearing in radians, or null if not defined + public static final org.apache.spark.sql.api.java.UDF1 tdirection = + (trip) -> { + if (trip == null) return null; + org.mobilitydb.spark.MeosThread.ensureReady(); + jnr.ffi.Pointer t = GeneratedFunctions.temporal_from_hexwkb(trip); + if (t == null) return null; + try { + jnr.ffi.Pointer outBuf = jnr.ffi.Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + boolean ok = org.mobilitydb.spark.MeosNative.INSTANCE.tpoint_direction(t, outBuf); + return ok ? outBuf.getDouble(0) : null; + } finally { org.mobilitydb.spark.MeosMemory.free(t); } + }; +} diff --git a/src/main/java/org/mobilitydb/spark/geo/GeoUDFs.java b/src/main/java/org/mobilitydb/spark/geo/GeoUDFs.java new file mode 100644 index 00000000..d1ffa1a0 --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/geo/GeoUDFs.java @@ -0,0 +1,953 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.geo; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import jnr.ffi.Runtime; +import org.apache.spark.sql.api.java.*; +import org.apache.spark.sql.types.DataTypes; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosThread; + +/** + * Spark SQL UDFs for spatial (geometry) operations on tgeompoint. + * + * Storage convention: + * tgeompoint β†’ hex-WKB STRING (temporal_as_hexwkb / temporal_from_hexwkb) + * geometry β†’ WKT STRING (e.g. "POINT(50 0)", parsed via geo_from_text) + * + * Memory management: every native Pointer allocated by MEOS must be freed via + * MeosMemory.free() in a finally block. MEOS standalone mode uses the system + * malloc/free (palloc/pfree map to malloc/free outside PostgreSQL), so native + * objects are NOT garbage-collected by the JVM. Failing to free them causes + * the native heap to grow without bound across UDF calls (one leaked object per + * row Γ— millions of rows in cross-join queries like Q2/Q4/Q5/Q6). + * + * MEOS function authority: meos/include/meos_geo.h + */ +public final class GeoUDFs { + + private GeoUDFs() {} + + // ------------------------------------------------------------------ + // eIntersects(trip STRING, geomWkt STRING) β†’ BOOLEAN + // + // Returns true if the trip's trajectory ever intersects geomWkt. + // + // SRID handling: extract the trip's SRID from its bounding box and + // pass it to geo_from_text so MEOS's ensure_same_srid check passes. + // BerlinMOD trips use SRID=3857; query regions use SRID=0 (plain WKT). + // + // For geodetic trips (tgeogpoint), the geometry is promoted to geography + // via geom_to_geog() to avoid MEOS "Operation on mixed SRID" errors. + // + // MEOS: geo_from_text, tspatial_to_stbox, stbox_srid, stbox_isgeodetic, + // geom_to_geog, eintersects_tgeo_geo (meos_geo.h) + // ------------------------------------------------------------------ + public static final UDF2 eIntersects = + (trip, geomWkt) -> { + if (trip == null || geomWkt == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + try { + Pointer bbox = GeneratedFunctions.tspatial_to_stbox(tptr); + boolean geodetic = (bbox != null && GeneratedFunctions.stbox_isgeodetic(bbox)); + int srid = (bbox != null) ? GeneratedFunctions.stbox_srid(bbox) : 0; + MeosMemory.free(bbox); + Pointer gptr = GeneratedFunctions.geo_from_text(geomWkt, srid); + if (gptr == null) return null; + if (geodetic) { + Pointer geog = GeneratedFunctions.geom_to_geog(gptr); + MeosMemory.free(gptr); + gptr = geog; + if (gptr == null) return null; + } + try { + return GeneratedFunctions.eintersects_tgeo_geo(tptr, gptr) == 1; + } finally { + MeosMemory.free(gptr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // ------------------------------------------------------------------ + // nearestApproachDistance(t1 STRING, t2 STRING) β†’ DOUBLE + // + // MEOS: nad_tgeo_tgeo(const Temporal *, const Temporal *) β†’ double + // Returns NULL when trips have no overlapping time extent (MEOS: DBL_MAX). + // ------------------------------------------------------------------ + public static final UDF2 nearestApproachDistance = + (trip1, trip2) -> { + if (trip1 == null || trip2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(trip1); + if (p1 == null) return null; + try { + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(trip2); + if (p2 == null) return null; + try { + double dist = GeneratedFunctions.nad_tgeo_tgeo(p1, p2); + return (dist == Double.MAX_VALUE) ? null : dist; + } finally { + MeosMemory.free(p2); + } + } finally { + MeosMemory.free(p1); + } + }; + + // ------------------------------------------------------------------ + // eDwithin(t1 STRING, t2 STRING, dist DOUBLE) β†’ BOOLEAN + // + // dist accepts Double or BigDecimal (Spark infers decimal(p,s) for + // numeric literals like 10.0 β€” use Number.doubleValue() to handle both). + // + // MEOS: edwithin_tgeo_tgeo(const Temporal *, const Temporal *, double) β†’ int + // ------------------------------------------------------------------ + public static final UDF3 eDwithin = + (trip1, trip2, dist) -> { + if (trip1 == null || trip2 == null || dist == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(trip1); + if (p1 == null) return null; + try { + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(trip2); + if (p2 == null) return null; + try { + return GeneratedFunctions.edwithin_tgeo_tgeo(p1, p2, dist.doubleValue()) == 1; + } finally { + MeosMemory.free(p2); + } + } finally { + MeosMemory.free(p1); + } + }; + + // ------------------------------------------------------------------ + // tgeompoint(wkt STRING) β†’ STRING (hex-WKB) + // + // Parses a tgeompoint WKT string and returns the MEOS hex-WKB encoding. + // + // MEOS: tgeompoint_in(const char *str) β†’ Temporal * meos_geo.h:618 + // temporal_as_hexwkb(const Temporal *, uint8_t, size_t *) meos.h + // ------------------------------------------------------------------ + public static final UDF1 tgeompoint = + (wkt) -> { + if (wkt == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.tgeompoint_in(wkt); + if (p == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(p, (byte) 0); + } finally { + MeosMemory.free(p); + } + }; + + // ------------------------------------------------------------------ + // trajectory(trip STRING) β†’ STRING (hex WKB geometry) + // + // Projects a tgeompoint to its spatial path: POINT for a single + // instant, LINESTRING for a linear sequence. Returns hex-EWKB. + // + // MEOS: tpoint_trajectory(const Temporal *, bool merge) β†’ GSERIALIZED * + // geo_as_hexewkb(const GSERIALIZED *, const char *endian) meos_geo.h + // ------------------------------------------------------------------ + public static final UDF1 trajectory = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + try { + Pointer gptr = GeneratedFunctions.tpoint_trajectory(tptr, true); + if (gptr == null) return null; + try { + return GeneratedFunctions.geo_as_hexewkb(gptr, null); + } finally { + MeosMemory.free(gptr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // ------------------------------------------------------------------ + // eDisjoint(trip STRING, geomWkt STRING) β†’ BOOLEAN + // + // Returns true if the moving object is ever disjoint from the geometry. + // + // MEOS: edisjoint_tgeo_geo(const Temporal *, const GSERIALIZED *) β†’ int + // ------------------------------------------------------------------ + public static final UDF2 eDisjoint = + (trip, geomWkt) -> { + if (trip == null || geomWkt == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + try { + Pointer bbox = GeneratedFunctions.tspatial_to_stbox(tptr); + int srid = (bbox != null) ? GeneratedFunctions.stbox_srid(bbox) : 0; + MeosMemory.free(bbox); + Pointer gptr = GeneratedFunctions.geo_from_text(geomWkt, srid); + if (gptr == null) return null; + try { + return GeneratedFunctions.edisjoint_tgeo_geo(tptr, gptr) == 1; + } finally { + MeosMemory.free(gptr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // ------------------------------------------------------------------ + // eTouches(trip STRING, geomWkt STRING) β†’ BOOLEAN + // + // Returns true if the moving object ever touches (shares boundary with) + // the static geometry. + // + // MEOS: etouches_tgeo_geo(const Temporal *, const GSERIALIZED *) β†’ int + // ------------------------------------------------------------------ + public static final UDF2 eTouches = + (trip, geomWkt) -> { + if (trip == null || geomWkt == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + try { + Pointer bbox = GeneratedFunctions.tspatial_to_stbox(tptr); + int srid = (bbox != null) ? GeneratedFunctions.stbox_srid(bbox) : 0; + MeosMemory.free(bbox); + Pointer gptr = GeneratedFunctions.geo_from_text(geomWkt, srid); + if (gptr == null) return null; + try { + return GeneratedFunctions.etouches_tgeo_geo(tptr, gptr) == 1; + } finally { + MeosMemory.free(gptr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // ------------------------------------------------------------------ + // eCovers(trip STRING, geomWkt STRING) β†’ BOOLEAN + // + // Returns true if the moving object ever covers the static geometry. + // + // MEOS: ecovers_tgeo_geo(const Temporal *, const GSERIALIZED *) β†’ int + // ------------------------------------------------------------------ + public static final UDF2 eCovers = + (trip, geomWkt) -> { + if (trip == null || geomWkt == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + try { + Pointer bbox = GeneratedFunctions.tspatial_to_stbox(tptr); + int srid = (bbox != null) ? GeneratedFunctions.stbox_srid(bbox) : 0; + MeosMemory.free(bbox); + Pointer gptr = GeneratedFunctions.geo_from_text(geomWkt, srid); + if (gptr == null) return null; + try { + return GeneratedFunctions.ecovers_tgeo_geo(tptr, gptr) == 1; + } finally { + MeosMemory.free(gptr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // ------------------------------------------------------------------ + // eDisjointTgeoTgeo(trip1 STRING, trip2 STRING) β†’ BOOLEAN + // eIntersectsTgeoTgeo(trip1 STRING, trip2 STRING) β†’ BOOLEAN + // + // MEOS: edisjoint_tgeo_tgeo, eintersects_tgeo_tgeo + // ------------------------------------------------------------------ + public static final UDF2 eDisjointTgeoTgeo = + (trip1, trip2) -> { + if (trip1 == null || trip2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(trip1); + if (p1 == null) return null; + try { + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(trip2); + if (p2 == null) return null; + try { + return GeneratedFunctions.edisjoint_tgeo_tgeo(p1, p2) == 1; + } finally { + MeosMemory.free(p2); + } + } finally { + MeosMemory.free(p1); + } + }; + + public static final UDF2 eIntersectsTgeoTgeo = + (trip1, trip2) -> { + if (trip1 == null || trip2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(trip1); + if (p1 == null) return null; + try { + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(trip2); + if (p2 == null) return null; + try { + return GeneratedFunctions.eintersects_tgeo_tgeo(p1, p2) == 1; + } finally { + MeosMemory.free(p2); + } + } finally { + MeosMemory.free(p1); + } + }; + + // ------------------------------------------------------------------ + // aIntersects(trip STRING, geomWkt STRING) β†’ BOOLEAN + // aDisjoint(trip STRING, geomWkt STRING) β†’ BOOLEAN + // + // Returns true if the moving object always intersects / is always + // disjoint from the static geometry. + // + // MEOS: aintersects_tgeo_geo, adisjoint_tgeo_geo + // ------------------------------------------------------------------ + public static final UDF2 aIntersects = + (trip, geomWkt) -> { + if (trip == null || geomWkt == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + try { + Pointer bbox = GeneratedFunctions.tspatial_to_stbox(tptr); + int srid = (bbox != null) ? GeneratedFunctions.stbox_srid(bbox) : 0; + MeosMemory.free(bbox); + Pointer gptr = GeneratedFunctions.geo_from_text(geomWkt, srid); + if (gptr == null) return null; + try { + return GeneratedFunctions.aintersects_tgeo_geo(tptr, gptr) == 1; + } finally { + MeosMemory.free(gptr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + public static final UDF2 aDisjoint = + (trip, geomWkt) -> { + if (trip == null || geomWkt == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + try { + Pointer bbox = GeneratedFunctions.tspatial_to_stbox(tptr); + int srid = (bbox != null) ? GeneratedFunctions.stbox_srid(bbox) : 0; + MeosMemory.free(bbox); + Pointer gptr = GeneratedFunctions.geo_from_text(geomWkt, srid); + if (gptr == null) return null; + try { + return GeneratedFunctions.adisjoint_tgeo_geo(tptr, gptr) == 1; + } finally { + MeosMemory.free(gptr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // ------------------------------------------------------------------ + // aDwithin(trip1 STRING, trip2 STRING, dist DOUBLE) β†’ BOOLEAN + // eDwithinGeo(trip STRING, geomWkt STRING, dist DOUBLE) β†’ BOOLEAN + // aDwithinGeo(trip STRING, geomWkt STRING, dist DOUBLE) β†’ BOOLEAN + // + // MEOS: adwithin_tgeo_tgeo, edwithin_tgeo_geo, adwithin_tgeo_geo + // ------------------------------------------------------------------ + public static final UDF3 aDwithin = + (trip1, trip2, dist) -> { + if (trip1 == null || trip2 == null || dist == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(trip1); + if (p1 == null) return null; + try { + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(trip2); + if (p2 == null) return null; + try { + return GeneratedFunctions.adwithin_tgeo_tgeo(p1, p2, dist.doubleValue()) == 1; + } finally { + MeosMemory.free(p2); + } + } finally { + MeosMemory.free(p1); + } + }; + + public static final UDF3 eDwithinGeo = + (trip, geomWkt, dist) -> { + if (trip == null || geomWkt == null || dist == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + try { + Pointer bbox = GeneratedFunctions.tspatial_to_stbox(tptr); + int srid = (bbox != null) ? GeneratedFunctions.stbox_srid(bbox) : 0; + MeosMemory.free(bbox); + Pointer gptr = GeneratedFunctions.geo_from_text(geomWkt, srid); + if (gptr == null) return null; + try { + return GeneratedFunctions.edwithin_tgeo_geo(tptr, gptr, dist.doubleValue()) == 1; + } finally { + MeosMemory.free(gptr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + public static final UDF3 aDwithinGeo = + (trip, geomWkt, dist) -> { + if (trip == null || geomWkt == null || dist == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + try { + Pointer bbox = GeneratedFunctions.tspatial_to_stbox(tptr); + int srid = (bbox != null) ? GeneratedFunctions.stbox_srid(bbox) : 0; + MeosMemory.free(bbox); + Pointer gptr = GeneratedFunctions.geo_from_text(geomWkt, srid); + if (gptr == null) return null; + try { + return GeneratedFunctions.adwithin_tgeo_geo(tptr, gptr, dist.doubleValue()) == 1; + } finally { + MeosMemory.free(gptr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // ------------------------------------------------------------------ + // eContains(geomWKT STRING, trip STRING) β†’ BOOLEAN + // + // Returns true if the static geometry ever contains the moving object. + // Argument order: eContains(container, contained). + // + // MEOS: econtains_geo_tgeo(const GSERIALIZED *, const Temporal *) β†’ int + // ------------------------------------------------------------------ + public static final UDF2 eContains = + (geomWkt, trip) -> { + if (geomWkt == null || trip == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + try { + Pointer bbox = GeneratedFunctions.tspatial_to_stbox(tptr); + int srid = (bbox != null) ? GeneratedFunctions.stbox_srid(bbox) : 0; + MeosMemory.free(bbox); + Pointer gptr = GeneratedFunctions.geo_from_text(geomWkt, srid); + if (gptr == null) return null; + try { + return GeneratedFunctions.econtains_geo_tgeo(gptr, tptr) == 1; + } finally { + MeosMemory.free(gptr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // ------------------------------------------------------------------ + // geomFromText(wkt STRING) β†’ STRING (hex-EWKB) + // + // MEOS: geo_from_text(const char *, int32_t srid) β†’ GSERIALIZED * + // geo_as_hexewkb(const GSERIALIZED *, const char *) + // ------------------------------------------------------------------ + public static final UDF1 geomFromText = + (wkt) -> { + if (wkt == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.geo_from_text(wkt, 0); + if (p == null) return null; + try { + return GeneratedFunctions.geo_as_hexewkb(p, null); + } finally { + MeosMemory.free(p); + } + }; + + // ------------------------------------------------------------------ + // getX / getY / getZ(trip STRING) β†’ STRING (tfloat hex-WKB) + // cumulativeLength(trip STRING) β†’ STRING (tfloat hex-WKB) + // + // MEOS: tpoint_get_x/y/z, tpoint_cumulative_length meos_geo.h + // ------------------------------------------------------------------ + public static final UDF1 getX = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + try { + Pointer r = GeneratedFunctions.tpoint_get_x(tptr); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(tptr); + } + }; + + public static final UDF1 getY = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + try { + Pointer r = GeneratedFunctions.tpoint_get_y(tptr); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(tptr); + } + }; + + public static final UDF1 getZ = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + try { + // tpoint_get_z raises a MEOS error for 2D points; guard with Z-presence check. + Pointer bbox = GeneratedFunctions.tspatial_to_stbox(tptr); + if (bbox == null || !GeneratedFunctions.stbox_hasz(bbox)) { + MeosMemory.free(bbox); + return null; + } + MeosMemory.free(bbox); + Pointer r = GeneratedFunctions.tpoint_get_z(tptr); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(tptr); + } + }; + + public static final UDF1 cumulativeLength = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + try { + Pointer r = GeneratedFunctions.tpoint_cumulative_length(tptr); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // ------------------------------------------------------------------ + // stops(trip STRING, maxDist DOUBLE, minDuration STRING) β†’ STRING + // + // Returns the sub-trajectories where the vehicle stayed within maxDist + // for at least minDuration ("1 second"). + // + // MEOS: temporal_stops(const Temporal *, double, const Interval *) meos.h + // ------------------------------------------------------------------ + public static final UDF3 stops = + (trip, maxDist, minDuration) -> { + if (trip == null || maxDist == null || minDuration == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + try { + Pointer iv = GeneratedFunctions.pg_interval_in(minDuration, -1); + if (iv == null) return null; + try { + Pointer r = GeneratedFunctions.temporal_stops(tptr, maxDist, iv); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(iv); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // ------------------------------------------------------------------ + // isSimple(trip STRING) β†’ BOOLEAN + // + // True when the trip has no self-intersections. + // + // MEOS: tpoint_is_simple(const Temporal *) meos_geo.h + // ------------------------------------------------------------------ + public static final UDF1 isSimple = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + try { + return GeneratedFunctions.tpoint_is_simple(tptr); + } finally { + MeosMemory.free(tptr); + } + }; + + // ------------------------------------------------------------------ + // shortestLine(trip1 STRING, trip2 STRING) β†’ STRING (WKT geometry) + // + // MEOS: shortestline_tpoint_tpoint(const Temporal *, const Temporal *) + // geo_as_text(const GSERIALIZED *, int precision) + // ------------------------------------------------------------------ + public static final UDF2 shortestLine = + (trip1, trip2) -> { + if (trip1 == null || trip2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(trip1); + if (p1 == null) return null; + try { + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(trip2); + if (p2 == null) return null; + try { + Pointer g = GeneratedFunctions.shortestline_tgeo_tgeo(p1, p2); + if (g == null) return null; + try { + return GeneratedFunctions.geo_as_text(g, 15); + } finally { + MeosMemory.free(g); + } + } finally { + MeosMemory.free(p2); + } + } finally { + MeosMemory.free(p1); + } + }; + + // ------------------------------------------------------------------ + // tpointTransform(trip STRING, srid INTEGER) β†’ STRING + // + // Reprojects all instants of the temporal point to a different CRS. + // + // MEOS: tspatial_transform(const Temporal *, int srid) β†’ Temporal * + // ------------------------------------------------------------------ + public static final UDF2 tpointTransform = + (trip, srid) -> { + if (trip == null || srid == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + try { + Pointer result = GeneratedFunctions.tspatial_transform(tptr, srid); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // ------------------------------------------------------------------ + // tpointAsText(trip STRING, precision INTEGER) β†’ STRING + // + // Returns WKT for each instant of the temporal point. + // + // MEOS: tspatial_as_text(const Temporal *, int precision) β†’ char * + // ------------------------------------------------------------------ + public static final UDF2 tpointAsText = + (trip, precision) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + try { + return GeneratedFunctions.tspatial_as_text(tptr, precision != null ? precision : 15); + } finally { + MeosMemory.free(tptr); + } + }; + + // ------------------------------------------------------------------ + // tpointAsEWKT(trip STRING, precision INTEGER) β†’ STRING + // + // Returns Extended WKT (SRID=N;...) for each instant of the temporal point. + // + // MEOS: tspatial_as_ewkt(const Temporal *, int precision) β†’ char * + // ------------------------------------------------------------------ + public static final UDF2 tpointAsEWKT = + (trip, precision) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + try { + return GeneratedFunctions.tspatial_as_ewkt(tptr, precision != null ? precision : 15); + } finally { + MeosMemory.free(tptr); + } + }; + + // ------------------------------------------------------------------ + // tpointSRID(trip STRING) β†’ INTEGER + // + // Returns the SRID of the temporal point via its bounding box. + // + // MEOS: tspatial_to_stbox(const Temporal *) β†’ STBox * + // stbox_srid(const STBox *) β†’ int + // ------------------------------------------------------------------ + public static final UDF1 tpointSRID = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + try { + Pointer stbox = GeneratedFunctions.tspatial_to_stbox(tptr); + if (stbox == null) return null; + try { + return GeneratedFunctions.stbox_srid(stbox); + } finally { + MeosMemory.free(stbox); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // ------------------------------------------------------------------ + // tpointSetSRID(trip STRING, srid INTEGER) β†’ STRING + // + // Returns a copy of the temporal point with SRID set to srid. + // + // MEOS: tspatial_set_srid(const Temporal *, int srid) β†’ Temporal * + // ------------------------------------------------------------------ + public static final UDF2 tpointSetSRID = + (trip, srid) -> { + if (trip == null || srid == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + try { + Pointer result = GeneratedFunctions.tspatial_set_srid(tptr, srid); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // ------------------------------------------------------------------ + // tpointRound(trip STRING, decimals INTEGER) β†’ STRING + // + // Returns the temporal point with coordinates rounded to decimals places. + // + // MEOS: temporal_round(const Temporal *, int maxdd) β†’ Temporal * + // ------------------------------------------------------------------ + public static final UDF2 tpointRound = + (trip, decimals) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + try { + Pointer result = GeneratedFunctions.temporal_round(tptr, decimals != null ? decimals : 6); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // ------------------------------------------------------------------ + // tpointToStbox(trip STRING) β†’ STRING (hex-WKB STBOX) + // + // Returns the spatiotemporal bounding box of the temporal point. + // + // MEOS: tspatial_to_stbox(const Temporal *) β†’ STBox * + // stbox_as_hexwkb(const STBox *, uint8_t variant, size_t *size) + // ------------------------------------------------------------------ + public static final UDF1 tpointToStbox = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + try { + Pointer stbox = GeneratedFunctions.tspatial_to_stbox(tptr); + if (stbox == null) return null; + try { + Pointer sizeOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + return GeneratedFunctions.stbox_as_hexwkb(stbox, (byte) 0, sizeOut); + } finally { + MeosMemory.free(stbox); + } + } finally { + MeosMemory.free(tptr); + } + }; + + public static void registerAll(org.apache.spark.sql.SparkSession spark) { + spark.udf().register("eIntersects", eIntersects, DataTypes.BooleanType); + spark.udf().register("eDisjoint", eDisjoint, DataTypes.BooleanType); + spark.udf().register("eTouches", eTouches, DataTypes.BooleanType); + spark.udf().register("eCovers", eCovers, DataTypes.BooleanType); + spark.udf().register("eContains", eContains, DataTypes.BooleanType); + spark.udf().register("eDisjointTgeoTgeo", eDisjointTgeoTgeo, DataTypes.BooleanType); + spark.udf().register("eIntersectsTgeoTgeo", eIntersectsTgeoTgeo, DataTypes.BooleanType); + spark.udf().register("nearestApproachDistance", nearestApproachDistance, DataTypes.DoubleType); + spark.udf().register("eDwithin", eDwithin, DataTypes.BooleanType); + spark.udf().register("eDwithinGeo", eDwithinGeo, DataTypes.BooleanType); + spark.udf().register("aIntersects", aIntersects, DataTypes.BooleanType); + spark.udf().register("aDisjoint", aDisjoint, DataTypes.BooleanType); + spark.udf().register("aDwithin", aDwithin, DataTypes.BooleanType); + spark.udf().register("aDwithinGeo", aDwithinGeo, DataTypes.BooleanType); + spark.udf().register("tgeompoint", tgeompoint, DataTypes.StringType); + spark.udf().register("trajectory", trajectory, DataTypes.StringType); + spark.udf().register("geomFromText", geomFromText, DataTypes.StringType); + spark.udf().register("getX", getX, DataTypes.StringType); + spark.udf().register("getY", getY, DataTypes.StringType); + spark.udf().register("getZ", getZ, DataTypes.StringType); + spark.udf().register("cumulativeLength", cumulativeLength, DataTypes.StringType); + spark.udf().register("stops", stops, DataTypes.StringType); + spark.udf().register("isSimple", isSimple, DataTypes.BooleanType); + spark.udf().register("shortestLine", shortestLine, DataTypes.StringType); + spark.udf().register("tpointTransform", tpointTransform, DataTypes.StringType); + spark.udf().register("tpointAsText", tpointAsText, DataTypes.StringType); + spark.udf().register("tpointAsEWKT", tpointAsEWKT, DataTypes.StringType); + spark.udf().register("tpointSRID", tpointSRID, DataTypes.IntegerType); + spark.udf().register("tpointSetSRID", tpointSetSRID, DataTypes.StringType); + spark.udf().register("tpointRound", tpointRound, DataTypes.StringType); + spark.udf().register("tpointToStbox", tpointToStbox, DataTypes.StringType); + spark.udf().register("geoAsEwkt", geoAsEwkt, DataTypes.StringType); + spark.udf().register("geoAsGeojson", geoAsGeojson, DataTypes.StringType); + spark.udf().register("geoFromGeojson", geoFromGeojson, DataTypes.StringType); + } + + // ------------------------------------------------------------------ + // geoAsEwkt(wkt STRING, precision INTEGER) β†’ STRING + // + // Returns Extended WKT (SRID=N;...) for a geometry given as WKT. + // + // MEOS: geo_as_ewkt(const GSERIALIZED *, int precision) + // ------------------------------------------------------------------ + public static final UDF2 geoAsEwkt = + (wkt, precision) -> { + if (wkt == null) return null; + MeosThread.ensureReady(); + int prec = (precision == null) ? 15 : precision; + Pointer gptr = GeneratedFunctions.geo_from_text(wkt, 0); + if (gptr == null) return null; + try { + return GeneratedFunctions.geo_as_ewkt(gptr, prec); + } finally { + MeosMemory.free(gptr); + } + }; + + // ------------------------------------------------------------------ + // geoAsGeojson(wkt STRING, options INTEGER, precision INTEGER) β†’ STRING + // + // Returns GeoJSON for a geometry. options: 0=no bbox, 1=bbox, 2=short CRS. + // + // MEOS: geo_as_geojson(const GSERIALIZED *, int options, int precision, + // const char *srs) + // ------------------------------------------------------------------ + public static final UDF3 geoAsGeojson = + (wkt, options, precision) -> { + if (wkt == null) return null; + MeosThread.ensureReady(); + int opts = (options == null) ? 0 : options; + int prec = (precision == null) ? 9 : precision; + Pointer gptr = GeneratedFunctions.geo_from_text(wkt, 0); + if (gptr == null) return null; + try { + return GeneratedFunctions.geo_as_geojson(gptr, opts, prec, null); + } finally { + MeosMemory.free(gptr); + } + }; + + // ------------------------------------------------------------------ + // geoFromGeojson(geojson STRING) β†’ STRING (WKT) + // + // Parses GeoJSON and returns the geometry as WKT. + // + // MEOS: geo_from_geojson(const char *) β†’ GSERIALIZED * + // geo_as_text(const GSERIALIZED *, int precision) + // ------------------------------------------------------------------ + public static final UDF1 geoFromGeojson = + (geojson) -> { + if (geojson == null) return null; + MeosThread.ensureReady(); + Pointer gptr = GeneratedFunctions.geo_from_geojson(geojson); + if (gptr == null) return null; + try { + return GeneratedFunctions.geo_as_text(gptr, 15); + } finally { + MeosMemory.free(gptr); + } + }; +} diff --git a/src/main/java/org/mobilitydb/spark/geo/STBoxUDFs.java b/src/main/java/org/mobilitydb/spark/geo/STBoxUDFs.java new file mode 100644 index 00000000..d00b6cb8 --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/geo/STBoxUDFs.java @@ -0,0 +1,694 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.geo; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import jnr.ffi.Runtime; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosThread; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.api.java.UDF1; +import org.apache.spark.sql.api.java.UDF2; +import org.apache.spark.sql.api.java.UDF3; +import org.apache.spark.sql.api.java.UDF4; +import org.apache.spark.sql.api.java.UDF5; +import org.apache.spark.sql.api.java.UDF6; +import org.apache.spark.sql.api.java.UDF7; +import org.apache.spark.sql.types.DataTypes; + +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import org.mobilitydb.spark.util.TimeUtil; + +/** + * Spark SQL UDFs for STBox accessor and expansion operations. + * + * Storage convention: STBox values are stored as hex-WKB strings produced by + * stbox_as_hexwkb (which requires a non-null size_out scratch Pointer). + * + * Spatial bound accessors (xmin/xmax/ymin/ymax/zmin/zmax): the JMEOS wrapper + * allocates an 8-byte buffer, passes it as out-pointer to the C function which + * writes the double there, and returns the buffer Pointer (null = absent). + * Temporal bound accessors (tmin/tmax): same pattern, int64 PG-epoch ΞΌs. + * Inclusivity flag accessors (tmin_inc/tmax_inc): same pattern, byte (0/1). + * + * MEOS function authority: meos/include/meos_geo.h + */ +public final class STBoxUDFs { + + private STBoxUDFs() {} + + // milliseconds from Unix epoch (1970-01-01) to PG epoch (2000-01-01) + private static Pointer stboxPtr(String hex) { + if (hex == null) return null; + return GeneratedFunctions.stbox_from_hexwkb(hex); + } + + // stbox_as_hexwkb requires a non-null size_out scratch Pointer + private static String stboxHex(Pointer p) { + if (p == null) return null; + Pointer sizeOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + return GeneratedFunctions.stbox_as_hexwkb(p, (byte) 0, sizeOut); + } + + // ------------------------------------------------------------------ + // Has-component flags + // ------------------------------------------------------------------ + + public static final UDF1 stboxHasx = + (hex) -> { + MeosThread.ensureReady(); + Pointer p = stboxPtr(hex); + return p == null ? null : GeneratedFunctions.stbox_hasx(p); + }; + + public static final UDF1 stboxHast = + (hex) -> { + MeosThread.ensureReady(); + Pointer p = stboxPtr(hex); + return p == null ? null : GeneratedFunctions.stbox_hast(p); + }; + + public static final UDF1 stboxHasz = + (hex) -> { + MeosThread.ensureReady(); + Pointer p = stboxPtr(hex); + return p == null ? null : GeneratedFunctions.stbox_hasz(p); + }; + + // ------------------------------------------------------------------ + // Spatial bound accessors (Pointer β†’ double at offset 0) + // ------------------------------------------------------------------ + + public static final UDF1 stboxXmin = + (hex) -> { + MeosThread.ensureReady(); + Pointer p = stboxPtr(hex); + if (p == null) return null; + Pointer r = GeneratedFunctions.stbox_xmin(p); + return r == null ? null : r.getDouble(0); + }; + + public static final UDF1 stboxXmax = + (hex) -> { + MeosThread.ensureReady(); + Pointer p = stboxPtr(hex); + if (p == null) return null; + Pointer r = GeneratedFunctions.stbox_xmax(p); + return r == null ? null : r.getDouble(0); + }; + + public static final UDF1 stboxYmin = + (hex) -> { + MeosThread.ensureReady(); + Pointer p = stboxPtr(hex); + if (p == null) return null; + Pointer r = GeneratedFunctions.stbox_ymin(p); + return r == null ? null : r.getDouble(0); + }; + + public static final UDF1 stboxYmax = + (hex) -> { + MeosThread.ensureReady(); + Pointer p = stboxPtr(hex); + if (p == null) return null; + Pointer r = GeneratedFunctions.stbox_ymax(p); + return r == null ? null : r.getDouble(0); + }; + + public static final UDF1 stboxZmin = + (hex) -> { + MeosThread.ensureReady(); + Pointer p = stboxPtr(hex); + if (p == null) return null; + Pointer r = GeneratedFunctions.stbox_zmin(p); + return r == null ? null : r.getDouble(0); + }; + + public static final UDF1 stboxZmax = + (hex) -> { + MeosThread.ensureReady(); + Pointer p = stboxPtr(hex); + if (p == null) return null; + Pointer r = GeneratedFunctions.stbox_zmax(p); + return r == null ? null : r.getDouble(0); + }; + + // ------------------------------------------------------------------ + // Temporal bound accessors (Pointer β†’ int64 PG-epoch ΞΌs at offset 0) + // ------------------------------------------------------------------ + + public static final UDF1 stboxTmin = + (hex) -> { + MeosThread.ensureReady(); + Pointer p = stboxPtr(hex); + if (p == null) return null; + Pointer r = GeneratedFunctions.stbox_tmin(p); + if (r == null) return null; + return new java.sql.Timestamp(r.getLong(0) / 1000L + TimeUtil.PG_UNIX_EPOCH_OFFSET_MS); + }; + + public static final UDF1 stboxTmax = + (hex) -> { + MeosThread.ensureReady(); + Pointer p = stboxPtr(hex); + if (p == null) return null; + Pointer r = GeneratedFunctions.stbox_tmax(p); + if (r == null) return null; + return new java.sql.Timestamp(r.getLong(0) / 1000L + TimeUtil.PG_UNIX_EPOCH_OFFSET_MS); + }; + + // ------------------------------------------------------------------ + // Temporal inclusivity flags (Pointer β†’ byte at offset 0) + // ------------------------------------------------------------------ + + public static final UDF1 stboxTminInc = + (hex) -> { + MeosThread.ensureReady(); + Pointer p = stboxPtr(hex); + if (p == null) return null; + Pointer r = GeneratedFunctions.stbox_tmin_inc(p); + return r == null ? null : r.getByte(0) != 0; + }; + + public static final UDF1 stboxTmaxInc = + (hex) -> { + MeosThread.ensureReady(); + Pointer p = stboxPtr(hex); + if (p == null) return null; + Pointer r = GeneratedFunctions.stbox_tmax_inc(p); + return r == null ? null : r.getByte(0) != 0; + }; + + // ------------------------------------------------------------------ + // SRID + // ------------------------------------------------------------------ + + public static final UDF1 stboxSrid = + (hex) -> { + MeosThread.ensureReady(); + Pointer p = stboxPtr(hex); + return p == null ? null : GeneratedFunctions.stbox_srid(p); + }; + + // ------------------------------------------------------------------ + // Expansion operations + // ------------------------------------------------------------------ + + // stboxExpandSpace(stboxHex STRING, d DOUBLE) β†’ STRING + public static final UDF2 stboxExpandSpace = + (hex, d) -> { + if (hex == null || d == null) return null; + MeosThread.ensureReady(); + Pointer p = stboxPtr(hex); + if (p == null) return null; + Pointer r = GeneratedFunctions.stbox_expand_space(p, d); + return stboxHex(r); + }; + + // stboxExpandTime(stboxHex STRING, intervalStr STRING) β†’ STRING + public static final UDF2 stboxExpandTime = + (hex, intervalStr) -> { + if (hex == null || intervalStr == null) return null; + MeosThread.ensureReady(); + Pointer p = stboxPtr(hex); + if (p == null) return null; + Pointer iv = GeneratedFunctions.pg_interval_in(intervalStr, -1); + if (iv == null) return null; + Pointer r = GeneratedFunctions.stbox_expand_time(p, iv); + return stboxHex(r); + }; + + // ------------------------------------------------------------------ + // Spatial analytics (hex-WKB in, scalar out) + // + // MEOS: stbox_area(box, spheroid) meos_geo.h + // stbox_perimeter(box, spheroid) meos_geo.h + // stbox_volume(box) meos_geo.h + // ------------------------------------------------------------------ + + public static final UDF1 stboxArea = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = stboxPtr(hex); + if (p == null) return null; + return GeneratedFunctions.stbox_area(p, false); + }; + + public static final UDF1 stboxPerimeter = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = stboxPtr(hex); + if (p == null) return null; + return GeneratedFunctions.stbox_perimeter(p, false); + }; + + public static final UDF1 stboxVolume = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = stboxPtr(hex); + if (p == null) return null; + return GeneratedFunctions.stbox_volume(p); + }; + + // stboxIsGeodetic(hex) β†’ Boolean + public static final UDF1 stboxIsGeodetic = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = stboxPtr(hex); + if (p == null) return null; + return GeneratedFunctions.stbox_isgeodetic(p); + }; + + // stboxToGeo(hex) β†’ WKT of the bounding envelope polygon + public static final UDF1 stboxToGeo = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = stboxPtr(hex); + if (p == null) return null; + Pointer g = GeneratedFunctions.stbox_to_geo(p); + if (g == null) return null; + return GeneratedFunctions.geo_as_text(g, 15); + }; + + // stboxToTstzspan(hex) β†’ tstzspan hex-WKB + public static final UDF1 stboxToTstzspan = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = stboxPtr(hex); + if (p == null) return null; + Pointer span = GeneratedFunctions.stbox_to_tstzspan(p); + if (span == null) return null; + return GeneratedFunctions.span_as_hexwkb(span, (byte) 0); + }; + + // ------------------------------------------------------------------ + // Rounding + // ------------------------------------------------------------------ + + // stboxRound(hex STRING, maxDecimals INT) β†’ STRING + // MEOS: stbox_round(const STBox *, int) β†’ STBox * + public static final UDF2 stboxRound = + (hex, maxDecimals) -> { + if (hex == null || maxDecimals == null) return null; + MeosThread.ensureReady(); + Pointer p = stboxPtr(hex); + if (p == null) return null; + Pointer result = GeneratedFunctions.stbox_round(p, maxDecimals); + if (result == null) return null; + try { + return stboxHex(result); + } finally { + MeosMemory.free(result); + } + }; + + // ------------------------------------------------------------------ + // SRID assignment + // ------------------------------------------------------------------ + + // stboxSetSrid(hex STRING, srid INT) β†’ STRING + // MEOS: stbox_set_srid(const STBox *, int) β†’ STBox * + public static final UDF2 stboxSetSrid = + (hex, srid) -> { + if (hex == null || srid == null) return null; + MeosThread.ensureReady(); + Pointer p = stboxPtr(hex); + if (p == null) return null; + Pointer result = GeneratedFunctions.stbox_set_srid(p, srid); + if (result == null) return null; + try { + return stboxHex(result); + } finally { + MeosMemory.free(result); + } + }; + + // ------------------------------------------------------------------ + // Time-domain shifting and scaling + // ------------------------------------------------------------------ + + // stboxShiftScaleTime(hex STRING, shift STRING, scale STRING) β†’ STRING + // MEOS: stbox_shift_scale_time(const STBox *, Interval *, Interval *) β†’ STBox * + // Either shift or scale may be null (pass null to MEOS for no-op). + public static final UDF3 stboxShiftScaleTime = + (hex, shift, scale) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = stboxPtr(hex); + if (p == null) return null; + Pointer shiftIv = shift == null ? null : GeneratedFunctions.pg_interval_in(shift, Integer.MIN_VALUE); + Pointer scaleIv = scale == null ? null : GeneratedFunctions.pg_interval_in(scale, Integer.MIN_VALUE); + try { + Pointer result = GeneratedFunctions.stbox_shift_scale_time(p, shiftIv, scaleIv); + if (result == null) return null; + try { + return stboxHex(result); + } finally { + MeosMemory.free(result); + } + } finally { + if (shiftIv != null) MeosMemory.free(shiftIv); + if (scaleIv != null) MeosMemory.free(scaleIv); + } + }; + + // ------------------------------------------------------------------ + // STBox constructors from geometry / span / timestamptz + // + // MEOS: geo_to_stbox, tstzspan_to_stbox, timestamptz_to_stbox + // ------------------------------------------------------------------ + + // geoToStbox(wkt STRING) β†’ STBox hex-WKB + // Creates an STBox with the bounding box of a geometry. + // MEOS: geo_to_stbox(const GSERIALIZED *) β†’ STBox * + public static final UDF1 geoToStbox = + (wkt) -> { + if (wkt == null) return null; + MeosThread.ensureReady(); + Pointer geo = GeneratedFunctions.geo_from_text(wkt, 0); + if (geo == null) return null; + Pointer result = GeneratedFunctions.geo_to_stbox(geo); + if (result == null) return null; + try { + return stboxHex(result); + } finally { + MeosMemory.free(result); + } + }; + + // tstzspanToStbox(spanHex STRING) β†’ STBox hex-WKB + // Creates a time-only STBox from a tstzspan. + // MEOS: tstzspan_to_stbox(const Span *) β†’ STBox * + public static final UDF1 tstzspanToStbox = + (spanHex) -> { + if (spanHex == null) return null; + MeosThread.ensureReady(); + Pointer span = GeneratedFunctions.span_from_hexwkb(spanHex); + if (span == null) return null; + Pointer result = GeneratedFunctions.tstzspan_to_stbox(span); + if (result == null) return null; + try { + return stboxHex(result); + } finally { + MeosMemory.free(result); + } + }; + + // timestamptzToStbox(ts TIMESTAMP) β†’ STBox hex-WKB + // Creates a point-time STBox from a single timestamp. + // MEOS: timestamptz_to_stbox(TimestampTz) β†’ STBox * + public static final UDF1 timestamptzToStbox = + (ts) -> { + if (ts == null) return null; + MeosThread.ensureReady(); + long pgMicros = (ts.getTime() - TimeUtil.PG_UNIX_EPOCH_OFFSET_MS) * 1000L; + OffsetDateTime odt = OffsetDateTime.ofInstant(Instant.ofEpochSecond(pgMicros, 0), ZoneOffset.UTC); + Pointer result = GeneratedFunctions.timestamptz_to_stbox(odt); + if (result == null) return null; + try { + return stboxHex(result); + } finally { + MeosMemory.free(result); + } + }; + + // ------------------------------------------------------------------ + // Spatial component extraction + // + // MEOS: stbox_get_space(const STBox *) β†’ STBox * (spatial dims only, no T) + // ------------------------------------------------------------------ + + public static final UDF1 stboxGetSpace = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = stboxPtr(hex); + if (p == null) return null; + Pointer result = GeneratedFunctions.stbox_get_space(p); + if (result == null) return null; + try { + return stboxHex(result); + } finally { + MeosMemory.free(result); + } + }; + + public static void registerAll(SparkSession spark) { + spark.udf().register("stboxHasx", stboxHasx, DataTypes.BooleanType); + spark.udf().register("stboxHast", stboxHast, DataTypes.BooleanType); + spark.udf().register("stboxHasz", stboxHasz, DataTypes.BooleanType); + spark.udf().register("stboxXmin", stboxXmin, DataTypes.DoubleType); + spark.udf().register("stboxXmax", stboxXmax, DataTypes.DoubleType); + spark.udf().register("stboxYmin", stboxYmin, DataTypes.DoubleType); + spark.udf().register("stboxYmax", stboxYmax, DataTypes.DoubleType); + spark.udf().register("stboxZmin", stboxZmin, DataTypes.DoubleType); + spark.udf().register("stboxZmax", stboxZmax, DataTypes.DoubleType); + spark.udf().register("stboxTmin", stboxTmin, DataTypes.TimestampType); + spark.udf().register("stboxTmax", stboxTmax, DataTypes.TimestampType); + spark.udf().register("stboxTminInc", stboxTminInc, DataTypes.BooleanType); + spark.udf().register("stboxTmaxInc", stboxTmaxInc, DataTypes.BooleanType); + spark.udf().register("stboxSrid", stboxSrid, DataTypes.IntegerType); + spark.udf().register("stboxExpandSpace", stboxExpandSpace, DataTypes.StringType); + spark.udf().register("stboxExpandTime", stboxExpandTime, DataTypes.StringType); + spark.udf().register("stboxArea", stboxArea, DataTypes.DoubleType); + spark.udf().register("stboxPerimeter", stboxPerimeter, DataTypes.DoubleType); + spark.udf().register("stboxVolume", stboxVolume, DataTypes.DoubleType); + spark.udf().register("stboxIsGeodetic", stboxIsGeodetic, DataTypes.BooleanType); + spark.udf().register("stboxToGeo", stboxToGeo, DataTypes.StringType); + spark.udf().register("stboxToTstzspan", stboxToTstzspan, DataTypes.StringType); + spark.udf().register("stboxRound", stboxRound, DataTypes.StringType); + spark.udf().register("stboxSetSrid", stboxSetSrid, DataTypes.StringType); + spark.udf().register("stboxShiftScaleTime", stboxShiftScaleTime, DataTypes.StringType); + spark.udf().register("stboxGetSpace", stboxGetSpace, DataTypes.StringType); + // STBox constructors from geometry / span / timestamp + spark.udf().register("geoToStbox", geoToStbox, DataTypes.StringType); + spark.udf().register("tstzspanToStbox", tstzspanToStbox, DataTypes.StringType); + spark.udf().register("timestamptzToStbox", timestamptzToStbox, DataTypes.StringType); + // STBox set operations + spark.udf().register("intersectionStboxStbox", intersectionStboxStbox, DataTypes.StringType); + spark.udf().register("unionStboxStbox", unionStboxStbox, DataTypes.StringType); + // MobilityDB SQL bare-name aliases for the same lambdas + spark.udf().register("stboxIntersection", intersectionStboxStbox, DataTypes.StringType); + spark.udf().register("stboxUnion", unionStboxStbox, DataTypes.StringType); + // Typed STBox constructors + spark.udf().register("stboxX", stboxX, DataTypes.StringType); + spark.udf().register("stboxT", stboxT, DataTypes.StringType); + spark.udf().register("stboxXT", stboxXT, DataTypes.StringType); + spark.udf().register("stboxZ", stboxZ, DataTypes.StringType); + spark.udf().register("stboxZT", stboxZT, DataTypes.StringType); + spark.udf().register("geodstboxZ", geodstboxZ, DataTypes.StringType); + spark.udf().register("geodstboxT", geodstboxT, DataTypes.StringType); + spark.udf().register("geodstboxZT", geodstboxZT, DataTypes.StringType); + // STBox topology predicates (stbox, stbox) + spark.udf().register("stboxContains", stboxContains, DataTypes.BooleanType); + spark.udf().register("stboxContained", stboxContained, DataTypes.BooleanType); + spark.udf().register("stboxOverlaps", stboxOverlaps, DataTypes.BooleanType); + // STBox positional predicates (stbox, stbox) + spark.udf().register("stboxLeft", stboxLeft, DataTypes.BooleanType); + spark.udf().register("stboxOverleft", stboxOverleft, DataTypes.BooleanType); + spark.udf().register("stboxRight", stboxRight, DataTypes.BooleanType); + spark.udf().register("stboxOverright", stboxOverright, DataTypes.BooleanType); + spark.udf().register("stboxBelow", stboxBelow, DataTypes.BooleanType); + spark.udf().register("stboxOverbelow", stboxOverbelow, DataTypes.BooleanType); + spark.udf().register("stboxAbove", stboxAbove, DataTypes.BooleanType); + spark.udf().register("stboxOverabove", stboxOverabove, DataTypes.BooleanType); + spark.udf().register("stboxBefore", stboxBefore, DataTypes.BooleanType); + spark.udf().register("stboxOverbefore", stboxOverbefore, DataTypes.BooleanType); + spark.udf().register("stboxAfter", stboxAfter, DataTypes.BooleanType); + spark.udf().register("stboxOverafter", stboxOverafter, DataTypes.BooleanType); + spark.udf().register("stboxAdjacent", stboxAdjacent, DataTypes.BooleanType); + } + + // ------------------------------------------------------------------ + // STBox set operations + // MEOS: intersection_stbox_stbox(STBox *, STBox *) β†’ STBox * (NULL if empty) + // union_stbox_stbox(STBox *, STBox *, bool strict) β†’ STBox * + // ------------------------------------------------------------------ + + private static String stboxBinOp(String h1, String h2, + java.util.function.BiFunction fn) { + if (h1 == null || h2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = stboxPtr(h1), p2 = stboxPtr(h2); + if (p1 == null || p2 == null) return null; + try { + Pointer r = fn.apply(p1, p2); + if (r == null) return null; + try { + return stboxHex(r); + } finally { MeosMemory.free(r); } + } finally { + MeosMemory.free(p1); + MeosMemory.free(p2); + } + } + + public static final UDF2 intersectionStboxStbox = + (h1, h2) -> stboxBinOp(h1, h2, GeneratedFunctions::intersection_stbox_stbox); + + public static final UDF2 unionStboxStbox = + (h1, h2) -> stboxBinOp(h1, h2, (p1, p2) -> GeneratedFunctions.union_stbox_stbox(p1, p2, false)); + + // ------------------------------------------------------------------ + // STBox positional predicates (stbox, stbox) β†’ Boolean + // MEOS: left/overleft/right/overright/below/overbelow/above/overabove/ + // before/overbefore/after/overafter/adjacent_stbox_stbox β†’ bool + // ------------------------------------------------------------------ + + private static Boolean stboxBoolOp(String h1, String h2, + java.util.function.BiFunction fn) { + if (h1 == null || h2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = stboxPtr(h1), p2 = stboxPtr(h2); + if (p1 == null || p2 == null) return null; + return fn.apply(p1, p2); + } + + // ------------------------------------------------------------------ + // STBox topology predicates (stbox, stbox) β†’ Boolean + // MEOS: contains/contained/overlaps_stbox_stbox β†’ bool + // ------------------------------------------------------------------ + + public static final UDF2 stboxContains = + (h1, h2) -> stboxBoolOp(h1, h2, GeneratedFunctions::contains_stbox_stbox); + public static final UDF2 stboxContained = + (h1, h2) -> stboxBoolOp(h1, h2, GeneratedFunctions::contained_stbox_stbox); + public static final UDF2 stboxOverlaps = + (h1, h2) -> stboxBoolOp(h1, h2, GeneratedFunctions::overlaps_stbox_stbox); + + public static final UDF2 stboxLeft = + (h1, h2) -> stboxBoolOp(h1, h2, GeneratedFunctions::left_stbox_stbox); + public static final UDF2 stboxOverleft = + (h1, h2) -> stboxBoolOp(h1, h2, GeneratedFunctions::overleft_stbox_stbox); + public static final UDF2 stboxRight = + (h1, h2) -> stboxBoolOp(h1, h2, GeneratedFunctions::right_stbox_stbox); + public static final UDF2 stboxOverright = + (h1, h2) -> stboxBoolOp(h1, h2, GeneratedFunctions::overright_stbox_stbox); + public static final UDF2 stboxBelow = + (h1, h2) -> stboxBoolOp(h1, h2, GeneratedFunctions::below_stbox_stbox); + public static final UDF2 stboxOverbelow = + (h1, h2) -> stboxBoolOp(h1, h2, GeneratedFunctions::overbelow_stbox_stbox); + public static final UDF2 stboxAbove = + (h1, h2) -> stboxBoolOp(h1, h2, GeneratedFunctions::above_stbox_stbox); + public static final UDF2 stboxOverabove = + (h1, h2) -> stboxBoolOp(h1, h2, GeneratedFunctions::overabove_stbox_stbox); + public static final UDF2 stboxBefore = + (h1, h2) -> stboxBoolOp(h1, h2, GeneratedFunctions::before_stbox_stbox); + public static final UDF2 stboxOverbefore = + (h1, h2) -> stboxBoolOp(h1, h2, GeneratedFunctions::overbefore_stbox_stbox); + public static final UDF2 stboxAfter = + (h1, h2) -> stboxBoolOp(h1, h2, GeneratedFunctions::after_stbox_stbox); + public static final UDF2 stboxOverafter = + (h1, h2) -> stboxBoolOp(h1, h2, GeneratedFunctions::overafter_stbox_stbox); + public static final UDF2 stboxAdjacent = + (h1, h2) -> stboxBoolOp(h1, h2, GeneratedFunctions::adjacent_stbox_stbox); + + // ------------------------------------------------------------------ + // Typed STBox constructors β€” delegate to stbox_make with the correct + // hasx/hasz/geodetic/srid flags. tstzspan input is hex-WKB. + // MEOS: stbox_make(hasx, hasz, geodetic, srid, xmin, ymin, zmin, xmax, + // ymax, zmax, periodPtr) β†’ STBox* + // ------------------------------------------------------------------ + + private static String stboxMakeHelper(boolean hasx, boolean hasz, boolean geodetic, int srid, + double xmin, double ymin, double zmin, double xmax, double ymax, double zmax, + String tstzspanHex) { + MeosThread.ensureReady(); + Pointer period = (tstzspanHex == null) ? null : GeneratedFunctions.span_from_hexwkb(tstzspanHex); + try { + Pointer p = GeneratedFunctions.stbox_make(hasx, hasz, geodetic, srid, + xmin, ymin, zmin, xmax, ymax, zmax, period); + if (p == null) return null; + try { + Pointer sizeOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + return GeneratedFunctions.stbox_as_hexwkb(p, (byte) 0, sizeOut); + } finally { MeosMemory.free(p); } + } finally { if (period != null) MeosMemory.free(period); } + } + + // stboxX(xmin, ymin, xmax, ymax) β€” 2D spatial box, no time, no geodetic + public static final UDF4 stboxX = + (xmin, ymin, xmax, ymax) -> { + if (xmin == null || ymin == null || xmax == null || ymax == null) return null; + return stboxMakeHelper(true, false, false, 0, xmin, ymin, 0, xmax, ymax, 0, null); + }; + + // stboxT(tstzspanHex) β€” time-only box + public static final UDF1 stboxT = + tstzspanHex -> { + if (tstzspanHex == null) return null; + return stboxMakeHelper(false, false, false, 0, 0, 0, 0, 0, 0, 0, tstzspanHex); + }; + + // stboxXT(xmin, ymin, xmax, ymax, tstzspanHex) + public static final UDF5 stboxXT = + (xmin, ymin, xmax, ymax, tstzspanHex) -> { + if (xmin == null || ymin == null || xmax == null || ymax == null || tstzspanHex == null) return null; + return stboxMakeHelper(true, false, false, 0, xmin, ymin, 0, xmax, ymax, 0, tstzspanHex); + }; + + // stboxZ(xmin, ymin, zmin, xmax, ymax, zmax) β€” 3D spatial box + public static final UDF6 stboxZ = + (xmin, ymin, zmin, xmax, ymax, zmax) -> { + if (xmin == null || ymin == null || zmin == null || xmax == null || ymax == null || zmax == null) return null; + return stboxMakeHelper(true, true, false, 0, xmin, ymin, zmin, xmax, ymax, zmax, null); + }; + + // stboxZT(xmin, ymin, zmin, xmax, ymax, zmax, tstzspanHex) β€” 3D + time + public static final UDF7 stboxZT = + (xmin, ymin, zmin, xmax, ymax, zmax, tstzspanHex) -> { + if (xmin == null || ymin == null || zmin == null || xmax == null || ymax == null || zmax == null || tstzspanHex == null) return null; + return stboxMakeHelper(true, true, false, 0, xmin, ymin, zmin, xmax, ymax, zmax, tstzspanHex); + }; + + // Geodetic variants β€” geodetic=true, default SRID 4326 + public static final UDF6 geodstboxZ = + (xmin, ymin, zmin, xmax, ymax, zmax) -> { + if (xmin == null || ymin == null || zmin == null || xmax == null || ymax == null || zmax == null) return null; + return stboxMakeHelper(true, true, true, 4326, xmin, ymin, zmin, xmax, ymax, zmax, null); + }; + + public static final UDF1 geodstboxT = + tstzspanHex -> { + if (tstzspanHex == null) return null; + return stboxMakeHelper(false, false, true, 4326, 0, 0, 0, 0, 0, 0, tstzspanHex); + }; + + public static final UDF7 geodstboxZT = + (xmin, ymin, zmin, xmax, ymax, zmax, tstzspanHex) -> { + if (xmin == null || ymin == null || zmin == null || xmax == null || ymax == null || zmax == null || tstzspanHex == null) return null; + return stboxMakeHelper(true, true, true, 4326, xmin, ymin, zmin, xmax, ymax, zmax, tstzspanHex); + }; +} diff --git a/src/main/java/org/mobilitydb/spark/geo/StaticGeoUDFs.java b/src/main/java/org/mobilitydb/spark/geo/StaticGeoUDFs.java new file mode 100644 index 00000000..95aa8d87 --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/geo/StaticGeoUDFs.java @@ -0,0 +1,409 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.geo; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.api.java.UDF1; +import org.apache.spark.sql.api.java.UDF2; +import org.apache.spark.sql.api.java.UDF3; +import org.apache.spark.sql.types.DataTypes; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosThread; + +/** + * Spark SQL UDFs for static (non-temporal) geometry operations. + * + * All inputs and WKT/WKB outputs use WKT string encoding via geo_from_text / + * geo_as_text. Scalar outputs (Double, Boolean) are returned as Java primitives. + * + * Memory management: every Pointer returned by MEOS must be freed via + * MeosMemory.free() in a finally block. + * + * MEOS function authority: meos/include/meos_geo.h + */ +public final class StaticGeoUDFs { + + private StaticGeoUDFs() {} + + // ------------------------------------------------------------------ + // Geometry predicates (WKT Γ— WKT β†’ Boolean) + // ------------------------------------------------------------------ + + public static final UDF2 geomContains = + (wkt1, wkt2) -> { + if (wkt1 == null || wkt2 == null) return null; + MeosThread.ensureReady(); + Pointer g1 = GeneratedFunctions.geo_from_text(wkt1, 0); + if (g1 == null) return null; + try { + Pointer g2 = GeneratedFunctions.geo_from_text(wkt2, 0); + if (g2 == null) return null; + try { + return GeneratedFunctions.geom_contains(g1, g2); + } finally { + MeosMemory.free(g2); + } + } finally { + MeosMemory.free(g1); + } + }; + + public static final UDF2 geomCovers = + (wkt1, wkt2) -> { + if (wkt1 == null || wkt2 == null) return null; + MeosThread.ensureReady(); + Pointer g1 = GeneratedFunctions.geo_from_text(wkt1, 0); + if (g1 == null) return null; + try { + Pointer g2 = GeneratedFunctions.geo_from_text(wkt2, 0); + if (g2 == null) return null; + try { + return GeneratedFunctions.geom_covers(g1, g2); + } finally { + MeosMemory.free(g2); + } + } finally { + MeosMemory.free(g1); + } + }; + + public static final UDF2 geomDisjoint = + (wkt1, wkt2) -> { + if (wkt1 == null || wkt2 == null) return null; + MeosThread.ensureReady(); + Pointer g1 = GeneratedFunctions.geo_from_text(wkt1, 0); + if (g1 == null) return null; + try { + Pointer g2 = GeneratedFunctions.geo_from_text(wkt2, 0); + if (g2 == null) return null; + try { + return GeneratedFunctions.geom_disjoint2d(g1, g2); + } finally { + MeosMemory.free(g2); + } + } finally { + MeosMemory.free(g1); + } + }; + + public static final UDF2 geomIntersects = + (wkt1, wkt2) -> { + if (wkt1 == null || wkt2 == null) return null; + MeosThread.ensureReady(); + Pointer g1 = GeneratedFunctions.geo_from_text(wkt1, 0); + if (g1 == null) return null; + try { + Pointer g2 = GeneratedFunctions.geo_from_text(wkt2, 0); + if (g2 == null) return null; + try { + return GeneratedFunctions.geom_intersects2d(g1, g2); + } finally { + MeosMemory.free(g2); + } + } finally { + MeosMemory.free(g1); + } + }; + + public static final UDF2 geomTouches = + (wkt1, wkt2) -> { + if (wkt1 == null || wkt2 == null) return null; + MeosThread.ensureReady(); + Pointer g1 = GeneratedFunctions.geo_from_text(wkt1, 0); + if (g1 == null) return null; + try { + Pointer g2 = GeneratedFunctions.geo_from_text(wkt2, 0); + if (g2 == null) return null; + try { + return GeneratedFunctions.geom_touches(g1, g2); + } finally { + MeosMemory.free(g2); + } + } finally { + MeosMemory.free(g1); + } + }; + + public static final UDF3 geomDwithin = + (wkt1, wkt2, dist) -> { + if (wkt1 == null || wkt2 == null || dist == null) return null; + MeosThread.ensureReady(); + Pointer g1 = GeneratedFunctions.geo_from_text(wkt1, 0); + if (g1 == null) return null; + try { + Pointer g2 = GeneratedFunctions.geo_from_text(wkt2, 0); + if (g2 == null) return null; + try { + return GeneratedFunctions.geom_dwithin2d(g1, g2, dist.doubleValue()); + } finally { + MeosMemory.free(g2); + } + } finally { + MeosMemory.free(g1); + } + }; + + // ------------------------------------------------------------------ + // Geometry metrics (WKT β†’ Double) + // ------------------------------------------------------------------ + + public static final UDF2 geomDistance = + (wkt1, wkt2) -> { + if (wkt1 == null || wkt2 == null) return null; + MeosThread.ensureReady(); + Pointer g1 = GeneratedFunctions.geo_from_text(wkt1, 0); + if (g1 == null) return null; + try { + Pointer g2 = GeneratedFunctions.geo_from_text(wkt2, 0); + if (g2 == null) return null; + try { + return GeneratedFunctions.geom_distance2d(g1, g2); + } finally { + MeosMemory.free(g2); + } + } finally { + MeosMemory.free(g1); + } + }; + + public static final UDF1 geomLength = + (wkt) -> { + if (wkt == null) return null; + MeosThread.ensureReady(); + Pointer g = GeneratedFunctions.geo_from_text(wkt, 0); + if (g == null) return null; + try { + return GeneratedFunctions.geom_length(g); + } finally { + MeosMemory.free(g); + } + }; + + public static final UDF1 geomPerimeter = + (wkt) -> { + if (wkt == null) return null; + MeosThread.ensureReady(); + Pointer g = GeneratedFunctions.geo_from_text(wkt, 0); + if (g == null) return null; + try { + return GeneratedFunctions.geom_perimeter(g); + } finally { + MeosMemory.free(g); + } + }; + + // ------------------------------------------------------------------ + // Geometry transforms (WKT β†’ WKT) + // ------------------------------------------------------------------ + + public static final UDF1 geomCentroid = + (wkt) -> { + if (wkt == null) return null; + MeosThread.ensureReady(); + Pointer g = GeneratedFunctions.geo_from_text(wkt, 0); + if (g == null) return null; + try { + Pointer r = GeneratedFunctions.geom_centroid(g); + if (r == null) return null; + try { + return GeneratedFunctions.geo_as_text(r, 15); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(g); + } + }; + + public static final UDF1 geomBoundary = + (wkt) -> { + if (wkt == null) return null; + MeosThread.ensureReady(); + Pointer g = GeneratedFunctions.geo_from_text(wkt, 0); + if (g == null) return null; + try { + Pointer r = GeneratedFunctions.geom_boundary(g); + if (r == null) return null; + try { + return GeneratedFunctions.geo_as_text(r, 15); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(g); + } + }; + + public static final UDF2 geomDifference = + (wkt1, wkt2) -> { + if (wkt1 == null || wkt2 == null) return null; + MeosThread.ensureReady(); + Pointer g1 = GeneratedFunctions.geo_from_text(wkt1, 0); + if (g1 == null) return null; + try { + Pointer g2 = GeneratedFunctions.geo_from_text(wkt2, 0); + if (g2 == null) return null; + try { + Pointer r = GeneratedFunctions.geom_difference2d(g1, g2); + if (r == null) return null; + try { + return GeneratedFunctions.geo_as_text(r, 15); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(g2); + } + } finally { + MeosMemory.free(g1); + } + }; + + public static final UDF2 geomUnaryUnion = + (wkt, prec) -> { + if (wkt == null || prec == null) return null; + MeosThread.ensureReady(); + Pointer g = GeneratedFunctions.geo_from_text(wkt, 0); + if (g == null) return null; + try { + Pointer r = GeneratedFunctions.geom_unary_union(g, prec); + if (r == null) return null; + try { + return GeneratedFunctions.geo_as_text(r, 15); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(g); + } + }; + + public static final UDF1 geoReverse = + (wkt) -> { + if (wkt == null) return null; + MeosThread.ensureReady(); + Pointer g = GeneratedFunctions.geo_from_text(wkt, 0); + if (g == null) return null; + try { + Pointer r = GeneratedFunctions.geo_reverse(g); + if (r == null) return null; + try { + return GeneratedFunctions.geo_as_text(r, 15); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(g); + } + }; + + public static final UDF2 geoRound = + (wkt, maxdd) -> { + if (wkt == null || maxdd == null) return null; + MeosThread.ensureReady(); + Pointer g = GeneratedFunctions.geo_from_text(wkt, 0); + if (g == null) return null; + try { + Pointer r = GeneratedFunctions.geo_round(g, maxdd); + if (r == null) return null; + try { + return GeneratedFunctions.geo_as_text(r, maxdd); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(g); + } + }; + + // ------------------------------------------------------------------ + // Line functions (WKT β†’ WKT) + // ------------------------------------------------------------------ + + public static final UDF2 lineInterpolatePoint = + (wkt, fraction) -> { + if (wkt == null || fraction == null) return null; + MeosThread.ensureReady(); + Pointer g = GeneratedFunctions.geo_from_text(wkt, 0); + if (g == null) return null; + try { + Pointer r = GeneratedFunctions.line_interpolate_point(g, fraction, false); + if (r == null) return null; + try { + return GeneratedFunctions.geo_as_text(r, 15); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(g); + } + }; + + public static final UDF3 lineSubstring = + (wkt, from, to) -> { + if (wkt == null || from == null || to == null) return null; + MeosThread.ensureReady(); + Pointer g = GeneratedFunctions.geo_from_text(wkt, 0); + if (g == null) return null; + try { + Pointer r = GeneratedFunctions.line_substring(g, from, to); + if (r == null) return null; + try { + return GeneratedFunctions.geo_as_text(r, 15); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(g); + } + }; + + // ------------------------------------------------------------------ + // Registration + // ------------------------------------------------------------------ + + public static void registerAll(SparkSession spark) { + spark.udf().register("geomContains", geomContains, DataTypes.BooleanType); + spark.udf().register("geomCovers", geomCovers, DataTypes.BooleanType); + spark.udf().register("geomDisjoint", geomDisjoint, DataTypes.BooleanType); + spark.udf().register("geomIntersects", geomIntersects, DataTypes.BooleanType); + spark.udf().register("geomTouches", geomTouches, DataTypes.BooleanType); + spark.udf().register("geomDwithin", geomDwithin, DataTypes.BooleanType); + spark.udf().register("geomDistance", geomDistance, DataTypes.DoubleType); + spark.udf().register("geomLength", geomLength, DataTypes.DoubleType); + spark.udf().register("geomPerimeter", geomPerimeter, DataTypes.DoubleType); + spark.udf().register("geomCentroid", geomCentroid, DataTypes.StringType); + spark.udf().register("geomBoundary", geomBoundary, DataTypes.StringType); + spark.udf().register("geomDifference", geomDifference, DataTypes.StringType); + spark.udf().register("geomUnaryUnion", geomUnaryUnion, DataTypes.StringType); + spark.udf().register("geoReverse", geoReverse, DataTypes.StringType); + spark.udf().register("geoRound", geoRound, DataTypes.StringType); + spark.udf().register("lineInterpolatePoint", lineInterpolatePoint, DataTypes.StringType); + spark.udf().register("lineSubstring", lineSubstring, DataTypes.StringType); + } +} diff --git a/src/main/java/org/mobilitydb/spark/geo/TPointSTBoxOpsUDFs.java b/src/main/java/org/mobilitydb/spark/geo/TPointSTBoxOpsUDFs.java new file mode 100644 index 00000000..841e5c38 --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/geo/TPointSTBoxOpsUDFs.java @@ -0,0 +1,519 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.geo; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosNative; +import org.mobilitydb.spark.MeosThread; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.api.java.UDF2; +import org.apache.spark.sql.types.DataTypes; + +/** + * Spark SQL UDFs for cross-type positional and topological predicates between + * STBox and tgeopoint (tspatial). + * + * MEOS function authority: meos/include/meos_geo.h + * + * Naming: stbox = hex-WKB STBox, tpoint = hex-WKB tgeopoint. + * All predicates return Boolean (null if either input is null or invalid). + */ +public final class TPointSTBoxOpsUDFs { + + private TPointSTBoxOpsUDFs() {} + + // ------------------------------------------------------------------ + // Helper: deserialize STBox from hex-WKB, check null + // ------------------------------------------------------------------ + + private static Pointer stboxPtr(String hex) { + return hex == null ? null : GeneratedFunctions.stbox_from_hexwkb(hex); + } + + private static Pointer tpointPtr(String hex) { + return hex == null ? null : GeneratedFunctions.temporal_from_hexwkb(hex); + } + + // ------------------------------------------------------------------ + // STBox Γ— TPoint β€” spatial direction + // ------------------------------------------------------------------ + + public static final UDF2 stboxLeftTpoint = + (sb, tp) -> { + MeosThread.ensureReady(); + Pointer s = stboxPtr(sb); if (s == null) return null; + Pointer t = tpointPtr(tp); if (t == null) { MeosMemory.free(s); return null; } + try { return MeosNative.INSTANCE.left_stbox_tspatial(s, t); } + finally { MeosMemory.free(s, t); } + }; + + public static final UDF2 stboxOverleftTpoint = + (sb, tp) -> { + MeosThread.ensureReady(); + Pointer s = stboxPtr(sb); if (s == null) return null; + Pointer t = tpointPtr(tp); if (t == null) { MeosMemory.free(s); return null; } + try { return MeosNative.INSTANCE.overleft_stbox_tspatial(s, t); } + finally { MeosMemory.free(s, t); } + }; + + public static final UDF2 stboxRightTpoint = + (sb, tp) -> { + MeosThread.ensureReady(); + Pointer s = stboxPtr(sb); if (s == null) return null; + Pointer t = tpointPtr(tp); if (t == null) { MeosMemory.free(s); return null; } + try { return MeosNative.INSTANCE.right_stbox_tspatial(s, t); } + finally { MeosMemory.free(s, t); } + }; + + public static final UDF2 stboxOverrightTpoint = + (sb, tp) -> { + MeosThread.ensureReady(); + Pointer s = stboxPtr(sb); if (s == null) return null; + Pointer t = tpointPtr(tp); if (t == null) { MeosMemory.free(s); return null; } + try { return MeosNative.INSTANCE.overright_stbox_tspatial(s, t); } + finally { MeosMemory.free(s, t); } + }; + + public static final UDF2 stboxBelowTpoint = + (sb, tp) -> { + MeosThread.ensureReady(); + Pointer s = stboxPtr(sb); if (s == null) return null; + Pointer t = tpointPtr(tp); if (t == null) { MeosMemory.free(s); return null; } + try { return MeosNative.INSTANCE.below_stbox_tspatial(s, t); } + finally { MeosMemory.free(s, t); } + }; + + public static final UDF2 stboxOverbelowTpoint = + (sb, tp) -> { + MeosThread.ensureReady(); + Pointer s = stboxPtr(sb); if (s == null) return null; + Pointer t = tpointPtr(tp); if (t == null) { MeosMemory.free(s); return null; } + try { return MeosNative.INSTANCE.overbelow_stbox_tspatial(s, t); } + finally { MeosMemory.free(s, t); } + }; + + public static final UDF2 stboxAboveTpoint = + (sb, tp) -> { + MeosThread.ensureReady(); + Pointer s = stboxPtr(sb); if (s == null) return null; + Pointer t = tpointPtr(tp); if (t == null) { MeosMemory.free(s); return null; } + try { return MeosNative.INSTANCE.above_stbox_tspatial(s, t); } + finally { MeosMemory.free(s, t); } + }; + + public static final UDF2 stboxOveraboveTpoint = + (sb, tp) -> { + MeosThread.ensureReady(); + Pointer s = stboxPtr(sb); if (s == null) return null; + Pointer t = tpointPtr(tp); if (t == null) { MeosMemory.free(s); return null; } + try { return MeosNative.INSTANCE.overabove_stbox_tspatial(s, t); } + finally { MeosMemory.free(s, t); } + }; + + public static final UDF2 stboxFrontTpoint = + (sb, tp) -> { + MeosThread.ensureReady(); + Pointer s = stboxPtr(sb); if (s == null) return null; + Pointer t = tpointPtr(tp); if (t == null) { MeosMemory.free(s); return null; } + try { return MeosNative.INSTANCE.front_stbox_tspatial(s, t); } + finally { MeosMemory.free(s, t); } + }; + + public static final UDF2 stboxOverfrontTpoint = + (sb, tp) -> { + MeosThread.ensureReady(); + Pointer s = stboxPtr(sb); if (s == null) return null; + Pointer t = tpointPtr(tp); if (t == null) { MeosMemory.free(s); return null; } + try { return MeosNative.INSTANCE.overfront_stbox_tspatial(s, t); } + finally { MeosMemory.free(s, t); } + }; + + public static final UDF2 stboxBackTpoint = + (sb, tp) -> { + MeosThread.ensureReady(); + Pointer s = stboxPtr(sb); if (s == null) return null; + Pointer t = tpointPtr(tp); if (t == null) { MeosMemory.free(s); return null; } + try { return MeosNative.INSTANCE.back_stbox_tspatial(s, t); } + finally { MeosMemory.free(s, t); } + }; + + public static final UDF2 stboxOverbackTpoint = + (sb, tp) -> { + MeosThread.ensureReady(); + Pointer s = stboxPtr(sb); if (s == null) return null; + Pointer t = tpointPtr(tp); if (t == null) { MeosMemory.free(s); return null; } + try { return MeosNative.INSTANCE.overback_stbox_tspatial(s, t); } + finally { MeosMemory.free(s, t); } + }; + + // ------------------------------------------------------------------ + // STBox Γ— TPoint β€” temporal direction + // ------------------------------------------------------------------ + + public static final UDF2 stboxBeforeTpoint = + (sb, tp) -> { + MeosThread.ensureReady(); + Pointer s = stboxPtr(sb); if (s == null) return null; + Pointer t = tpointPtr(tp); if (t == null) { MeosMemory.free(s); return null; } + try { return MeosNative.INSTANCE.before_stbox_tspatial(s, t); } + finally { MeosMemory.free(s, t); } + }; + + public static final UDF2 stboxOverbeforeTpoint = + (sb, tp) -> { + MeosThread.ensureReady(); + Pointer s = stboxPtr(sb); if (s == null) return null; + Pointer t = tpointPtr(tp); if (t == null) { MeosMemory.free(s); return null; } + try { return MeosNative.INSTANCE.overbefore_stbox_tspatial(s, t); } + finally { MeosMemory.free(s, t); } + }; + + public static final UDF2 stboxAfterTpoint = + (sb, tp) -> { + MeosThread.ensureReady(); + Pointer s = stboxPtr(sb); if (s == null) return null; + Pointer t = tpointPtr(tp); if (t == null) { MeosMemory.free(s); return null; } + try { return MeosNative.INSTANCE.after_stbox_tspatial(s, t); } + finally { MeosMemory.free(s, t); } + }; + + public static final UDF2 stboxOverafterTpoint = + (sb, tp) -> { + MeosThread.ensureReady(); + Pointer s = stboxPtr(sb); if (s == null) return null; + Pointer t = tpointPtr(tp); if (t == null) { MeosMemory.free(s); return null; } + try { return MeosNative.INSTANCE.overafter_stbox_tspatial(s, t); } + finally { MeosMemory.free(s, t); } + }; + + // ------------------------------------------------------------------ + // STBox Γ— TPoint β€” topological + // ------------------------------------------------------------------ + + public static final UDF2 stboxAdjacentTpoint = + (sb, tp) -> { + MeosThread.ensureReady(); + Pointer s = stboxPtr(sb); if (s == null) return null; + Pointer t = tpointPtr(tp); if (t == null) { MeosMemory.free(s); return null; } + try { return MeosNative.INSTANCE.adjacent_stbox_tspatial(s, t); } + finally { MeosMemory.free(s, t); } + }; + + public static final UDF2 stboxContainsTpoint = + (sb, tp) -> { + MeosThread.ensureReady(); + Pointer s = stboxPtr(sb); if (s == null) return null; + Pointer t = tpointPtr(tp); if (t == null) { MeosMemory.free(s); return null; } + try { return MeosNative.INSTANCE.contains_stbox_tspatial(s, t); } + finally { MeosMemory.free(s, t); } + }; + + public static final UDF2 stboxContainedTpoint = + (sb, tp) -> { + MeosThread.ensureReady(); + Pointer s = stboxPtr(sb); if (s == null) return null; + Pointer t = tpointPtr(tp); if (t == null) { MeosMemory.free(s); return null; } + try { return MeosNative.INSTANCE.contained_stbox_tspatial(s, t); } + finally { MeosMemory.free(s, t); } + }; + + public static final UDF2 stboxOverlapsTpoint = + (sb, tp) -> { + MeosThread.ensureReady(); + Pointer s = stboxPtr(sb); if (s == null) return null; + Pointer t = tpointPtr(tp); if (t == null) { MeosMemory.free(s); return null; } + try { return MeosNative.INSTANCE.overlaps_stbox_tspatial(s, t); } + finally { MeosMemory.free(s, t); } + }; + + public static final UDF2 stboxSameTpoint = + (sb, tp) -> { + MeosThread.ensureReady(); + Pointer s = stboxPtr(sb); if (s == null) return null; + Pointer t = tpointPtr(tp); if (t == null) { MeosMemory.free(s); return null; } + try { return MeosNative.INSTANCE.same_stbox_tspatial(s, t); } + finally { MeosMemory.free(s, t); } + }; + + // ------------------------------------------------------------------ + // TPoint Γ— STBox β€” spatial direction + // ------------------------------------------------------------------ + + public static final UDF2 tpointLeftStbox = + (tp, sb) -> { + MeosThread.ensureReady(); + Pointer t = tpointPtr(tp); if (t == null) return null; + Pointer s = stboxPtr(sb); if (s == null) { MeosMemory.free(t); return null; } + try { return MeosNative.INSTANCE.left_tspatial_stbox(t, s); } + finally { MeosMemory.free(t, s); } + }; + + public static final UDF2 tpointOverleftStbox = + (tp, sb) -> { + MeosThread.ensureReady(); + Pointer t = tpointPtr(tp); if (t == null) return null; + Pointer s = stboxPtr(sb); if (s == null) { MeosMemory.free(t); return null; } + try { return MeosNative.INSTANCE.overleft_tspatial_stbox(t, s); } + finally { MeosMemory.free(t, s); } + }; + + public static final UDF2 tpointRightStbox = + (tp, sb) -> { + MeosThread.ensureReady(); + Pointer t = tpointPtr(tp); if (t == null) return null; + Pointer s = stboxPtr(sb); if (s == null) { MeosMemory.free(t); return null; } + try { return MeosNative.INSTANCE.right_tspatial_stbox(t, s); } + finally { MeosMemory.free(t, s); } + }; + + public static final UDF2 tpointOverrightStbox = + (tp, sb) -> { + MeosThread.ensureReady(); + Pointer t = tpointPtr(tp); if (t == null) return null; + Pointer s = stboxPtr(sb); if (s == null) { MeosMemory.free(t); return null; } + try { return MeosNative.INSTANCE.overright_tspatial_stbox(t, s); } + finally { MeosMemory.free(t, s); } + }; + + public static final UDF2 tpointBelowStbox = + (tp, sb) -> { + MeosThread.ensureReady(); + Pointer t = tpointPtr(tp); if (t == null) return null; + Pointer s = stboxPtr(sb); if (s == null) { MeosMemory.free(t); return null; } + try { return MeosNative.INSTANCE.below_tspatial_stbox(t, s); } + finally { MeosMemory.free(t, s); } + }; + + public static final UDF2 tpointOverbelowStbox = + (tp, sb) -> { + MeosThread.ensureReady(); + Pointer t = tpointPtr(tp); if (t == null) return null; + Pointer s = stboxPtr(sb); if (s == null) { MeosMemory.free(t); return null; } + try { return MeosNative.INSTANCE.overbelow_tspatial_stbox(t, s); } + finally { MeosMemory.free(t, s); } + }; + + public static final UDF2 tpointAboveStbox = + (tp, sb) -> { + MeosThread.ensureReady(); + Pointer t = tpointPtr(tp); if (t == null) return null; + Pointer s = stboxPtr(sb); if (s == null) { MeosMemory.free(t); return null; } + try { return MeosNative.INSTANCE.above_tspatial_stbox(t, s); } + finally { MeosMemory.free(t, s); } + }; + + public static final UDF2 tpointOveraboveStbox = + (tp, sb) -> { + MeosThread.ensureReady(); + Pointer t = tpointPtr(tp); if (t == null) return null; + Pointer s = stboxPtr(sb); if (s == null) { MeosMemory.free(t); return null; } + try { return MeosNative.INSTANCE.overabove_tspatial_stbox(t, s); } + finally { MeosMemory.free(t, s); } + }; + + public static final UDF2 tpointFrontStbox = + (tp, sb) -> { + MeosThread.ensureReady(); + Pointer t = tpointPtr(tp); if (t == null) return null; + Pointer s = stboxPtr(sb); if (s == null) { MeosMemory.free(t); return null; } + try { return MeosNative.INSTANCE.front_tspatial_stbox(t, s); } + finally { MeosMemory.free(t, s); } + }; + + public static final UDF2 tpointOverfrontStbox = + (tp, sb) -> { + MeosThread.ensureReady(); + Pointer t = tpointPtr(tp); if (t == null) return null; + Pointer s = stboxPtr(sb); if (s == null) { MeosMemory.free(t); return null; } + try { return MeosNative.INSTANCE.overfront_tspatial_stbox(t, s); } + finally { MeosMemory.free(t, s); } + }; + + public static final UDF2 tpointBackStbox = + (tp, sb) -> { + MeosThread.ensureReady(); + Pointer t = tpointPtr(tp); if (t == null) return null; + Pointer s = stboxPtr(sb); if (s == null) { MeosMemory.free(t); return null; } + try { return MeosNative.INSTANCE.back_tspatial_stbox(t, s); } + finally { MeosMemory.free(t, s); } + }; + + public static final UDF2 tpointOverbackStbox = + (tp, sb) -> { + MeosThread.ensureReady(); + Pointer t = tpointPtr(tp); if (t == null) return null; + Pointer s = stboxPtr(sb); if (s == null) { MeosMemory.free(t); return null; } + try { return MeosNative.INSTANCE.overback_tspatial_stbox(t, s); } + finally { MeosMemory.free(t, s); } + }; + + // ------------------------------------------------------------------ + // TPoint Γ— STBox β€” temporal direction + // ------------------------------------------------------------------ + + public static final UDF2 tpointBeforeStbox = + (tp, sb) -> { + MeosThread.ensureReady(); + Pointer t = tpointPtr(tp); if (t == null) return null; + Pointer s = stboxPtr(sb); if (s == null) { MeosMemory.free(t); return null; } + try { return MeosNative.INSTANCE.before_tspatial_stbox(t, s); } + finally { MeosMemory.free(t, s); } + }; + + public static final UDF2 tpointOverbeforeStbox = + (tp, sb) -> { + MeosThread.ensureReady(); + Pointer t = tpointPtr(tp); if (t == null) return null; + Pointer s = stboxPtr(sb); if (s == null) { MeosMemory.free(t); return null; } + try { return MeosNative.INSTANCE.overbefore_tspatial_stbox(t, s); } + finally { MeosMemory.free(t, s); } + }; + + public static final UDF2 tpointAfterStbox = + (tp, sb) -> { + MeosThread.ensureReady(); + Pointer t = tpointPtr(tp); if (t == null) return null; + Pointer s = stboxPtr(sb); if (s == null) { MeosMemory.free(t); return null; } + try { return MeosNative.INSTANCE.after_tspatial_stbox(t, s); } + finally { MeosMemory.free(t, s); } + }; + + public static final UDF2 tpointOverafterStbox = + (tp, sb) -> { + MeosThread.ensureReady(); + Pointer t = tpointPtr(tp); if (t == null) return null; + Pointer s = stboxPtr(sb); if (s == null) { MeosMemory.free(t); return null; } + try { return MeosNative.INSTANCE.overafter_tspatial_stbox(t, s); } + finally { MeosMemory.free(t, s); } + }; + + // ------------------------------------------------------------------ + // TPoint Γ— STBox β€” topological + // ------------------------------------------------------------------ + + public static final UDF2 tpointAdjacentStbox = + (tp, sb) -> { + MeosThread.ensureReady(); + Pointer t = tpointPtr(tp); if (t == null) return null; + Pointer s = stboxPtr(sb); if (s == null) { MeosMemory.free(t); return null; } + try { return MeosNative.INSTANCE.adjacent_tspatial_stbox(t, s); } + finally { MeosMemory.free(t, s); } + }; + + public static final UDF2 tpointContainsStbox = + (tp, sb) -> { + MeosThread.ensureReady(); + Pointer t = tpointPtr(tp); if (t == null) return null; + Pointer s = stboxPtr(sb); if (s == null) { MeosMemory.free(t); return null; } + try { return MeosNative.INSTANCE.contains_tspatial_stbox(t, s); } + finally { MeosMemory.free(t, s); } + }; + + public static final UDF2 tpointContainedStbox = + (tp, sb) -> { + MeosThread.ensureReady(); + Pointer t = tpointPtr(tp); if (t == null) return null; + Pointer s = stboxPtr(sb); if (s == null) { MeosMemory.free(t); return null; } + try { return MeosNative.INSTANCE.contained_tspatial_stbox(t, s); } + finally { MeosMemory.free(t, s); } + }; + + public static final UDF2 tpointOverlapsStbox = + (tp, sb) -> { + MeosThread.ensureReady(); + Pointer t = tpointPtr(tp); if (t == null) return null; + Pointer s = stboxPtr(sb); if (s == null) { MeosMemory.free(t); return null; } + try { return MeosNative.INSTANCE.overlaps_tspatial_stbox(t, s); } + finally { MeosMemory.free(t, s); } + }; + + public static final UDF2 tpointSameStbox = + (tp, sb) -> { + MeosThread.ensureReady(); + Pointer t = tpointPtr(tp); if (t == null) return null; + Pointer s = stboxPtr(sb); if (s == null) { MeosMemory.free(t); return null; } + try { return MeosNative.INSTANCE.same_tspatial_stbox(t, s); } + finally { MeosMemory.free(t, s); } + }; + + // ------------------------------------------------------------------ + // Register all + // ------------------------------------------------------------------ + + public static void registerAll(SparkSession spark) { + // STBox Γ— TPoint β€” spatial direction + spark.udf().register("stboxLeftTpoint", stboxLeftTpoint, DataTypes.BooleanType); + spark.udf().register("stboxOverleftTpoint", stboxOverleftTpoint, DataTypes.BooleanType); + spark.udf().register("stboxRightTpoint", stboxRightTpoint, DataTypes.BooleanType); + spark.udf().register("stboxOverrightTpoint", stboxOverrightTpoint, DataTypes.BooleanType); + spark.udf().register("stboxBelowTpoint", stboxBelowTpoint, DataTypes.BooleanType); + spark.udf().register("stboxOverbelowTpoint", stboxOverbelowTpoint, DataTypes.BooleanType); + spark.udf().register("stboxAboveTpoint", stboxAboveTpoint, DataTypes.BooleanType); + spark.udf().register("stboxOveraboveTpoint", stboxOveraboveTpoint, DataTypes.BooleanType); + spark.udf().register("stboxFrontTpoint", stboxFrontTpoint, DataTypes.BooleanType); + spark.udf().register("stboxOverfrontTpoint", stboxOverfrontTpoint, DataTypes.BooleanType); + spark.udf().register("stboxBackTpoint", stboxBackTpoint, DataTypes.BooleanType); + spark.udf().register("stboxOverbackTpoint", stboxOverbackTpoint, DataTypes.BooleanType); + // STBox Γ— TPoint β€” temporal direction + spark.udf().register("stboxBeforeTpoint", stboxBeforeTpoint, DataTypes.BooleanType); + spark.udf().register("stboxOverbeforeTpoint", stboxOverbeforeTpoint, DataTypes.BooleanType); + spark.udf().register("stboxAfterTpoint", stboxAfterTpoint, DataTypes.BooleanType); + spark.udf().register("stboxOverafterTpoint", stboxOverafterTpoint, DataTypes.BooleanType); + // STBox Γ— TPoint β€” topological + spark.udf().register("stboxAdjacentTpoint", stboxAdjacentTpoint, DataTypes.BooleanType); + spark.udf().register("stboxContainsTpoint", stboxContainsTpoint, DataTypes.BooleanType); + spark.udf().register("stboxContainedTpoint", stboxContainedTpoint, DataTypes.BooleanType); + spark.udf().register("stboxOverlapsTpoint", stboxOverlapsTpoint, DataTypes.BooleanType); + spark.udf().register("stboxSameTpoint", stboxSameTpoint, DataTypes.BooleanType); + + // TPoint Γ— STBox β€” spatial direction + spark.udf().register("tpointLeftStbox", tpointLeftStbox, DataTypes.BooleanType); + spark.udf().register("tpointOverleftStbox", tpointOverleftStbox, DataTypes.BooleanType); + spark.udf().register("tpointRightStbox", tpointRightStbox, DataTypes.BooleanType); + spark.udf().register("tpointOverrightStbox", tpointOverrightStbox, DataTypes.BooleanType); + spark.udf().register("tpointBelowStbox", tpointBelowStbox, DataTypes.BooleanType); + spark.udf().register("tpointOverbelowStbox", tpointOverbelowStbox, DataTypes.BooleanType); + spark.udf().register("tpointAboveStbox", tpointAboveStbox, DataTypes.BooleanType); + spark.udf().register("tpointOveraboveStbox", tpointOveraboveStbox, DataTypes.BooleanType); + spark.udf().register("tpointFrontStbox", tpointFrontStbox, DataTypes.BooleanType); + spark.udf().register("tpointOverfrontStbox", tpointOverfrontStbox, DataTypes.BooleanType); + spark.udf().register("tpointBackStbox", tpointBackStbox, DataTypes.BooleanType); + spark.udf().register("tpointOverbackStbox", tpointOverbackStbox, DataTypes.BooleanType); + // TPoint Γ— STBox β€” temporal direction + spark.udf().register("tpointBeforeStbox", tpointBeforeStbox, DataTypes.BooleanType); + spark.udf().register("tpointOverbeforeStbox", tpointOverbeforeStbox, DataTypes.BooleanType); + spark.udf().register("tpointAfterStbox", tpointAfterStbox, DataTypes.BooleanType); + spark.udf().register("tpointOverafterStbox", tpointOverafterStbox, DataTypes.BooleanType); + // TPoint Γ— STBox β€” topological + spark.udf().register("tpointAdjacentStbox", tpointAdjacentStbox, DataTypes.BooleanType); + spark.udf().register("tpointContainsStbox", tpointContainsStbox, DataTypes.BooleanType); + spark.udf().register("tpointContainedStbox", tpointContainedStbox, DataTypes.BooleanType); + spark.udf().register("tpointOverlapsStbox", tpointOverlapsStbox, DataTypes.BooleanType); + spark.udf().register("tpointSameStbox", tpointSameStbox, DataTypes.BooleanType); + } +} diff --git a/src/main/java/org/mobilitydb/spark/geo/TempSpatialRelsUDFs.java b/src/main/java/org/mobilitydb/spark/geo/TempSpatialRelsUDFs.java new file mode 100644 index 00000000..814289fe --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/geo/TempSpatialRelsUDFs.java @@ -0,0 +1,308 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.geo; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosThread; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.api.java.UDF2; +import org.apache.spark.sql.api.java.UDF3; +import org.apache.spark.sql.types.DataTypes; + +/** + * Spark SQL UDFs for temporal spatial relationships on tpoint. + * + * These UDFs return a tbool (encoded as hex-WKB STRING) that is true at each + * instant where the spatial relationship holds. This complements the "ever" + * predicates in GeoUDFs (eIntersects, eContains, eDwithin) which return a + * scalar Boolean. + * + * Covered relationships: + * tDisjoint β€” tgeompoint is disjoint from geometry at each instant + * tIntersects β€” tgeompoint intersects geometry at each instant + * tTouches β€” tgeompoint touches geometry at each instant + * + * (tContains, tCovers, tDwithin are already provided in GeoAnalyticsUDFs.) + * + * Storage convention: + * tgeompoint β†’ hex-WKB STRING (temporal_as_hexwkb) + * geometry β†’ WKT STRING (geo_from_text with SRID from trip bbox) + * tbool resultβ†’ hex-WKB STRING + * + * MEOS function authority: meos/include/meos_geo.h (072_tgeo_tempspatialrels) + */ +public final class TempSpatialRelsUDFs { + + private TempSpatialRelsUDFs() {} + + private static int tripSrid(Pointer tptr) { + Pointer bbox = GeneratedFunctions.tspatial_to_stbox(tptr); + if (bbox == null) return 0; + try { + return GeneratedFunctions.stbox_srid(bbox); + } finally { + MeosMemory.free(bbox); + } + } + + private static String tempHexOut(Pointer r) { + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } + + // ------------------------------------------------------------------ + // tDisjoint(tpoint STRING, geomWkt STRING) β†’ STRING (tbool hex-WKB) + // + // Returns a tbool that is true at instants where the moving point is + // disjoint from (i.e. does not intersect) the static geometry. + // + // MEOS: tdisjoint_tgeo_geo(Temporal *, GSERIALIZED *) + // restr=false β†’ return full tbool (not restricted to true/false instants) + // ------------------------------------------------------------------ + public static final UDF2 tDisjoint = + (trip, geomWkt) -> { + if (trip == null || geomWkt == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + try { + int srid = tripSrid(tptr); + Pointer gptr = GeneratedFunctions.geo_from_text(geomWkt, srid); + if (gptr == null) return null; + try { + return tempHexOut(GeneratedFunctions.tdisjoint_tgeo_geo(tptr, gptr)); + } finally { MeosMemory.free(gptr); } + } finally { MeosMemory.free(tptr); } + }; + + // ------------------------------------------------------------------ + // tIntersects(tpoint STRING, geomWkt STRING) β†’ STRING (tbool hex-WKB) + // + // Returns a tbool that is true at instants where the moving point + // intersects the static geometry. + // + // MEOS: tintersects_tgeo_geo(Temporal *, GSERIALIZED *) + // ------------------------------------------------------------------ + public static final UDF2 tIntersects = + (trip, geomWkt) -> { + if (trip == null || geomWkt == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + try { + int srid = tripSrid(tptr); + Pointer gptr = GeneratedFunctions.geo_from_text(geomWkt, srid); + if (gptr == null) return null; + try { + return tempHexOut(GeneratedFunctions.tintersects_tgeo_geo(tptr, gptr)); + } finally { MeosMemory.free(gptr); } + } finally { MeosMemory.free(tptr); } + }; + + // ------------------------------------------------------------------ + // tTouches(tpoint STRING, geomWkt STRING) β†’ STRING (tbool hex-WKB) + // + // Returns a tbool that is true at instants where the moving point + // touches (shares boundary with) the static geometry. + // + // MEOS: ttouches_tgeo_geo(Temporal *, GSERIALIZED *) + // ------------------------------------------------------------------ + public static final UDF2 tTouches = + (trip, geomWkt) -> { + if (trip == null || geomWkt == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + try { + int srid = tripSrid(tptr); + Pointer gptr = GeneratedFunctions.geo_from_text(geomWkt, srid); + if (gptr == null) return null; + try { + return tempHexOut(GeneratedFunctions.ttouches_tgeo_geo(tptr, gptr)); + } finally { MeosMemory.free(gptr); } + } finally { MeosMemory.free(tptr); } + }; + + // ------------------------------------------------------------------ + // tDisjointTgeoTgeo(trip1 STRING, trip2 STRING) β†’ STRING (tbool hex-WKB) + // tIntersectsTgeoTgeo(trip1 STRING, trip2 STRING) β†’ STRING + // tTouchesTogeoTgeo(trip1 STRING, trip2 STRING) β†’ STRING + // + // Temporal predicates for two moving objects. + // + // MEOS: tdisjoint_tgeo_tgeo, tintersects_tgeo_tgeo, ttouches_tgeo_tgeo + // ------------------------------------------------------------------ + public static final UDF2 tDisjointTgeoTgeo = + (trip1, trip2) -> { + if (trip1 == null || trip2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(trip1); + if (p1 == null) return null; + try { + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(trip2); + if (p2 == null) return null; + try { + return tempHexOut(GeneratedFunctions.tdisjoint_tgeo_tgeo(p1, p2)); + } finally { MeosMemory.free(p2); } + } finally { MeosMemory.free(p1); } + }; + + public static final UDF2 tIntersectsTgeoTgeo = + (trip1, trip2) -> { + if (trip1 == null || trip2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(trip1); + if (p1 == null) return null; + try { + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(trip2); + if (p2 == null) return null; + try { + return tempHexOut(GeneratedFunctions.tintersects_tgeo_tgeo(p1, p2)); + } finally { MeosMemory.free(p2); } + } finally { MeosMemory.free(p1); } + }; + + public static final UDF2 tTouchesTogeoTgeo = + (trip1, trip2) -> { + if (trip1 == null || trip2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(trip1); + if (p1 == null) return null; + try { + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(trip2); + if (p2 == null) return null; + try { + return tempHexOut(GeneratedFunctions.ttouches_tgeo_tgeo(p1, p2)); + } finally { MeosMemory.free(p2); } + } finally { MeosMemory.free(p1); } + }; + + // ------------------------------------------------------------------ + // tContainsTgeoGeo(trip STRING, geomWkt STRING) β†’ STRING + // tContainsTgeoTgeo(trip1 STRING, trip2 STRING) β†’ STRING + // + // Note: tContains(geomWkt, trip) already exists in GeoAnalyticsUDFs + // (tcontains_geo_tgeo β€” container is static). These variants cover + // the case where the moving object is the container. + // + // MEOS: tcontains_tgeo_geo, tcontains_tgeo_tgeo + // ------------------------------------------------------------------ + public static final UDF2 tContainsTgeoGeo = + (trip, geomWkt) -> { + if (trip == null || geomWkt == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + try { + int srid = tripSrid(tptr); + Pointer gptr = GeneratedFunctions.geo_from_text(geomWkt, srid); + if (gptr == null) return null; + try { + return tempHexOut(GeneratedFunctions.tcontains_tgeo_geo(tptr, gptr)); + } finally { MeosMemory.free(gptr); } + } finally { MeosMemory.free(tptr); } + }; + + public static final UDF2 tContainsTgeoTgeo = + (trip1, trip2) -> { + if (trip1 == null || trip2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(trip1); + if (p1 == null) return null; + try { + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(trip2); + if (p2 == null) return null; + try { + return tempHexOut(GeneratedFunctions.tcontains_tgeo_tgeo(p1, p2)); + } finally { MeosMemory.free(p2); } + } finally { MeosMemory.free(p1); } + }; + + // ------------------------------------------------------------------ + // tCoversTgeoGeo(trip STRING, geomWkt STRING) β†’ STRING + // + // MEOS: tcovers_tgeo_geo + // ------------------------------------------------------------------ + public static final UDF2 tCoversTgeoGeo = + (trip, geomWkt) -> { + if (trip == null || geomWkt == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + try { + int srid = tripSrid(tptr); + Pointer gptr = GeneratedFunctions.geo_from_text(geomWkt, srid); + if (gptr == null) return null; + try { + return tempHexOut(GeneratedFunctions.tcovers_tgeo_geo(tptr, gptr)); + } finally { MeosMemory.free(gptr); } + } finally { MeosMemory.free(tptr); } + }; + + // ------------------------------------------------------------------ + // tDwithinTgeoGeo(trip STRING, geomWkt STRING, dist DOUBLE) β†’ STRING + // + // MEOS: tdwithin_tgeo_geo + // ------------------------------------------------------------------ + public static final UDF3 tDwithinTgeoGeo = + (trip, geomWkt, dist) -> { + if (trip == null || geomWkt == null || dist == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + try { + int srid = tripSrid(tptr); + Pointer gptr = GeneratedFunctions.geo_from_text(geomWkt, srid); + if (gptr == null) return null; + try { + return tempHexOut(GeneratedFunctions.tdwithin_tgeo_geo(tptr, gptr, dist.doubleValue())); + } finally { MeosMemory.free(gptr); } + } finally { MeosMemory.free(tptr); } + }; + + // ------------------------------------------------------------------ + // REGISTRATION + // ------------------------------------------------------------------ + + public static void registerAll(SparkSession spark) { + spark.udf().register("tDisjoint", tDisjoint, DataTypes.StringType); + spark.udf().register("tIntersects", tIntersects, DataTypes.StringType); + spark.udf().register("tTouches", tTouches, DataTypes.StringType); + spark.udf().register("tDisjointTgeoTgeo", tDisjointTgeoTgeo, DataTypes.StringType); + spark.udf().register("tIntersectsTgeoTgeo", tIntersectsTgeoTgeo, DataTypes.StringType); + spark.udf().register("tTouchesTogeoTgeo", tTouchesTogeoTgeo, DataTypes.StringType); + spark.udf().register("tContainsTgeoGeo", tContainsTgeoGeo, DataTypes.StringType); + spark.udf().register("tContainsTgeoTgeo", tContainsTgeoTgeo, DataTypes.StringType); + spark.udf().register("tCoversTgeoGeo", tCoversTgeoGeo, DataTypes.StringType); + spark.udf().register("tDwithinTgeoGeo", tDwithinTgeoGeo, DataTypes.StringType); + } +} diff --git a/src/main/java/org/mobilitydb/spark/h3/H3IndexJnrBindings.java b/src/main/java/org/mobilitydb/spark/h3/H3IndexJnrBindings.java new file mode 100644 index 00000000..e44c73fe --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/h3/H3IndexJnrBindings.java @@ -0,0 +1,66 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.h3; + +import jnr.ffi.LibraryLoader; +import jnr.ffi.Pointer; + +/** + * Direct JNR-FFI bindings for the MEOS H3 prefilter surface used by + * the BerlinMOD th3index variant queries. + * + * The mainline JMEOS jar shipped with MobilitySpark does not include + * the H3 family because the JMEOS function generator does not yet + * support the H3Index typedef. Until JMEOS regenerates against the + * H3-aware MEOS headers, these bindings expose the four MEOS symbols + * the bench harness actually needs: + * + * tgeompoint_to_th3index β€” TH3 from a tgeompoint trajectory + H3 resolution. + * geo_to_h3index_set β€” Set from a static geometry (EPSG:4326) + * at a given H3 resolution. + * ever_eq_th3index_th3index + * β€” trip x trip prefilter: do two trip H3 cell + * sequences ever share a cell at the same instant. + * ever_eq_anyof_h3indexset_th3index + * β€” trip x static prefilter: does the trip H3 + * sequence ever pass through any cell of a static + * H3 set. + */ +public final class H3IndexJnrBindings { + + private H3IndexJnrBindings() {} + + /** JNR view of the four MEOS H3 symbols. All return MEOS pointers or int. */ + public interface LibMeosH3 { + Pointer tgeompoint_to_th3index(Pointer temp, int resolution); + Pointer geo_to_h3index_set(Pointer gs, int resolution); + int ever_eq_th3index_th3index(Pointer t1, Pointer t2); + int ever_eq_anyof_h3indexset_th3index(Pointer set, Pointer t); + } + + public static final LibMeosH3 LIB = + LibraryLoader.create(LibMeosH3.class).load("meos"); +} diff --git a/src/main/java/org/mobilitydb/spark/h3/Th3IndexPrefilterUDFs.java b/src/main/java/org/mobilitydb/spark/h3/Th3IndexPrefilterUDFs.java new file mode 100644 index 00000000..0afedb76 --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/h3/Th3IndexPrefilterUDFs.java @@ -0,0 +1,151 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.h3; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.api.java.UDF2; +import org.apache.spark.sql.api.java.UDF3; +import org.apache.spark.sql.types.DataTypes; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosThread; + +/** + * Spark UDFs for the H3 prefilter surface used by the BerlinMOD + * th3index variant queries. The MEOS symbols are bound directly via + * JNR-FFI (see {@link H3IndexJnrBindings}) so this works against + * any libmeos.so that exports the four h3 prefilter functions, + * independent of whether the JMEOS jar generator supports H3Index. + * + * UDFs: + * + * tgeompointToTh3index(STRING tgeompoint_hex, INT resolution) -> STRING th3index_hex + * geoToH3IndexSet(STRING ewkb_hex, INT resolution) -> STRING h3indexset_hex + * everEqTh3IndexTh3Index(STRING th3index1_hex, STRING th3index2_hex) -> BOOLEAN + * everIntersectsH3IndexSetTh3Index(STRING h3indexset_hex, STRING th3index_hex) -> BOOLEAN + * + * Each STRING is the MEOS hex-WKB encoding of the underlying value. + */ +public final class Th3IndexPrefilterUDFs { + + private Th3IndexPrefilterUDFs() {} + + /* tgeompoint hex-WKB -> th3index hex-WKB at the given resolution. */ + public static final UDF2 tgeompointToTh3index = + (tripHex, resolution) -> { + if (tripHex == null || resolution == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(tripHex); + if (t == null) return null; + try { + Pointer th3 = H3IndexJnrBindings.LIB.tgeompoint_to_th3index(t, resolution); + if (th3 == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(th3, (byte) 0); + } finally { + MeosMemory.free(th3); + } + } finally { + MeosMemory.free(t); + } + }; + + /* static geometry WKT -> h3indexset hex-WKB at the given resolution. + * Caller passes the geometry's WKT in EPSG:4326 coordinates (e.g. + * `ST_AsText(ST_Transform(g, 4326))`). The MEOS GSERIALIZED carries + * the SRID via the second argument to geo_from_text (4326 here, since + * H3 cells are inherently geographic). */ + public static final UDF2 geoToH3IndexSet = + (geoWkt, resolution) -> { + if (geoWkt == null || resolution == null) return null; + MeosThread.ensureReady(); + Pointer gs = GeneratedFunctions.geo_from_text(geoWkt, 4326); + if (gs == null) return null; + try { + Pointer set = H3IndexJnrBindings.LIB.geo_to_h3index_set(gs, resolution); + if (set == null) return null; + try { + return GeneratedFunctions.set_as_hexwkb(set, (byte) 0); + } finally { + MeosMemory.free(set); + } + } finally { + MeosMemory.free(gs); + } + }; + + /* trip x trip h3 prefilter: do two trip H3 cell sequences share a cell at the same instant? */ + public static final UDF2 everEqTh3IndexTh3Index = + (h1, h2) -> { + if (h1 == null || h2 == null) return null; + MeosThread.ensureReady(); + Pointer t1 = GeneratedFunctions.temporal_from_hexwkb(h1); + if (t1 == null) return null; + try { + Pointer t2 = GeneratedFunctions.temporal_from_hexwkb(h2); + if (t2 == null) return null; + try { + int r = H3IndexJnrBindings.LIB.ever_eq_th3index_th3index(t1, t2); + return r == 1; + } finally { + MeosMemory.free(t2); + } + } finally { + MeosMemory.free(t1); + } + }; + + /* trip x static h3 prefilter: does the trip H3 sequence ever pass through any + * cell of a static H3 set? */ + public static final UDF2 everIntersectsH3IndexSetTh3Index = + (setHex, tripH3Hex) -> { + if (setHex == null || tripH3Hex == null) return null; + MeosThread.ensureReady(); + Pointer set = GeneratedFunctions.set_from_hexwkb(setHex); + if (set == null) return null; + try { + Pointer t = GeneratedFunctions.temporal_from_hexwkb(tripH3Hex); + if (t == null) return null; + try { + int r = H3IndexJnrBindings.LIB.ever_eq_anyof_h3indexset_th3index(set, t); + return r == 1; + } finally { + MeosMemory.free(t); + } + } finally { + MeosMemory.free(set); + } + }; + + /** Register all four prefilter UDFs on the given Spark session. */ + public static void registerAll(SparkSession spark) { + spark.udf().register("tgeompointToTh3index", tgeompointToTh3index, DataTypes.StringType); + spark.udf().register("geoToH3IndexSet", geoToH3IndexSet, DataTypes.StringType); + spark.udf().register("everEqTh3IndexTh3Index", everEqTh3IndexTh3Index, DataTypes.BooleanType); + spark.udf().register("everIntersectsH3IndexSetTh3Index", everIntersectsH3IndexSetTh3Index, DataTypes.BooleanType); + } +} diff --git a/src/main/java/org/mobilitydb/spark/h3/Th3IndexUDFs.java b/src/main/java/org/mobilitydb/spark/h3/Th3IndexUDFs.java new file mode 100644 index 00000000..5bedfa77 --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/h3/Th3IndexUDFs.java @@ -0,0 +1,1153 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.h3; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import jnr.ffi.Runtime; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.api.java.UDF1; +import org.apache.spark.sql.api.java.UDF2; +import org.apache.spark.sql.api.java.UDF3; +import org.apache.spark.sql.api.java.UDF5; +import org.apache.spark.sql.types.DataTypes; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosThread; +import org.mobilitydb.spark.util.TimeUtil; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; + +/** + * Spark SQL UDFs for the temporal H3 index type (th3index) and its supporting + * static h3index / h3indexset surfaces. Mirrors the public C API at + * + * meos/include/meos_h3.h β€” th3index temporal type (66 fns) + * meos/include/h3/h3index.h β€” static h3index scalar ops (10 fns) + * meos/include/h3/h3index_sets.h β€” h3indexset (Set of cells) ops (9 fns) + * + * The class targets 100% parity with the public h3 API. Sections below + * mirror the layout of meos_h3.h for traceability. + * + * Storage convention: + * tgeompoint / tgeogpoint / th3index β†’ hex-WKB STRING (Temporal hex-WKB) + * H3Index β†’ BIGINT (uint64 fits in Java long) + * h3indexset (Set of H3Index) β†’ hex-WKB STRING (Set hex-WKB) + * TimestampTz β†’ java.sql.Timestamp / String + * interpType β†’ INTEGER (NONE=0, DISCRETE=1, + * STEP=2, LINEAR=3) + * + * Memory management: every native Pointer allocated by MEOS must be freed + * via MeosMemory.free() in a finally block. Pointers returned by JNR- + * allocated output buffers (the *_value_at_timestamptz / *_value_n forms) + * have a JNR Cleaner attached and must NOT be MeosMemory.free'd β€” see + * feedback_jnr_allocated_buffer_nofree.md. + */ +public final class Th3IndexUDFs { + + private Th3IndexUDFs() {} + + // ================================================================== + // Helpers β€” keep the per-UDF code concise + // ================================================================== + + /** Default H3 resolution for the BerlinMOD prefilter β€” ~1.2 km cells. */ + public static final int DEFAULT_RESOLUTION = 7; + + private static final DateTimeFormatter PG_FMT = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ssXXX"); + + /** Spark Timestamp / String β†’ MEOS OffsetDateTime via pg_timestamptz_in. */ + private static OffsetDateTime parseTs(Object arg) { + if (arg == null) return null; + if (arg instanceof java.sql.Timestamp) { + return GeneratedFunctions.pg_timestamptz_in( + ((java.sql.Timestamp) arg).toInstant().atOffset(ZoneOffset.UTC).format(PG_FMT), -1); + } + return GeneratedFunctions.pg_timestamptz_in(arg.toString().trim(), -1); + } + + /** Convert a Number arg (Spark sends Int / Long / BigDecimal) to int. */ + private static int toInt(Number n) { return n == null ? 0 : n.intValue(); } + + /** Serialise a Temporal* result as hex-WKB and free the input pointer. */ + private static String tempHex(Pointer t) { + if (t == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(t, (byte) 0); } + finally { MeosMemory.free(t); } + } + + /** Serialise a Set* result as hex-WKB and free the input pointer. */ + private static String setHex(Pointer s) { + if (s == null) return null; + try { return GeneratedFunctions.set_as_hexwkb(s, (byte) 0); } + finally { MeosMemory.free(s); } + } + + // ================================================================== + // Static h3index SQL type β€” meos/include/h3/h3index.h + // ================================================================== + + public static final UDF1 h3IndexFromText = (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + return GeneratedFunctions.h3index_in(s); + }; + + public static final UDF1 h3IndexAsText = (cell) -> { + if (cell == null) return null; + MeosThread.ensureReady(); + return GeneratedFunctions.h3index_out(cell); + }; + + public static final UDF1 h3IndexParse = (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + return GeneratedFunctions.h3index_in(s); + }; + + public static final UDF1 h3IndexToString = (cell) -> { + if (cell == null) return null; + MeosThread.ensureReady(); + return GeneratedFunctions.h3index_out(cell); + }; + + public static final UDF2 h3IndexEq = (a, b) -> { + if (a == null || b == null) return null; + MeosThread.ensureReady(); + return GeneratedFunctions.h3index_eq(a, b); + }; + + public static final UDF2 h3IndexNe = (a, b) -> { + if (a == null || b == null) return null; + MeosThread.ensureReady(); + return GeneratedFunctions.h3index_ne(a, b); + }; + + public static final UDF2 h3IndexLt = (a, b) -> { + if (a == null || b == null) return null; + MeosThread.ensureReady(); + return GeneratedFunctions.h3index_lt(a, b); + }; + + public static final UDF2 h3IndexLe = (a, b) -> { + if (a == null || b == null) return null; + MeosThread.ensureReady(); + return GeneratedFunctions.h3index_le(a, b); + }; + + public static final UDF2 h3IndexGt = (a, b) -> { + if (a == null || b == null) return null; + MeosThread.ensureReady(); + return GeneratedFunctions.h3index_gt(a, b); + }; + + public static final UDF2 h3IndexGe = (a, b) -> { + if (a == null || b == null) return null; + MeosThread.ensureReady(); + return GeneratedFunctions.h3index_ge(a, b); + }; + + public static final UDF2 h3IndexCmp = (a, b) -> { + if (a == null || b == null) return null; + MeosThread.ensureReady(); + return GeneratedFunctions.h3index_cmp(a, b); + }; + + public static final UDF1 h3IndexHash = (cell) -> { + if (cell == null) return null; + MeosThread.ensureReady(); + return (long) GeneratedFunctions.h3index_hash(cell); + }; + + // ================================================================== + // h3indexset (Set of H3Index) β€” meos/include/h3/h3index_sets.h + // All return Set* serialised as hex-WKB STRING. + // ================================================================== + + public static final UDF2 h3GridDisk = (origin, k) -> { + if (origin == null || k == null) return null; + MeosThread.ensureReady(); + return setHex(GeneratedFunctions.h3_grid_disk(origin, k)); + }; + + public static final UDF2 h3GridRing = (origin, k) -> { + if (origin == null || k == null) return null; + MeosThread.ensureReady(); + return setHex(GeneratedFunctions.h3_grid_ring(origin, k)); + }; + + public static final UDF2 h3GridPathCells = (start, end) -> { + if (start == null || end == null) return null; + MeosThread.ensureReady(); + return setHex(GeneratedFunctions.h3_grid_path_cells(start, end)); + }; + + public static final UDF2 h3CellToChildren = (origin, childRes) -> { + if (origin == null || childRes == null) return null; + MeosThread.ensureReady(); + return setHex(GeneratedFunctions.h3_cell_to_children(origin, childRes)); + }; + + public static final UDF1 h3CompactCells = (cellsHex) -> { + if (cellsHex == null) return null; + MeosThread.ensureReady(); + Pointer in = GeneratedFunctions.set_from_hexwkb(cellsHex); + if (in == null) return null; + try { return setHex(GeneratedFunctions.h3_compact_cells(in)); } + finally { MeosMemory.free(in); } + }; + + public static final UDF2 h3UncompactCells = (cellsHex, res) -> { + if (cellsHex == null || res == null) return null; + MeosThread.ensureReady(); + Pointer in = GeneratedFunctions.set_from_hexwkb(cellsHex); + if (in == null) return null; + try { return setHex(GeneratedFunctions.h3_uncompact_cells(in, res)); } + finally { MeosMemory.free(in); } + }; + + public static final UDF1 h3OriginToDirectedEdges = (origin) -> { + if (origin == null) return null; + MeosThread.ensureReady(); + return setHex(GeneratedFunctions.h3_origin_to_directed_edges(origin)); + }; + + public static final UDF1 h3CellToVertexes = (cell) -> { + if (cell == null) return null; + MeosThread.ensureReady(); + return setHex(GeneratedFunctions.h3_cell_to_vertexes(cell)); + }; + + public static final UDF1 h3GetIcosahedronFaces = (cell) -> { + if (cell == null) return null; + MeosThread.ensureReady(); + return setHex(GeneratedFunctions.h3_get_icosahedron_faces(cell)); + }; + + // ================================================================== + // th3index input / output β€” meos_h3.h "Type inheritance" + // ================================================================== + + public static final UDF1 th3IndexFromText = (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + return tempHex(GeneratedFunctions.th3index_in(s)); + }; + + public static final UDF1 th3IndexInstFromText = (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.th3indexinst_in(s); + if (p == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(p, (byte) 0); } + finally { MeosMemory.free(p); } + }; + + public static final UDF2 th3IndexSeqFromText = (s, interp) -> { + if (s == null || interp == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.th3indexseq_in(s, interp); + if (p == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(p, (byte) 0); } + finally { MeosMemory.free(p); } + }; + + public static final UDF1 th3IndexSeqSetFromText = (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.th3indexseqset_in(s); + if (p == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(p, (byte) 0); } + finally { MeosMemory.free(p); } + }; + + // ================================================================== + // th3index constructors β€” meos_h3.h "Constructors" + // + // The instant + scalar make() forms are scalar-arg. The seq / seqset + // forms accept arrays which are not exposed here β€” callers can compose + // by parsing instants and concatenating via temporal_merge / temporal_seq. + // ================================================================== + + public static final UDF2 th3IndexMake = (cell, tsArg) -> { + if (cell == null || tsArg == null) return null; + MeosThread.ensureReady(); + OffsetDateTime t = parseTs(tsArg); + return tempHex(GeneratedFunctions.th3index_make(cell, t)); + }; + + public static final UDF2 th3IndexInstMake = (cell, tsArg) -> { + if (cell == null || tsArg == null) return null; + MeosThread.ensureReady(); + OffsetDateTime t = parseTs(tsArg); + Pointer p = GeneratedFunctions.th3indexinst_make(cell, t); + if (p == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(p, (byte) 0); } + finally { MeosMemory.free(p); } + }; + + /** + * th3indexseq_make(values[], times[], count, lower_inc, upper_inc) β†’ TSequence hex-WKB. + * + * Spark passes parallel ARRAY + ARRAY; we marshal both + * to JNR-FFI native arrays and call the MEOS constructor. The two arrays + * must have the same length; count is inferred from the value array. + */ + public static final UDF5 th3IndexSeqMake = + (values, timestamps, lowerInc, upperInc, ignored) -> { + if (values == null || timestamps == null) return null; + if (values.length != timestamps.length) return null; + MeosThread.ensureReady(); + int n = values.length; + // Marshal the H3Index value array and the TimestampTz array into + // native int64 buffers; th3indexseq_make takes raw MEOS arrays. + Runtime rt = Runtime.getSystemRuntime(); + Pointer vbuf = rt.getMemoryManager().allocateDirect(8L * n); + vbuf.put(0, values, 0, n); + Pointer tbuf = rt.getMemoryManager().allocateDirect(8L * n); + for (int i = 0; i < n; i++) { + OffsetDateTime odt = parseTs(timestamps[i]); + if (odt == null) return null; + tbuf.putLong(8L * i, TimeUtil.toMeosTimestamp(odt)); + } + Pointer p = GeneratedFunctions.th3indexseq_make( + vbuf, tbuf, n, + lowerInc != null && lowerInc, + upperInc != null && upperInc); + if (p == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(p, (byte) 0); } + finally { MeosMemory.free(p); } + }; + + /** + * th3indexseqset_make(sequences[]) β†’ TSequenceSet hex-WKB. + * + * Spark passes ARRAY of hex-WKB TSequence; we parse each into a + * native Pointer, hand the array to the MEOS constructor, free the + * intermediate pointers. + */ + public static final UDF1 th3IndexSeqSetMake = (sequencesHex) -> { + if (sequencesHex == null) return null; + MeosThread.ensureReady(); + Pointer[] seqs = new Pointer[sequencesHex.length]; + try { + for (int i = 0; i < sequencesHex.length; i++) { + seqs[i] = GeneratedFunctions.temporal_from_hexwkb(sequencesHex[i]); + if (seqs[i] == null) return null; + } + // Marshal the Pointer[] of sequences into a native pointer array. + Runtime rt = Runtime.getSystemRuntime(); + Pointer sbuf = rt.getMemoryManager().allocateDirect(8L * seqs.length); + for (int i = 0; i < seqs.length; i++) sbuf.putPointer(8L * i, seqs[i]); + Pointer p = GeneratedFunctions.th3indexseqset_make(sbuf, seqs.length); + if (p == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(p, (byte) 0); } + finally { MeosMemory.free(p); } + } finally { + for (Pointer s : seqs) if (s != null) MeosMemory.free(s); + } + }; + + // ================================================================== + // Accessors β€” meos_h3.h "Accessors" + // ================================================================== + + public static final UDF1 th3IndexStartValue = (th3idx) -> { + if (th3idx == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(th3idx); + if (t == null) return null; + try { return GeneratedFunctions.th3index_start_value(t); } + finally { MeosMemory.free(t); } + }; + + public static final UDF1 th3IndexEndValue = (th3idx) -> { + if (th3idx == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(th3idx); + if (t == null) return null; + try { return GeneratedFunctions.th3index_end_value(t); } + finally { MeosMemory.free(t); } + }; + + /** + * th3index_values(th3idx) β†’ ARRAY<LONG> (all distinct H3 cells in the trip's path). + * + * MEOS signature: H3Index *th3index_values(const Temporal *temp, int *count); + * the H3Index buffer is owned by MEOS and must be freed by the caller. + */ + public static final UDF1 th3IndexValues = (th3idx) -> { + if (th3idx == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(th3idx); + if (t == null) return null; + try { + Pointer countPtr = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(4); + Pointer arr = GeneratedFunctions.th3index_values(t, countPtr); + if (arr == null) return null; + int n = countPtr.getInt(0); + long[] out = new long[n]; + arr.get(0, out, 0, n); + MeosMemory.free(arr); + return out; + } finally { MeosMemory.free(t); } + }; + + /** + * th3index_value_n(th3idx, n) β†’ H3Index. + * MEOS signature: bool th3index_value_n(const Temporal *, int n, H3Index *result). + * JMEOS auto-allocates the H3Index output buffer; we read the value and + * let the JNR Cleaner reclaim it (do NOT MeosMemory.free). + */ + public static final UDF2 th3IndexValueN = (th3idx, n) -> { + if (th3idx == null || n == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(th3idx); + if (t == null) return null; + try { + Pointer result = GeneratedFunctions.th3index_value_n(t, n); + return result == null ? null : result.getLong(0); + } finally { + MeosMemory.free(t); + } + }; + + /** + * th3index_value_at_timestamptz(th3idx, ts, strict) β†’ H3Index. + * MEOS signature: bool th3index_value_at_timestamptz(const Temporal *, + * TimestampTz, bool, H3Index*). + * JMEOS auto-allocates the output; treat the returned Pointer as a JNR + * buffer per feedback_jnr_allocated_buffer_nofree.md. + */ + public static final UDF3 th3IndexValueAtTimestamp = + (th3idx, tsArg, strict) -> { + if (th3idx == null || tsArg == null) return null; + MeosThread.ensureReady(); + OffsetDateTime ts = parseTs(tsArg); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(th3idx); + if (t == null) return null; + try { + Pointer result = GeneratedFunctions.th3index_value_at_timestamptz( + t, ts, strict != null && strict); + return result == null ? null : result.getLong(0); + } finally { + MeosMemory.free(t); + } + }; + + // ================================================================== + // MEOS-level conversions β€” meos_h3.h "MEOS-level conversions" + // ================================================================== + + public static final UDF1 tbigintToTh3Index = (tbi) -> { + if (tbi == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(tbi); + if (t == null) return null; + try { return tempHex(GeneratedFunctions.tbigint_to_th3index(t)); } + finally { MeosMemory.free(t); } + }; + + public static final UDF1 th3IndexToTbigint = (th3idx) -> { + if (th3idx == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(th3idx); + if (t == null) return null; + try { return tempHex(GeneratedFunctions.th3index_to_tbigint(t)); } + finally { MeosMemory.free(t); } + }; + + // ================================================================== + // Ever / always comparison operators β€” meos_h3.h + // ================================================================== + + public static final UDF2 everEqH3IndexTh3Index = + (cell, th3idx) -> evCmp(cell, th3idx, true, "ever_eq"); + public static final UDF2 everEqTh3IndexH3Index = + (th3idx, cell) -> evCmp(cell, th3idx, true, "ever_eq_t"); + public static final UDF2 everNeH3IndexTh3Index = + (cell, th3idx) -> evCmp(cell, th3idx, true, "ever_ne"); + public static final UDF2 everNeTh3IndexH3Index = + (th3idx, cell) -> evCmp(cell, th3idx, true, "ever_ne_t"); + public static final UDF2 alwaysEqH3IndexTh3Index = + (cell, th3idx) -> evCmp(cell, th3idx, false, "always_eq"); + public static final UDF2 alwaysEqTh3IndexH3Index = + (th3idx, cell) -> evCmp(cell, th3idx, false, "always_eq_t"); + public static final UDF2 alwaysNeH3IndexTh3Index = + (cell, th3idx) -> evCmp(cell, th3idx, false, "always_ne"); + public static final UDF2 alwaysNeTh3IndexH3Index = + (th3idx, cell) -> evCmp(cell, th3idx, false, "always_ne_t"); + + /** Dispatch helper for the 8 ever/always Γ— eq/ne Γ— cell-side variants. */ + private static Boolean evCmp(Long cell, String th3idx, boolean ever, String op) { + if (cell == null || th3idx == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(th3idx); + if (t == null) return null; + try { + int r; + switch (op) { + case "ever_eq": r = GeneratedFunctions.ever_eq_h3index_th3index(cell, t); break; + case "ever_eq_t": r = GeneratedFunctions.ever_eq_th3index_h3index(t, cell); break; + case "ever_ne": r = GeneratedFunctions.ever_ne_h3index_th3index(cell, t); break; + case "ever_ne_t": r = GeneratedFunctions.ever_ne_th3index_h3index(t, cell); break; + case "always_eq": r = GeneratedFunctions.always_eq_h3index_th3index(cell, t); break; + case "always_eq_t": r = GeneratedFunctions.always_eq_th3index_h3index(t, cell); break; + case "always_ne": r = GeneratedFunctions.always_ne_h3index_th3index(cell, t); break; + case "always_ne_t": r = GeneratedFunctions.always_ne_th3index_h3index(t, cell); break; + default: throw new IllegalStateException(op); + } + return r < 0 ? null : r == 1; + } finally { + MeosMemory.free(t); + } + } + + public static final UDF2 everEqTh3IndexTh3Index = + (a, b) -> ttCmp(a, b, "ever_eq"); + public static final UDF2 everNeTh3IndexTh3Index = + (a, b) -> ttCmp(a, b, "ever_ne"); + public static final UDF2 alwaysEqTh3IndexTh3Index = + (a, b) -> ttCmp(a, b, "always_eq"); + public static final UDF2 alwaysNeTh3IndexTh3Index = + (a, b) -> ttCmp(a, b, "always_ne"); + + /** Dispatch helper for the 4 tripΓ—trip ever/always Γ— eq/ne variants. */ + private static Boolean ttCmp(String a, String b, String op) { + if (a == null || b == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.temporal_from_hexwkb(a); + if (p == null) return null; + try { + Pointer q = GeneratedFunctions.temporal_from_hexwkb(b); + if (q == null) return null; + try { + int r; + switch (op) { + case "ever_eq": r = GeneratedFunctions.ever_eq_th3index_th3index(p, q); break; + case "ever_ne": r = GeneratedFunctions.ever_ne_th3index_th3index(p, q); break; + case "always_eq": r = GeneratedFunctions.always_eq_th3index_th3index(p, q); break; + case "always_ne": r = GeneratedFunctions.always_ne_th3index_th3index(p, q); break; + default: throw new IllegalStateException(op); + } + return r < 0 ? null : r == 1; + } finally { + MeosMemory.free(q); + } + } finally { + MeosMemory.free(p); + } + } + + // ================================================================== + // Temporal comparison operators β€” meos_h3.h + // Return tbool serialised as hex-WKB. + // ================================================================== + + public static final UDF2 teqH3IndexTh3Index = (cell, th3idx) -> { + if (cell == null || th3idx == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(th3idx); + if (t == null) return null; + try { return tempHex(GeneratedFunctions.teq_h3index_th3index(cell, t)); } + finally { MeosMemory.free(t); } + }; + + public static final UDF2 teqTh3IndexH3Index = (th3idx, cell) -> { + if (th3idx == null || cell == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(th3idx); + if (t == null) return null; + try { return tempHex(GeneratedFunctions.teq_th3index_h3index(t, cell)); } + finally { MeosMemory.free(t); } + }; + + public static final UDF2 teqTh3IndexTh3Index = (a, b) -> { + if (a == null || b == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.temporal_from_hexwkb(a); + if (p == null) return null; + try { + Pointer q = GeneratedFunctions.temporal_from_hexwkb(b); + if (q == null) return null; + try { return tempHex(GeneratedFunctions.teq_th3index_th3index(p, q)); } + finally { MeosMemory.free(q); } + } finally { MeosMemory.free(p); } + }; + + public static final UDF2 tneH3IndexTh3Index = (cell, th3idx) -> { + if (cell == null || th3idx == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(th3idx); + if (t == null) return null; + try { return tempHex(GeneratedFunctions.tne_h3index_th3index(cell, t)); } + finally { MeosMemory.free(t); } + }; + + public static final UDF2 tneTh3IndexH3Index = (th3idx, cell) -> { + if (th3idx == null || cell == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(th3idx); + if (t == null) return null; + try { return tempHex(GeneratedFunctions.tne_th3index_h3index(t, cell)); } + finally { MeosMemory.free(t); } + }; + + public static final UDF2 tneTh3IndexTh3Index = (a, b) -> { + if (a == null || b == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.temporal_from_hexwkb(a); + if (p == null) return null; + try { + Pointer q = GeneratedFunctions.temporal_from_hexwkb(b); + if (q == null) return null; + try { return tempHex(GeneratedFunctions.tne_th3index_th3index(p, q)); } + finally { MeosMemory.free(q); } + } finally { MeosMemory.free(p); } + }; + + // ================================================================== + // Inspection β€” meos_h3.h + // All return Temporal* (tint or tbool) serialised as hex-WKB. + // ================================================================== + + public static final UDF1 th3IndexGetResolution = + (h) -> tempUnary(h, "get_resolution"); + public static final UDF1 th3IndexGetBaseCellNumber = + (h) -> tempUnary(h, "get_base_cell_number"); + public static final UDF1 th3IndexIsValidCell = + (h) -> tempUnary(h, "is_valid_cell"); + public static final UDF1 th3IndexIsResClassIii = + (h) -> tempUnary(h, "is_res_class_iii"); + public static final UDF1 th3IndexIsPentagon = + (h) -> tempUnary(h, "is_pentagon"); + + /** Dispatch helper for unary Temporal* β†’ Temporal* th3index inspections. */ + private static String tempUnary(String h, String op) { + if (h == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(h); + if (t == null) return null; + try { + Pointer r; + switch (op) { + case "get_resolution": r = GeneratedFunctions.th3index_get_resolution(t); break; + case "get_base_cell_number":r = GeneratedFunctions.th3index_get_base_cell_number(t); break; + case "is_valid_cell": r = GeneratedFunctions.th3index_is_valid_cell(t); break; + case "is_res_class_iii": r = GeneratedFunctions.th3index_is_res_class_iii(t); break; + case "is_pentagon": r = GeneratedFunctions.th3index_is_pentagon(t); break; + case "cell_to_parent_next": r = GeneratedFunctions.th3index_cell_to_parent_next(t); break; + case "cell_to_center_child_next": + r = GeneratedFunctions.th3index_cell_to_center_child_next(t); break; + case "is_valid_directed_edge": + r = GeneratedFunctions.th3index_is_valid_directed_edge(t); break; + case "get_directed_edge_origin": + r = GeneratedFunctions.th3index_get_directed_edge_origin(t); break; + case "get_directed_edge_destination": + r = GeneratedFunctions.th3index_get_directed_edge_destination(t); break; + case "directed_edge_to_boundary": + r = GeneratedFunctions.th3index_directed_edge_to_boundary(t); break; + case "vertex_to_latlng": r = GeneratedFunctions.th3index_vertex_to_latlng(t); break; + case "is_valid_vertex": r = GeneratedFunctions.th3index_is_valid_vertex(t); break; + case "to_tgeogpoint": r = GeneratedFunctions.th3index_to_tgeogpoint(t); break; + case "to_tgeompoint": r = GeneratedFunctions.th3index_to_tgeompoint(t); break; + case "cell_to_boundary": r = GeneratedFunctions.th3index_cell_to_boundary(t); break; + default: throw new IllegalStateException(op); + } + return tempHex(r); + } finally { + MeosMemory.free(t); + } + } + + // ================================================================== + // Hierarchy β€” meos_h3.h + // ================================================================== + + public static final UDF2 th3IndexCellToParent = (h, res) -> { + if (h == null || res == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(h); + if (t == null) return null; + try { return tempHex(GeneratedFunctions.th3index_cell_to_parent(t, res)); } + finally { MeosMemory.free(t); } + }; + + public static final UDF1 th3IndexCellToParentNext = + (h) -> tempUnary(h, "cell_to_parent_next"); + + public static final UDF2 th3IndexCellToCenterChild = (h, res) -> { + if (h == null || res == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(h); + if (t == null) return null; + try { return tempHex(GeneratedFunctions.th3index_cell_to_center_child(t, res)); } + finally { MeosMemory.free(t); } + }; + + public static final UDF1 th3IndexCellToCenterChildNext = + (h) -> tempUnary(h, "cell_to_center_child_next"); + + public static final UDF2 th3IndexCellToChildPos = (h, parentRes) -> { + if (h == null || parentRes == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(h); + if (t == null) return null; + try { return tempHex(GeneratedFunctions.th3index_cell_to_child_pos(t, parentRes)); } + finally { MeosMemory.free(t); } + }; + + public static final UDF3 th3IndexChildPosToCell = + (childPos, parent, childRes) -> { + if (childPos == null || parent == null || childRes == null) return null; + MeosThread.ensureReady(); + Pointer cp = GeneratedFunctions.temporal_from_hexwkb(childPos); + if (cp == null) return null; + try { + Pointer pa = GeneratedFunctions.temporal_from_hexwkb(parent); + if (pa == null) return null; + try { return tempHex(GeneratedFunctions.th3index_child_pos_to_cell(cp, pa, childRes)); } + finally { MeosMemory.free(pa); } + } finally { + MeosMemory.free(cp); + } + }; + + // ================================================================== + // Lat/Lng conversion β€” meos_h3.h + // ================================================================== + + public static final UDF2 tgeogpointToTh3Index = (h, res) -> { + if (h == null || res == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(h); + if (t == null) return null; + try { return tempHex(GeneratedFunctions.tgeogpoint_to_th3index(t, res)); } + finally { MeosMemory.free(t); } + }; + + public static final UDF2 tgeompointToTh3Index = (h, res) -> { + if (h == null || res == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(h); + if (t == null) return null; + try { return tempHex(GeneratedFunctions.tgeompoint_to_th3index(t, res)); } + finally { MeosMemory.free(t); } + }; + + public static final UDF1 th3IndexToTgeogpoint = + (h) -> tempUnary(h, "to_tgeogpoint"); + public static final UDF1 th3IndexToTgeompoint = + (h) -> tempUnary(h, "to_tgeompoint"); + public static final UDF1 th3IndexCellToBoundary = + (h) -> tempUnary(h, "cell_to_boundary"); + + /** + * geomToH3Cell(geomWkt, resolution) β†’ H3Index. + * + * Composed from the public API: a static POINT geometry is wrapped in a + * single-instant tgeompoint, converted to th3index, and the start value is + * extracted as the H3Index. Returns NULL for non-POINT geometries (the + * prefilter consumer treats NULL as "no prefilter for this row"). Polygon + * coverage requires the upstream geo_to_h3index_set helper (separate PR). + */ + public static final UDF2 geomToH3Cell = + (geomWkt, resolution) -> { + if (geomWkt == null || resolution == null) return null; + MeosThread.ensureReady(); + Pointer gs = GeneratedFunctions.geo_from_text(geomWkt, 0); + if (gs == null) return null; + try { + // The instant time is irrelevant for cell extraction; use the + // MEOS epoch (2000-01-01Z). tpointinst_make takes OffsetDateTime. + Pointer inst = GeneratedFunctions.tpointinst_make(gs, + OffsetDateTime.of(2000, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)); + if (inst == null) return null; + try { + Pointer th3 = GeneratedFunctions.tgeompoint_to_th3index(inst, resolution); + if (th3 == null) return null; + try { return GeneratedFunctions.th3index_start_value(th3); } + finally { MeosMemory.free(th3); } + } finally { + MeosMemory.free(inst); + } + } finally { + MeosMemory.free(gs); + } + }; + + /** + * geoToH3IndexSet(geomWkt, resolution) β†’ STRING (hex-WKB h3indexset). + * + * Cross-platform spatial prefilter source for polygon-side queries + * (BerlinMOD Q2 et al.). Handles every WKT geometry type β€” POINT, + * LINESTRING, POLYGON, MULTI*, GEOMETRYCOLLECTION β€” via the public + * MEOS kernel geo_to_h3index_set (MobilityDB PR #938). + */ + public static final UDF2 geoToH3IndexSet = + (geomWkt, resolution) -> { + if (geomWkt == null || resolution == null) return null; + MeosThread.ensureReady(); + Pointer gs = GeneratedFunctions.geo_from_text(geomWkt, 0); + if (gs == null) return null; + try { + Pointer set = H3IndexJnrBindings.LIB.geo_to_h3index_set(gs, resolution); + return setHex(set); + } finally { + MeosMemory.free(gs); + } + }; + + /** + * everIntersectsH3IndexSetTh3Index(cellSetHex, th3idx) β†’ BOOLEAN. + * + * Returns TRUE iff the trip's th3index sequence ever lies in any cell + * of the candidate set. Pair with geoToH3IndexSet to prefilter + * polygon-side cross-join queries. Wraps + * ever_eq_anyof_h3indexset_th3index (MobilityDB PR #938). + */ + public static final UDF2 everIntersectsH3IndexSetTh3Index = + (cellSetHex, th3idx) -> { + if (cellSetHex == null || th3idx == null) return null; + MeosThread.ensureReady(); + Pointer cells = GeneratedFunctions.set_from_hexwkb(cellSetHex); + if (cells == null) return null; + try { + Pointer t = GeneratedFunctions.temporal_from_hexwkb(th3idx); + if (t == null) return null; + try { + int r = H3IndexJnrBindings.LIB.ever_eq_anyof_h3indexset_th3index(cells, t); + return r < 0 ? null : r == 1; + } finally { + MeosMemory.free(t); + } + } finally { + MeosMemory.free(cells); + } + }; + + // ================================================================== + // Directed edges β€” meos_h3.h + // ================================================================== + + public static final UDF2 th3IndexAreNeighborCells = (a, b) -> { + if (a == null || b == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.temporal_from_hexwkb(a); + if (p == null) return null; + try { + Pointer q = GeneratedFunctions.temporal_from_hexwkb(b); + if (q == null) return null; + try { return tempHex(GeneratedFunctions.th3index_are_neighbor_cells(p, q)); } + finally { MeosMemory.free(q); } + } finally { MeosMemory.free(p); } + }; + + public static final UDF2 th3IndexCellsToDirectedEdge = (a, b) -> { + if (a == null || b == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.temporal_from_hexwkb(a); + if (p == null) return null; + try { + Pointer q = GeneratedFunctions.temporal_from_hexwkb(b); + if (q == null) return null; + try { return tempHex(GeneratedFunctions.th3index_cells_to_directed_edge(p, q)); } + finally { MeosMemory.free(q); } + } finally { MeosMemory.free(p); } + }; + + public static final UDF1 th3IndexIsValidDirectedEdge = + (h) -> tempUnary(h, "is_valid_directed_edge"); + public static final UDF1 th3IndexGetDirectedEdgeOrigin = + (h) -> tempUnary(h, "get_directed_edge_origin"); + public static final UDF1 th3IndexGetDirectedEdgeDestination = + (h) -> tempUnary(h, "get_directed_edge_destination"); + public static final UDF1 th3IndexDirectedEdgeToBoundary = + (h) -> tempUnary(h, "directed_edge_to_boundary"); + + // ================================================================== + // Vertices β€” meos_h3.h + // ================================================================== + + public static final UDF2 th3IndexCellToVertex = (h, vertexNum) -> { + if (h == null || vertexNum == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(h); + if (t == null) return null; + try { return tempHex(GeneratedFunctions.th3index_cell_to_vertex(t, vertexNum)); } + finally { MeosMemory.free(t); } + }; + + public static final UDF1 th3IndexVertexToLatlng = + (h) -> tempUnary(h, "vertex_to_latlng"); + public static final UDF1 th3IndexIsValidVertex = + (h) -> tempUnary(h, "is_valid_vertex"); + + // ================================================================== + // Grid traversal β€” meos_h3.h + // ================================================================== + + public static final UDF2 th3IndexGridDistance = (a, b) -> { + if (a == null || b == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.temporal_from_hexwkb(a); + if (p == null) return null; + try { + Pointer q = GeneratedFunctions.temporal_from_hexwkb(b); + if (q == null) return null; + try { return tempHex(GeneratedFunctions.th3index_grid_distance(p, q)); } + finally { MeosMemory.free(q); } + } finally { MeosMemory.free(p); } + }; + + public static final UDF2 th3IndexCellToLocalIj = (origin, cell) -> { + if (origin == null || cell == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.temporal_from_hexwkb(origin); + if (p == null) return null; + try { + Pointer q = GeneratedFunctions.temporal_from_hexwkb(cell); + if (q == null) return null; + try { return tempHex(GeneratedFunctions.th3index_cell_to_local_ij(p, q)); } + finally { MeosMemory.free(q); } + } finally { MeosMemory.free(p); } + }; + + public static final UDF2 th3IndexLocalIjToCell = (origin, coord) -> { + if (origin == null || coord == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.temporal_from_hexwkb(origin); + if (p == null) return null; + try { + Pointer q = GeneratedFunctions.temporal_from_hexwkb(coord); + if (q == null) return null; + try { return tempHex(GeneratedFunctions.th3index_local_ij_to_cell(p, q)); } + finally { MeosMemory.free(q); } + } finally { MeosMemory.free(p); } + }; + + // ================================================================== + // Metrics β€” meos_h3.h + // ================================================================== + + public static final UDF2 th3IndexCellArea = (h, unit) -> { + if (h == null || unit == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(h); + if (t == null) return null; + try { return tempHex(GeneratedFunctions.th3index_cell_area(t, unit)); } + finally { MeosMemory.free(t); } + }; + + public static final UDF2 th3IndexEdgeLength = (h, unit) -> { + if (h == null || unit == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(h); + if (t == null) return null; + try { return tempHex(GeneratedFunctions.th3index_edge_length(t, unit)); } + finally { MeosMemory.free(t); } + }; + + public static final UDF3 tgeogpointGreatCircleDistance = + (a, b, unit) -> { + if (a == null || b == null || unit == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.temporal_from_hexwkb(a); + if (p == null) return null; + try { + Pointer q = GeneratedFunctions.temporal_from_hexwkb(b); + if (q == null) return null; + try { return tempHex(GeneratedFunctions.tgeogpoint_great_circle_distance(p, q, unit)); } + finally { MeosMemory.free(q); } + } finally { MeosMemory.free(p); } + }; + + // ================================================================== + // Registration β€” exposes every UDF above to Spark SQL. + // ================================================================== + + public static void registerAll(SparkSession spark) { + // Static h3index ops + spark.udf().register("h3IndexFromText", h3IndexFromText, DataTypes.LongType); + spark.udf().register("h3IndexAsText", h3IndexAsText, DataTypes.StringType); + spark.udf().register("h3IndexParse", h3IndexParse, DataTypes.LongType); + spark.udf().register("h3IndexToString", h3IndexToString, DataTypes.StringType); + spark.udf().register("h3IndexEq", h3IndexEq, DataTypes.BooleanType); + spark.udf().register("h3IndexNe", h3IndexNe, DataTypes.BooleanType); + spark.udf().register("h3IndexLt", h3IndexLt, DataTypes.BooleanType); + spark.udf().register("h3IndexLe", h3IndexLe, DataTypes.BooleanType); + spark.udf().register("h3IndexGt", h3IndexGt, DataTypes.BooleanType); + spark.udf().register("h3IndexGe", h3IndexGe, DataTypes.BooleanType); + spark.udf().register("h3IndexCmp", h3IndexCmp, DataTypes.IntegerType); + spark.udf().register("h3IndexHash", h3IndexHash, DataTypes.LongType); + + // h3indexset (Set ops) + spark.udf().register("h3GridDisk", h3GridDisk, DataTypes.StringType); + spark.udf().register("h3GridRing", h3GridRing, DataTypes.StringType); + spark.udf().register("h3GridPathCells", h3GridPathCells, DataTypes.StringType); + spark.udf().register("h3CellToChildren", h3CellToChildren, DataTypes.StringType); + spark.udf().register("h3CompactCells", h3CompactCells, DataTypes.StringType); + spark.udf().register("h3UncompactCells", h3UncompactCells, DataTypes.StringType); + spark.udf().register("h3OriginToDirectedEdges", + h3OriginToDirectedEdges, DataTypes.StringType); + spark.udf().register("h3CellToVertexes", h3CellToVertexes, DataTypes.StringType); + spark.udf().register("h3GetIcosahedronFaces", + h3GetIcosahedronFaces, DataTypes.StringType); + + // th3index I/O + constructors + spark.udf().register("th3IndexFromText", th3IndexFromText, DataTypes.StringType); + spark.udf().register("th3IndexInstFromText", th3IndexInstFromText, DataTypes.StringType); + spark.udf().register("th3IndexSeqFromText", th3IndexSeqFromText, DataTypes.StringType); + spark.udf().register("th3IndexSeqSetFromText", th3IndexSeqSetFromText, DataTypes.StringType); + spark.udf().register("th3IndexMake", th3IndexMake, DataTypes.StringType); + spark.udf().register("th3IndexInstMake", th3IndexInstMake, DataTypes.StringType); + spark.udf().register("th3IndexSeqMake", th3IndexSeqMake, DataTypes.StringType); + spark.udf().register("th3IndexSeqSetMake", th3IndexSeqSetMake, DataTypes.StringType); + + // Accessors + spark.udf().register("th3IndexStartValue", th3IndexStartValue, + DataTypes.LongType); + spark.udf().register("th3IndexEndValue", th3IndexEndValue, + DataTypes.LongType); + spark.udf().register("th3IndexValues", th3IndexValues, + DataTypes.createArrayType(DataTypes.LongType)); + spark.udf().register("th3IndexValueN", th3IndexValueN, + DataTypes.LongType); + spark.udf().register("th3IndexValueAtTimestamp", th3IndexValueAtTimestamp, + DataTypes.LongType); + + // Conversions + spark.udf().register("tbigintToTh3Index", tbigintToTh3Index, DataTypes.StringType); + spark.udf().register("th3IndexToTbigint", th3IndexToTbigint, DataTypes.StringType); + + // Ever / always (cell-side variants) + spark.udf().register("everEqH3IndexTh3Index", everEqH3IndexTh3Index, + DataTypes.BooleanType); + spark.udf().register("everEqTh3IndexH3Index", everEqTh3IndexH3Index, + DataTypes.BooleanType); + spark.udf().register("everNeH3IndexTh3Index", everNeH3IndexTh3Index, + DataTypes.BooleanType); + spark.udf().register("everNeTh3IndexH3Index", everNeTh3IndexH3Index, + DataTypes.BooleanType); + spark.udf().register("alwaysEqH3IndexTh3Index", alwaysEqH3IndexTh3Index, + DataTypes.BooleanType); + spark.udf().register("alwaysEqTh3IndexH3Index", alwaysEqTh3IndexH3Index, + DataTypes.BooleanType); + spark.udf().register("alwaysNeH3IndexTh3Index", alwaysNeH3IndexTh3Index, + DataTypes.BooleanType); + spark.udf().register("alwaysNeTh3IndexH3Index", alwaysNeTh3IndexH3Index, + DataTypes.BooleanType); + + // Ever / always (trip Γ— trip variants) + spark.udf().register("everEqTh3IndexTh3Index", everEqTh3IndexTh3Index, + DataTypes.BooleanType); + spark.udf().register("everNeTh3IndexTh3Index", everNeTh3IndexTh3Index, + DataTypes.BooleanType); + spark.udf().register("alwaysEqTh3IndexTh3Index", alwaysEqTh3IndexTh3Index, + DataTypes.BooleanType); + spark.udf().register("alwaysNeTh3IndexTh3Index", alwaysNeTh3IndexTh3Index, + DataTypes.BooleanType); + + // Temporal comparisons + spark.udf().register("teqH3IndexTh3Index", teqH3IndexTh3Index, DataTypes.StringType); + spark.udf().register("teqTh3IndexH3Index", teqTh3IndexH3Index, DataTypes.StringType); + spark.udf().register("teqTh3IndexTh3Index", teqTh3IndexTh3Index, DataTypes.StringType); + spark.udf().register("tneH3IndexTh3Index", tneH3IndexTh3Index, DataTypes.StringType); + spark.udf().register("tneTh3IndexH3Index", tneTh3IndexH3Index, DataTypes.StringType); + spark.udf().register("tneTh3IndexTh3Index", tneTh3IndexTh3Index, DataTypes.StringType); + + // Inspection + spark.udf().register("th3IndexGetResolution", th3IndexGetResolution, + DataTypes.StringType); + spark.udf().register("th3IndexGetBaseCellNumber", th3IndexGetBaseCellNumber, + DataTypes.StringType); + spark.udf().register("th3IndexIsValidCell", th3IndexIsValidCell, + DataTypes.StringType); + spark.udf().register("th3IndexIsResClassIii", th3IndexIsResClassIii, + DataTypes.StringType); + spark.udf().register("th3IndexIsPentagon", th3IndexIsPentagon, + DataTypes.StringType); + + // Hierarchy + spark.udf().register("th3IndexCellToParent", th3IndexCellToParent, + DataTypes.StringType); + spark.udf().register("th3IndexCellToParentNext", th3IndexCellToParentNext, + DataTypes.StringType); + spark.udf().register("th3IndexCellToCenterChild", th3IndexCellToCenterChild, + DataTypes.StringType); + spark.udf().register("th3IndexCellToCenterChildNext", th3IndexCellToCenterChildNext, + DataTypes.StringType); + spark.udf().register("th3IndexCellToChildPos", th3IndexCellToChildPos, + DataTypes.StringType); + spark.udf().register("th3IndexChildPosToCell", th3IndexChildPosToCell, + DataTypes.StringType); + + // Lat/Lng conversion + spark.udf().register("tgeogpointToTh3Index", tgeogpointToTh3Index, DataTypes.StringType); + spark.udf().register("tgeompointToTh3Index", tgeompointToTh3Index, DataTypes.StringType); + spark.udf().register("th3IndexToTgeogpoint", th3IndexToTgeogpoint, DataTypes.StringType); + spark.udf().register("th3IndexToTgeompoint", th3IndexToTgeompoint, DataTypes.StringType); + spark.udf().register("th3IndexCellToBoundary", th3IndexCellToBoundary, DataTypes.StringType); + spark.udf().register("geomToH3Cell", geomToH3Cell, DataTypes.LongType); + spark.udf().register("geoToH3IndexSet", geoToH3IndexSet, DataTypes.StringType); + spark.udf().register("everIntersectsH3IndexSetTh3Index", + everIntersectsH3IndexSetTh3Index, DataTypes.BooleanType); + + // Directed edges + spark.udf().register("th3IndexAreNeighborCells", th3IndexAreNeighborCells, + DataTypes.StringType); + spark.udf().register("th3IndexCellsToDirectedEdge", th3IndexCellsToDirectedEdge, + DataTypes.StringType); + spark.udf().register("th3IndexIsValidDirectedEdge", th3IndexIsValidDirectedEdge, + DataTypes.StringType); + spark.udf().register("th3IndexGetDirectedEdgeOrigin", th3IndexGetDirectedEdgeOrigin, + DataTypes.StringType); + spark.udf().register("th3IndexGetDirectedEdgeDestination", + th3IndexGetDirectedEdgeDestination, DataTypes.StringType); + spark.udf().register("th3IndexDirectedEdgeToBoundary", th3IndexDirectedEdgeToBoundary, + DataTypes.StringType); + + // Vertices + spark.udf().register("th3IndexCellToVertex", th3IndexCellToVertex, DataTypes.StringType); + spark.udf().register("th3IndexVertexToLatlng", th3IndexVertexToLatlng, DataTypes.StringType); + spark.udf().register("th3IndexIsValidVertex", th3IndexIsValidVertex, DataTypes.StringType); + + // Grid traversal + spark.udf().register("th3IndexGridDistance", th3IndexGridDistance, DataTypes.StringType); + spark.udf().register("th3IndexCellToLocalIj", th3IndexCellToLocalIj, DataTypes.StringType); + spark.udf().register("th3IndexLocalIjToCell", th3IndexLocalIjToCell, DataTypes.StringType); + + // Metrics + spark.udf().register("th3IndexCellArea", th3IndexCellArea, + DataTypes.StringType); + spark.udf().register("th3IndexEdgeLength", th3IndexEdgeLength, + DataTypes.StringType); + spark.udf().register("tgeogpointGreatCircleDistance", tgeogpointGreatCircleDistance, + DataTypes.StringType); + } +} diff --git a/src/main/java/org/mobilitydb/spark/temporal/AccessorAliasUDFs.java b/src/main/java/org/mobilitydb/spark/temporal/AccessorAliasUDFs.java new file mode 100644 index 00000000..c171c46b --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/temporal/AccessorAliasUDFs.java @@ -0,0 +1,884 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosThread; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.api.java.UDF1; +import org.apache.spark.sql.api.java.UDF2; +import org.apache.spark.sql.api.java.UDF3; +import org.apache.spark.sql.types.DataTypes; + +import java.util.HexFormat; +import java.util.function.Function; +import java.util.function.ToIntFunction; +import java.util.function.ToLongFunction; +import java.util.function.ToDoubleFunction; +import org.mobilitydb.spark.util.TimeUtil; + +/** + * Spark SQL UDFs for typed accessor aliases bridging MobilityDB SQL bare + * names (e.g. `intspan_width`, `dates`, `valueSpans`, `tnumberToSpan`) + * to existing JMEOS bindings. + * + * MEOS function authority: meos/include/meos.h + */ +public final class AccessorAliasUDFs { + + private AccessorAliasUDFs() {} + + // ------------------------------------------------------------------ + // Per-type span/spanset width accessors + // ------------------------------------------------------------------ + + public static final UDF1 intspanWidth = + hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.span_from_hexwkb(hex); + if (p == null) return null; + try { return GeneratedFunctions.intspan_width(p); } + finally { MeosMemory.free(p); } + }; + + public static final UDF1 bigintspanWidth = + hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.span_from_hexwkb(hex); + if (p == null) return null; + try { return GeneratedFunctions.bigintspan_width(p); } + finally { MeosMemory.free(p); } + }; + + public static final UDF1 floatspanWidth = + hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.span_from_hexwkb(hex); + if (p == null) return null; + try { return GeneratedFunctions.floatspan_width(p); } + finally { MeosMemory.free(p); } + }; + + // boundspan param: TRUE = use the span of the union of all components, + // FALSE = sum of individual span widths. + public static final UDF2 intspansetWidth = + (hex, boundspan) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.spanset_from_hexwkb(hex); + if (p == null) return null; + try { return GeneratedFunctions.intspanset_width(p, boundspan != null && boundspan); } + finally { MeosMemory.free(p); } + }; + + public static final UDF2 bigintspansetWidth = + (hex, boundspan) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.spanset_from_hexwkb(hex); + if (p == null) return null; + try { return GeneratedFunctions.bigintspanset_width(p, boundspan != null && boundspan); } + finally { MeosMemory.free(p); } + }; + + public static final UDF2 floatspansetWidth = + (hex, boundspan) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.spanset_from_hexwkb(hex); + if (p == null) return null; + try { return GeneratedFunctions.floatspanset_width(p, boundspan != null && boundspan); } + finally { MeosMemory.free(p); } + }; + + // ------------------------------------------------------------------ + // Date span/spanset accessors (date represented as int days) + // ------------------------------------------------------------------ + + public static final UDF1 startDate = + hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.spanset_from_hexwkb(hex); + if (p == null) return null; + try { return GeneratedFunctions.datespanset_start_date(p); } + finally { MeosMemory.free(p); } + }; + + public static final UDF1 endDate = + hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.spanset_from_hexwkb(hex); + if (p == null) return null; + try { return GeneratedFunctions.datespanset_end_date(p); } + finally { MeosMemory.free(p); } + }; + + public static final UDF1 numDates = + hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.spanset_from_hexwkb(hex); + if (p == null) return null; + try { return GeneratedFunctions.datespanset_num_dates(p); } + finally { MeosMemory.free(p); } + }; + + public static final UDF2 dateN = + (hex, n) -> { + if (hex == null || n == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.spanset_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer r = GeneratedFunctions.datespanset_date_n(p, n); + if (r == null) return null; + // Pointer has the date as 4 bytes β€” read as int + int date = r.getInt(0); + return date; + } finally { MeosMemory.free(p); } + }; + + public static final UDF1 dates = + hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.spanset_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer r = GeneratedFunctions.datespanset_dates(p); + if (r == null) return null; + try { return GeneratedFunctions.set_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(p); } + }; + + // ------------------------------------------------------------------ + // tnumber valueSpans (already exists as tnumberValuespans, add alias) + // ------------------------------------------------------------------ + + public static final UDF1 valueSpan = + hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.temporal_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer r = GeneratedFunctions.tnumber_to_span(p); + if (r == null) return null; + try { return GeneratedFunctions.span_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(p); } + }; + + // ------------------------------------------------------------------ + // Typed set values β€” return Java arrays of primitive boxes + // ------------------------------------------------------------------ + + public static final UDF1 intsetValues = + hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.set_from_hexwkb(hex); + if (p == null) return null; + try { + int n = GeneratedFunctions.set_num_values(p); + Integer[] out = new Integer[n]; + jnr.ffi.Runtime rt = jnr.ffi.Runtime.getSystemRuntime(); + Pointer outBuf = rt.getMemoryManager().allocateDirect(8); + for (int i = 0; i < n; i++) { + if (org.mobilitydb.spark.MeosNative.INSTANCE.intset_value_n(p, i + 1, outBuf)) { + out[i] = outBuf.getInt(0); + } + } + return out; + } finally { MeosMemory.free(p); } + }; + + public static final UDF1 bigintsetValues = + hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.set_from_hexwkb(hex); + if (p == null) return null; + try { + int n = GeneratedFunctions.set_num_values(p); + Long[] out = new Long[n]; + jnr.ffi.Runtime rt = jnr.ffi.Runtime.getSystemRuntime(); + Pointer outBuf = rt.getMemoryManager().allocateDirect(8); + for (int i = 0; i < n; i++) { + if (org.mobilitydb.spark.MeosNative.INSTANCE.bigintset_value_n(p, i + 1, outBuf)) { + out[i] = outBuf.getLong(0); + } + } + return out; + } finally { MeosMemory.free(p); } + }; + + // ------------------------------------------------------------------ + // Array-returning bbox accessors (Spark ArrayType) + // + // Note on TBox/STBox struct sizes: the returned pointer is a flat array + // of contiguous structs, so we need sizeof to advance per element. + // Both struct sizes are stable across MEOS releases since adding a new + // field would break ABI; if MEOS changes them, regenerate JMEOS and + // adjust here. + // ------------------------------------------------------------------ + + private static final int TBOX_SIZE = 56; // Span period(24) + Span span(24) + int16 flags(2) + 6 bytes padding + private static final int STBOX_SIZE = 80; // Span period(24) + 6Γ—double(48) + int32 srid(4) + int16 flags(2) + 2 bytes padding + + public static final UDF1 tboxes = + hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.temporal_from_hexwkb(hex); + if (p == null) return null; + try { + jnr.ffi.Runtime rt = jnr.ffi.Runtime.getSystemRuntime(); + Pointer countOut = rt.getMemoryManager().allocateDirect(4); + Pointer arr = org.mobilitydb.spark.MeosNative.INSTANCE.tnumber_tboxes(p, countOut); + if (arr == null) return null; + try { + int n = countOut.getInt(0); + String[] out = new String[n]; + Pointer sizeOut = rt.getMemoryManager().allocateDirect(8); + for (int i = 0; i < n; i++) { + Pointer elem = arr.slice(i * TBOX_SIZE); + out[i] = GeneratedFunctions.tbox_as_hexwkb(elem, (byte) 0, sizeOut); + } + return out; + } finally { MeosMemory.free(arr); } + } finally { MeosMemory.free(p); } + }; + + private static final int SPAN_SIZE = 24; // 4 byte header + 4 padding + 2 Datum (8 each) + + public static final UDF3 intspanBins = + (hex, vsize, vorigin) -> { + if (hex == null || vsize == null || vorigin == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.span_from_hexwkb(hex); + if (p == null) return null; + try { + jnr.ffi.Runtime rt = jnr.ffi.Runtime.getSystemRuntime(); + Pointer countOut = rt.getMemoryManager().allocateDirect(4); + Pointer arr = org.mobilitydb.spark.MeosNative.INSTANCE.intspan_bins(p, vsize, vorigin, countOut); + if (arr == null) return null; + try { + int n = countOut.getInt(0); + String[] out = new String[n]; + for (int i = 0; i < n; i++) out[i] = GeneratedFunctions.span_as_hexwkb(arr.slice(i * SPAN_SIZE), (byte) 0); + return out; + } finally { MeosMemory.free(arr); } + } finally { MeosMemory.free(p); } + }; + + public static final UDF3 bigintspanBins = + (hex, vsize, vorigin) -> { + if (hex == null || vsize == null || vorigin == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.span_from_hexwkb(hex); + if (p == null) return null; + try { + jnr.ffi.Runtime rt = jnr.ffi.Runtime.getSystemRuntime(); + Pointer countOut = rt.getMemoryManager().allocateDirect(4); + Pointer arr = org.mobilitydb.spark.MeosNative.INSTANCE.bigintspan_bins(p, vsize, vorigin, countOut); + if (arr == null) return null; + try { + int n = countOut.getInt(0); + String[] out = new String[n]; + for (int i = 0; i < n; i++) out[i] = GeneratedFunctions.span_as_hexwkb(arr.slice(i * SPAN_SIZE), (byte) 0); + return out; + } finally { MeosMemory.free(arr); } + } finally { MeosMemory.free(p); } + }; + + // ------------------------------------------------------------------ + // Split-by-N functions (return arrays of Span/TBox/STBox hex-WKB) + // ------------------------------------------------------------------ + + private interface SplitFn { Pointer apply(Pointer t, int n, Pointer count); } + + private static String[] splitArrSpan(String hex, Integer n, SplitFn fn) { + if (hex == null || n == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(hex); + if (t == null) return null; + try { + jnr.ffi.Runtime rt = jnr.ffi.Runtime.getSystemRuntime(); + Pointer countOut = rt.getMemoryManager().allocateDirect(4); + Pointer arr = fn.apply(t, n, countOut); + if (arr == null) return null; + try { + int cnt = countOut.getInt(0); + String[] out = new String[cnt]; + for (int i = 0; i < cnt; i++) out[i] = GeneratedFunctions.span_as_hexwkb(arr.slice(i * SPAN_SIZE), (byte) 0); + return out; + } finally { MeosMemory.free(arr); } + } finally { MeosMemory.free(t); } + } + + private static String[] splitArrTbox(String hex, Integer n, SplitFn fn) { + if (hex == null || n == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(hex); + if (t == null) return null; + try { + jnr.ffi.Runtime rt = jnr.ffi.Runtime.getSystemRuntime(); + Pointer countOut = rt.getMemoryManager().allocateDirect(4); + Pointer arr = fn.apply(t, n, countOut); + if (arr == null) return null; + try { + int cnt = countOut.getInt(0); + String[] out = new String[cnt]; + Pointer sizeOut = rt.getMemoryManager().allocateDirect(8); + for (int i = 0; i < cnt; i++) out[i] = GeneratedFunctions.tbox_as_hexwkb(arr.slice(i * TBOX_SIZE), (byte) 0, sizeOut); + return out; + } finally { MeosMemory.free(arr); } + } finally { MeosMemory.free(t); } + } + + private static String[] splitArrStbox(String hex, Integer n, SplitFn fn) { + if (hex == null || n == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(hex); + if (t == null) return null; + try { + jnr.ffi.Runtime rt = jnr.ffi.Runtime.getSystemRuntime(); + Pointer countOut = rt.getMemoryManager().allocateDirect(4); + Pointer arr = fn.apply(t, n, countOut); + if (arr == null) return null; + try { + int cnt = countOut.getInt(0); + String[] out = new String[cnt]; + Pointer sizeOut = rt.getMemoryManager().allocateDirect(8); + for (int i = 0; i < cnt; i++) out[i] = GeneratedFunctions.stbox_as_hexwkb(arr.slice(i * STBOX_SIZE), (byte) 0, sizeOut); + return out; + } finally { MeosMemory.free(arr); } + } finally { MeosMemory.free(t); } + } + + public static final UDF2 splitNSpans = + (h, n) -> splitArrSpan(h, n, org.mobilitydb.spark.MeosNative.INSTANCE::temporal_split_n_spans); + + public static final UDF2 splitEachNSpans = + (h, n) -> splitArrSpan(h, n, org.mobilitydb.spark.MeosNative.INSTANCE::temporal_split_each_n_spans); + + public static final UDF2 splitNTboxes = + (h, n) -> splitArrTbox(h, n, org.mobilitydb.spark.MeosNative.INSTANCE::tnumber_split_n_tboxes); + + public static final UDF2 splitEachNTboxes = + (h, n) -> splitArrTbox(h, n, org.mobilitydb.spark.MeosNative.INSTANCE::tnumber_split_each_n_tboxes); + + public static final UDF2 splitNStboxes = + (h, n) -> splitArrStbox(h, n, org.mobilitydb.spark.MeosNative.INSTANCE::tgeo_split_n_stboxes); + + public static final UDF2 splitEachNStboxes = + (h, n) -> splitArrStbox(h, n, org.mobilitydb.spark.MeosNative.INSTANCE::tgeo_split_each_n_stboxes); + + // ------------------------------------------------------------------ + // Temporal time bins / tstzspan bins (interval input as ISO 8601 string) + // ------------------------------------------------------------------ + + private static long tsToPgEpochMicros(java.sql.Timestamp ts) { + return (ts.getTime() - TimeUtil.PG_UNIX_EPOCH_OFFSET_MS) * 1000L; + } + + public static final org.apache.spark.sql.api.java.UDF3 + timeBins = (hex, intervalStr, origin) -> { + if (hex == null || intervalStr == null || origin == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(hex); + if (t == null) return null; + Pointer iv = GeneratedFunctions.pg_interval_in(intervalStr, -1); + if (iv == null) { MeosMemory.free(t); return null; } + try { + jnr.ffi.Runtime rt = jnr.ffi.Runtime.getSystemRuntime(); + Pointer countOut = rt.getMemoryManager().allocateDirect(4); + Pointer arr = org.mobilitydb.spark.MeosNative.INSTANCE + .temporal_time_bins(t, iv, tsToPgEpochMicros(origin), countOut); + if (arr == null) return null; + try { + int n = countOut.getInt(0); + String[] out = new String[n]; + for (int i = 0; i < n; i++) out[i] = GeneratedFunctions.span_as_hexwkb(arr.slice(i * SPAN_SIZE), (byte) 0); + return out; + } finally { MeosMemory.free(arr); } + } finally { MeosMemory.free(t, iv); } + }; + + // ------------------------------------------------------------------ + // stbox_quad_split + getBin scalar + // ------------------------------------------------------------------ + + public static final UDF1 quadSplit = + hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer s = GeneratedFunctions.stbox_from_hexwkb(hex); + if (s == null) return null; + try { + jnr.ffi.Runtime rt = jnr.ffi.Runtime.getSystemRuntime(); + Pointer countOut = rt.getMemoryManager().allocateDirect(4); + Pointer arr = org.mobilitydb.spark.MeosNative.INSTANCE.stbox_quad_split(s, countOut); + if (arr == null) return null; + try { + int n = countOut.getInt(0); + String[] out = new String[n]; + Pointer sizeOut = rt.getMemoryManager().allocateDirect(8); + for (int i = 0; i < n; i++) out[i] = GeneratedFunctions.stbox_as_hexwkb(arr.slice(i * STBOX_SIZE), (byte) 0, sizeOut); + return out; + } finally { MeosMemory.free(arr); } + } finally { MeosMemory.free(s); } + }; + + // getBin(timestamptz, interval, origin) β†’ timestamptz (start of bin) + public static final org.apache.spark.sql.api.java.UDF3 + timestamptzGetBin = (ts, intervalStr, origin) -> { + if (ts == null || intervalStr == null || origin == null) return null; + MeosThread.ensureReady(); + Pointer iv = GeneratedFunctions.pg_interval_in(intervalStr, -1); + if (iv == null) return null; + try { + long binStart = org.mobilitydb.spark.MeosNative.INSTANCE + .timestamptz_get_bin(tsToPgEpochMicros(ts), iv, tsToPgEpochMicros(origin)); + // Convert PG-epoch micros β†’ java Timestamp + long unixMicros = binStart + TimeUtil.PG_UNIX_EPOCH_OFFSET_US; + return new java.sql.Timestamp(unixMicros / 1000); + } finally { MeosMemory.free(iv); } + }; + + public static final UDF3 tintValueBins = + (hex, vsize, vorigin) -> { + if (hex == null || vsize == null || vorigin == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(hex); + if (t == null) return null; + try { + jnr.ffi.Runtime rt = jnr.ffi.Runtime.getSystemRuntime(); + Pointer countOut = rt.getMemoryManager().allocateDirect(4); + Pointer arr = org.mobilitydb.spark.MeosNative.INSTANCE.tint_value_bins(t, vsize, vorigin, countOut); + if (arr == null) return null; + try { + int n = countOut.getInt(0); + String[] out = new String[n]; + for (int i = 0; i < n; i++) out[i] = GeneratedFunctions.span_as_hexwkb(arr.slice(i * SPAN_SIZE), (byte) 0); + return out; + } finally { MeosMemory.free(arr); } + } finally { MeosMemory.free(t); } + }; + + public static final UDF3 tfloatValueBins = + (hex, vsize, vorigin) -> { + if (hex == null || vsize == null || vorigin == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(hex); + if (t == null) return null; + try { + jnr.ffi.Runtime rt = jnr.ffi.Runtime.getSystemRuntime(); + Pointer countOut = rt.getMemoryManager().allocateDirect(4); + Pointer arr = org.mobilitydb.spark.MeosNative.INSTANCE.tfloat_value_bins(t, vsize, vorigin, countOut); + if (arr == null) return null; + try { + int n = countOut.getInt(0); + String[] out = new String[n]; + for (int i = 0; i < n; i++) out[i] = GeneratedFunctions.span_as_hexwkb(arr.slice(i * SPAN_SIZE), (byte) 0); + return out; + } finally { MeosMemory.free(arr); } + } finally { MeosMemory.free(t); } + }; + + public static final org.apache.spark.sql.api.java.UDF3 + tstzspanBins = (hex, intervalStr, origin) -> { + if (hex == null || intervalStr == null || origin == null) return null; + MeosThread.ensureReady(); + Pointer s = GeneratedFunctions.span_from_hexwkb(hex); + if (s == null) return null; + Pointer iv = GeneratedFunctions.pg_interval_in(intervalStr, -1); + if (iv == null) { MeosMemory.free(s); return null; } + try { + jnr.ffi.Runtime rt = jnr.ffi.Runtime.getSystemRuntime(); + Pointer countOut = rt.getMemoryManager().allocateDirect(4); + Pointer arr = org.mobilitydb.spark.MeosNative.INSTANCE + .tstzspan_bins(s, iv, tsToPgEpochMicros(origin), countOut); + if (arr == null) return null; + try { + int n = countOut.getInt(0); + String[] out = new String[n]; + for (int i = 0; i < n; i++) out[i] = GeneratedFunctions.span_as_hexwkb(arr.slice(i * SPAN_SIZE), (byte) 0); + return out; + } finally { MeosMemory.free(arr); } + } finally { MeosMemory.free(s, iv); } + }; + + public static final UDF3 floatspanBins = + (hex, vsize, vorigin) -> { + if (hex == null || vsize == null || vorigin == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.span_from_hexwkb(hex); + if (p == null) return null; + try { + jnr.ffi.Runtime rt = jnr.ffi.Runtime.getSystemRuntime(); + Pointer countOut = rt.getMemoryManager().allocateDirect(4); + Pointer arr = org.mobilitydb.spark.MeosNative.INSTANCE.floatspan_bins(p, vsize, vorigin, countOut); + if (arr == null) return null; + try { + int n = countOut.getInt(0); + String[] out = new String[n]; + for (int i = 0; i < n; i++) out[i] = GeneratedFunctions.span_as_hexwkb(arr.slice(i * SPAN_SIZE), (byte) 0); + return out; + } finally { MeosMemory.free(arr); } + } finally { MeosMemory.free(p); } + }; + + public static final UDF1 stboxes = + hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.temporal_from_hexwkb(hex); + if (p == null) return null; + try { + jnr.ffi.Runtime rt = jnr.ffi.Runtime.getSystemRuntime(); + Pointer countOut = rt.getMemoryManager().allocateDirect(4); + Pointer arr = org.mobilitydb.spark.MeosNative.INSTANCE.tgeo_stboxes(p, countOut); + if (arr == null) return null; + try { + int n = countOut.getInt(0); + String[] out = new String[n]; + Pointer sizeOut = rt.getMemoryManager().allocateDirect(8); + for (int i = 0; i < n; i++) { + Pointer elem = arr.slice(i * STBOX_SIZE); + out[i] = GeneratedFunctions.stbox_as_hexwkb(elem, (byte) 0, sizeOut); + } + return out; + } finally { MeosMemory.free(arr); } + } finally { MeosMemory.free(p); } + }; + + // ------------------------------------------------------------------ + // Time-weighted average value of tnumber + // ------------------------------------------------------------------ + + public static final UDF1 avgValue = + hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.temporal_from_hexwkb(hex); + if (p == null) return null; + try { return org.mobilitydb.spark.MeosNative.INSTANCE.tnumber_avg_value(p); } + finally { MeosMemory.free(p); } + }; + + public static final UDF1 floatsetValues = + hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.set_from_hexwkb(hex); + if (p == null) return null; + try { + int n = GeneratedFunctions.set_num_values(p); + Double[] out = new Double[n]; + jnr.ffi.Runtime rt = jnr.ffi.Runtime.getSystemRuntime(); + Pointer outBuf = rt.getMemoryManager().allocateDirect(8); + for (int i = 0; i < n; i++) { + if (org.mobilitydb.spark.MeosNative.INSTANCE.floatset_value_n(p, i + 1, outBuf)) { + out[i] = outBuf.getDouble(0); + } + } + return out; + } finally { MeosMemory.free(p); } + }; + + // ------------------------------------------------------------------ + // Per-type setFromBinary aliases β€” generic byte[]β†’hexβ†’set_from_hexwkb + // ------------------------------------------------------------------ + + private static UDF1 setFromBinaryFn() { + return bytes -> { + if (bytes == null) return null; + MeosThread.ensureReady(); + String hex = HexFormat.of().formatHex(bytes).toUpperCase(); + Pointer p = GeneratedFunctions.set_from_hexwkb(hex); + if (p == null) return null; + try { return GeneratedFunctions.set_as_hexwkb(p, (byte) 0); } + finally { MeosMemory.free(p); } + }; + } + + // ------------------------------------------------------------------ + // Array-returning accessors (return Spark ArrayType) + // ------------------------------------------------------------------ + + // spans(spanset) β†’ array of span hex-WKB strings + public static final UDF1 spans = + hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.spanset_from_hexwkb(hex); + if (p == null) return null; + try { + int n = GeneratedFunctions.spanset_num_spans(p); + String[] out = new String[n]; + for (int i = 0; i < n; i++) { + // 1-based per MEOS convention + Pointer span = GeneratedFunctions.spanset_span_n(p, i + 1); + if (span == null) { out[i] = null; continue; } + out[i] = GeneratedFunctions.span_as_hexwkb(span, (byte) 0); + // span_n returns a fresh copy in newer MEOS; free it + MeosMemory.free(span); + } + return out; + } finally { MeosMemory.free(p); } + }; + + public static final UDF1 intsetFromBinary = setFromBinaryFn(); + public static final UDF1 bigintsetFromBinary = setFromBinaryFn(); + public static final UDF1 floatsetFromBinary = setFromBinaryFn(); + public static final UDF1 textsetFromBinary = setFromBinaryFn(); + public static final UDF1 tstzsetFromBinary = setFromBinaryFn(); + public static final UDF1 datesetFromBinary = setFromBinaryFn(); + + public static void registerAll(SparkSession spark) { + spark.udf().register("intspanWidth", intspanWidth, DataTypes.IntegerType); + spark.udf().register("bigintspanWidth", bigintspanWidth, DataTypes.LongType); + spark.udf().register("floatspanWidth", floatspanWidth, DataTypes.DoubleType); + spark.udf().register("intspansetWidth", intspansetWidth, DataTypes.IntegerType); + spark.udf().register("bigintspansetWidth", bigintspansetWidth, DataTypes.LongType); + spark.udf().register("floatspansetWidth", floatspansetWidth, DataTypes.DoubleType); + // single-arg width aliases (boundspan defaults to false in tests) + spark.udf().register("width", floatspanWidth, DataTypes.DoubleType); + + spark.udf().register("startDate", startDate, DataTypes.IntegerType); + spark.udf().register("endDate", endDate, DataTypes.IntegerType); + spark.udf().register("numDates", numDates, DataTypes.IntegerType); + spark.udf().register("dateN", dateN, DataTypes.IntegerType); + spark.udf().register("dates", dates, DataTypes.StringType); + + spark.udf().register("valueSpan", valueSpan, DataTypes.StringType); + + spark.udf().register("intsetFromBinary", intsetFromBinary, DataTypes.StringType); + spark.udf().register("bigintsetFromBinary", bigintsetFromBinary, DataTypes.StringType); + spark.udf().register("floatsetFromBinary", floatsetFromBinary, DataTypes.StringType); + spark.udf().register("textsetFromBinary", textsetFromBinary, DataTypes.StringType); + spark.udf().register("tstzsetFromBinary", tstzsetFromBinary, DataTypes.StringType); + spark.udf().register("datesetFromBinary", datesetFromBinary, DataTypes.StringType); + + // Array-returning accessors + spark.udf().register("spans", spans, DataTypes.createArrayType(DataTypes.StringType)); + + // tgeo type conversions + spark.udf().register("tgeometry", tgeometry, DataTypes.StringType); + spark.udf().register("tgeography", tgeography, DataTypes.StringType); + // MobilityDB SQL bare-name aliases + spark.udf().register("geometry", tgeometry, DataTypes.StringType); + spark.udf().register("geography", tgeography, DataTypes.StringType); + // Introspection + spark.udf().register("mobilitydbVersion", mobilitydbVersion, DataTypes.StringType); + spark.udf().register("mobilitydbFullVersion", mobilitydbFullVersion, DataTypes.StringType); + // valueSet (Datum-array β†’ typed Set), segmentMin/MaxDuration, box3d + spark.udf().register("valueSet", valueSet, DataTypes.StringType); + spark.udf().register("segmentMinDuration", segmentMinDuration, DataTypes.StringType); + spark.udf().register("segmentMaxDuration", segmentMaxDuration, DataTypes.StringType); + spark.udf().register("box2d", box2d, DataTypes.StringType); + spark.udf().register("box3d", box3d, DataTypes.StringType); + // Typed set values (array-returning) + spark.udf().register("intsetValues", intsetValues, DataTypes.createArrayType(DataTypes.IntegerType)); + spark.udf().register("bigintsetValues", bigintsetValues, DataTypes.createArrayType(DataTypes.LongType)); + spark.udf().register("floatsetValues", floatsetValues, DataTypes.createArrayType(DataTypes.DoubleType)); + // MobilityDB SQL bare-name aliases (typed dispatchers β€” picks float as default) + spark.udf().register("getValues", floatsetValues, DataTypes.createArrayType(DataTypes.DoubleType)); + spark.udf().register("unnest", floatsetValues, DataTypes.createArrayType(DataTypes.DoubleType)); + // Time tiling + spark.udf().register("timeBins", timeBins, DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("tstzspanBins", tstzspanBins, DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("tintValueBins", tintValueBins, DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("tfloatValueBins", tfloatValueBins, DataTypes.createArrayType(DataTypes.StringType)); + // Bare-name alias (defaults to float) + spark.udf().register("valueBins", tfloatValueBins, DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("quadSplit", quadSplit, DataTypes.createArrayType(DataTypes.StringType)); + // Scalar getBin for timestamptz; numeric variants are floatBucket/intBucket + spark.udf().register("timestamptzGetBin", timestamptzGetBin, DataTypes.TimestampType); + spark.udf().register("getBin", timestamptzGetBin, DataTypes.TimestampType); + spark.udf().register("avgValue", avgValue, DataTypes.DoubleType); + spark.udf().register("tboxes", tboxes, DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("stboxes", stboxes, DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("intspanBins", intspanBins, DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("bigintspanBins", bigintspanBins, DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("floatspanBins", floatspanBins, DataTypes.createArrayType(DataTypes.StringType)); + // MobilityDB SQL bare-name alias + spark.udf().register("bins", floatspanBins, DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("splitNSpans", splitNSpans, DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("splitEachNSpans", splitEachNSpans, DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("splitNTboxes", splitNTboxes, DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("splitEachNTboxes", splitEachNTboxes, DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("splitNStboxes", splitNStboxes, DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("splitEachNStboxes", splitEachNStboxes, DataTypes.createArrayType(DataTypes.StringType)); + } + + // ------------------------------------------------------------------ + // Introspection + // ------------------------------------------------------------------ + + public static final org.apache.spark.sql.api.java.UDF0 mobilitydbVersion = + () -> { + MeosThread.ensureReady(); + return org.mobilitydb.spark.MeosNative.INSTANCE.mobilitydb_version(); + }; + + public static final org.apache.spark.sql.api.java.UDF0 mobilitydbFullVersion = + () -> { + MeosThread.ensureReady(); + return org.mobilitydb.spark.MeosNative.INSTANCE.mobilitydb_full_version(); + }; + + // ------------------------------------------------------------------ + // tgeo type conversions + // ------------------------------------------------------------------ + + // ------------------------------------------------------------------ + // valueSet, segmentMin/MaxDuration, box3d (PostGIS embedded in MEOS) + // ------------------------------------------------------------------ + + // valueSet(temporal) β†’ set hex containing distinct values. + // Mirrors MobilityDB's PG-side Temporal_valueset: + // 1. temporal_values_p β†’ Datum* + count + // 2. temptype_basetype(temptype) β†’ MeosType basetype + // 3. set_make_free β†’ Set* (consumes the Datum* array) + public static final UDF1 valueSet = + hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(hex); + if (t == null) return null; + try { + int temptype = t.getByte(4) & 0xff; + int basetype = org.mobilitydb.spark.MeosNative.INSTANCE.temptype_basetype(temptype); + jnr.ffi.Runtime rt = jnr.ffi.Runtime.getSystemRuntime(); + Pointer countOut = rt.getMemoryManager().allocateDirect(4); + Pointer values = org.mobilitydb.spark.MeosNative.INSTANCE.temporal_values_p(t, countOut); + if (values == null) return null; + int count = countOut.getInt(0); + Pointer s = org.mobilitydb.spark.MeosNative.INSTANCE + .set_make_free(values, count, basetype, false); + if (s == null) return null; + try { return GeneratedFunctions.set_as_hexwkb(s, (byte) 0); } + finally { MeosMemory.free(s); } + } finally { MeosMemory.free(t); } + }; + + // segmentMinDuration(temporal, intervalStr, strict) β†’ temporal sequence-set + public static final org.apache.spark.sql.api.java.UDF3 + segmentMinDuration = (hex, intervalStr, strict) -> segmDuration(hex, intervalStr, strict, true); + + public static final org.apache.spark.sql.api.java.UDF3 + segmentMaxDuration = (hex, intervalStr, strict) -> segmDuration(hex, intervalStr, strict, false); + + private static String segmDuration(String hex, String intervalStr, Boolean strict, boolean atleast) { + if (hex == null || intervalStr == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(hex); + if (t == null) return null; + Pointer iv = GeneratedFunctions.pg_interval_in(intervalStr, -1); + if (iv == null) { MeosMemory.free(t); return null; } + try { + Pointer r = org.mobilitydb.spark.MeosNative.INSTANCE + .temporal_segm_duration(t, iv, atleast, strict != null && strict); + if (r == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(t, iv); } + } + + // box2d(stbox) β†’ text representation "BOX(xmin ymin,xmax ymax)" + // PostGIS BOX2D / GBOX type (embedded in MEOS). + public static final UDF1 box2d = + hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer s = GeneratedFunctions.stbox_from_hexwkb(hex); + if (s == null) return null; + try { + Pointer b = org.mobilitydb.spark.MeosNative.INSTANCE.stbox_to_gbox(s); + if (b == null) return null; + try { return org.mobilitydb.spark.MeosNative.INSTANCE.gbox_out(b, 15); } + finally { MeosMemory.free(b); } + } finally { MeosMemory.free(s); } + }; + + // box3d(stbox) β†’ text representation "BOX3D(xmin ymin zmin,xmax ymax zmax)" + // PostGIS BOX3D type (embedded in MEOS). + public static final UDF1 box3d = + hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer s = GeneratedFunctions.stbox_from_hexwkb(hex); + if (s == null) return null; + try { + Pointer b = org.mobilitydb.spark.MeosNative.INSTANCE.stbox_to_box3d(s); + if (b == null) return null; + try { return org.mobilitydb.spark.MeosNative.INSTANCE.box3d_out(b, 15); } + finally { MeosMemory.free(b); } + } finally { MeosMemory.free(s); } + }; + + public static final UDF1 tgeometry = + hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.temporal_from_hexwkb(hex); + if (p == null) return null; + try { + // Try tgeompoint β†’ tgeometry first; if that fails, try tgeography β†’ tgeometry + Pointer r = org.mobilitydb.spark.MeosNative.INSTANCE.tgeompoint_to_tgeometry(p); + if (r == null) r = org.mobilitydb.spark.MeosNative.INSTANCE.tgeography_to_tgeometry(p); + if (r == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(p); } + }; + + public static final UDF1 tgeography = + hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.temporal_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer r = org.mobilitydb.spark.MeosNative.INSTANCE.tgeogpoint_to_tgeography(p); + if (r == null) r = org.mobilitydb.spark.MeosNative.INSTANCE.tgeometry_to_tgeography(p); + if (r == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(p); } + }; +} diff --git a/src/main/java/org/mobilitydb/spark/temporal/AccessorUDFs.java b/src/main/java/org/mobilitydb/spark/temporal/AccessorUDFs.java new file mode 100644 index 00000000..ff65211d --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/temporal/AccessorUDFs.java @@ -0,0 +1,654 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosThread; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.api.java.UDF1; +import org.apache.spark.sql.api.java.UDF2; +import org.apache.spark.sql.types.DataTypes; + +import java.sql.Timestamp; + +/** + * Spark SQL UDFs for temporal accessor and manipulation operations. + * + * Naming convention mirrors MobilityDuck where possible, using camelCase for + * Spark SQL UDF names. Type-specific UDFs use a type prefix (tfloat*, tint*) + * where the return type depends on the base temporal type. + * + * MEOS function authority: meos/include/meos.h + */ +public final class AccessorUDFs { + + private AccessorUDFs() {} + + // ------------------------------------------------------------------ + // Type-agnostic accessors (return type does not depend on base type) + // ------------------------------------------------------------------ + + // numSequences(trip STRING) β†’ INT + // MEOS: temporal_num_sequences(const Temporal *) β†’ int + public static final UDF1 numSequences = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + return GeneratedFunctions.temporal_num_sequences(ptr); + }; + + // interp(trip STRING) β†’ STRING ("Discrete" | "Stepwise" | "Linear") + // MEOS: temporal_interp(const Temporal *) β†’ char * + public static final UDF1 interp = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + return GeneratedFunctions.temporal_interp(ptr); + }; + + // time(trip STRING) β†’ STRING (hex-WKB of tstzspanset bounding the instants) + // MEOS: temporal_time(const Temporal *) β†’ SpanSet * + public static final UDF1 time = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + Pointer ss = GeneratedFunctions.temporal_time(ptr); + if (ss == null) return null; + return GeneratedFunctions.spanset_as_hexwkb(ss, (byte) 0); + }; + + // timespan(trip STRING) β†’ STRING (hex-WKB of tstzspan β€” overall bounding period) + // MEOS: temporal_to_tstzspan(const Temporal *) β†’ Span * + public static final UDF1 timespan = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + Pointer s = GeneratedFunctions.temporal_to_tstzspan(ptr); + if (s == null) return null; + return GeneratedFunctions.span_as_hexwkb(s, (byte) 0); + }; + + // merge(trip1 STRING, trip2 STRING) β†’ STRING + // MEOS: temporal_merge(const Temporal *, const Temporal *) β†’ Temporal * + public static final UDF2 merge = + (trip1, trip2) -> { + if (trip1 == null || trip2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(trip1); + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(trip2); + if (p1 == null || p2 == null) return null; + Pointer result = GeneratedFunctions.temporal_merge(p1, p2); + if (result == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + }; + + // shift(trip STRING, deltaStr STRING) β†’ STRING + // deltaStr is a PostgreSQL interval literal, e.g. "1 day" or "01:00:00". + // MEOS: temporal_shift_time(const Temporal *, const Interval *) β†’ Temporal * + public static final UDF2 shift = + (trip, deltaStr) -> { + if (trip == null || deltaStr == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + Pointer ivPtr = GeneratedFunctions.pg_interval_in(deltaStr, -1); + if (tptr == null || ivPtr == null) return null; + Pointer result = GeneratedFunctions.temporal_shift_time(tptr, ivPtr); + if (result == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + }; + + // scale(trip STRING, durationStr STRING) β†’ STRING + // MEOS: temporal_scale_time(const Temporal *, const Interval *) β†’ Temporal * + public static final UDF2 scale = + (trip, durationStr) -> { + if (trip == null || durationStr == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + Pointer ivPtr = GeneratedFunctions.pg_interval_in(durationStr, -1); + if (tptr == null || ivPtr == null) return null; + Pointer result = GeneratedFunctions.temporal_scale_time(tptr, ivPtr); + if (result == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + }; + + // atSpan(trip STRING, spanHex STRING) β†’ STRING + // MEOS: temporal_at_tstzspan(const Temporal *, const Span *) β†’ Temporal * + public static final UDF2 atSpan = + (trip, spanHex) -> { + if (trip == null || spanHex == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + Pointer sptr = GeneratedFunctions.span_from_hexwkb(spanHex); + if (tptr == null || sptr == null) return null; + Pointer result = GeneratedFunctions.temporal_at_tstzspan(tptr, sptr); + if (result == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + }; + + // atSpanset(trip STRING, spansetHex STRING) β†’ STRING + // MEOS: temporal_at_tstzspanset(const Temporal *, const SpanSet *) β†’ Temporal * + public static final UDF2 atSpanset = + (trip, spansetHex) -> { + if (trip == null || spansetHex == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + Pointer ssptr = GeneratedFunctions.spanset_from_hexwkb(spansetHex); + if (tptr == null || ssptr == null) return null; + Pointer result = GeneratedFunctions.temporal_at_tstzspanset(tptr, ssptr); + if (result == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + }; + + // insert(trip1 STRING, trip2 STRING) β†’ STRING + // MEOS: temporal_insert(const Temporal *, const Temporal *, bool connect) β†’ Temporal * + public static final UDF2 insert = + (trip1, trip2) -> { + if (trip1 == null || trip2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(trip1); + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(trip2); + if (p1 == null || p2 == null) return null; + Pointer result = GeneratedFunctions.temporal_insert(p1, p2, true); + if (result == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + }; + + // update(trip1 STRING, trip2 STRING) β†’ STRING + // MEOS: temporal_update(const Temporal *, const Temporal *, bool connect) β†’ Temporal * + public static final UDF2 update = + (trip1, trip2) -> { + if (trip1 == null || trip2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(trip1); + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(trip2); + if (p1 == null || p2 == null) return null; + Pointer result = GeneratedFunctions.temporal_update(p1, p2, true); + if (result == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + }; + + // ------------------------------------------------------------------ + // Type-specific value accessors (return type matches the base type) + // ------------------------------------------------------------------ + + // tfloatStartValue(trip STRING) β†’ DOUBLE + // MEOS: tfloat_start_value(const Temporal *) β†’ double + public static final UDF1 tfloatStartValue = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + return GeneratedFunctions.tfloat_start_value(ptr); + }; + + // tfloatEndValue(trip STRING) β†’ DOUBLE + // MEOS: tfloat_end_value(const Temporal *) β†’ double + public static final UDF1 tfloatEndValue = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + return GeneratedFunctions.tfloat_end_value(ptr); + }; + + // tfloatMinValue(trip STRING) β†’ DOUBLE + // MEOS: tfloat_min_value(const Temporal *) β†’ double + public static final UDF1 tfloatMinValue = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + return GeneratedFunctions.tfloat_min_value(ptr); + }; + + // tfloatMaxValue(trip STRING) β†’ DOUBLE + // MEOS: tfloat_max_value(const Temporal *) β†’ double + public static final UDF1 tfloatMaxValue = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + return GeneratedFunctions.tfloat_max_value(ptr); + }; + + // tintStartValue(trip STRING) β†’ INT + // MEOS: tint_start_value(const Temporal *) β†’ int + public static final UDF1 tintStartValue = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + return GeneratedFunctions.tint_start_value(ptr); + }; + + // tintEndValue(trip STRING) β†’ INT + // MEOS: tint_end_value(const Temporal *) β†’ int + public static final UDF1 tintEndValue = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + return GeneratedFunctions.tint_end_value(ptr); + }; + + // tintMinValue(trip STRING) β†’ INT + // MEOS: tint_min_value(const Temporal *) β†’ int + public static final UDF1 tintMinValue = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + return GeneratedFunctions.tint_min_value(ptr); + }; + + // tintMaxValue(trip STRING) β†’ INT + // MEOS: tint_max_value(const Temporal *) β†’ int + public static final UDF1 tintMaxValue = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + return GeneratedFunctions.tint_max_value(ptr); + }; + + // tboolStartValue(trip STRING) β†’ BOOLEAN + // MEOS: tbool_start_value(const Temporal *) β†’ bool + public static final UDF1 tboolStartValue = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + return GeneratedFunctions.tbool_start_value(ptr); + }; + + // tboolEndValue(trip STRING) β†’ BOOLEAN + // MEOS: tbool_end_value(const Temporal *) β†’ bool + public static final UDF1 tboolEndValue = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + return GeneratedFunctions.tbool_end_value(ptr); + }; + + // tpointStartValue(trip STRING) β†’ STRING (WKT of start geometry) + // MEOS: tpoint_start_value(const Temporal *) β†’ GSERIALIZED * + public static final UDF1 tpointStartValue = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + Pointer gs = GeneratedFunctions.tgeo_start_value(ptr); + if (gs == null) return null; + return GeneratedFunctions.geo_as_text(gs, 6); + }; + + // tpointEndValue(trip STRING) β†’ STRING (WKT of end geometry) + // MEOS: tpoint_end_value(const Temporal *) β†’ GSERIALIZED * + public static final UDF1 tpointEndValue = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + Pointer gs = GeneratedFunctions.tgeo_end_value(ptr); + if (gs == null) return null; + return GeneratedFunctions.geo_as_text(gs, 6); + }; + + // ttextStartValue(trip STRING) β†’ STRING (text value at start) + // MEOS: ttext_start_value(const Temporal *) β†’ text * + public static final UDF1 ttextStartValue = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + Pointer txt = GeneratedFunctions.ttext_start_value(ptr); + if (txt == null) return null; + return GeneratedFunctions.text_out(txt); + }; + + // ttextEndValue(trip STRING) β†’ STRING (text value at end) + // MEOS: ttext_end_value(const Temporal *) β†’ text * + public static final UDF1 ttextEndValue = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + Pointer txt = GeneratedFunctions.ttext_end_value(ptr); + if (txt == null) return null; + return GeneratedFunctions.text_out(txt); + }; + + // ------------------------------------------------------------------ + // Value restriction: atMin / atMax / atValues / minusTime / minusMin / minusMax + // ------------------------------------------------------------------ + + // atMin(trip STRING) β†’ STRING + public static final UDF1 atMin = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + Pointer r = GeneratedFunctions.temporal_at_min(ptr); + if (r == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + }; + + // atMax(trip STRING) β†’ STRING + public static final UDF1 atMax = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + Pointer r = GeneratedFunctions.temporal_at_max(ptr); + if (r == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + }; + + // atValues(trip STRING, setHex STRING) β†’ STRING + public static final UDF2 atValues = + (trip, setHex) -> { + if (trip == null || setHex == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + Pointer sptr = GeneratedFunctions.set_from_hexwkb(setHex); + if (tptr == null || sptr == null) return null; + Pointer r = GeneratedFunctions.temporal_at_values(tptr, sptr); + if (r == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + }; + + // minusTime(trip STRING, tstzspanHex STRING) β†’ STRING + public static final UDF2 minusTime = + (trip, spanHex) -> { + if (trip == null || spanHex == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + Pointer sptr = GeneratedFunctions.span_from_hexwkb(spanHex); + if (tptr == null || sptr == null) return null; + Pointer r = GeneratedFunctions.temporal_minus_tstzspan(tptr, sptr); + if (r == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + }; + + // minusMin(trip STRING) β†’ STRING + public static final UDF1 minusMin = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + Pointer r = GeneratedFunctions.temporal_minus_min(ptr); + if (r == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + }; + + // minusMax(trip STRING) β†’ STRING + public static final UDF1 minusMax = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + Pointer r = GeneratedFunctions.temporal_minus_max(ptr); + if (r == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + }; + + // ------------------------------------------------------------------ + // Spatio-temporal restriction: atStbox / minusStbox / tnumberAtTbox / tnumberMinusTbox + // ------------------------------------------------------------------ + + // atStbox(trip STRING, stboxHex STRING) β†’ STRING + public static final UDF2 atStbox = + (trip, stboxHex) -> { + if (trip == null || stboxHex == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + Pointer bptr = GeneratedFunctions.stbox_from_hexwkb(stboxHex); + if (tptr == null || bptr == null) return null; + Pointer r = GeneratedFunctions.tgeo_at_stbox(tptr, bptr, true); + if (r == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + }; + + // minusStbox(trip STRING, stboxHex STRING) β†’ STRING + public static final UDF2 minusStbox = + (trip, stboxHex) -> { + if (trip == null || stboxHex == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + Pointer bptr = GeneratedFunctions.stbox_from_hexwkb(stboxHex); + if (tptr == null || bptr == null) return null; + Pointer r = GeneratedFunctions.tgeo_minus_stbox(tptr, bptr, true); + if (r == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + }; + + // tnumberAtTbox(trip STRING, tboxHex STRING) β†’ STRING + public static final UDF2 tnumberAtTbox = + (trip, tboxHex) -> { + if (trip == null || tboxHex == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + Pointer bptr = GeneratedFunctions.tbox_from_hexwkb(tboxHex); + if (tptr == null || bptr == null) return null; + Pointer r = GeneratedFunctions.tnumber_at_tbox(tptr, bptr); + if (r == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + }; + + // tnumberMinusTbox(trip STRING, tboxHex STRING) β†’ STRING + public static final UDF2 tnumberMinusTbox = + (trip, tboxHex) -> { + if (trip == null || tboxHex == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + Pointer bptr = GeneratedFunctions.tbox_from_hexwkb(tboxHex); + if (tptr == null || bptr == null) return null; + Pointer r = GeneratedFunctions.tnumber_minus_tbox(tptr, bptr); + if (r == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + }; + + // ------------------------------------------------------------------ + // Append operations + // ------------------------------------------------------------------ + + // appendInstant(trip STRING, instantHex STRING) β†’ STRING + public static final UDF2 appendInstant = + (trip, instantHex) -> { + if (trip == null || instantHex == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + Pointer iptr = GeneratedFunctions.temporal_from_hexwkb(instantHex); + if (tptr == null || iptr == null) return null; + Pointer r = GeneratedFunctions.temporal_append_tinstant(tptr, iptr, 3 /* LINEAR */, 0.0, null, false); + if (r == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + }; + + // appendSequence(trip STRING, seqHex STRING) β†’ STRING + public static final UDF2 appendSequence = + (trip, seqHex) -> { + if (trip == null || seqHex == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + Pointer sptr = GeneratedFunctions.temporal_from_hexwkb(seqHex); + if (tptr == null || sptr == null) return null; + Pointer r = GeneratedFunctions.temporal_append_tsequence(tptr, sptr, false); + if (r == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + }; + + // ------------------------------------------------------------------ + // Value span: tnumberValuespans, tnumberToSpan, tnumberToTbox + // ------------------------------------------------------------------ + + // tnumberValuespans(trip STRING) β†’ STRING (hex-WKB of value spanset) + public static final UDF1 tnumberValuespans = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + Pointer ss = GeneratedFunctions.tnumber_valuespans(ptr); + if (ss == null) return null; + return GeneratedFunctions.spanset_as_hexwkb(ss, (byte) 0); + }; + + // tnumberToSpan(trip STRING) β†’ STRING (hex-WKB of value span covering all values) + // MEOS: tnumber_to_span(const Temporal *) β†’ Span * + public static final UDF1 tnumberToSpan = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + try { + Pointer sp = GeneratedFunctions.tnumber_to_span(ptr); + if (sp == null) return null; + try { + return GeneratedFunctions.span_as_hexwkb(sp, (byte) 0); + } finally { + MeosMemory.free(sp); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // tnumberToTbox(trip STRING) β†’ STRING (hex-WKB of TBox bounding box) + // MEOS: tnumber_to_tbox(const Temporal *) β†’ TBox * + public static final UDF1 tnumberToTbox = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + try { + Pointer tb = GeneratedFunctions.tnumber_to_tbox(ptr); + if (tb == null) return null; + try { + jnr.ffi.Runtime rt = jnr.ffi.Runtime.getSystemRuntime(); + jnr.ffi.Pointer sizeOut = rt.getMemoryManager().allocateDirect(8); + return GeneratedFunctions.tbox_as_hexwkb(tb, (byte) 0, sizeOut); + } finally { + MeosMemory.free(tb); + } + } finally { + MeosMemory.free(ptr); + } + }; + + public static void registerAll(SparkSession spark) { + // Type-agnostic + spark.udf().register("numSequences", numSequences, DataTypes.IntegerType); + spark.udf().register("interp", interp, DataTypes.StringType); + spark.udf().register("time", time, DataTypes.StringType); + spark.udf().register("timespan", timespan, DataTypes.StringType); + spark.udf().register("merge", merge, DataTypes.StringType); + spark.udf().register("shift", shift, DataTypes.StringType); + spark.udf().register("scale", scale, DataTypes.StringType); + spark.udf().register("atSpan", atSpan, DataTypes.StringType); + spark.udf().register("atSpanset", atSpanset, DataTypes.StringType); + spark.udf().register("insert", insert, DataTypes.StringType); + spark.udf().register("update", update, DataTypes.StringType); + // Type-specific float + spark.udf().register("tfloatStartValue", tfloatStartValue, DataTypes.DoubleType); + spark.udf().register("tfloatEndValue", tfloatEndValue, DataTypes.DoubleType); + spark.udf().register("tfloatMinValue", tfloatMinValue, DataTypes.DoubleType); + spark.udf().register("tfloatMaxValue", tfloatMaxValue, DataTypes.DoubleType); + // Type-specific int + spark.udf().register("tintStartValue", tintStartValue, DataTypes.IntegerType); + spark.udf().register("tintEndValue", tintEndValue, DataTypes.IntegerType); + spark.udf().register("tintMinValue", tintMinValue, DataTypes.IntegerType); + spark.udf().register("tintMaxValue", tintMaxValue, DataTypes.IntegerType); + // Type-specific bool + spark.udf().register("tboolStartValue", tboolStartValue, DataTypes.BooleanType); + spark.udf().register("tboolEndValue", tboolEndValue, DataTypes.BooleanType); + // Type-specific point (returns WKT) + spark.udf().register("tpointStartValue", tpointStartValue, DataTypes.StringType); + spark.udf().register("tpointEndValue", tpointEndValue, DataTypes.StringType); + // Type-specific text + spark.udf().register("ttextStartValue", ttextStartValue, DataTypes.StringType); + spark.udf().register("ttextEndValue", ttextEndValue, DataTypes.StringType); + // MobilityDB SQL bare-name `getValue` aliases β€” for an instant temporal, + // value-at-instant === start-value. Per-type variants for type safety. + spark.udf().register("tintGetValue", tintStartValue, DataTypes.IntegerType); + spark.udf().register("tfloatGetValue", tfloatStartValue, DataTypes.DoubleType); + spark.udf().register("tboolGetValue", tboolStartValue, DataTypes.BooleanType); + spark.udf().register("ttextGetValue", ttextStartValue, DataTypes.StringType); + spark.udf().register("tpointGetValue", tpointStartValue, DataTypes.StringType); + // Bare-name alias defaults to tfloat (most common analytics case) + spark.udf().register("getValue", tfloatStartValue, DataTypes.DoubleType); + // Value restriction + spark.udf().register("atMin", atMin, DataTypes.StringType); + spark.udf().register("atMax", atMax, DataTypes.StringType); + spark.udf().register("atValues", atValues, DataTypes.StringType); + spark.udf().register("minusTime", minusTime, DataTypes.StringType); + spark.udf().register("minusMin", minusMin, DataTypes.StringType); + spark.udf().register("minusMax", minusMax, DataTypes.StringType); + // Spatio-temporal restriction + spark.udf().register("atStbox", atStbox, DataTypes.StringType); + spark.udf().register("minusStbox", minusStbox, DataTypes.StringType); + spark.udf().register("tnumberAtTbox", tnumberAtTbox, DataTypes.StringType); + spark.udf().register("tnumberMinusTbox", tnumberMinusTbox, DataTypes.StringType); + // Append + spark.udf().register("appendInstant", appendInstant, DataTypes.StringType); + spark.udf().register("appendSequence", appendSequence, DataTypes.StringType); + // Value spans + spark.udf().register("tnumberValuespans", tnumberValuespans, DataTypes.StringType); + spark.udf().register("tnumberToSpan", tnumberToSpan, DataTypes.StringType); + spark.udf().register("tnumberToTbox", tnumberToTbox, DataTypes.StringType); + } +} diff --git a/src/main/java/org/mobilitydb/spark/temporal/AggregateUDAFs.java b/src/main/java/org/mobilitydb/spark/temporal/AggregateUDAFs.java new file mode 100644 index 00000000..c2d570bd --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/temporal/AggregateUDAFs.java @@ -0,0 +1,510 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import jnr.ffi.Runtime; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosThread; +import org.apache.spark.sql.Encoder; +import org.apache.spark.sql.Encoders; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.expressions.Aggregator; + +import java.io.Serializable; + +/** + * Spark SQL UDAFs (typed Aggregators) for temporal aggregate GeneratedFunctions. + * + * Each UDAF collects hex-WKB strings from each row into a newline-delimited + * buffer (BUF = String). The actual MEOS aggregation runs inside finish() + * by replaying the transfn over each collected value and calling finalfn. + * This design keeps the buffer serializable between Spark stages while still + * using the correct MEOS aggregate semantics. + * + * MEOS function authority: meos/include/meos.h (temporal aggregate transfns) + * + * Registration: call registerAll(spark). In SQL, use tCount(col), + * tAnd(col), tOr(col), tIntMin(col), tIntMax(col), tIntSum(col), + * tFloatMin(col), tFloatMax(col), tFloatSum(col), tTextMin(col), + * tTextMax(col), tCentroid(col), tExtent(col). + */ +public final class AggregateUDAFs { + + private AggregateUDAFs() {} + + // ------------------------------------------------------------------ + // Shared helpers + // ------------------------------------------------------------------ + + /** Split buffer on newlines; skip blank entries. */ + private static String[] entries(String buf) { + if (buf == null || buf.isBlank()) return new String[0]; + return buf.split("\n"); + } + + private static String append(String buf, String hex) { + if (hex == null || hex.isBlank()) return buf; + if (buf == null || buf.isBlank()) return hex; + return buf + "\n" + hex; + } + + private static String merge(String b1, String b2) { + if (b1 == null || b1.isBlank()) return b2; + if (b2 == null || b2.isBlank()) return b1; + return b1 + "\n" + b2; + } + + /** Serialize a temporal Pointer to hex-WKB and free it. */ + private static String hexOut(Pointer r) { + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } + + /** Serialize an STBox Pointer to hex-WKB and free it. */ + private static String stboxHex(Pointer p) { + if (p == null) return null; + try { + Pointer sizeOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + return GeneratedFunctions.stbox_as_hexwkb(p, (byte) 0, sizeOut); + } finally { + MeosMemory.free(p); + } + } + + // ------------------------------------------------------------------ + // tCount β€” count how many temporal values are defined at each instant + // Returns: tint hex-WKB + // MEOS: temporal_tcount_transfn + temporal_tagg_finalfn + // ------------------------------------------------------------------ + + public static final class TCountFn extends Aggregator + implements Serializable { + @Override public String zero() { return ""; } + + @Override public String reduce(String buf, String hex) { + return append(buf, hex); + } + + @Override public String merge(String b1, String b2) { + return AggregateUDAFs.merge(b1, b2); + } + + @Override public String finish(String buf) { + MeosThread.ensureReady(); + String[] hexes = entries(buf); + if (hexes.length == 0) return null; + Pointer state = null; + for (String hex : hexes) { + Pointer inp = GeneratedFunctions.temporal_from_hexwkb(hex); + if (inp == null) continue; + Pointer next = GeneratedFunctions.temporal_tcount_transfn(state, inp); + MeosMemory.free(inp); + state = next; + } + if (state == null) return null; + return hexOut(GeneratedFunctions.temporal_tagg_finalfn(state)); + } + + @Override public Encoder bufferEncoder() { return Encoders.STRING(); } + @Override public Encoder outputEncoder() { return Encoders.STRING(); } + } + + // ------------------------------------------------------------------ + // tAnd β€” temporal AND over tbool values + // Returns: tbool hex-WKB + // MEOS: tbool_tand_transfn + temporal_tagg_finalfn + // ------------------------------------------------------------------ + + public static final class TAndFn extends Aggregator + implements Serializable { + @Override public String zero() { return ""; } + @Override public String reduce(String buf, String hex) { return append(buf, hex); } + @Override public String merge(String b1, String b2) { return AggregateUDAFs.merge(b1, b2); } + + @Override public String finish(String buf) { + MeosThread.ensureReady(); + String[] hexes = entries(buf); + if (hexes.length == 0) return null; + Pointer state = null; + for (String hex : hexes) { + Pointer inp = GeneratedFunctions.temporal_from_hexwkb(hex); + if (inp == null) continue; + Pointer next = GeneratedFunctions.tbool_tand_transfn(state, inp); + MeosMemory.free(inp); + state = next; + } + if (state == null) return null; + return hexOut(GeneratedFunctions.temporal_tagg_finalfn(state)); + } + + @Override public Encoder bufferEncoder() { return Encoders.STRING(); } + @Override public Encoder outputEncoder() { return Encoders.STRING(); } + } + + // ------------------------------------------------------------------ + // tOr β€” temporal OR over tbool values + // Returns: tbool hex-WKB + // MEOS: tbool_tor_transfn + temporal_tagg_finalfn + // ------------------------------------------------------------------ + + public static final class TOrFn extends Aggregator + implements Serializable { + @Override public String zero() { return ""; } + @Override public String reduce(String buf, String hex) { return append(buf, hex); } + @Override public String merge(String b1, String b2) { return AggregateUDAFs.merge(b1, b2); } + + @Override public String finish(String buf) { + MeosThread.ensureReady(); + String[] hexes = entries(buf); + if (hexes.length == 0) return null; + Pointer state = null; + for (String hex : hexes) { + Pointer inp = GeneratedFunctions.temporal_from_hexwkb(hex); + if (inp == null) continue; + Pointer next = GeneratedFunctions.tbool_tor_transfn(state, inp); + MeosMemory.free(inp); + state = next; + } + if (state == null) return null; + return hexOut(GeneratedFunctions.temporal_tagg_finalfn(state)); + } + + @Override public Encoder bufferEncoder() { return Encoders.STRING(); } + @Override public Encoder outputEncoder() { return Encoders.STRING(); } + } + + // ------------------------------------------------------------------ + // tIntMin / tIntMax / tIntSum β€” temporal aggregates on tint + // Returns: tint hex-WKB + // ------------------------------------------------------------------ + + public static final class TIntMinFn extends Aggregator + implements Serializable { + @Override public String zero() { return ""; } + @Override public String reduce(String buf, String hex) { return append(buf, hex); } + @Override public String merge(String b1, String b2) { return AggregateUDAFs.merge(b1, b2); } + + @Override public String finish(String buf) { + MeosThread.ensureReady(); + String[] hexes = entries(buf); + if (hexes.length == 0) return null; + Pointer state = null; + for (String hex : hexes) { + Pointer inp = GeneratedFunctions.temporal_from_hexwkb(hex); + if (inp == null) continue; + Pointer next = GeneratedFunctions.tint_tmin_transfn(state, inp); + MeosMemory.free(inp); + state = next; + } + if (state == null) return null; + return hexOut(GeneratedFunctions.temporal_tagg_finalfn(state)); + } + + @Override public Encoder bufferEncoder() { return Encoders.STRING(); } + @Override public Encoder outputEncoder() { return Encoders.STRING(); } + } + + public static final class TIntMaxFn extends Aggregator + implements Serializable { + @Override public String zero() { return ""; } + @Override public String reduce(String buf, String hex) { return append(buf, hex); } + @Override public String merge(String b1, String b2) { return AggregateUDAFs.merge(b1, b2); } + + @Override public String finish(String buf) { + MeosThread.ensureReady(); + String[] hexes = entries(buf); + if (hexes.length == 0) return null; + Pointer state = null; + for (String hex : hexes) { + Pointer inp = GeneratedFunctions.temporal_from_hexwkb(hex); + if (inp == null) continue; + Pointer next = GeneratedFunctions.tint_tmax_transfn(state, inp); + MeosMemory.free(inp); + state = next; + } + if (state == null) return null; + return hexOut(GeneratedFunctions.temporal_tagg_finalfn(state)); + } + + @Override public Encoder bufferEncoder() { return Encoders.STRING(); } + @Override public Encoder outputEncoder() { return Encoders.STRING(); } + } + + public static final class TIntSumFn extends Aggregator + implements Serializable { + @Override public String zero() { return ""; } + @Override public String reduce(String buf, String hex) { return append(buf, hex); } + @Override public String merge(String b1, String b2) { return AggregateUDAFs.merge(b1, b2); } + + @Override public String finish(String buf) { + MeosThread.ensureReady(); + String[] hexes = entries(buf); + if (hexes.length == 0) return null; + Pointer state = null; + for (String hex : hexes) { + Pointer inp = GeneratedFunctions.temporal_from_hexwkb(hex); + if (inp == null) continue; + Pointer next = GeneratedFunctions.tint_tsum_transfn(state, inp); + MeosMemory.free(inp); + state = next; + } + if (state == null) return null; + return hexOut(GeneratedFunctions.temporal_tagg_finalfn(state)); + } + + @Override public Encoder bufferEncoder() { return Encoders.STRING(); } + @Override public Encoder outputEncoder() { return Encoders.STRING(); } + } + + // ------------------------------------------------------------------ + // tFloatMin / tFloatMax / tFloatSum β€” temporal aggregates on tfloat + // Returns: tfloat hex-WKB + // ------------------------------------------------------------------ + + public static final class TFloatMinFn extends Aggregator + implements Serializable { + @Override public String zero() { return ""; } + @Override public String reduce(String buf, String hex) { return append(buf, hex); } + @Override public String merge(String b1, String b2) { return AggregateUDAFs.merge(b1, b2); } + + @Override public String finish(String buf) { + MeosThread.ensureReady(); + String[] hexes = entries(buf); + if (hexes.length == 0) return null; + Pointer state = null; + for (String hex : hexes) { + Pointer inp = GeneratedFunctions.temporal_from_hexwkb(hex); + if (inp == null) continue; + Pointer next = GeneratedFunctions.tfloat_tmin_transfn(state, inp); + MeosMemory.free(inp); + state = next; + } + if (state == null) return null; + return hexOut(GeneratedFunctions.temporal_tagg_finalfn(state)); + } + + @Override public Encoder bufferEncoder() { return Encoders.STRING(); } + @Override public Encoder outputEncoder() { return Encoders.STRING(); } + } + + public static final class TFloatMaxFn extends Aggregator + implements Serializable { + @Override public String zero() { return ""; } + @Override public String reduce(String buf, String hex) { return append(buf, hex); } + @Override public String merge(String b1, String b2) { return AggregateUDAFs.merge(b1, b2); } + + @Override public String finish(String buf) { + MeosThread.ensureReady(); + String[] hexes = entries(buf); + if (hexes.length == 0) return null; + Pointer state = null; + for (String hex : hexes) { + Pointer inp = GeneratedFunctions.temporal_from_hexwkb(hex); + if (inp == null) continue; + Pointer next = GeneratedFunctions.tfloat_tmax_transfn(state, inp); + MeosMemory.free(inp); + state = next; + } + if (state == null) return null; + return hexOut(GeneratedFunctions.temporal_tagg_finalfn(state)); + } + + @Override public Encoder bufferEncoder() { return Encoders.STRING(); } + @Override public Encoder outputEncoder() { return Encoders.STRING(); } + } + + public static final class TFloatSumFn extends Aggregator + implements Serializable { + @Override public String zero() { return ""; } + @Override public String reduce(String buf, String hex) { return append(buf, hex); } + @Override public String merge(String b1, String b2) { return AggregateUDAFs.merge(b1, b2); } + + @Override public String finish(String buf) { + MeosThread.ensureReady(); + String[] hexes = entries(buf); + if (hexes.length == 0) return null; + Pointer state = null; + for (String hex : hexes) { + Pointer inp = GeneratedFunctions.temporal_from_hexwkb(hex); + if (inp == null) continue; + Pointer next = GeneratedFunctions.tfloat_tsum_transfn(state, inp); + MeosMemory.free(inp); + state = next; + } + if (state == null) return null; + return hexOut(GeneratedFunctions.temporal_tagg_finalfn(state)); + } + + @Override public Encoder bufferEncoder() { return Encoders.STRING(); } + @Override public Encoder outputEncoder() { return Encoders.STRING(); } + } + + // ------------------------------------------------------------------ + // tTextMin / tTextMax β€” temporal aggregates on ttext + // Returns: ttext hex-WKB + // ------------------------------------------------------------------ + + public static final class TTextMinFn extends Aggregator + implements Serializable { + @Override public String zero() { return ""; } + @Override public String reduce(String buf, String hex) { return append(buf, hex); } + @Override public String merge(String b1, String b2) { return AggregateUDAFs.merge(b1, b2); } + + @Override public String finish(String buf) { + MeosThread.ensureReady(); + String[] hexes = entries(buf); + if (hexes.length == 0) return null; + Pointer state = null; + for (String hex : hexes) { + Pointer inp = GeneratedFunctions.temporal_from_hexwkb(hex); + if (inp == null) continue; + Pointer next = GeneratedFunctions.ttext_tmin_transfn(state, inp); + MeosMemory.free(inp); + state = next; + } + if (state == null) return null; + return hexOut(GeneratedFunctions.temporal_tagg_finalfn(state)); + } + + @Override public Encoder bufferEncoder() { return Encoders.STRING(); } + @Override public Encoder outputEncoder() { return Encoders.STRING(); } + } + + public static final class TTextMaxFn extends Aggregator + implements Serializable { + @Override public String zero() { return ""; } + @Override public String reduce(String buf, String hex) { return append(buf, hex); } + @Override public String merge(String b1, String b2) { return AggregateUDAFs.merge(b1, b2); } + + @Override public String finish(String buf) { + MeosThread.ensureReady(); + String[] hexes = entries(buf); + if (hexes.length == 0) return null; + Pointer state = null; + for (String hex : hexes) { + Pointer inp = GeneratedFunctions.temporal_from_hexwkb(hex); + if (inp == null) continue; + Pointer next = GeneratedFunctions.ttext_tmax_transfn(state, inp); + MeosMemory.free(inp); + state = next; + } + if (state == null) return null; + return hexOut(GeneratedFunctions.temporal_tagg_finalfn(state)); + } + + @Override public Encoder bufferEncoder() { return Encoders.STRING(); } + @Override public Encoder outputEncoder() { return Encoders.STRING(); } + } + + // ------------------------------------------------------------------ + // tCentroid β€” temporal centroid over tpoint values + // Returns: tpoint hex-WKB (the moving centroid of the input points) + // MEOS: tpoint_tcentroid_transfn + tpoint_tcentroid_finalfn + // ------------------------------------------------------------------ + + public static final class TCentroidFn extends Aggregator + implements Serializable { + @Override public String zero() { return ""; } + @Override public String reduce(String buf, String hex) { return append(buf, hex); } + @Override public String merge(String b1, String b2) { return AggregateUDAFs.merge(b1, b2); } + + @Override public String finish(String buf) { + MeosThread.ensureReady(); + String[] hexes = entries(buf); + if (hexes.length == 0) return null; + Pointer state = null; + for (String hex : hexes) { + Pointer inp = GeneratedFunctions.temporal_from_hexwkb(hex); + if (inp == null) continue; + Pointer next = GeneratedFunctions.tpoint_tcentroid_transfn(state, inp); + MeosMemory.free(inp); + state = next; + } + if (state == null) return null; + return hexOut(GeneratedFunctions.tpoint_tcentroid_finalfn(state)); + } + + @Override public Encoder bufferEncoder() { return Encoders.STRING(); } + @Override public Encoder outputEncoder() { return Encoders.STRING(); } + } + + // ------------------------------------------------------------------ + // tExtent β€” bounding STBox over all tpoint values + // Returns: stbox hex-WKB + // MEOS: tspatial_extent_transfn (state is STBox*, not SkipList*) + // ------------------------------------------------------------------ + + public static final class TExtentFn extends Aggregator + implements Serializable { + @Override public String zero() { return ""; } + @Override public String reduce(String buf, String hex) { return append(buf, hex); } + @Override public String merge(String b1, String b2) { return AggregateUDAFs.merge(b1, b2); } + + @Override public String finish(String buf) { + MeosThread.ensureReady(); + String[] hexes = entries(buf); + if (hexes.length == 0) return null; + Pointer state = null; + for (String hex : hexes) { + Pointer inp = GeneratedFunctions.temporal_from_hexwkb(hex); + if (inp == null) continue; + Pointer next = GeneratedFunctions.tspatial_extent_transfn(state, inp); + MeosMemory.free(inp); + state = next; + } + return stboxHex(state); + } + + @Override public Encoder bufferEncoder() { return Encoders.STRING(); } + @Override public Encoder outputEncoder() { return Encoders.STRING(); } + } + + // ------------------------------------------------------------------ + // REGISTRATION + // ------------------------------------------------------------------ + + public static void registerAll(SparkSession spark) { + spark.udf().register("tCount", org.apache.spark.sql.functions.udaf(new TCountFn(), Encoders.STRING())); + spark.udf().register("tAnd", org.apache.spark.sql.functions.udaf(new TAndFn(), Encoders.STRING())); + spark.udf().register("tOr", org.apache.spark.sql.functions.udaf(new TOrFn(), Encoders.STRING())); + spark.udf().register("tIntMin", org.apache.spark.sql.functions.udaf(new TIntMinFn(), Encoders.STRING())); + spark.udf().register("tIntMax", org.apache.spark.sql.functions.udaf(new TIntMaxFn(), Encoders.STRING())); + spark.udf().register("tIntSum", org.apache.spark.sql.functions.udaf(new TIntSumFn(), Encoders.STRING())); + spark.udf().register("tFloatMin", org.apache.spark.sql.functions.udaf(new TFloatMinFn(), Encoders.STRING())); + spark.udf().register("tFloatMax", org.apache.spark.sql.functions.udaf(new TFloatMaxFn(), Encoders.STRING())); + spark.udf().register("tFloatSum", org.apache.spark.sql.functions.udaf(new TFloatSumFn(), Encoders.STRING())); + spark.udf().register("tTextMin", org.apache.spark.sql.functions.udaf(new TTextMinFn(), Encoders.STRING())); + spark.udf().register("tTextMax", org.apache.spark.sql.functions.udaf(new TTextMaxFn(), Encoders.STRING())); + spark.udf().register("tCentroid", org.apache.spark.sql.functions.udaf(new TCentroidFn(), Encoders.STRING())); + spark.udf().register("tExtent", org.apache.spark.sql.functions.udaf(new TExtentFn(), Encoders.STRING())); + } +} diff --git a/src/main/java/org/mobilitydb/spark/temporal/AnalyticsUDFs.java b/src/main/java/org/mobilitydb/spark/temporal/AnalyticsUDFs.java new file mode 100644 index 00000000..34343ed7 --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/temporal/AnalyticsUDFs.java @@ -0,0 +1,356 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosThread; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.api.java.UDF1; +import org.apache.spark.sql.api.java.UDF2; +import org.apache.spark.sql.types.DataTypes; + +/** + * Spark SQL UDFs for temporal analytics: numeric math and spatial aggregates. + * + * All temporal inputs use hex-WKB string encoding. Scalar outputs (length, + * integral, twavg) are returned as Java primitive types. + * + * Memory management: every Pointer returned by MEOS must be freed via + * MeosMemory.free() β€” see GeoUDFs for the rationale. + * + * MEOS function authority: meos/include/meos.h and meos/include/meos_geo.h + */ +public final class AnalyticsUDFs { + + private AnalyticsUDFs() {} + + // ------------------------------------------------------------------ + // tfloat math (hex-WKB in, hex-WKB out) + // ------------------------------------------------------------------ + + public static final UDF1 tfloatDerivative = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + Pointer r = GeneratedFunctions.temporal_derivative(ptr); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(ptr); + } + }; + + public static final UDF2 tfloatRound = + (s, maxdd) -> { + if (s == null || maxdd == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + Pointer r = GeneratedFunctions.temporal_round(ptr, maxdd); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(ptr); + } + }; + + public static final UDF1 tfloatFloor = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + Pointer r = GeneratedFunctions.tfloat_floor(ptr); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(ptr); + } + }; + + public static final UDF1 tfloatCeil = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + Pointer r = GeneratedFunctions.tfloat_ceil(ptr); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(ptr); + } + }; + + public static final UDF2 tfloatDegrees = + (s, normalize) -> { + if (s == null || normalize == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + Pointer r = GeneratedFunctions.tfloat_degrees(ptr, normalize); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(ptr); + } + }; + + public static final UDF1 tfloatRadians = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + Pointer r = GeneratedFunctions.tfloat_radians(ptr); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // ------------------------------------------------------------------ + // tnumber scalar aggregates (hex-WKB in, scalar out) + // ------------------------------------------------------------------ + + public static final UDF1 tnumberIntegral = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + return GeneratedFunctions.tnumber_integral(ptr); + } finally { + MeosMemory.free(ptr); + } + }; + + public static final UDF1 tnumberTwavg = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + return GeneratedFunctions.tnumber_twavg(ptr); + } finally { + MeosMemory.free(ptr); + } + }; + + // tnumberTrend(tnumber_hex) β†’ tfloat hex-WKB + // MEOS: tnumber_trend(const Temporal *) β†’ Temporal * + public static final UDF1 tnumberTrend = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + Pointer r = GeneratedFunctions.tnumber_trend(ptr); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // ------------------------------------------------------------------ + // tpoint spatial analytics (hex-WKB in, scalar/hex out) + // ------------------------------------------------------------------ + + public static final UDF1 tpointLength = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + return GeneratedFunctions.tpoint_length(ptr); + } finally { + MeosMemory.free(ptr); + } + }; + + public static final UDF1 tpointSpeed = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + Pointer r = GeneratedFunctions.tpoint_speed(ptr); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(ptr); + } + }; + + public static final UDF1 tpointAzimuth = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + Pointer r = GeneratedFunctions.tpoint_azimuth(ptr); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(ptr); + } + }; + + public static final UDF1 tpointDirection = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + Pointer r = GeneratedFunctions.tpoint_direction(ptr); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // tpointCumulativeLength(tpoint_hex) β†’ tfloat hex-WKB + // Returns the cumulative distance along the trajectory. + // MEOS: tpoint_cumulative_length(const Temporal *) β†’ Temporal * + public static final UDF1 tpointCumulativeLength = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + Pointer r = GeneratedFunctions.tpoint_cumulative_length(ptr); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // tgeoTraversedArea(tgeo_hex) β†’ WKT STRING + // Returns the geometry swept out by a temporal geometry. + // MEOS: tgeo_traversed_area(const Temporal *, bool unary_union) β†’ GSERIALIZED * + public static final UDF1 tgeoTraversedArea = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + Pointer gsPtr = GeneratedFunctions.tgeo_traversed_area(ptr, false); + if (gsPtr == null) return null; + try { + return GeneratedFunctions.geo_as_text(gsPtr, 6); + } finally { + MeosMemory.free(gsPtr); + } + } finally { + MeosMemory.free(ptr); + } + }; + + public static void registerAll(SparkSession spark) { + spark.udf().register("tfloatDerivative", tfloatDerivative, DataTypes.StringType); + spark.udf().register("tfloatRound", tfloatRound, DataTypes.StringType); + spark.udf().register("tfloatFloor", tfloatFloor, DataTypes.StringType); + spark.udf().register("tfloatCeil", tfloatCeil, DataTypes.StringType); + spark.udf().register("tfloatDegrees", tfloatDegrees, DataTypes.StringType); + spark.udf().register("tfloatRadians", tfloatRadians, DataTypes.StringType); + spark.udf().register("tnumberIntegral", tnumberIntegral, DataTypes.DoubleType); + spark.udf().register("tnumberTwavg", tnumberTwavg, DataTypes.DoubleType); + spark.udf().register("tnumberTrend", tnumberTrend, DataTypes.StringType); + spark.udf().register("tpointLength", tpointLength, DataTypes.DoubleType); + spark.udf().register("tpointSpeed", tpointSpeed, DataTypes.StringType); + spark.udf().register("tpointAzimuth", tpointAzimuth, DataTypes.StringType); + spark.udf().register("tpointDirection", tpointDirection, DataTypes.StringType); + spark.udf().register("tpointCumulativeLength", tpointCumulativeLength, DataTypes.StringType); + spark.udf().register("tgeoTraversedArea", tgeoTraversedArea, DataTypes.StringType); + } +} diff --git a/src/main/java/org/mobilitydb/spark/temporal/BoolOpsUDFs.java b/src/main/java/org/mobilitydb/spark/temporal/BoolOpsUDFs.java new file mode 100644 index 00000000..a06bd4b6 --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/temporal/BoolOpsUDFs.java @@ -0,0 +1,385 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosThread; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.api.java.UDF1; +import org.apache.spark.sql.api.java.UDF2; +import org.apache.spark.sql.types.DataTypes; + +/** + * Spark SQL UDFs for temporal boolean AND/OR operations on tbool. + * + * Spark SQL cannot register two UDFs with the same name but different + * argument types, so the three arities of tand/tor are registered with + * type-qualified names: + * + * tandBool(tbool, bool) β†’ tand_tbool_bool + * tandBoolTbool(bool, tbool) β†’ tand_bool_tbool + * tandTboolTbool(tbool,tbool)β†’ tand_tbool_tbool + * + * (Likewise for tor.) + * + * All temporal inputs and outputs use hex-WKB string encoding. + * + * MEOS function authority: meos/include/meos.h (028_tbool_boolops) + */ +public final class BoolOpsUDFs { + + private BoolOpsUDFs() {} + + private static String hexOut(Pointer r) { + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } + + // ------------------------------------------------------------------ + // tnot: temporal NOT + // MEOS: tnot_tbool(const Temporal *) β†’ Temporal * + // ------------------------------------------------------------------ + + public static final UDF1 tnotTbool = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + return hexOut(GeneratedFunctions.tnot_tbool(ptr)); + } finally { MeosMemory.free(ptr); } + }; + + // ------------------------------------------------------------------ + // tand: temporal AND + // MEOS: tand_tbool_bool / tand_bool_tbool / tand_tbool_tbool + // ------------------------------------------------------------------ + + // tandBool(tbool_hex, bool) β†’ tbool hex-WKB + public static final UDF2 tandBool = + (s, v) -> { + if (s == null || v == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + return hexOut(GeneratedFunctions.tand_tbool_bool(ptr, v)); + } finally { MeosMemory.free(ptr); } + }; + + // tandBoolTbool(bool, tbool_hex) β†’ tbool hex-WKB + public static final UDF2 tandBoolTbool = + (v, s) -> { + if (v == null || s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + return hexOut(GeneratedFunctions.tand_bool_tbool(v, ptr)); + } finally { MeosMemory.free(ptr); } + }; + + // tandTboolTbool(tbool1_hex, tbool2_hex) β†’ tbool hex-WKB + public static final UDF2 tandTboolTbool = + (s1, s2) -> { + if (s1 == null || s2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(s1); + if (p1 == null) return null; + try { + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(s2); + if (p2 == null) return null; + try { + return hexOut(GeneratedFunctions.tand_tbool_tbool(p1, p2)); + } finally { MeosMemory.free(p2); } + } finally { MeosMemory.free(p1); } + }; + + // ------------------------------------------------------------------ + // tor: temporal OR + // MEOS: tor_tbool_bool / tor_bool_tbool / tor_tbool_tbool + // ------------------------------------------------------------------ + + // torBool(tbool_hex, bool) β†’ tbool hex-WKB + public static final UDF2 torBool = + (s, v) -> { + if (s == null || v == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + return hexOut(GeneratedFunctions.tor_tbool_bool(ptr, v)); + } finally { MeosMemory.free(ptr); } + }; + + // torBoolTbool(bool, tbool_hex) β†’ tbool hex-WKB + public static final UDF2 torBoolTbool = + (v, s) -> { + if (v == null || s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + return hexOut(GeneratedFunctions.tor_bool_tbool(v, ptr)); + } finally { MeosMemory.free(ptr); } + }; + + // torTboolTbool(tbool1_hex, tbool2_hex) β†’ tbool hex-WKB + public static final UDF2 torTboolTbool = + (s1, s2) -> { + if (s1 == null || s2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(s1); + if (p1 == null) return null; + try { + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(s2); + if (p2 == null) return null; + try { + return hexOut(GeneratedFunctions.tor_tbool_tbool(p1, p2)); + } finally { MeosMemory.free(p2); } + } finally { MeosMemory.free(p1); } + }; + + // ------------------------------------------------------------------ + // Temporal boolean accessor + // ------------------------------------------------------------------ + + // tboolWhenTrue(tbool_hex) β†’ tstzspanset hex-WKB + // Returns the periods when the tbool is true. + // MEOS: tbool_when_true(const Temporal *) β†’ SpanSet * + public static final UDF1 tboolWhenTrue = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + Pointer r = GeneratedFunctions.tbool_when_true(ptr); + if (r == null) return null; + try { + return GeneratedFunctions.spanset_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // ------------------------------------------------------------------ + // Temporal comparison operators (temporal Γ— temporal β†’ tbool hex-WKB) + // + // MEOS: teq/tne/tlt/tle/tgt/tge_temporal_temporal meos.h + // ------------------------------------------------------------------ + + public static final UDF2 teqTemporalTemporal = + (s1, s2) -> { + if (s1 == null || s2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(s1); + if (p1 == null) return null; + try { + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(s2); + if (p2 == null) return null; + try { + Pointer r = GeneratedFunctions.teq_temporal_temporal(p1, p2); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(p2); + } + } finally { + MeosMemory.free(p1); + } + }; + + public static final UDF2 tneTemporalTemporal = + (s1, s2) -> { + if (s1 == null || s2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(s1); + if (p1 == null) return null; + try { + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(s2); + if (p2 == null) return null; + try { + Pointer r = GeneratedFunctions.tne_temporal_temporal(p1, p2); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(p2); + } + } finally { + MeosMemory.free(p1); + } + }; + + public static final UDF2 tltTemporalTemporal = + (s1, s2) -> { + if (s1 == null || s2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(s1); + if (p1 == null) return null; + try { + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(s2); + if (p2 == null) return null; + try { + Pointer r = GeneratedFunctions.tlt_temporal_temporal(p1, p2); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(p2); + } + } finally { + MeosMemory.free(p1); + } + }; + + public static final UDF2 tleTemporalTemporal = + (s1, s2) -> { + if (s1 == null || s2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(s1); + if (p1 == null) return null; + try { + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(s2); + if (p2 == null) return null; + try { + Pointer r = GeneratedFunctions.tle_temporal_temporal(p1, p2); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(p2); + } + } finally { + MeosMemory.free(p1); + } + }; + + public static final UDF2 tgtTemporalTemporal = + (s1, s2) -> { + if (s1 == null || s2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(s1); + if (p1 == null) return null; + try { + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(s2); + if (p2 == null) return null; + try { + Pointer r = GeneratedFunctions.tgt_temporal_temporal(p1, p2); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(p2); + } + } finally { + MeosMemory.free(p1); + } + }; + + public static final UDF2 tgeTemporalTemporal = + (s1, s2) -> { + if (s1 == null || s2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(s1); + if (p1 == null) return null; + try { + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(s2); + if (p2 == null) return null; + try { + Pointer r = GeneratedFunctions.tge_temporal_temporal(p1, p2); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(p2); + } + } finally { + MeosMemory.free(p1); + } + }; + + // ------------------------------------------------------------------ + // REGISTRATION + // ------------------------------------------------------------------ + + public static void registerAll(SparkSession spark) { + // tnot + spark.udf().register("tnotTbool", tnotTbool, DataTypes.StringType); + // tand + spark.udf().register("tandBool", tandBool, DataTypes.StringType); + spark.udf().register("tandBoolTbool", tandBoolTbool, DataTypes.StringType); + spark.udf().register("tandTboolTbool", tandTboolTbool, DataTypes.StringType); + // tor + spark.udf().register("torBool", torBool, DataTypes.StringType); + spark.udf().register("torBoolTbool", torBoolTbool, DataTypes.StringType); + spark.udf().register("torTboolTbool", torTboolTbool, DataTypes.StringType); + // tbool accessor + spark.udf().register("tboolWhenTrue", tboolWhenTrue, DataTypes.StringType); + // temporal comparison operators + spark.udf().register("teqTemporalTemporal", teqTemporalTemporal, DataTypes.StringType); + spark.udf().register("tneTemporalTemporal", tneTemporalTemporal, DataTypes.StringType); + spark.udf().register("tltTemporalTemporal", tltTemporalTemporal, DataTypes.StringType); + spark.udf().register("tleTemporalTemporal", tleTemporalTemporal, DataTypes.StringType); + spark.udf().register("tgtTemporalTemporal", tgtTemporalTemporal, DataTypes.StringType); + spark.udf().register("tgeTemporalTemporal", tgeTemporalTemporal, DataTypes.StringType); + + // MobilityDB SQL bare-name aliases for tbool boolops + spark.udf().register("tboolNot", tnotTbool, DataTypes.StringType); + spark.udf().register("tboolAnd", tandTboolTbool, DataTypes.StringType); + spark.udf().register("tboolOr", torTboolTbool, DataTypes.StringType); + } +} diff --git a/src/main/java/org/mobilitydb/spark/temporal/BucketUDFs.java b/src/main/java/org/mobilitydb/spark/temporal/BucketUDFs.java new file mode 100644 index 00000000..6165034b --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/temporal/BucketUDFs.java @@ -0,0 +1,70 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.mobilitydb.spark.MeosNative; +import org.mobilitydb.spark.MeosThread; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.api.java.UDF3; +import org.apache.spark.sql.types.DataTypes; + +/** + * Spark SQL UDFs for bucketing scalar values onto a regular grid β€” used to + * implement time-windowed aggregations and value histograms. + * + * MEOS: float_bucket / int_bucket round their input down to the nearest + * bucket boundary, given a bucket size and an origin offset. + * + * floatBucket(7.3, 1.0, 0.0) = 7.0 // bucket [7.0, 8.0) + * intBucket(17, 5, 0) = 15 // bucket [15, 20) + */ +public final class BucketUDFs { + + private BucketUDFs() {} + + // floatBucket(value DOUBLE, size DOUBLE, origin DOUBLE) β†’ DOUBLE + // MEOS: float_get_bin (renamed from float_bucket; not in JMEOS-1.4) + public static final UDF3 floatBucket = + (value, size, origin) -> { + if (value == null || size == null || origin == null) return null; + MeosThread.ensureReady(); + return MeosNative.INSTANCE.float_get_bin(value, size, origin); + }; + + // intBucket(value INT, size INT, origin INT) β†’ INT + // MEOS: int_get_bin (renamed from int_bucket; not in JMEOS-1.4) + public static final UDF3 intBucket = + (value, size, origin) -> { + if (value == null || size == null || origin == null) return null; + MeosThread.ensureReady(); + return MeosNative.INSTANCE.int_get_bin(value, size, origin); + }; + + public static void registerAll(SparkSession spark) { + spark.udf().register("floatBucket", floatBucket, DataTypes.DoubleType); + spark.udf().register("intBucket", intBucket, DataTypes.IntegerType); + } +} diff --git a/src/main/java/org/mobilitydb/spark/temporal/ConstructorUDFs.java b/src/main/java/org/mobilitydb/spark/temporal/ConstructorUDFs.java new file mode 100644 index 00000000..c6ed701f --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/temporal/ConstructorUDFs.java @@ -0,0 +1,483 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import jnr.ffi.Runtime; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosThread; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.api.java.UDF1; +import org.apache.spark.sql.api.java.UDF2; +import org.apache.spark.sql.types.DataTypes; + +/** + * Spark SQL UDFs for constructing temporal and span types from text literals. + * + * All UDFs accept a WKT/text literal and return the internal hex-WKB + * representation used throughout MobilitySpark. This matches the MobilityDuck + * constructor surface (tstzspan, intspan, tint, tfloat, …). + * + * Storage convention: temporal values and span/set values are stored as + * hex-WKB strings produced by temporal_as_hexwkb / span_as_hexwkb. + * + * MEOS function authority: meos/include/meos.h and meos/include/meos_geo.h + */ +public final class ConstructorUDFs { + + private ConstructorUDFs() {} + + // ------------------------------------------------------------------ + // Temporal type constructors: text literal β†’ hex-WKB STRING + // ------------------------------------------------------------------ + + // tint("[1@2020-01-01, 2@2020-01-02]") β†’ hex-WKB + // MEOS: tint_in(const char *) β†’ Temporal * + public static final UDF1 tint = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.tint_in(s); + if (ptr == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(ptr, (byte) 0); + }; + + // tfloat("1.5@2020-01-01") β†’ hex-WKB + // MEOS: tfloat_in(const char *) β†’ Temporal * + public static final UDF1 tfloat = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.tfloat_in(s); + if (ptr == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(ptr, (byte) 0); + }; + + // tbool("true@2020-01-01") β†’ hex-WKB + // MEOS: tbool_in(const char *) β†’ Temporal * + public static final UDF1 tbool = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.tbool_in(s); + if (ptr == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(ptr, (byte) 0); + }; + + // ttext("hello@2020-01-01") β†’ hex-WKB + // MEOS: ttext_in(const char *) β†’ Temporal * + public static final UDF1 ttext = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.ttext_in(s); + if (ptr == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(ptr, (byte) 0); + }; + + // tgeogpoint("POINT(4.35 50.85)@2020-01-01") β†’ hex-WKB + // MEOS: tgeogpoint_in(const char *) β†’ Temporal * + public static final UDF1 tgeogpoint = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.tgeogpoint_in(s); + if (ptr == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(ptr, (byte) 0); + }; + + // ------------------------------------------------------------------ + // Span type constructors: text literal β†’ hex-WKB STRING + // ------------------------------------------------------------------ + + // tstzspan("[2020-01-01, 2020-01-02)") β†’ hex-WKB + // MEOS: tstzspan_in(const char *) β†’ Span * + public static final UDF1 tstzspan = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.tstzspan_in(s); + if (ptr == null) return null; + return GeneratedFunctions.span_as_hexwkb(ptr, (byte) 0); + }; + + // tstzspanset("{[2020-01-01, 2020-01-02), [2020-03-01, 2020-04-01)}") β†’ hex-WKB + // MEOS: tstzspanset_in(const char *) β†’ SpanSet * + public static final UDF1 tstzspanset = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.tstzspanset_in(s); + if (ptr == null) return null; + return GeneratedFunctions.spanset_as_hexwkb(ptr, (byte) 0); + }; + + // intspan("[1, 10)") β†’ hex-WKB + // MEOS: intspan_in(const char *) β†’ Span * + public static final UDF1 intspan = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.intspan_in(s); + if (ptr == null) return null; + return GeneratedFunctions.span_as_hexwkb(ptr, (byte) 0); + }; + + // floatspan("[1.0, 10.0)") β†’ hex-WKB + // MEOS: floatspan_in(const char *) β†’ Span * + public static final UDF1 floatspan = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.floatspan_in(s); + if (ptr == null) return null; + return GeneratedFunctions.span_as_hexwkb(ptr, (byte) 0); + }; + + // datespan("[2020-01-01, 2020-01-31)") β†’ hex-WKB + // MEOS: datespan_in(const char *) β†’ Span * + public static final UDF1 datespan = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.datespan_in(s); + if (ptr == null) return null; + return GeneratedFunctions.span_as_hexwkb(ptr, (byte) 0); + }; + + // datespanset("{[2020-01-01, 2020-01-31), [2020-06-01, 2020-06-30)}") β†’ hex-WKB + // MEOS: datespanset_in(const char *) β†’ SpanSet * + public static final UDF1 datespanset = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.datespanset_in(s); + if (ptr == null) return null; + return GeneratedFunctions.spanset_as_hexwkb(ptr, (byte) 0); + }; + + // intset("{1, 2, 3, 4}") β†’ hex-WKB + // MEOS: intset_in(const char *) β†’ Set * + public static final UDF1 intset = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.intset_in(s); + if (ptr == null) return null; + return GeneratedFunctions.set_as_hexwkb(ptr, (byte) 0); + }; + + // floatset("{1.1, 2.2, 3.3}") β†’ hex-WKB + // MEOS: floatset_in(const char *) β†’ Set * + public static final UDF1 floatset = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.floatset_in(s); + if (ptr == null) return null; + return GeneratedFunctions.set_as_hexwkb(ptr, (byte) 0); + }; + + // tstzset("{2020-01-01, 2020-02-01, 2020-03-01}") β†’ hex-WKB + // MEOS: tstzset_in(const char *) β†’ Set * + public static final UDF1 tstzset = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.tstzset_in(s); + if (ptr == null) return null; + return GeneratedFunctions.set_as_hexwkb(ptr, (byte) 0); + }; + + // textset("{hello, world}") β†’ hex-WKB + // MEOS: textset_in(const char *) β†’ Set * + public static final UDF1 textset = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.textset_in(s); + if (ptr == null) return null; + return GeneratedFunctions.set_as_hexwkb(ptr, (byte) 0); + }; + + // bigintset("{1000, 2000, 3000}") β†’ hex-WKB + // MEOS: bigintset_in(const char *) β†’ Set * + public static final UDF1 bigintset = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.bigintset_in(s); + if (ptr == null) return null; + return GeneratedFunctions.set_as_hexwkb(ptr, (byte) 0); + }; + + // stbox("STBOX X((1,2),(3,4))") β†’ hex-WKB + // MEOS: stbox_in(const char *) β†’ STBox * + public static final UDF1 stbox = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.stbox_in(s); + if (ptr == null) return null; + Pointer sizeOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + return GeneratedFunctions.stbox_as_hexwkb(ptr, (byte) 0, sizeOut); + }; + + // tbox("TBOX T([2020-01-01,2020-01-02))") β†’ hex-WKB + // MEOS: tbox_in(const char *) β†’ TBox * + public static final UDF1 tbox = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.tbox_in(s); + if (ptr == null) return null; + Pointer sizeOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + return GeneratedFunctions.tbox_as_hexwkb(ptr, (byte) 0, sizeOut); + }; + + // ------------------------------------------------------------------ + // MFJSON constructors (JSON string in β†’ hex-WKB out) + // + // MEOS: tbool_from_mfjson, tint_from_mfjson, tfloat_from_mfjson, + // ttext_from_mfjson (meos.h) + // tgeompoint_from_mfjson, tgeogpoint_from_mfjson (meos_geo.h) + // ------------------------------------------------------------------ + + public static final UDF1 tboolFromMfjson = + (json) -> { + if (json == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.tbool_from_mfjson(json); + if (p == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(p, (byte) 0); + }; + + public static final UDF1 tintFromMfjson = + (json) -> { + if (json == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.tint_from_mfjson(json); + if (p == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(p, (byte) 0); + }; + + public static final UDF1 tfloatFromMfjson = + (json) -> { + if (json == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.tfloat_from_mfjson(json); + if (p == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(p, (byte) 0); + }; + + public static final UDF1 ttextFromMfjson = + (json) -> { + if (json == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.ttext_from_mfjson(json); + if (p == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(p, (byte) 0); + }; + + public static final UDF1 tgeompointFromMfjson = + (json) -> { + if (json == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.tgeompoint_from_mfjson(json); + if (p == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(p, (byte) 0); + }; + + public static final UDF1 tgeogpointFromMfjson = + (json) -> { + if (json == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.tgeogpoint_from_mfjson(json); + if (p == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(p, (byte) 0); + }; + + // ------------------------------------------------------------------ + // Constant temporal constructors (scalar value + reference temporal) + // + // Each function creates a temporal that is constant at the given value + // over the same time structure as the reference temporal. + // + // MEOS: tbool_from_base_temp, tint_from_base_temp, tfloat_from_base_temp, + // ttext_from_base_temp (meos.h) + // ------------------------------------------------------------------ + + // tboolFromBaseTemp(val BOOLEAN, refHex STRING) β†’ STRING + public static final UDF2 tboolFromBaseTemp = + (val, refHex) -> { + if (val == null || refHex == null) return null; + MeosThread.ensureReady(); + Pointer ref = GeneratedFunctions.temporal_from_hexwkb(refHex); + if (ref == null) return null; + try { + Pointer result = GeneratedFunctions.tbool_from_base_temp(val, ref); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(ref); + } + }; + + // tintFromBaseTemp(val INT, refHex STRING) β†’ STRING + public static final UDF2 tintFromBaseTemp = + (val, refHex) -> { + if (val == null || refHex == null) return null; + MeosThread.ensureReady(); + Pointer ref = GeneratedFunctions.temporal_from_hexwkb(refHex); + if (ref == null) return null; + try { + Pointer result = GeneratedFunctions.tint_from_base_temp(val, ref); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(ref); + } + }; + + // tfloatFromBaseTemp(val DOUBLE, refHex STRING) β†’ STRING + public static final UDF2 tfloatFromBaseTemp = + (val, refHex) -> { + if (val == null || refHex == null) return null; + MeosThread.ensureReady(); + Pointer ref = GeneratedFunctions.temporal_from_hexwkb(refHex); + if (ref == null) return null; + try { + Pointer result = GeneratedFunctions.tfloat_from_base_temp(val, ref); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(ref); + } + }; + + // ttextFromBaseTemp(val STRING, refHex STRING) β†’ STRING + // Uses ttext_in + ttext_value_n to materialise a text* from val. + public static final UDF2 ttextFromBaseTemp = + (val, refHex) -> { + if (val == null || refHex == null) return null; + MeosThread.ensureReady(); + // Wrap val in a single-instant ttext so we can extract a text* from it. + Pointer dummyTtext = GeneratedFunctions.ttext_in(val + "@2000-01-01 00:00:00+00"); + if (dummyTtext == null) return null; + Pointer textPtr = null; + Pointer ref = null; + Pointer result = null; + try { + textPtr = GeneratedFunctions.ttext_value_n(dummyTtext, 1); + if (textPtr == null) return null; + ref = GeneratedFunctions.temporal_from_hexwkb(refHex); + if (ref == null) return null; + result = GeneratedFunctions.ttext_from_base_temp(textPtr, ref); + if (result == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(dummyTtext); + if (textPtr != null) MeosMemory.free(textPtr); + if (ref != null) MeosMemory.free(ref); + if (result != null) MeosMemory.free(result); + } + }; + + // tpointFromBaseTemp(geoWkt STRING, refHex STRING) β†’ STRING + // Creates a constant tpoint that takes the temporal structure from refHex + // and uses the given geometry (WKT) as the base value. + // MEOS: tpoint_from_base_temp(const GSERIALIZED *, const Temporal *) β†’ Temporal * + public static final UDF2 tpointFromBaseTemp = + (geoWkt, refHex) -> { + if (geoWkt == null || refHex == null) return null; + MeosThread.ensureReady(); + Pointer gptr = GeneratedFunctions.geo_from_text(geoWkt, 0); + if (gptr == null) return null; + try { + Pointer ref = GeneratedFunctions.temporal_from_hexwkb(refHex); + if (ref == null) return null; + try { + Pointer result = GeneratedFunctions.tpoint_from_base_temp(gptr, ref); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(ref); + } + } finally { + MeosMemory.free(gptr); + } + }; + + public static void registerAll(SparkSession spark) { + spark.udf().register("tint", tint, DataTypes.StringType); + spark.udf().register("tfloat", tfloat, DataTypes.StringType); + spark.udf().register("tbool", tbool, DataTypes.StringType); + spark.udf().register("ttext", ttext, DataTypes.StringType); + spark.udf().register("tgeogpoint", tgeogpoint, DataTypes.StringType); + spark.udf().register("tstzspan", tstzspan, DataTypes.StringType); + spark.udf().register("tstzspanset", tstzspanset, DataTypes.StringType); + spark.udf().register("intspan", intspan, DataTypes.StringType); + spark.udf().register("floatspan", floatspan, DataTypes.StringType); + spark.udf().register("datespan", datespan, DataTypes.StringType); + spark.udf().register("datespanset", datespanset, DataTypes.StringType); + spark.udf().register("intset", intset, DataTypes.StringType); + spark.udf().register("floatset", floatset, DataTypes.StringType); + spark.udf().register("tstzset", tstzset, DataTypes.StringType); + spark.udf().register("textset", textset, DataTypes.StringType); + spark.udf().register("bigintset", bigintset, DataTypes.StringType); + spark.udf().register("stbox", stbox, DataTypes.StringType); + spark.udf().register("tbox", tbox, DataTypes.StringType); + spark.udf().register("tboolFromMfjson", tboolFromMfjson, DataTypes.StringType); + spark.udf().register("tintFromMfjson", tintFromMfjson, DataTypes.StringType); + spark.udf().register("tfloatFromMfjson", tfloatFromMfjson, DataTypes.StringType); + spark.udf().register("ttextFromMfjson", ttextFromMfjson, DataTypes.StringType); + spark.udf().register("tgeompointFromMfjson",tgeompointFromMfjson,DataTypes.StringType); + spark.udf().register("tgeogpointFromMfjson",tgeogpointFromMfjson,DataTypes.StringType); + // Constant temporal constructors + spark.udf().register("tboolFromBaseTemp", tboolFromBaseTemp, DataTypes.StringType); + spark.udf().register("tintFromBaseTemp", tintFromBaseTemp, DataTypes.StringType); + spark.udf().register("tfloatFromBaseTemp", tfloatFromBaseTemp, DataTypes.StringType); + spark.udf().register("ttextFromBaseTemp", ttextFromBaseTemp, DataTypes.StringType); + spark.udf().register("tpointFromBaseTemp", tpointFromBaseTemp, DataTypes.StringType); + } +} diff --git a/src/main/java/org/mobilitydb/spark/temporal/IOAliasUDFs.java b/src/main/java/org/mobilitydb/spark/temporal/IOAliasUDFs.java new file mode 100644 index 00000000..0501a769 --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/temporal/IOAliasUDFs.java @@ -0,0 +1,431 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosThread; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.api.java.UDF1; +import org.apache.spark.sql.types.DataTypes; + +import java.util.HexFormat; + +/** + * Spark SQL UDFs for typed alternative I/O constructors of set, span, and + * spanset types. MobilityDB SQL exposes typed names like + * `intsetFromHexWKB`, `floatspanFromHexWKB` etc. for type-safety in the + * SQL layer; in MobilitySpark a single generic constructor suffices since + * the WKB carries the element type, but we register typed aliases for + * MobilityDB SQL parity. + * + * MEOS function authority: meos/include/meos.h β€” set_from_hexwkb, + * span_from_hexwkb, spanset_from_hexwkb (generic). + */ +public final class IOAliasUDFs { + + private IOAliasUDFs() {} + + // ------------------------------------------------------------------ + // Generic helpers β€” round-trip a hex-WKB through deserializer + + // re-serializer to produce a normalised hex form (also validates it). + // ------------------------------------------------------------------ + + private static UDF1 setRoundtrip() { + return hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.set_from_hexwkb(hex); + if (p == null) return null; + try { return GeneratedFunctions.set_as_hexwkb(p, (byte) 0); } + finally { MeosMemory.free(p); } + }; + } + + private static UDF1 spanRoundtrip() { + return hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.span_from_hexwkb(hex); + if (p == null) return null; + try { return GeneratedFunctions.span_as_hexwkb(p, (byte) 0); } + finally { MeosMemory.free(p); } + }; + } + + private static UDF1 spansetRoundtrip() { + return hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.spanset_from_hexwkb(hex); + if (p == null) return null; + try { return GeneratedFunctions.spanset_as_hexwkb(p, (byte) 0); } + finally { MeosMemory.free(p); } + }; + } + + private static UDF1 temporalRoundtrip() { + return hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.temporal_from_hexwkb(hex); + if (p == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(p, (byte) 0); } + finally { MeosMemory.free(p); } + }; + } + + private static UDF1 mfjsonToHex(java.util.function.Function ctor) { + return mfjson -> { + if (mfjson == null) return null; + MeosThread.ensureReady(); + Pointer p = ctor.apply(mfjson); + if (p == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(p, (byte) 0); } + finally { MeosMemory.free(p); } + }; + } + + // ------------------------------------------------------------------ + // Set typed FromHexWKB + // ------------------------------------------------------------------ + + public static final UDF1 intsetFromHexWKB = setRoundtrip(); + public static final UDF1 bigintsetFromHexWKB = setRoundtrip(); + public static final UDF1 floatsetFromHexWKB = setRoundtrip(); + public static final UDF1 textsetFromHexWKB = setRoundtrip(); + public static final UDF1 tstzsetFromHexWKB = setRoundtrip(); + public static final UDF1 datesetFromHexWKB = setRoundtrip(); + + // ------------------------------------------------------------------ + // Span typed FromHexWKB + // ------------------------------------------------------------------ + + public static final UDF1 intspanFromHexWKB = spanRoundtrip(); + public static final UDF1 bigintspanFromHexWKB = spanRoundtrip(); + public static final UDF1 floatspanFromHexWKB = spanRoundtrip(); + public static final UDF1 tstzspanFromHexWKB = spanRoundtrip(); + public static final UDF1 datespanFromHexWKB = spanRoundtrip(); + + // ------------------------------------------------------------------ + // Spanset typed FromHexWKB + // ------------------------------------------------------------------ + + public static final UDF1 intspansetFromHexWKB = spansetRoundtrip(); + public static final UDF1 bigintspansetFromHexWKB = spansetRoundtrip(); + public static final UDF1 floatspansetFromHexWKB = spansetRoundtrip(); + public static final UDF1 tstzspansetFromHexWKB = spansetRoundtrip(); + public static final UDF1 datespansetFromHexWKB = spansetRoundtrip(); + + // ------------------------------------------------------------------ + // Temporal scalar typed FromHexWKB (tbool/tint/tfloat/ttext) + // ------------------------------------------------------------------ + + public static final UDF1 tboolFromHexWKB = temporalRoundtrip(); + public static final UDF1 tintFromHexWKB = temporalRoundtrip(); + public static final UDF1 tfloatFromHexWKB = temporalRoundtrip(); + public static final UDF1 ttextFromHexWKB = temporalRoundtrip(); + + // ------------------------------------------------------------------ + // Temporal-geo typed FromHexEWKB (tgeometry/tgeography) + // ------------------------------------------------------------------ + + public static final UDF1 tgeometryFromHexEWKB = temporalRoundtrip(); + public static final UDF1 tgeographyFromHexEWKB = temporalRoundtrip(); + public static final UDF1 tgeompointFromHexEWKB = temporalRoundtrip(); + public static final UDF1 tgeogpointFromHexEWKB = temporalRoundtrip(); + + // ------------------------------------------------------------------ + // Temporal-geo MFJSON constructors + // ------------------------------------------------------------------ + + public static final UDF1 tgeometryFromMFJSON = mfjsonToHex(org.mobilitydb.spark.MeosNative.INSTANCE::tgeometry_from_mfjson); + public static final UDF1 tgeographyFromMFJSON = mfjsonToHex(org.mobilitydb.spark.MeosNative.INSTANCE::tgeography_from_mfjson); + public static final UDF1 tgeompointFromMFJSON = mfjsonToHex(GeneratedFunctions::tgeompoint_from_mfjson); + public static final UDF1 tgeogpointFromMFJSON = mfjsonToHex(GeneratedFunctions::tgeogpoint_from_mfjson); + + // ------------------------------------------------------------------ + // Temporal scalar text constructors (FromText / FromEWKT) + // ------------------------------------------------------------------ + + public static final UDF1 tboolFromText = mfjsonToHex(GeneratedFunctions::tbool_in); + public static final UDF1 tintFromText = mfjsonToHex(GeneratedFunctions::tint_in); + public static final UDF1 tfloatFromText = mfjsonToHex(GeneratedFunctions::tfloat_in); + public static final UDF1 ttextFromText = mfjsonToHex(GeneratedFunctions::ttext_in); + + // Geo temporal text/EWKT constructors + public static final UDF1 tgeompointFromText = mfjsonToHex(GeneratedFunctions::tgeompoint_in); + public static final UDF1 tgeogpointFromText = mfjsonToHex(GeneratedFunctions::tgeogpoint_in); + public static final UDF1 tgeompointFromEWKT = mfjsonToHex(GeneratedFunctions::tgeompoint_in); + public static final UDF1 tgeogpointFromEWKT = mfjsonToHex(GeneratedFunctions::tgeogpoint_in); + public static final UDF1 tgeometryFromText = mfjsonToHex(org.mobilitydb.spark.MeosNative.INSTANCE::tgeometry_in); + public static final UDF1 tgeographyFromText = mfjsonToHex(org.mobilitydb.spark.MeosNative.INSTANCE::tgeography_in); + public static final UDF1 tgeometryFromEWKT = mfjsonToHex(org.mobilitydb.spark.MeosNative.INSTANCE::tgeometry_in); + public static final UDF1 tgeographyFromEWKT = mfjsonToHex(org.mobilitydb.spark.MeosNative.INSTANCE::tgeography_in); + + // ------------------------------------------------------------------ + // Binary I/O β€” bytes ↔ temporal via hex round-trip + // (matches the SpanUDFs pattern: byte[] β†’ hex β†’ from_hexwkb β†’ as_hexwkb) + // ------------------------------------------------------------------ + + private static UDF1 temporalFromBinary() { + return bytes -> { + if (bytes == null) return null; + MeosThread.ensureReady(); + String hex = HexFormat.of().formatHex(bytes).toUpperCase(); + Pointer p = GeneratedFunctions.temporal_from_hexwkb(hex); + if (p == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(p, (byte) 0); } + finally { MeosMemory.free(p); } + }; + } + + public static final UDF1 tboolFromBinary = temporalFromBinary(); + public static final UDF1 tintFromBinary = temporalFromBinary(); + public static final UDF1 tfloatFromBinary = temporalFromBinary(); + public static final UDF1 ttextFromBinary = temporalFromBinary(); + public static final UDF1 tgeompointFromBinary = temporalFromBinary(); + public static final UDF1 tgeogpointFromBinary = temporalFromBinary(); + public static final UDF1 tgeometryFromBinary = temporalFromBinary(); + public static final UDF1 tgeographyFromBinary = temporalFromBinary(); + // EWKB and WKB go through the same generic constructor. + public static final UDF1 tgeompointFromEWKB = temporalFromBinary(); + public static final UDF1 tgeogpointFromEWKB = temporalFromBinary(); + public static final UDF1 tgeometryFromEWKB = temporalFromBinary(); + public static final UDF1 tgeographyFromEWKB = temporalFromBinary(); + + // asBinary / asEWKB: hex β†’ byte[] inverse round-trip + public static final UDF1 asBinary = + hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + return HexFormat.of().parseHex(hex); + }; + + public static final UDF1 asEWKB = asBinary; + + // asHexEWKB: re-emit hex with WKB_EXTENDED (variant 4) so SRID is preserved + public static final UDF1 asHexEWKB = + hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.temporal_from_hexwkb(hex); + if (p == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(p, (byte) 4); } + finally { MeosMemory.free(p); } + }; + + // ------------------------------------------------------------------ + // Geoset typed I/O aliases (geomset / geogset) + // ------------------------------------------------------------------ + + private static UDF1 geomsetTextCtor() { + return wkt -> { + if (wkt == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.geomset_in(wkt); + if (p == null) return null; + try { return GeneratedFunctions.set_as_hexwkb(p, (byte) 0); } + finally { MeosMemory.free(p); } + }; + } + + private static UDF1 geogsetTextCtor() { + return wkt -> { + if (wkt == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.geogset_in(wkt); + if (p == null) return null; + try { return GeneratedFunctions.set_as_hexwkb(p, (byte) 0); } + finally { MeosMemory.free(p); } + }; + } + + private static UDF1 setFromBinary() { + return bytes -> { + if (bytes == null) return null; + MeosThread.ensureReady(); + String hex = HexFormat.of().formatHex(bytes).toUpperCase(); + Pointer p = GeneratedFunctions.set_from_hexwkb(hex); + if (p == null) return null; + try { return GeneratedFunctions.set_as_hexwkb(p, (byte) 0); } + finally { MeosMemory.free(p); } + }; + } + + public static final UDF1 geomsetFromText = geomsetTextCtor(); + public static final UDF1 geogsetFromText = geogsetTextCtor(); + public static final UDF1 geomsetFromEWKT = geomsetTextCtor(); + public static final UDF1 geogsetFromEWKT = geogsetTextCtor(); + public static final UDF1 geomsetFromHexWKB = setRoundtrip(); + public static final UDF1 geogsetFromHexWKB = setRoundtrip(); + public static final UDF1 geomsetFromBinary = setFromBinary(); + public static final UDF1 geogsetFromBinary = setFromBinary(); + public static final UDF1 geomsetFromEWKB = setFromBinary(); + public static final UDF1 geogsetFromEWKB = setFromBinary(); + + // ------------------------------------------------------------------ + // TBox typed I/O aliases β€” generic tbox_from_hexwkb covers all. + // ------------------------------------------------------------------ + + public static final UDF1 tboxFromHexWKB = + hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.tbox_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer sizeOut = jnr.ffi.Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + return GeneratedFunctions.tbox_as_hexwkb(p, (byte) 0, sizeOut); + } finally { MeosMemory.free(p); } + }; + + public static final UDF1 tboxFromBinary = + bytes -> { + if (bytes == null) return null; + MeosThread.ensureReady(); + String hex = HexFormat.of().formatHex(bytes).toUpperCase(); + Pointer p = GeneratedFunctions.tbox_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer sizeOut = jnr.ffi.Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + return GeneratedFunctions.tbox_as_hexwkb(p, (byte) 0, sizeOut); + } finally { MeosMemory.free(p); } + }; + + // ------------------------------------------------------------------ + // STBox typed I/O aliases + // ------------------------------------------------------------------ + + public static final UDF1 stboxFromHexWKB = + hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.stbox_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer sizeOut = jnr.ffi.Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + return GeneratedFunctions.stbox_as_hexwkb(p, (byte) 0, sizeOut); + } finally { MeosMemory.free(p); } + }; + + public static final UDF1 stboxFromBinary = + bytes -> { + if (bytes == null) return null; + MeosThread.ensureReady(); + String hex = HexFormat.of().formatHex(bytes).toUpperCase(); + Pointer p = GeneratedFunctions.stbox_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer sizeOut = jnr.ffi.Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + return GeneratedFunctions.stbox_as_hexwkb(p, (byte) 0, sizeOut); + } finally { MeosMemory.free(p); } + }; + + public static void registerAll(SparkSession spark) { + // Set + spark.udf().register("intsetFromHexWKB", intsetFromHexWKB, DataTypes.StringType); + spark.udf().register("bigintsetFromHexWKB", bigintsetFromHexWKB, DataTypes.StringType); + spark.udf().register("floatsetFromHexWKB", floatsetFromHexWKB, DataTypes.StringType); + spark.udf().register("textsetFromHexWKB", textsetFromHexWKB, DataTypes.StringType); + spark.udf().register("tstzsetFromHexWKB", tstzsetFromHexWKB, DataTypes.StringType); + spark.udf().register("datesetFromHexWKB", datesetFromHexWKB, DataTypes.StringType); + // Span + spark.udf().register("intspanFromHexWKB", intspanFromHexWKB, DataTypes.StringType); + spark.udf().register("bigintspanFromHexWKB", bigintspanFromHexWKB, DataTypes.StringType); + spark.udf().register("floatspanFromHexWKB", floatspanFromHexWKB, DataTypes.StringType); + spark.udf().register("tstzspanFromHexWKB", tstzspanFromHexWKB, DataTypes.StringType); + spark.udf().register("datespanFromHexWKB", datespanFromHexWKB, DataTypes.StringType); + // Spanset + spark.udf().register("intspansetFromHexWKB", intspansetFromHexWKB, DataTypes.StringType); + spark.udf().register("bigintspansetFromHexWKB", bigintspansetFromHexWKB, DataTypes.StringType); + spark.udf().register("floatspansetFromHexWKB", floatspansetFromHexWKB, DataTypes.StringType); + spark.udf().register("tstzspansetFromHexWKB", tstzspansetFromHexWKB, DataTypes.StringType); + spark.udf().register("datespansetFromHexWKB", datespansetFromHexWKB, DataTypes.StringType); + // Temporal scalar + spark.udf().register("tboolFromHexWKB", tboolFromHexWKB, DataTypes.StringType); + spark.udf().register("tintFromHexWKB", tintFromHexWKB, DataTypes.StringType); + spark.udf().register("tfloatFromHexWKB", tfloatFromHexWKB, DataTypes.StringType); + spark.udf().register("ttextFromHexWKB", ttextFromHexWKB, DataTypes.StringType); + // Temporal-geo + spark.udf().register("tgeometryFromHexEWKB", tgeometryFromHexEWKB, DataTypes.StringType); + spark.udf().register("tgeographyFromHexEWKB", tgeographyFromHexEWKB, DataTypes.StringType); + spark.udf().register("tgeompointFromHexEWKB", tgeompointFromHexEWKB, DataTypes.StringType); + spark.udf().register("tgeogpointFromHexEWKB", tgeogpointFromHexEWKB, DataTypes.StringType); + spark.udf().register("tgeometryFromMFJSON", tgeometryFromMFJSON, DataTypes.StringType); + spark.udf().register("tgeographyFromMFJSON", tgeographyFromMFJSON, DataTypes.StringType); + spark.udf().register("tgeompointFromMFJSON", tgeompointFromMFJSON, DataTypes.StringType); + spark.udf().register("tgeogpointFromMFJSON", tgeogpointFromMFJSON, DataTypes.StringType); + // Temporal scalar text + spark.udf().register("tboolFromText", tboolFromText, DataTypes.StringType); + spark.udf().register("tintFromText", tintFromText, DataTypes.StringType); + spark.udf().register("tfloatFromText", tfloatFromText, DataTypes.StringType); + spark.udf().register("ttextFromText", ttextFromText, DataTypes.StringType); + // Geo temporal text/EWKT + spark.udf().register("tgeompointFromText", tgeompointFromText, DataTypes.StringType); + spark.udf().register("tgeogpointFromText", tgeogpointFromText, DataTypes.StringType); + spark.udf().register("tgeompointFromEWKT", tgeompointFromEWKT, DataTypes.StringType); + spark.udf().register("tgeogpointFromEWKT", tgeogpointFromEWKT, DataTypes.StringType); + spark.udf().register("tgeometryFromText", tgeometryFromText, DataTypes.StringType); + spark.udf().register("tgeographyFromText", tgeographyFromText, DataTypes.StringType); + spark.udf().register("tgeometryFromEWKT", tgeometryFromEWKT, DataTypes.StringType); + spark.udf().register("tgeographyFromEWKT", tgeographyFromEWKT, DataTypes.StringType); + // Binary I/O + spark.udf().register("tboolFromBinary", tboolFromBinary, DataTypes.StringType); + spark.udf().register("tintFromBinary", tintFromBinary, DataTypes.StringType); + spark.udf().register("tfloatFromBinary", tfloatFromBinary, DataTypes.StringType); + spark.udf().register("ttextFromBinary", ttextFromBinary, DataTypes.StringType); + spark.udf().register("tgeompointFromBinary", tgeompointFromBinary, DataTypes.StringType); + spark.udf().register("tgeogpointFromBinary", tgeogpointFromBinary, DataTypes.StringType); + spark.udf().register("tgeometryFromBinary", tgeometryFromBinary, DataTypes.StringType); + spark.udf().register("tgeographyFromBinary", tgeographyFromBinary, DataTypes.StringType); + spark.udf().register("tgeompointFromEWKB", tgeompointFromEWKB, DataTypes.StringType); + spark.udf().register("tgeogpointFromEWKB", tgeogpointFromEWKB, DataTypes.StringType); + spark.udf().register("tgeometryFromEWKB", tgeometryFromEWKB, DataTypes.StringType); + spark.udf().register("tgeographyFromEWKB", tgeographyFromEWKB, DataTypes.StringType); + spark.udf().register("asBinary", asBinary, DataTypes.BinaryType); + spark.udf().register("asEWKB", asEWKB, DataTypes.BinaryType); + spark.udf().register("asHexEWKB", asHexEWKB, DataTypes.StringType); + // Geoset typed I/O + spark.udf().register("geomsetFromText", geomsetFromText, DataTypes.StringType); + spark.udf().register("geogsetFromText", geogsetFromText, DataTypes.StringType); + spark.udf().register("geomsetFromEWKT", geomsetFromEWKT, DataTypes.StringType); + spark.udf().register("geogsetFromEWKT", geogsetFromEWKT, DataTypes.StringType); + spark.udf().register("geomsetFromHexWKB", geomsetFromHexWKB, DataTypes.StringType); + spark.udf().register("geogsetFromHexWKB", geogsetFromHexWKB, DataTypes.StringType); + spark.udf().register("geomsetFromBinary", geomsetFromBinary, DataTypes.StringType); + spark.udf().register("geogsetFromBinary", geogsetFromBinary, DataTypes.StringType); + spark.udf().register("geomsetFromEWKB", geomsetFromEWKB, DataTypes.StringType); + spark.udf().register("geogsetFromEWKB", geogsetFromEWKB, DataTypes.StringType); + // TBox typed I/O + spark.udf().register("tboxFromHexWKB", tboxFromHexWKB, DataTypes.StringType); + spark.udf().register("tboxFromBinary", tboxFromBinary, DataTypes.StringType); + // STBox typed I/O + spark.udf().register("stboxFromHexWKB", stboxFromHexWKB, DataTypes.StringType); + spark.udf().register("stboxFromBinary", stboxFromBinary, DataTypes.StringType); + } +} diff --git a/src/main/java/org/mobilitydb/spark/temporal/MathUDFs.java b/src/main/java/org/mobilitydb/spark/temporal/MathUDFs.java new file mode 100644 index 00000000..3fbd7a87 --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/temporal/MathUDFs.java @@ -0,0 +1,355 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +// The temporal multiplication symbols were renamed mult_* -> mul_* in MEOS; +// the renamed entries live on the regenerated GeneratedFunctions surface. +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosThread; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.api.java.UDF1; +import org.apache.spark.sql.api.java.UDF2; +import org.apache.spark.sql.types.DataTypes; + +/** + * Spark SQL UDFs for arithmetic on tnumber (tint / tfloat). + * + * Three groups: + * 1. Unary analytics: abs, deltaValue, angularDifference (on tnumber), + * angularDifference (on tpoint β†’ tfloat). + * 2. Scalar arithmetic: add/sub/mult/div of tnumber with a Java scalar + * (tint+int, tfloat+double). + * 3. Temporal arithmetic: add/sub/mult/div of two tnumbers. + * + * All temporal inputs and outputs use hex-WKB string encoding. + * + * MEOS function authority: meos/include/meos.h (026_tnumber_mathfuncs) + */ +public final class MathUDFs { + + private MathUDFs() {} + + private static String hexOut(Pointer r) { + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } + + // ------------------------------------------------------------------ + // Unary analytics (hex-WKB in, hex-WKB out) + // + // MEOS: tnumber_abs, tnumber_delta_value, tnumber_angular_difference, + // tpoint_angular_difference (β†’ tfloat hex-WKB) + // ------------------------------------------------------------------ + + public static final UDF1 tnumberAbs = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + return hexOut(GeneratedFunctions.tnumber_abs(ptr)); + } finally { MeosMemory.free(ptr); } + }; + + public static final UDF1 tnumberDeltaValue = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + return hexOut(GeneratedFunctions.tnumber_delta_value(ptr)); + } finally { MeosMemory.free(ptr); } + }; + + public static final UDF1 tnumberAngularDifference = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + return hexOut(GeneratedFunctions.tnumber_angular_difference(ptr)); + } finally { MeosMemory.free(ptr); } + }; + + public static final UDF1 tpointAngularDifference = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + return hexOut(GeneratedFunctions.tpoint_angular_difference(ptr)); + } finally { MeosMemory.free(ptr); } + }; + + // ------------------------------------------------------------------ + // Transcendental functions (tfloat β†’ tfloat) + // + // MEOS: tfloat_exp / tfloat_ln / tfloat_log10 + // ------------------------------------------------------------------ + + public static final UDF1 tfloatExp = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + return hexOut(GeneratedFunctions.tfloat_exp(ptr)); + } finally { MeosMemory.free(ptr); } + }; + + public static final UDF1 tfloatLn = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + return hexOut(GeneratedFunctions.tfloat_ln(ptr)); + } finally { MeosMemory.free(ptr); } + }; + + public static final UDF1 tfloatLog10 = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + return hexOut(GeneratedFunctions.tfloat_log10(ptr)); + } finally { MeosMemory.free(ptr); } + }; + + // ------------------------------------------------------------------ + // Scalar arithmetic: tint OP int (hex-WKB in, int scalar, hex-WKB out) + // + // MEOS: add_tint_int / sub_tint_int / mul_tint_int / div_tint_int + // ------------------------------------------------------------------ + + public static final UDF2 addTintInt = + (s, v) -> { + if (s == null || v == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + return hexOut(GeneratedFunctions.add_tint_int(ptr, v)); + } finally { MeosMemory.free(ptr); } + }; + + public static final UDF2 subTintInt = + (s, v) -> { + if (s == null || v == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + return hexOut(GeneratedFunctions.sub_tint_int(ptr, v)); + } finally { MeosMemory.free(ptr); } + }; + + public static final UDF2 multTintInt = + (s, v) -> { + if (s == null || v == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + return hexOut(GeneratedFunctions.mul_tint_int(ptr, v)); + } finally { MeosMemory.free(ptr); } + }; + + public static final UDF2 divTintInt = + (s, v) -> { + if (s == null || v == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + return hexOut(GeneratedFunctions.div_tint_int(ptr, v)); + } finally { MeosMemory.free(ptr); } + }; + + // ------------------------------------------------------------------ + // Scalar arithmetic: tfloat OP double (hex-WKB in, double scalar, hex-WKB out) + // + // MEOS: add_tfloat_float / sub_tfloat_float / mul_tfloat_float / div_tfloat_float + // ------------------------------------------------------------------ + + public static final UDF2 addTfloatFloat = + (s, v) -> { + if (s == null || v == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + return hexOut(GeneratedFunctions.add_tfloat_float(ptr, v)); + } finally { MeosMemory.free(ptr); } + }; + + public static final UDF2 subTfloatFloat = + (s, v) -> { + if (s == null || v == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + return hexOut(GeneratedFunctions.sub_tfloat_float(ptr, v)); + } finally { MeosMemory.free(ptr); } + }; + + public static final UDF2 multTfloatFloat = + (s, v) -> { + if (s == null || v == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + return hexOut(GeneratedFunctions.mul_tfloat_float(ptr, v)); + } finally { MeosMemory.free(ptr); } + }; + + public static final UDF2 divTfloatFloat = + (s, v) -> { + if (s == null || v == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + return hexOut(GeneratedFunctions.div_tfloat_float(ptr, v)); + } finally { MeosMemory.free(ptr); } + }; + + // ------------------------------------------------------------------ + // Temporal arithmetic: tnumber OP tnumber (hex-WKB in, hex-WKB out) + // + // MEOS: add_tnumber_tnumber / sub_tnumber_tnumber / + // mul_tnumber_tnumber / div_tnumber_tnumber + // + // Both tnumbers must have the same value type (tint+tint or tfloat+tfloat). + // ------------------------------------------------------------------ + + public static final UDF2 addTnumberTnumber = + (s1, s2) -> { + if (s1 == null || s2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(s1); + if (p1 == null) return null; + try { + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(s2); + if (p2 == null) return null; + try { + return hexOut(GeneratedFunctions.add_tnumber_tnumber(p1, p2)); + } finally { MeosMemory.free(p2); } + } finally { MeosMemory.free(p1); } + }; + + public static final UDF2 subTnumberTnumber = + (s1, s2) -> { + if (s1 == null || s2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(s1); + if (p1 == null) return null; + try { + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(s2); + if (p2 == null) return null; + try { + return hexOut(GeneratedFunctions.sub_tnumber_tnumber(p1, p2)); + } finally { MeosMemory.free(p2); } + } finally { MeosMemory.free(p1); } + }; + + public static final UDF2 multTnumberTnumber = + (s1, s2) -> { + if (s1 == null || s2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(s1); + if (p1 == null) return null; + try { + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(s2); + if (p2 == null) return null; + try { + return hexOut(GeneratedFunctions.mul_tnumber_tnumber(p1, p2)); + } finally { MeosMemory.free(p2); } + } finally { MeosMemory.free(p1); } + }; + + public static final UDF2 divTnumberTnumber = + (s1, s2) -> { + if (s1 == null || s2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(s1); + if (p1 == null) return null; + try { + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(s2); + if (p2 == null) return null; + try { + return hexOut(GeneratedFunctions.div_tnumber_tnumber(p1, p2)); + } finally { MeosMemory.free(p2); } + } finally { MeosMemory.free(p1); } + }; + + // ------------------------------------------------------------------ + // REGISTRATION + // ------------------------------------------------------------------ + + public static void registerAll(SparkSession spark) { + // unary analytics + spark.udf().register("tnumberAbs", tnumberAbs, DataTypes.StringType); + spark.udf().register("tnumberDeltaValue", tnumberDeltaValue, DataTypes.StringType); + spark.udf().register("tnumberAngularDifference", tnumberAngularDifference, DataTypes.StringType); + spark.udf().register("tpointAngularDifference", tpointAngularDifference, DataTypes.StringType); + // transcendental + spark.udf().register("tfloatExp", tfloatExp, DataTypes.StringType); + spark.udf().register("tfloatLn", tfloatLn, DataTypes.StringType); + spark.udf().register("tfloatLog10", tfloatLog10, DataTypes.StringType); + // tint + scalar + spark.udf().register("addTintInt", addTintInt, DataTypes.StringType); + spark.udf().register("subTintInt", subTintInt, DataTypes.StringType); + spark.udf().register("multTintInt", multTintInt, DataTypes.StringType); + spark.udf().register("divTintInt", divTintInt, DataTypes.StringType); + // tfloat + scalar + spark.udf().register("addTfloatFloat", addTfloatFloat, DataTypes.StringType); + spark.udf().register("subTfloatFloat", subTfloatFloat, DataTypes.StringType); + spark.udf().register("multTfloatFloat", multTfloatFloat, DataTypes.StringType); + spark.udf().register("divTfloatFloat", divTfloatFloat, DataTypes.StringType); + // tnumber + tnumber + spark.udf().register("addTnumberTnumber", addTnumberTnumber, DataTypes.StringType); + spark.udf().register("subTnumberTnumber", subTnumberTnumber, DataTypes.StringType); + spark.udf().register("multTnumberTnumber", multTnumberTnumber, DataTypes.StringType); + spark.udf().register("divTnumberTnumber", divTnumberTnumber, DataTypes.StringType); + } +} diff --git a/src/main/java/org/mobilitydb/spark/temporal/MoreAccessorUDFs.java b/src/main/java/org/mobilitydb/spark/temporal/MoreAccessorUDFs.java new file mode 100644 index 00000000..77013b44 --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/temporal/MoreAccessorUDFs.java @@ -0,0 +1,890 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import jnr.ffi.Runtime; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosThread; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.api.java.UDF1; +import org.apache.spark.sql.api.java.UDF2; +import org.apache.spark.sql.types.DataTypes; + +import java.sql.Timestamp; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.List; +import org.mobilitydb.spark.util.TimeUtil; + +/** + * Spark SQL UDFs for temporal structure and value accessors not covered by + * AccessorUDFs.java or TemporalUDFs.java. + * + * Covers: subtype, instant/sequence navigation, timestampN, inclusivity flags, + * duration, type-specific valueN accessors (tbool, tfloat, ttext, tpoint), + * and tpoint geometry accessors (SRID, convex hull). + * + * MEOS function authority: meos/include/meos.h, meos/include/meos_geo.h + */ +public final class MoreAccessorUDFs { + + private MoreAccessorUDFs() {} + + // Milliseconds from Unix epoch (1970-01-01) to PG epoch (2000-01-01). + /** Convert a raw PG-epoch microsecond value to a Spark Timestamp. */ + static Timestamp fromPgMicros(long pgMicros) { + return new Timestamp(pgMicros / 1000L + TimeUtil.PG_UNIX_EPOCH_OFFSET_MS); + } + + // ------------------------------------------------------------------ + // Subtype + // ------------------------------------------------------------------ + + // temporalSubtype(trip STRING) β†’ STRING ("Instant" | "Sequence" | "SequenceSet") + // MEOS: temporal_subtype(const Temporal *) β†’ char * + public static final UDF1 temporalSubtype = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + try { + return GeneratedFunctions.temporal_subtype(ptr); + } finally { + MeosMemory.free(ptr); + } + }; + + // ------------------------------------------------------------------ + // Instant navigation + // ------------------------------------------------------------------ + + // startInstant(trip STRING) β†’ STRING (hex-WKB of first instant) + // MEOS: temporal_start_instant(const Temporal *) β†’ TInstant * (owned copy) + public static final UDF1 startInstant = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + try { + Pointer r = GeneratedFunctions.temporal_start_instant(ptr); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // endInstant(trip STRING) β†’ STRING (hex-WKB of last instant) + // MEOS: temporal_end_instant(const Temporal *) β†’ TInstant * (owned copy) + public static final UDF1 endInstant = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + try { + Pointer r = GeneratedFunctions.temporal_end_instant(ptr); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // instantN(trip STRING, n INT) β†’ STRING (hex-WKB of n-th instant, 1-based) + // MEOS: temporal_instant_n(const Temporal *, int) β†’ TInstant * (owned copy) + public static final UDF2 instantN = + (trip, n) -> { + if (trip == null || n == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + try { + Pointer r = GeneratedFunctions.temporal_instant_n(ptr, n); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // ------------------------------------------------------------------ + // Sequence navigation + // ------------------------------------------------------------------ + + // startSequence(trip STRING) β†’ STRING (hex-WKB of first sequence) + // MEOS: temporal_start_sequence(const Temporal *) β†’ TSequence * (owned copy) + public static final UDF1 startSequence = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + try { + Pointer r = GeneratedFunctions.temporal_start_sequence(ptr); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // endSequence(trip STRING) β†’ STRING (hex-WKB of last sequence) + // MEOS: temporal_end_sequence(const Temporal *) β†’ TSequence * (owned copy) + public static final UDF1 endSequence = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + try { + Pointer r = GeneratedFunctions.temporal_end_sequence(ptr); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // sequenceN(trip STRING, n INT) β†’ STRING (hex-WKB of n-th sequence, 1-based) + // MEOS: temporal_sequence_n(const Temporal *, int) β†’ TSequence * (owned copy) + public static final UDF2 sequenceN = + (trip, n) -> { + if (trip == null || n == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + try { + Pointer r = GeneratedFunctions.temporal_sequence_n(ptr, n); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // ------------------------------------------------------------------ + // Min/max instant + // ------------------------------------------------------------------ + + // minInstant(trip STRING) β†’ STRING (hex-WKB of instant with minimum value) + // MEOS: temporal_min_instant(const Temporal *) β†’ TInstant * (owned copy) + public static final UDF1 minInstant = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + try { + Pointer r = GeneratedFunctions.temporal_min_instant(ptr); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // maxInstant(trip STRING) β†’ STRING (hex-WKB of instant with maximum value) + // MEOS: temporal_max_instant(const Temporal *) β†’ TInstant * (owned copy) + public static final UDF1 maxInstant = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + try { + Pointer r = GeneratedFunctions.temporal_max_instant(ptr); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // ------------------------------------------------------------------ + // Timestamp accessors + // ------------------------------------------------------------------ + + // numTimestamps(trip STRING) β†’ INT + // MEOS: temporal_num_timestamps(const Temporal *) β†’ int + public static final UDF1 numTimestamps = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + try { + return GeneratedFunctions.temporal_num_timestamps(ptr); + } finally { + MeosMemory.free(ptr); + } + }; + + // timestampN(trip STRING, n INT) β†’ TIMESTAMP (n-th timestamp, 1-based) + // MEOS: temporal_timestamptz_n writes TimestampTz (int64) into a JNR-FFI buffer; + // the returned Pointer is JNR-FFI managed β€” DO NOT call MeosMemory.free on it. + public static final UDF2 timestampN = + (trip, n) -> { + if (trip == null || n == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + try { + Pointer tsPtr = GeneratedFunctions.temporal_timestamptz_n(ptr, n); + if (tsPtr == null) return null; + return fromPgMicros(tsPtr.getLong(0)); + } finally { + MeosMemory.free(ptr); + } + }; + + // ------------------------------------------------------------------ + // Inclusivity flags + // ------------------------------------------------------------------ + + // lowerInc(trip STRING) β†’ BOOLEAN (true if the lower bound is inclusive) + // MEOS: temporal_lower_inc(const Temporal *) β†’ int (nonzero = true) + public static final UDF1 lowerInc = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + try { + return GeneratedFunctions.temporal_lower_inc(ptr); + } finally { + MeosMemory.free(ptr); + } + }; + + // upperInc(trip STRING) β†’ BOOLEAN (true if the upper bound is inclusive) + // MEOS: temporal_upper_inc(const Temporal *) β†’ int (nonzero = true) + public static final UDF1 upperInc = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + try { + return GeneratedFunctions.temporal_upper_inc(ptr); + } finally { + MeosMemory.free(ptr); + } + }; + + // ------------------------------------------------------------------ + // Duration + // ------------------------------------------------------------------ + + // duration(trip STRING) β†’ STRING (PostgreSQL interval literal, e.g. "01:30:00") + // MEOS: temporal_duration(const Temporal *, bool) β†’ Interval * + // pg_interval_out(const Interval *) β†’ char * + public static final UDF1 duration = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + try { + Pointer ivPtr = GeneratedFunctions.temporal_duration(ptr, false); + if (ivPtr == null) return null; + try { + return GeneratedFunctions.pg_interval_out(ivPtr); + } finally { + MeosMemory.free(ivPtr); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // ------------------------------------------------------------------ + // tbool value accessor + // ------------------------------------------------------------------ + + // tboolValueN(trip STRING, n INT) β†’ BOOLEAN (n-th value, 1-based) + // MEOS: tbool_value_n writes bool directly into a JNR-FFI buffer; + // the returned Pointer is JNR-FFI managed β€” DO NOT call MeosMemory.free on it. + public static final UDF2 tboolValueN = + (trip, n) -> { + if (trip == null || n == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + try { + Pointer bPtr = GeneratedFunctions.tbool_value_n(ptr, n); + if (bPtr == null) return null; + return bPtr.getByte(0) != 0; + } finally { + MeosMemory.free(ptr); + } + }; + + // ------------------------------------------------------------------ + // tfloat value accessor + // ------------------------------------------------------------------ + + // tfloatValueN(trip STRING, n INT) β†’ DOUBLE (n-th value, 1-based) + // MEOS: tfloat_value_n writes double directly into a JNR-FFI buffer; + // the returned Pointer is JNR-FFI managed β€” DO NOT call MeosMemory.free on it. + public static final UDF2 tfloatValueN = + (trip, n) -> { + if (trip == null || n == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + try { + Pointer dPtr = GeneratedFunctions.tfloat_value_n(ptr, n); + if (dPtr == null) return null; + return dPtr.getDouble(0); + } finally { + MeosMemory.free(ptr); + } + }; + + // ------------------------------------------------------------------ + // ttext value accessors + // ------------------------------------------------------------------ + + // ttextMinValue(trip STRING) β†’ STRING + // MEOS: ttext_min_value(const Temporal *) β†’ text * + public static final UDF1 ttextMinValue = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + try { + Pointer txtPtr = GeneratedFunctions.ttext_min_value(ptr); + if (txtPtr == null) return null; + try { + return GeneratedFunctions.text_out(txtPtr); + } finally { + MeosMemory.free(txtPtr); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // ttextMaxValue(trip STRING) β†’ STRING + // MEOS: ttext_max_value(const Temporal *) β†’ text * + public static final UDF1 ttextMaxValue = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + try { + Pointer txtPtr = GeneratedFunctions.ttext_max_value(ptr); + if (txtPtr == null) return null; + try { + return GeneratedFunctions.text_out(txtPtr); + } finally { + MeosMemory.free(txtPtr); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // ttextValueN(trip STRING, n INT) β†’ STRING (n-th value, 1-based) + // MEOS: ttext_value_n(const Temporal *, int) β†’ text * + public static final UDF2 ttextValueN = + (trip, n) -> { + if (trip == null || n == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + try { + Pointer txtPtr = GeneratedFunctions.ttext_value_n(ptr, n); + if (txtPtr == null) return null; + try { + return GeneratedFunctions.text_out(txtPtr); + } finally { + MeosMemory.free(txtPtr); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // ------------------------------------------------------------------ + // tpoint accessors + // ------------------------------------------------------------------ + + // tpointSrid(trip STRING) β†’ INT + // MEOS: tspatial_srid(const Temporal *) β†’ int + public static final UDF1 tpointSrid = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + try { + return GeneratedFunctions.tspatial_srid(ptr); + } finally { + MeosMemory.free(ptr); + } + }; + + // tpointValueN(trip STRING, n INT) β†’ STRING (WKT of n-th point, 1-based) + // MEOS: tgeo_value_n(const Temporal *, int, GSERIALIZED **) β†’ bool + public static final UDF2 tpointValueN = + (trip, n) -> { + if (trip == null || n == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + try { + Pointer gsPtr = GeneratedFunctions.tgeo_value_n(ptr, n); + if (gsPtr == null) return null; + try { + return GeneratedFunctions.geo_as_text(gsPtr, 6); + } finally { + MeosMemory.free(gsPtr); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // tpointConvexHull(trip STRING) β†’ STRING (WKT of convex hull geometry) + // MEOS: tgeo_convex_hull(const Temporal *) β†’ GSERIALIZED * + public static final UDF1 tpointConvexHull = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + try { + Pointer gsPtr = GeneratedFunctions.tgeo_convex_hull(ptr); + if (gsPtr == null) return null; + try { + return GeneratedFunctions.geo_as_text(gsPtr, 6); + } finally { + MeosMemory.free(gsPtr); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // ------------------------------------------------------------------ + // tint value accessor + // ------------------------------------------------------------------ + + // tintValueN(trip STRING, n INT) β†’ INT (n-th distinct value, 1-based) + // MEOS: tint_value_n(const Temporal *, int n) β†’ int * (JNR-allocated; do NOT MeosMemory.free) + public static final UDF2 tintValueN = + (trip, n) -> { + if (trip == null || n == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + try { + Pointer valPtr = GeneratedFunctions.tint_value_n(ptr, n); + if (valPtr == null) return null; + return valPtr.getInt(0); + } finally { + MeosMemory.free(ptr); + } + }; + + // ------------------------------------------------------------------ + // value_at_timestamptz: retrieve value at a given instant + // + // MEOS: tbool_value_at_timestamptz / tint_value_at_timestamptz / + // tfloat_value_at_timestamptz (output-pointer pattern) + // ------------------------------------------------------------------ + + // tboolValueAtTimestamptz(tval STRING, ts TIMESTAMP) β†’ BOOLEAN + public static final UDF2 tboolValueAtTimestamptz = + (tval, ts) -> { + if (tval == null || ts == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(tval); + if (ptr == null) return null; + try { + long pgEpochMicros = (ts.getTime() - TimeUtil.PG_UNIX_EPOCH_OFFSET_MS) * 1000L; + OffsetDateTime odt = OffsetDateTime.ofInstant( + Instant.ofEpochSecond(pgEpochMicros, 0), ZoneOffset.UTC); + Pointer outVal = GeneratedFunctions.tbool_value_at_timestamptz(ptr, odt, false); + if (outVal == null) return null; + return outVal.getByte(0) != 0; + } finally { + MeosMemory.free(ptr); + } + }; + + // tintValueAtTimestamptz(tval STRING, ts TIMESTAMP) β†’ INT + public static final UDF2 tintValueAtTimestamptz = + (tval, ts) -> { + if (tval == null || ts == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(tval); + if (ptr == null) return null; + try { + long pgEpochMicros = (ts.getTime() - TimeUtil.PG_UNIX_EPOCH_OFFSET_MS) * 1000L; + OffsetDateTime odt = OffsetDateTime.ofInstant( + Instant.ofEpochSecond(pgEpochMicros, 0), ZoneOffset.UTC); + Pointer outVal = GeneratedFunctions.tint_value_at_timestamptz(ptr, odt, false); + if (outVal == null) return null; + return outVal.getInt(0); + } finally { + MeosMemory.free(ptr); + } + }; + + // tfloatValueAtTimestamptz(tval STRING, ts TIMESTAMP) β†’ DOUBLE + public static final UDF2 tfloatValueAtTimestamptz = + (tval, ts) -> { + if (tval == null || ts == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(tval); + if (ptr == null) return null; + try { + long pgEpochMicros = (ts.getTime() - TimeUtil.PG_UNIX_EPOCH_OFFSET_MS) * 1000L; + OffsetDateTime odt = OffsetDateTime.ofInstant( + Instant.ofEpochSecond(pgEpochMicros, 0), ZoneOffset.UTC); + Pointer outVal = GeneratedFunctions.tfloat_value_at_timestamptz(ptr, odt, false); + if (outVal == null) return null; + return outVal.getDouble(0); + } finally { + MeosMemory.free(ptr); + } + }; + + // ttextValueAtTimestamptz(tval STRING, ts TIMESTAMP) β†’ STRING + // MEOS: ttext_value_at_timestamptz(temp, t, strict, text **value) β†’ bool + // The generated wrapper dereferences text** and returns the text* + // directly (MEOS-allocated) or null β€” use text_out then free. + public static final UDF2 ttextValueAtTimestamptz = + (tval, ts) -> { + if (tval == null || ts == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(tval); + if (ptr == null) return null; + try { + long pgEpochMicros = (ts.getTime() - TimeUtil.PG_UNIX_EPOCH_OFFSET_MS) * 1000L; + OffsetDateTime odt = OffsetDateTime.ofInstant( + Instant.ofEpochSecond(pgEpochMicros, 0), ZoneOffset.UTC); + Pointer textPtr = GeneratedFunctions.ttext_value_at_timestamptz(ptr, odt, false); + if (textPtr == null) return null; + try { + return GeneratedFunctions.text_out(textPtr); + } finally { + MeosMemory.free(textPtr); + } + } finally { + MeosMemory.free(ptr); + } + }; + + public static void registerAll(SparkSession spark) { + // Subtype + spark.udf().register("temporalSubtype", temporalSubtype, DataTypes.StringType); + // Instant navigation + spark.udf().register("startInstant", startInstant, DataTypes.StringType); + spark.udf().register("endInstant", endInstant, DataTypes.StringType); + spark.udf().register("instantN", instantN, DataTypes.StringType); + // Sequence navigation + spark.udf().register("startSequence", startSequence, DataTypes.StringType); + spark.udf().register("endSequence", endSequence, DataTypes.StringType); + spark.udf().register("sequenceN", sequenceN, DataTypes.StringType); + // Min/max instant + spark.udf().register("minInstant", minInstant, DataTypes.StringType); + spark.udf().register("maxInstant", maxInstant, DataTypes.StringType); + // Timestamp accessors + spark.udf().register("numTimestamps", numTimestamps, DataTypes.IntegerType); + spark.udf().register("timestampN", timestampN, DataTypes.TimestampType); + // Inclusivity flags + spark.udf().register("lowerInc", lowerInc, DataTypes.BooleanType); + spark.udf().register("upperInc", upperInc, DataTypes.BooleanType); + // Duration + spark.udf().register("duration", duration, DataTypes.StringType); + // tbool value accessor + spark.udf().register("tboolValueN", tboolValueN, DataTypes.BooleanType); + // tfloat value accessor + spark.udf().register("tfloatValueN", tfloatValueN, DataTypes.DoubleType); + // ttext value accessors + spark.udf().register("ttextMinValue", ttextMinValue, DataTypes.StringType); + spark.udf().register("ttextMaxValue", ttextMaxValue, DataTypes.StringType); + spark.udf().register("ttextValueN", ttextValueN, DataTypes.StringType); + // tpoint accessors + spark.udf().register("tpointSrid", tpointSrid, DataTypes.IntegerType); + spark.udf().register("tpointValueN", tpointValueN, DataTypes.StringType); + spark.udf().register("tpointConvexHull", tpointConvexHull, DataTypes.StringType); + // value_at_timestamptz + spark.udf().register("tintValueN", tintValueN, DataTypes.IntegerType); + spark.udf().register("tboolValueAtTimestamptz", tboolValueAtTimestamptz, DataTypes.BooleanType); + spark.udf().register("tintValueAtTimestamptz", tintValueAtTimestamptz, DataTypes.IntegerType); + spark.udf().register("tfloatValueAtTimestamptz", tfloatValueAtTimestamptz, DataTypes.DoubleType); + spark.udf().register("ttextValueAtTimestamptz", ttextValueAtTimestamptz, DataTypes.StringType); + // Array-returning accessors + spark.udf().register("temporalTimestamps", temporalTimestamps, + DataTypes.createArrayType(DataTypes.TimestampType)); + spark.udf().register("tboolValues", tboolValues, + DataTypes.createArrayType(DataTypes.BooleanType)); + spark.udf().register("tintValues", tintValues, + DataTypes.createArrayType(DataTypes.IntegerType)); + spark.udf().register("tfloatValues", tfloatValues, + DataTypes.createArrayType(DataTypes.DoubleType)); + spark.udf().register("temporalInstants", temporalInstants, + DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("temporalSequences", temporalSequences, + DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("temporalSegments", temporalSegments, + DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("ttextValues", ttextValues, + DataTypes.createArrayType(DataTypes.StringType)); + } + + // ------------------------------------------------------------------ + // Array-returning accessors + // + // temporalTimestamps: temporal_timestamps(temp, sizeOut) β†’ TimestampTz[] + // tboolValues: tbool_values(temp, sizeOut) β†’ bool[] + // + // MEOS convention: sizeOut is int * (4 bytes); the returned C array is + // palloc'd and must be freed after use. + // ------------------------------------------------------------------ + + // temporalTimestamps(hex STRING) β†’ ARRAY + // Returns the distinct timestamps at which the temporal has an instant. + public static final UDF1> temporalTimestamps = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(hex); + if (ptr == null) return null; + try { + Pointer sizeOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + Pointer arrPtr = GeneratedFunctions.temporal_timestamps(ptr, sizeOut); + if (arrPtr == null) return null; + int count = sizeOut.getInt(0); + List result = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + long pgMicros = arrPtr.getLong((long) i * 8); + result.add(new Timestamp(pgMicros / 1000L + TimeUtil.PG_UNIX_EPOCH_OFFSET_MS)); + } + MeosMemory.free(arrPtr); + return result; + } finally { + MeosMemory.free(ptr); + } + }; + + // tboolValues(hex STRING) β†’ ARRAY + // Returns the distinct boolean values present in a tbool. + public static final UDF1> tboolValues = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(hex); + if (ptr == null) return null; + try { + Pointer sizeOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + Pointer arrPtr = GeneratedFunctions.tbool_values(ptr, sizeOut); + if (arrPtr == null) return null; + int count = sizeOut.getInt(0); + List result = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + result.add(arrPtr.getByte(i) != 0); + } + MeosMemory.free(arrPtr); + return result; + } finally { + MeosMemory.free(ptr); + } + }; + + // tintValues(hex STRING) β†’ ARRAY + // Returns the distinct integer values present in a tint. + public static final UDF1> tintValues = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(hex); + if (ptr == null) return null; + try { + Pointer sizeOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + Pointer arrPtr = GeneratedFunctions.tint_values(ptr, sizeOut); + if (arrPtr == null) return null; + int count = sizeOut.getInt(0); + List result = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + result.add(arrPtr.getInt((long) i * 4)); + } + MeosMemory.free(arrPtr); + return result; + } finally { + MeosMemory.free(ptr); + } + }; + + // tfloatValues(hex STRING) β†’ ARRAY + // Returns the distinct float values present in a tfloat. + public static final UDF1> tfloatValues = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(hex); + if (ptr == null) return null; + try { + Pointer sizeOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + Pointer arrPtr = GeneratedFunctions.tfloat_values(ptr, sizeOut); + if (arrPtr == null) return null; + int count = sizeOut.getInt(0); + List result = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + result.add(arrPtr.getDouble((long) i * 8)); + } + MeosMemory.free(arrPtr); + return result; + } finally { + MeosMemory.free(ptr); + } + }; + + // ------------------------------------------------------------------ + // temporal_instants / temporal_sequences / temporal_segments + // + // Each function returns a Temporal** (array of Temporal* view pointers). + // The array itself is palloc'd and must be freed; the elements are views + // into the original temporal and must NOT be freed. + // + // MEOS: temporal_instants(Temporal *, int *count) β†’ TInstant ** + // temporal_sequences(Temporal *, int *count) β†’ TSequence ** + // temporal_segments(Temporal *, int *count) β†’ TSequence ** + // ------------------------------------------------------------------ + + private static List temporalPtrArray(String hex, + java.util.function.BiFunction fn) { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(hex); + if (ptr == null) return null; + try { + Pointer sizeOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + Pointer arrPtr = fn.apply(ptr, sizeOut); + if (arrPtr == null) return null; + int count = sizeOut.getInt(0); + List result = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + Pointer elem = arrPtr.getPointer((long) i * 8); + if (elem != null) { + String h = GeneratedFunctions.temporal_as_hexwkb(elem, (byte) 0); + if (h != null) result.add(h); + } + } + MeosMemory.free(arrPtr); + return result; + } finally { + MeosMemory.free(ptr); + } + } + + // temporalInstants(hex STRING) β†’ ARRAY + // Returns each instant of the temporal value as a hex-WKB string. + public static final UDF1> temporalInstants = + (hex) -> temporalPtrArray(hex, GeneratedFunctions::temporal_instants); + + // temporalSequences(hex STRING) β†’ ARRAY + // Returns each sequence of a TSequenceSet as hex-WKB strings. + public static final UDF1> temporalSequences = + (hex) -> temporalPtrArray(hex, GeneratedFunctions::temporal_sequences); + + // temporalSegments(hex STRING) β†’ ARRAY + // Returns each linear segment of the temporal value as hex-WKB strings. + public static final UDF1> temporalSegments = + (hex) -> temporalPtrArray(hex, GeneratedFunctions::temporal_segments); + + // ttextValues(hex STRING) β†’ ARRAY + // Returns the distinct text values of a ttext as Strings. + // MEOS: ttext_values(const Temporal *, int *count) β†’ text ** + // Elements are text* pointers; read via text_out; free the outer array. + public static final UDF1> ttextValues = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(hex); + if (ptr == null) return null; + try { + Pointer sizeOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + Pointer arrPtr = GeneratedFunctions.ttext_values(ptr, sizeOut); + if (arrPtr == null) return null; + int count = sizeOut.getInt(0); + List result = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + Pointer textPtr = arrPtr.getPointer((long) i * 8); + if (textPtr != null) result.add(GeneratedFunctions.text_out(textPtr)); + } + MeosMemory.free(arrPtr); + return result; + } finally { + MeosMemory.free(ptr); + } + }; +} diff --git a/src/main/java/org/mobilitydb/spark/temporal/PosOpsUDFs.java b/src/main/java/org/mobilitydb/spark/temporal/PosOpsUDFs.java new file mode 100644 index 00000000..e4979c4a --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/temporal/PosOpsUDFs.java @@ -0,0 +1,470 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosThread; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.api.java.UDF2; +import org.apache.spark.sql.types.DataTypes; + +/** + * Spark SQL UDFs for temporal and spatial positional operators. + * + * Three families of operators: + * 1. Time-direction (before/after/overbefore/overafter) on any temporal value. + * 2. Value-direction (left/right/overleft/overright) on tnumber (tint/tfloat). + * 3. Spatial-direction (left/right/overleft/overright/below/above/overbelow/ + * overabove/front/back/overfront/overback) on tpoint (tgeompoint/tgeogpoint). + * + * All inputs are hex-WKB strings; tstzspan inputs also use hex-WKB (span_from_hexwkb). + * All outputs are Boolean. + * + * MEOS function authority: meos/include/meos.h (temporal), meos/include/meos_geo.h (tpoint) + */ +public final class PosOpsUDFs { + + private PosOpsUDFs() {} + + private static Pointer tempPtr(String hex) { + return hex == null ? null : GeneratedFunctions.temporal_from_hexwkb(hex); + } + + private static Pointer spanPtr(String hex) { + return hex == null ? null : GeneratedFunctions.span_from_hexwkb(hex); + } + + // ------------------------------------------------------------------ + // Time-direction: temporal ↔ temporal + // MEOS: before/after/overbefore/overafter_temporal_temporal β†’ boolean + // ------------------------------------------------------------------ + + public static final UDF2 temporalBefore = + (s1, s2) -> { + if (s1 == null || s2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1); + if (p1 == null) return null; + try { + Pointer p2 = tempPtr(s2); + if (p2 == null) return null; + try { + return GeneratedFunctions.before_temporal_temporal(p1, p2); + } finally { MeosMemory.free(p2); } + } finally { MeosMemory.free(p1); } + }; + + public static final UDF2 temporalAfter = + (s1, s2) -> { + if (s1 == null || s2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1); + if (p1 == null) return null; + try { + Pointer p2 = tempPtr(s2); + if (p2 == null) return null; + try { + return GeneratedFunctions.after_temporal_temporal(p1, p2); + } finally { MeosMemory.free(p2); } + } finally { MeosMemory.free(p1); } + }; + + public static final UDF2 temporalOverbefore = + (s1, s2) -> { + if (s1 == null || s2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1); + if (p1 == null) return null; + try { + Pointer p2 = tempPtr(s2); + if (p2 == null) return null; + try { + return GeneratedFunctions.overbefore_temporal_temporal(p1, p2); + } finally { MeosMemory.free(p2); } + } finally { MeosMemory.free(p1); } + }; + + public static final UDF2 temporalOverafter = + (s1, s2) -> { + if (s1 == null || s2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1); + if (p1 == null) return null; + try { + Pointer p2 = tempPtr(s2); + if (p2 == null) return null; + try { + return GeneratedFunctions.overafter_temporal_temporal(p1, p2); + } finally { MeosMemory.free(p2); } + } finally { MeosMemory.free(p1); } + }; + + // ------------------------------------------------------------------ + // Time-direction: temporal ↔ tstzspan (hex-WKB span as second arg) + // MEOS: before/after/overbefore/overafter_temporal_tstzspan β†’ boolean + // ------------------------------------------------------------------ + + public static final UDF2 temporalBeforeSpan = + (tHex, spanHex) -> { + if (tHex == null || spanHex == null) return null; + MeosThread.ensureReady(); + Pointer p = tempPtr(tHex); + if (p == null) return null; + try { + Pointer sp = spanPtr(spanHex); + if (sp == null) return null; + try { + return GeneratedFunctions.before_temporal_tstzspan(p, sp); + } finally { MeosMemory.free(sp); } + } finally { MeosMemory.free(p); } + }; + + public static final UDF2 temporalAfterSpan = + (tHex, spanHex) -> { + if (tHex == null || spanHex == null) return null; + MeosThread.ensureReady(); + Pointer p = tempPtr(tHex); + if (p == null) return null; + try { + Pointer sp = spanPtr(spanHex); + if (sp == null) return null; + try { + return GeneratedFunctions.after_temporal_tstzspan(p, sp); + } finally { MeosMemory.free(sp); } + } finally { MeosMemory.free(p); } + }; + + public static final UDF2 temporalOverbeforeSpan = + (tHex, spanHex) -> { + if (tHex == null || spanHex == null) return null; + MeosThread.ensureReady(); + Pointer p = tempPtr(tHex); + if (p == null) return null; + try { + Pointer sp = spanPtr(spanHex); + if (sp == null) return null; + try { + return GeneratedFunctions.overbefore_temporal_tstzspan(p, sp); + } finally { MeosMemory.free(sp); } + } finally { MeosMemory.free(p); } + }; + + public static final UDF2 temporalOverafterSpan = + (tHex, spanHex) -> { + if (tHex == null || spanHex == null) return null; + MeosThread.ensureReady(); + Pointer p = tempPtr(tHex); + if (p == null) return null; + try { + Pointer sp = spanPtr(spanHex); + if (sp == null) return null; + try { + return GeneratedFunctions.overafter_temporal_tstzspan(p, sp); + } finally { MeosMemory.free(sp); } + } finally { MeosMemory.free(p); } + }; + + // ------------------------------------------------------------------ + // Value-direction: tnumber ↔ tnumber + // MEOS: left/right/overleft/overright_tnumber_tnumber β†’ boolean + // ------------------------------------------------------------------ + + public static final UDF2 tnumberLeft = + (s1, s2) -> { + if (s1 == null || s2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1); + if (p1 == null) return null; + try { + Pointer p2 = tempPtr(s2); + if (p2 == null) return null; + try { + return GeneratedFunctions.left_tnumber_tnumber(p1, p2); + } finally { MeosMemory.free(p2); } + } finally { MeosMemory.free(p1); } + }; + + public static final UDF2 tnumberRight = + (s1, s2) -> { + if (s1 == null || s2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1); + if (p1 == null) return null; + try { + Pointer p2 = tempPtr(s2); + if (p2 == null) return null; + try { + return GeneratedFunctions.right_tnumber_tnumber(p1, p2); + } finally { MeosMemory.free(p2); } + } finally { MeosMemory.free(p1); } + }; + + public static final UDF2 tnumberOverleft = + (s1, s2) -> { + if (s1 == null || s2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1); + if (p1 == null) return null; + try { + Pointer p2 = tempPtr(s2); + if (p2 == null) return null; + try { + return GeneratedFunctions.overleft_tnumber_tnumber(p1, p2); + } finally { MeosMemory.free(p2); } + } finally { MeosMemory.free(p1); } + }; + + public static final UDF2 tnumberOverright = + (s1, s2) -> { + if (s1 == null || s2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1); + if (p1 == null) return null; + try { + Pointer p2 = tempPtr(s2); + if (p2 == null) return null; + try { + return GeneratedFunctions.overright_tnumber_tnumber(p1, p2); + } finally { MeosMemory.free(p2); } + } finally { MeosMemory.free(p1); } + }; + + // ------------------------------------------------------------------ + // Spatial-direction x-axis: tpoint ↔ tpoint + // MEOS: left/right/overleft/overright_tspatial_tspatial β†’ boolean + // ------------------------------------------------------------------ + + public static final UDF2 tpointLeft = + (s1, s2) -> { + if (s1 == null || s2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1); + if (p1 == null) return null; + try { + Pointer p2 = tempPtr(s2); + if (p2 == null) return null; + try { + return GeneratedFunctions.left_tspatial_tspatial(p1, p2); + } finally { MeosMemory.free(p2); } + } finally { MeosMemory.free(p1); } + }; + + public static final UDF2 tpointRight = + (s1, s2) -> { + if (s1 == null || s2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1); + if (p1 == null) return null; + try { + Pointer p2 = tempPtr(s2); + if (p2 == null) return null; + try { + return GeneratedFunctions.right_tspatial_tspatial(p1, p2); + } finally { MeosMemory.free(p2); } + } finally { MeosMemory.free(p1); } + }; + + public static final UDF2 tpointOverleft = + (s1, s2) -> { + if (s1 == null || s2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1); + if (p1 == null) return null; + try { + Pointer p2 = tempPtr(s2); + if (p2 == null) return null; + try { + return GeneratedFunctions.overleft_tspatial_tspatial(p1, p2); + } finally { MeosMemory.free(p2); } + } finally { MeosMemory.free(p1); } + }; + + public static final UDF2 tpointOverright = + (s1, s2) -> { + if (s1 == null || s2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1); + if (p1 == null) return null; + try { + Pointer p2 = tempPtr(s2); + if (p2 == null) return null; + try { + return GeneratedFunctions.overright_tspatial_tspatial(p1, p2); + } finally { MeosMemory.free(p2); } + } finally { MeosMemory.free(p1); } + }; + + // ------------------------------------------------------------------ + // Spatial-direction y-axis: tpoint ↔ tpoint + // MEOS: below/above/overbelow/overabove_tspatial_tspatial β†’ boolean + // ------------------------------------------------------------------ + + public static final UDF2 tpointBelow = + (s1, s2) -> { + if (s1 == null || s2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1); + if (p1 == null) return null; + try { + Pointer p2 = tempPtr(s2); + if (p2 == null) return null; + try { + return GeneratedFunctions.below_tspatial_tspatial(p1, p2); + } finally { MeosMemory.free(p2); } + } finally { MeosMemory.free(p1); } + }; + + public static final UDF2 tpointAbove = + (s1, s2) -> { + if (s1 == null || s2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1); + if (p1 == null) return null; + try { + Pointer p2 = tempPtr(s2); + if (p2 == null) return null; + try { + return GeneratedFunctions.above_tspatial_tspatial(p1, p2); + } finally { MeosMemory.free(p2); } + } finally { MeosMemory.free(p1); } + }; + + public static final UDF2 tpointOverbelow = + (s1, s2) -> { + if (s1 == null || s2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1); + if (p1 == null) return null; + try { + Pointer p2 = tempPtr(s2); + if (p2 == null) return null; + try { + return GeneratedFunctions.overbelow_tspatial_tspatial(p1, p2); + } finally { MeosMemory.free(p2); } + } finally { MeosMemory.free(p1); } + }; + + public static final UDF2 tpointOverabove = + (s1, s2) -> { + if (s1 == null || s2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1); + if (p1 == null) return null; + try { + Pointer p2 = tempPtr(s2); + if (p2 == null) return null; + try { + return GeneratedFunctions.overabove_tspatial_tspatial(p1, p2); + } finally { MeosMemory.free(p2); } + } finally { MeosMemory.free(p1); } + }; + + // ------------------------------------------------------------------ + // Spatial-direction z-axis (3D): tpoint ↔ tpoint + // MEOS: front/back/overfront/overback_tspatial_tspatial β†’ boolean + // ------------------------------------------------------------------ + + public static final UDF2 tpointFront = + (s1, s2) -> { + if (s1 == null || s2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1); + if (p1 == null) return null; + try { + Pointer p2 = tempPtr(s2); + if (p2 == null) return null; + try { + return GeneratedFunctions.front_tspatial_tspatial(p1, p2); + } finally { MeosMemory.free(p2); } + } finally { MeosMemory.free(p1); } + }; + + public static final UDF2 tpointBack = + (s1, s2) -> { + if (s1 == null || s2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1); + if (p1 == null) return null; + try { + Pointer p2 = tempPtr(s2); + if (p2 == null) return null; + try { + return GeneratedFunctions.back_tspatial_tspatial(p1, p2); + } finally { MeosMemory.free(p2); } + } finally { MeosMemory.free(p1); } + }; + + public static final UDF2 tpointOverfront = + (s1, s2) -> { + if (s1 == null || s2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1); + if (p1 == null) return null; + try { + Pointer p2 = tempPtr(s2); + if (p2 == null) return null; + try { + return GeneratedFunctions.overfront_tspatial_tspatial(p1, p2); + } finally { MeosMemory.free(p2); } + } finally { MeosMemory.free(p1); } + }; + + public static final UDF2 tpointOverback = + (s1, s2) -> { + if (s1 == null || s2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1); + if (p1 == null) return null; + try { + Pointer p2 = tempPtr(s2); + if (p2 == null) return null; + try { + return GeneratedFunctions.overback_tspatial_tspatial(p1, p2); + } finally { MeosMemory.free(p2); } + } finally { MeosMemory.free(p1); } + }; + + // ------------------------------------------------------------------ + // REGISTRATION + // ------------------------------------------------------------------ + + public static void registerAll(SparkSession spark) { + // The portable bare names before/after/overbefore/overafter (time) + // and left/right/overleft/overright/below/above/overbelow/overabove/ + // front/back/overfront/overback (space) supersede the type-qualified + // temporal*/tnumber*/tpoint* spellings 1:1 and are registered by + // org.mobilitydb.spark.portable.PortableOperatorAliasUDFs, which + // reuses the very backing fields below. The distinct + // temporal ↔ tstzspan argument-class surface has no single bare + // spelling and is retained here. + spark.udf().register("temporalBeforeSpan", temporalBeforeSpan, DataTypes.BooleanType); + spark.udf().register("temporalAfterSpan", temporalAfterSpan, DataTypes.BooleanType); + spark.udf().register("temporalOverbeforeSpan",temporalOverbeforeSpan,DataTypes.BooleanType); + spark.udf().register("temporalOverafterSpan", temporalOverafterSpan, DataTypes.BooleanType); + } +} diff --git a/src/main/java/org/mobilitydb/spark/temporal/PredicateUDFs.java b/src/main/java/org/mobilitydb/spark/temporal/PredicateUDFs.java new file mode 100644 index 00000000..0e35a43f --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/temporal/PredicateUDFs.java @@ -0,0 +1,795 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosThread; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.api.java.UDF1; +import org.apache.spark.sql.api.java.UDF2; +import org.apache.spark.sql.types.DataTypes; +import java.util.function.BiFunction; + +/** + * Spark SQL UDFs for temporal comparisons and ever/always predicate lifting. + * + * Temporal order comparison (temporalEq, temporalLt, …) compares two temporal + * values lexicographically by their sequence of instants. + * + * Ever/always predicates follow the MEOS convention of returning int: + * 1 = predicate holds for every/some instant, 0 = it does not, -1 = error. + * All UDFs here convert that int to Boolean (null for error). + * + * MEOS function authority: meos/include/meos.h + */ +public final class PredicateUDFs { + + private PredicateUDFs() {} + + private static Pointer tempPtr(String hex) { + return hex == null ? null : GeneratedFunctions.temporal_from_hexwkb(hex); + } + + /** Convert MEOS ever/always int result to Boolean; null on error (-1). */ + private static Boolean intToBool(int v) { + if (v < 0) return null; + return v != 0; + } + + // ------------------------------------------------------------------ + // Temporal order comparisons (hex, hex) β†’ Boolean + // + // These compare two temporal values as ordered objects (lexicographic + // by instant sequence), not instant-by-instant. + // + // MEOS: temporal_eq / temporal_ne / temporal_lt / temporal_le + // temporal_gt / temporal_ge + // ------------------------------------------------------------------ + + // temporalEq(t1, t2) β†’ true if t1 and t2 have identical instant sequences + public static final UDF2 temporalEq = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1), p2 = tempPtr(s2); + if (p1 == null || p2 == null) return null; + return GeneratedFunctions.temporal_eq(p1, p2); + }; + + // temporalNe(t1, t2) β†’ true if t1 and t2 differ + public static final UDF2 temporalNe = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1), p2 = tempPtr(s2); + if (p1 == null || p2 == null) return null; + return GeneratedFunctions.temporal_ne(p1, p2); + }; + + // temporalLt(t1, t2) β†’ true if t1 < t2 (lexicographic) + public static final UDF2 temporalLt = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1), p2 = tempPtr(s2); + if (p1 == null || p2 == null) return null; + return GeneratedFunctions.temporal_lt(p1, p2); + }; + + // temporalLe(t1, t2) β†’ true if t1 ≀ t2 + public static final UDF2 temporalLe = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1), p2 = tempPtr(s2); + if (p1 == null || p2 == null) return null; + return GeneratedFunctions.temporal_le(p1, p2); + }; + + // temporalGt(t1, t2) β†’ true if t1 > t2 + public static final UDF2 temporalGt = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1), p2 = tempPtr(s2); + if (p1 == null || p2 == null) return null; + return GeneratedFunctions.temporal_gt(p1, p2); + }; + + // temporalGe(t1, t2) β†’ true if t1 β‰₯ t2 + public static final UDF2 temporalGe = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1), p2 = tempPtr(s2); + if (p1 == null || p2 == null) return null; + return GeneratedFunctions.temporal_ge(p1, p2); + }; + + // ------------------------------------------------------------------ + // ever_eq predicates: did the temporal value ever equal the scalar? + // + // MEOS: ever_eq_tint_int / ever_eq_tfloat_float + // ever_eq_temporal_temporal + // ------------------------------------------------------------------ + + // everEqTintInt(tint_hex, 5) β†’ true if tint ever equals 5 + // MEOS: ever_eq_tint_int(Temporal *, int) β†’ int + public static final UDF2 everEqTintInt = + (s, v) -> { + MeosThread.ensureReady(); + Pointer ptr = tempPtr(s); + if (ptr == null || v == null) return null; + return intToBool(GeneratedFunctions.ever_eq_tint_int(ptr, v)); + }; + + // everEqTfloatFloat(tfloat_hex, 1.5) β†’ true if tfloat ever equals 1.5 + // MEOS: ever_eq_tfloat_float(Temporal *, double) β†’ int + public static final UDF2 everEqTfloatFloat = + (s, v) -> { + MeosThread.ensureReady(); + Pointer ptr = tempPtr(s); + if (ptr == null || v == null) return null; + return intToBool(GeneratedFunctions.ever_eq_tfloat_float(ptr, v)); + }; + + // everEqTemporal(t1, t2) β†’ true if t1 and t2 ever have the same value + // MEOS: ever_eq_temporal_temporal(Temporal *, Temporal *) β†’ int + public static final UDF2 everEqTemporal = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1), p2 = tempPtr(s2); + if (p1 == null || p2 == null) return null; + return intToBool(GeneratedFunctions.ever_eq_temporal_temporal(p1, p2)); + }; + + // ------------------------------------------------------------------ + // ever_ne predicates: did the temporal value ever differ from the scalar? + // + // MEOS: ever_ne_tint_int / ever_ne_tfloat_float / ever_ne_temporal_temporal + // ------------------------------------------------------------------ + + public static final UDF2 everNeTintInt = + (s, v) -> { + MeosThread.ensureReady(); + Pointer ptr = tempPtr(s); + if (ptr == null || v == null) return null; + return intToBool(GeneratedFunctions.ever_ne_tint_int(ptr, v)); + }; + + public static final UDF2 everNeTfloatFloat = + (s, v) -> { + MeosThread.ensureReady(); + Pointer ptr = tempPtr(s); + if (ptr == null || v == null) return null; + return intToBool(GeneratedFunctions.ever_ne_tfloat_float(ptr, v)); + }; + + public static final UDF2 everNeTemporal = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1), p2 = tempPtr(s2); + if (p1 == null || p2 == null) return null; + return intToBool(GeneratedFunctions.ever_ne_temporal_temporal(p1, p2)); + }; + + // ------------------------------------------------------------------ + // ever_lt predicates + // + // MEOS: ever_lt_tint_int / ever_lt_tfloat_float / ever_lt_temporal_temporal + // ------------------------------------------------------------------ + + // everLtTintInt(tint_hex, 10) β†’ true if tint ever < 10 + public static final UDF2 everLtTintInt = + (s, v) -> { + MeosThread.ensureReady(); + Pointer ptr = tempPtr(s); + if (ptr == null || v == null) return null; + return intToBool(GeneratedFunctions.ever_lt_tint_int(ptr, v)); + }; + + // everLtTfloatFloat(tfloat_hex, 2.0) β†’ true if tfloat ever < 2.0 + public static final UDF2 everLtTfloatFloat = + (s, v) -> { + MeosThread.ensureReady(); + Pointer ptr = tempPtr(s); + if (ptr == null || v == null) return null; + return intToBool(GeneratedFunctions.ever_lt_tfloat_float(ptr, v)); + }; + + // everLtTemporal(t1, t2) β†’ true if t1 ever < t2 at any shared instant + public static final UDF2 everLtTemporal = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1), p2 = tempPtr(s2); + if (p1 == null || p2 == null) return null; + return intToBool(GeneratedFunctions.ever_lt_temporal_temporal(p1, p2)); + }; + + // ------------------------------------------------------------------ + // ever_le predicates + // ------------------------------------------------------------------ + + public static final UDF2 everLeTintInt = + (s, v) -> { + MeosThread.ensureReady(); + Pointer ptr = tempPtr(s); + if (ptr == null || v == null) return null; + return intToBool(GeneratedFunctions.ever_le_tint_int(ptr, v)); + }; + + public static final UDF2 everLeTfloatFloat = + (s, v) -> { + MeosThread.ensureReady(); + Pointer ptr = tempPtr(s); + if (ptr == null || v == null) return null; + return intToBool(GeneratedFunctions.ever_le_tfloat_float(ptr, v)); + }; + + public static final UDF2 everLeTemporal = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1), p2 = tempPtr(s2); + if (p1 == null || p2 == null) return null; + return intToBool(GeneratedFunctions.ever_le_temporal_temporal(p1, p2)); + }; + + // ------------------------------------------------------------------ + // ever_gt / ever_ge predicates + // ------------------------------------------------------------------ + + public static final UDF2 everGtTintInt = + (s, v) -> { + MeosThread.ensureReady(); + Pointer ptr = tempPtr(s); + if (ptr == null || v == null) return null; + return intToBool(GeneratedFunctions.ever_gt_tint_int(ptr, v)); + }; + + public static final UDF2 everGtTfloatFloat = + (s, v) -> { + MeosThread.ensureReady(); + Pointer ptr = tempPtr(s); + if (ptr == null || v == null) return null; + return intToBool(GeneratedFunctions.ever_gt_tfloat_float(ptr, v)); + }; + + public static final UDF2 everGtTemporal = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1), p2 = tempPtr(s2); + if (p1 == null || p2 == null) return null; + return intToBool(GeneratedFunctions.ever_gt_temporal_temporal(p1, p2)); + }; + + public static final UDF2 everGeTintInt = + (s, v) -> { + MeosThread.ensureReady(); + Pointer ptr = tempPtr(s); + if (ptr == null || v == null) return null; + return intToBool(GeneratedFunctions.ever_ge_tint_int(ptr, v)); + }; + + public static final UDF2 everGeTfloatFloat = + (s, v) -> { + MeosThread.ensureReady(); + Pointer ptr = tempPtr(s); + if (ptr == null || v == null) return null; + return intToBool(GeneratedFunctions.ever_ge_tfloat_float(ptr, v)); + }; + + public static final UDF2 everGeTemporal = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1), p2 = tempPtr(s2); + if (p1 == null || p2 == null) return null; + return intToBool(GeneratedFunctions.ever_ge_temporal_temporal(p1, p2)); + }; + + // ------------------------------------------------------------------ + // always_eq predicates + // + // MEOS: always_eq_tint_int / always_eq_tfloat_float + // always_eq_temporal_temporal + // ------------------------------------------------------------------ + + public static final UDF2 alwaysEqTintInt = + (s, v) -> { + MeosThread.ensureReady(); + Pointer ptr = tempPtr(s); + if (ptr == null || v == null) return null; + return intToBool(GeneratedFunctions.always_eq_tint_int(ptr, v)); + }; + + public static final UDF2 alwaysEqTfloatFloat = + (s, v) -> { + MeosThread.ensureReady(); + Pointer ptr = tempPtr(s); + if (ptr == null || v == null) return null; + return intToBool(GeneratedFunctions.always_eq_tfloat_float(ptr, v)); + }; + + public static final UDF2 alwaysEqTemporal = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1), p2 = tempPtr(s2); + if (p1 == null || p2 == null) return null; + return intToBool(GeneratedFunctions.always_eq_temporal_temporal(p1, p2)); + }; + + // ------------------------------------------------------------------ + // always_ne predicates + // + // MEOS: always_ne_tint_int / always_ne_tfloat_float / always_ne_temporal_temporal + // ------------------------------------------------------------------ + + public static final UDF2 alwaysNeTintInt = + (s, v) -> { + MeosThread.ensureReady(); + Pointer ptr = tempPtr(s); + if (ptr == null || v == null) return null; + return intToBool(GeneratedFunctions.always_ne_tint_int(ptr, v)); + }; + + public static final UDF2 alwaysNeTfloatFloat = + (s, v) -> { + MeosThread.ensureReady(); + Pointer ptr = tempPtr(s); + if (ptr == null || v == null) return null; + return intToBool(GeneratedFunctions.always_ne_tfloat_float(ptr, v)); + }; + + public static final UDF2 alwaysNeTemporal = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1), p2 = tempPtr(s2); + if (p1 == null || p2 == null) return null; + return intToBool(GeneratedFunctions.always_ne_temporal_temporal(p1, p2)); + }; + + // ------------------------------------------------------------------ + // always_lt predicates + // ------------------------------------------------------------------ + + public static final UDF2 alwaysLtTintInt = + (s, v) -> { + MeosThread.ensureReady(); + Pointer ptr = tempPtr(s); + if (ptr == null || v == null) return null; + return intToBool(GeneratedFunctions.always_lt_tint_int(ptr, v)); + }; + + public static final UDF2 alwaysLtTfloatFloat = + (s, v) -> { + MeosThread.ensureReady(); + Pointer ptr = tempPtr(s); + if (ptr == null || v == null) return null; + return intToBool(GeneratedFunctions.always_lt_tfloat_float(ptr, v)); + }; + + public static final UDF2 alwaysLtTemporal = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1), p2 = tempPtr(s2); + if (p1 == null || p2 == null) return null; + return intToBool(GeneratedFunctions.always_lt_temporal_temporal(p1, p2)); + }; + + // ------------------------------------------------------------------ + // always_le predicates + // ------------------------------------------------------------------ + + public static final UDF2 alwaysLeTintInt = + (s, v) -> { + MeosThread.ensureReady(); + Pointer ptr = tempPtr(s); + if (ptr == null || v == null) return null; + return intToBool(GeneratedFunctions.always_le_tint_int(ptr, v)); + }; + + public static final UDF2 alwaysLeTfloatFloat = + (s, v) -> { + MeosThread.ensureReady(); + Pointer ptr = tempPtr(s); + if (ptr == null || v == null) return null; + return intToBool(GeneratedFunctions.always_le_tfloat_float(ptr, v)); + }; + + public static final UDF2 alwaysLeTemporal = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1), p2 = tempPtr(s2); + if (p1 == null || p2 == null) return null; + return intToBool(GeneratedFunctions.always_le_temporal_temporal(p1, p2)); + }; + + // ------------------------------------------------------------------ + // always_gt / always_ge predicates + // ------------------------------------------------------------------ + + public static final UDF2 alwaysGtTintInt = + (s, v) -> { + MeosThread.ensureReady(); + Pointer ptr = tempPtr(s); + if (ptr == null || v == null) return null; + return intToBool(GeneratedFunctions.always_gt_tint_int(ptr, v)); + }; + + public static final UDF2 alwaysGtTfloatFloat = + (s, v) -> { + MeosThread.ensureReady(); + Pointer ptr = tempPtr(s); + if (ptr == null || v == null) return null; + return intToBool(GeneratedFunctions.always_gt_tfloat_float(ptr, v)); + }; + + public static final UDF2 alwaysGtTemporal = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1), p2 = tempPtr(s2); + if (p1 == null || p2 == null) return null; + return intToBool(GeneratedFunctions.always_gt_temporal_temporal(p1, p2)); + }; + + public static final UDF2 alwaysGeTintInt = + (s, v) -> { + MeosThread.ensureReady(); + Pointer ptr = tempPtr(s); + if (ptr == null || v == null) return null; + return intToBool(GeneratedFunctions.always_ge_tint_int(ptr, v)); + }; + + public static final UDF2 alwaysGeTfloatFloat = + (s, v) -> { + MeosThread.ensureReady(); + Pointer ptr = tempPtr(s); + if (ptr == null || v == null) return null; + return intToBool(GeneratedFunctions.always_ge_tfloat_float(ptr, v)); + }; + + public static final UDF2 alwaysGeTemporal = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = tempPtr(s1), p2 = tempPtr(s2); + if (p1 == null || p2 == null) return null; + return intToBool(GeneratedFunctions.always_ge_temporal_temporal(p1, p2)); + }; + + // ------------------------------------------------------------------ + // Scalar-first reversed forms: (int OP tint), (float OP tfloat) + // MEOS: always_eq_int_tint(int, Temporal *) β†’ int, etc. + // ------------------------------------------------------------------ + + public static final UDF2 alwaysEqIntTint = + (v, s) -> { MeosThread.ensureReady(); Pointer p = tempPtr(s); if (p == null || v == null) return null; return intToBool(GeneratedFunctions.always_eq_int_tint(v, p)); }; + public static final UDF2 alwaysNeIntTint = + (v, s) -> { MeosThread.ensureReady(); Pointer p = tempPtr(s); if (p == null || v == null) return null; return intToBool(GeneratedFunctions.always_ne_int_tint(v, p)); }; + public static final UDF2 alwaysLtIntTint = + (v, s) -> { MeosThread.ensureReady(); Pointer p = tempPtr(s); if (p == null || v == null) return null; return intToBool(GeneratedFunctions.always_lt_int_tint(v, p)); }; + public static final UDF2 alwaysLeIntTint = + (v, s) -> { MeosThread.ensureReady(); Pointer p = tempPtr(s); if (p == null || v == null) return null; return intToBool(GeneratedFunctions.always_le_int_tint(v, p)); }; + public static final UDF2 alwaysGtIntTint = + (v, s) -> { MeosThread.ensureReady(); Pointer p = tempPtr(s); if (p == null || v == null) return null; return intToBool(GeneratedFunctions.always_gt_int_tint(v, p)); }; + public static final UDF2 alwaysGeIntTint = + (v, s) -> { MeosThread.ensureReady(); Pointer p = tempPtr(s); if (p == null || v == null) return null; return intToBool(GeneratedFunctions.always_ge_int_tint(v, p)); }; + + public static final UDF2 alwaysEqFloatTfloat = + (v, s) -> { MeosThread.ensureReady(); Pointer p = tempPtr(s); if (p == null || v == null) return null; return intToBool(GeneratedFunctions.always_eq_float_tfloat(v, p)); }; + public static final UDF2 alwaysNeFloatTfloat = + (v, s) -> { MeosThread.ensureReady(); Pointer p = tempPtr(s); if (p == null || v == null) return null; return intToBool(GeneratedFunctions.always_ne_float_tfloat(v, p)); }; + public static final UDF2 alwaysLtFloatTfloat = + (v, s) -> { MeosThread.ensureReady(); Pointer p = tempPtr(s); if (p == null || v == null) return null; return intToBool(GeneratedFunctions.always_lt_float_tfloat(v, p)); }; + public static final UDF2 alwaysLeFloatTfloat = + (v, s) -> { MeosThread.ensureReady(); Pointer p = tempPtr(s); if (p == null || v == null) return null; return intToBool(GeneratedFunctions.always_le_float_tfloat(v, p)); }; + public static final UDF2 alwaysGtFloatTfloat = + (v, s) -> { MeosThread.ensureReady(); Pointer p = tempPtr(s); if (p == null || v == null) return null; return intToBool(GeneratedFunctions.always_gt_float_tfloat(v, p)); }; + public static final UDF2 alwaysGeFloatTfloat = + (v, s) -> { MeosThread.ensureReady(); Pointer p = tempPtr(s); if (p == null || v == null) return null; return intToBool(GeneratedFunctions.always_ge_float_tfloat(v, p)); }; + + public static final UDF2 everEqIntTint = + (v, s) -> { MeosThread.ensureReady(); Pointer p = tempPtr(s); if (p == null || v == null) return null; return intToBool(GeneratedFunctions.ever_eq_int_tint(v, p)); }; + public static final UDF2 everNeIntTint = + (v, s) -> { MeosThread.ensureReady(); Pointer p = tempPtr(s); if (p == null || v == null) return null; return intToBool(GeneratedFunctions.ever_ne_int_tint(v, p)); }; + public static final UDF2 everLtIntTint = + (v, s) -> { MeosThread.ensureReady(); Pointer p = tempPtr(s); if (p == null || v == null) return null; return intToBool(GeneratedFunctions.ever_lt_int_tint(v, p)); }; + public static final UDF2 everLeIntTint = + (v, s) -> { MeosThread.ensureReady(); Pointer p = tempPtr(s); if (p == null || v == null) return null; return intToBool(GeneratedFunctions.ever_le_int_tint(v, p)); }; + public static final UDF2 everGtIntTint = + (v, s) -> { MeosThread.ensureReady(); Pointer p = tempPtr(s); if (p == null || v == null) return null; return intToBool(GeneratedFunctions.ever_gt_int_tint(v, p)); }; + public static final UDF2 everGeIntTint = + (v, s) -> { MeosThread.ensureReady(); Pointer p = tempPtr(s); if (p == null || v == null) return null; return intToBool(GeneratedFunctions.ever_ge_int_tint(v, p)); }; + + public static final UDF2 everEqFloatTfloat = + (v, s) -> { MeosThread.ensureReady(); Pointer p = tempPtr(s); if (p == null || v == null) return null; return intToBool(GeneratedFunctions.ever_eq_float_tfloat(v, p)); }; + public static final UDF2 everNeFloatTfloat = + (v, s) -> { MeosThread.ensureReady(); Pointer p = tempPtr(s); if (p == null || v == null) return null; return intToBool(GeneratedFunctions.ever_ne_float_tfloat(v, p)); }; + public static final UDF2 everLtFloatTfloat = + (v, s) -> { MeosThread.ensureReady(); Pointer p = tempPtr(s); if (p == null || v == null) return null; return intToBool(GeneratedFunctions.ever_lt_float_tfloat(v, p)); }; + public static final UDF2 everLeFloatTfloat = + (v, s) -> { MeosThread.ensureReady(); Pointer p = tempPtr(s); if (p == null || v == null) return null; return intToBool(GeneratedFunctions.ever_le_float_tfloat(v, p)); }; + public static final UDF2 everGtFloatTfloat = + (v, s) -> { MeosThread.ensureReady(); Pointer p = tempPtr(s); if (p == null || v == null) return null; return intToBool(GeneratedFunctions.ever_gt_float_tfloat(v, p)); }; + public static final UDF2 everGeFloatTfloat = + (v, s) -> { MeosThread.ensureReady(); Pointer p = tempPtr(s); if (p == null || v == null) return null; return intToBool(GeneratedFunctions.ever_ge_float_tfloat(v, p)); }; + + // ------------------------------------------------------------------ + // tbool Γ— bool predicates (only eq and ne meaningful for booleans) + // MEOS: ever_eq_tbool_bool(Temporal *, bool) β†’ int, etc. + // ------------------------------------------------------------------ + + public static final UDF2 alwaysEqTboolBool = + (s, v) -> { MeosThread.ensureReady(); Pointer p = tempPtr(s); if (p == null || v == null) return null; return intToBool(GeneratedFunctions.always_eq_tbool_bool(p, v)); }; + public static final UDF2 alwaysNeTboolBool = + (s, v) -> { MeosThread.ensureReady(); Pointer p = tempPtr(s); if (p == null || v == null) return null; return intToBool(GeneratedFunctions.always_ne_tbool_bool(p, v)); }; + public static final UDF2 alwaysEqBoolTbool = + (v, s) -> { MeosThread.ensureReady(); Pointer p = tempPtr(s); if (p == null || v == null) return null; return intToBool(GeneratedFunctions.always_eq_bool_tbool(v, p)); }; + public static final UDF2 alwaysNeBoolTbool = + (v, s) -> { MeosThread.ensureReady(); Pointer p = tempPtr(s); if (p == null || v == null) return null; return intToBool(GeneratedFunctions.always_ne_bool_tbool(v, p)); }; + + public static final UDF2 everEqTboolBool = + (s, v) -> { MeosThread.ensureReady(); Pointer p = tempPtr(s); if (p == null || v == null) return null; return intToBool(GeneratedFunctions.ever_eq_tbool_bool(p, v)); }; + public static final UDF2 everNeTboolBool = + (s, v) -> { MeosThread.ensureReady(); Pointer p = tempPtr(s); if (p == null || v == null) return null; return intToBool(GeneratedFunctions.ever_ne_tbool_bool(p, v)); }; + public static final UDF2 everEqBoolTbool = + (v, s) -> { MeosThread.ensureReady(); Pointer p = tempPtr(s); if (p == null || v == null) return null; return intToBool(GeneratedFunctions.ever_eq_bool_tbool(v, p)); }; + public static final UDF2 everNeBoolTbool = + (v, s) -> { MeosThread.ensureReady(); Pointer p = tempPtr(s); if (p == null || v == null) return null; return intToBool(GeneratedFunctions.ever_ne_bool_tbool(v, p)); }; + + // ------------------------------------------------------------------ + // ttext Γ— text predicates + // text* is obtained via ttext_in + ttext_value_n (text_in not exposed). + // MEOS: always_eq_text_ttext(text *, Temporal *) β†’ int, etc. + // ------------------------------------------------------------------ + + private static Pointer[] makeTextPtr(String val) { + Pointer dummy = GeneratedFunctions.ttext_in(val + "@2000-01-01 00:00:00+00"); + if (dummy == null) return null; + Pointer textPtr = GeneratedFunctions.ttext_value_n(dummy, 1); + if (textPtr == null) { MeosMemory.free(dummy); return null; } + return new Pointer[]{textPtr, dummy}; + } + + private static Boolean textTtextPred(String textVal, String ttextHex, + BiFunction fn) { + if (textVal == null || ttextHex == null) return null; + MeosThread.ensureReady(); + Pointer[] tp = makeTextPtr(textVal); + if (tp == null) return null; + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(ttextHex); + if (tptr == null) { MeosMemory.free(tp[0]); MeosMemory.free(tp[1]); return null; } + try { + return intToBool(fn.apply(tp[0], tptr)); + } finally { + MeosMemory.free(tptr); + MeosMemory.free(tp[0]); + MeosMemory.free(tp[1]); + } + } + + private static Boolean ttextTextPred(String ttextHex, String textVal, + BiFunction fn) { + if (ttextHex == null || textVal == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(ttextHex); + if (tptr == null) return null; + Pointer[] tp = makeTextPtr(textVal); + if (tp == null) { MeosMemory.free(tptr); return null; } + try { + return intToBool(fn.apply(tptr, tp[0])); + } finally { + MeosMemory.free(tptr); + MeosMemory.free(tp[0]); + MeosMemory.free(tp[1]); + } + } + + // always: text OP ttext + public static final UDF2 alwaysEqTextTtext = + (t, s) -> textTtextPred(t, s, GeneratedFunctions::always_eq_text_ttext); + public static final UDF2 alwaysNeTextTtext = + (t, s) -> textTtextPred(t, s, GeneratedFunctions::always_ne_text_ttext); + public static final UDF2 alwaysLtTextTtext = + (t, s) -> textTtextPred(t, s, GeneratedFunctions::always_lt_text_ttext); + public static final UDF2 alwaysLeTextTtext = + (t, s) -> textTtextPred(t, s, GeneratedFunctions::always_le_text_ttext); + public static final UDF2 alwaysGtTextTtext = + (t, s) -> textTtextPred(t, s, GeneratedFunctions::always_gt_text_ttext); + public static final UDF2 alwaysGeTextTtext = + (t, s) -> textTtextPred(t, s, GeneratedFunctions::always_ge_text_ttext); + + // always: ttext OP text + public static final UDF2 alwaysEqTtextText = + (s, t) -> ttextTextPred(s, t, GeneratedFunctions::always_eq_ttext_text); + public static final UDF2 alwaysNeTtextText = + (s, t) -> ttextTextPred(s, t, GeneratedFunctions::always_ne_ttext_text); + public static final UDF2 alwaysLtTtextText = + (s, t) -> ttextTextPred(s, t, GeneratedFunctions::always_lt_ttext_text); + public static final UDF2 alwaysLeTtextText = + (s, t) -> ttextTextPred(s, t, GeneratedFunctions::always_le_ttext_text); + public static final UDF2 alwaysGtTtextText = + (s, t) -> ttextTextPred(s, t, GeneratedFunctions::always_gt_ttext_text); + public static final UDF2 alwaysGeTtextText = + (s, t) -> ttextTextPred(s, t, GeneratedFunctions::always_ge_ttext_text); + + // ever: text OP ttext + public static final UDF2 everEqTextTtext = + (t, s) -> textTtextPred(t, s, GeneratedFunctions::ever_eq_text_ttext); + public static final UDF2 everNeTextTtext = + (t, s) -> textTtextPred(t, s, GeneratedFunctions::ever_ne_text_ttext); + public static final UDF2 everLtTextTtext = + (t, s) -> textTtextPred(t, s, GeneratedFunctions::ever_lt_text_ttext); + public static final UDF2 everLeTextTtext = + (t, s) -> textTtextPred(t, s, GeneratedFunctions::ever_le_text_ttext); + public static final UDF2 everGtTextTtext = + (t, s) -> textTtextPred(t, s, GeneratedFunctions::ever_gt_text_ttext); + public static final UDF2 everGeTextTtext = + (t, s) -> textTtextPred(t, s, GeneratedFunctions::ever_ge_text_ttext); + + // ever: ttext OP text + public static final UDF2 everEqTtextText = + (s, t) -> ttextTextPred(s, t, GeneratedFunctions::ever_eq_ttext_text); + public static final UDF2 everNeTtextText = + (s, t) -> ttextTextPred(s, t, GeneratedFunctions::ever_ne_ttext_text); + public static final UDF2 everLtTtextText = + (s, t) -> ttextTextPred(s, t, GeneratedFunctions::ever_lt_ttext_text); + public static final UDF2 everLeTtextText = + (s, t) -> ttextTextPred(s, t, GeneratedFunctions::ever_le_ttext_text); + public static final UDF2 everGtTtextText = + (s, t) -> ttextTextPred(s, t, GeneratedFunctions::ever_gt_ttext_text); + public static final UDF2 everGeTtextText = + (s, t) -> ttextTextPred(s, t, GeneratedFunctions::ever_ge_ttext_text); + + // tpointIsSimple(tpoint_hex) β†’ Boolean + // Returns true if the trajectory has no self-intersections. + // MEOS: tpoint_is_simple(const Temporal *) β†’ bool + public static final UDF1 tpointIsSimple = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + return GeneratedFunctions.tpoint_is_simple(ptr); + } finally { + MeosMemory.free(ptr); + } + }; + + public static void registerAll(SparkSession spark) { + // Temporal order comparisons + spark.udf().register("temporalEq", temporalEq, DataTypes.BooleanType); + spark.udf().register("temporalNe", temporalNe, DataTypes.BooleanType); + spark.udf().register("temporalLt", temporalLt, DataTypes.BooleanType); + spark.udf().register("temporalLe", temporalLe, DataTypes.BooleanType); + spark.udf().register("temporalGt", temporalGt, DataTypes.BooleanType); + spark.udf().register("temporalGe", temporalGe, DataTypes.BooleanType); + // ever_eq + spark.udf().register("everEqTintInt", everEqTintInt, DataTypes.BooleanType); + spark.udf().register("everEqTfloatFloat", everEqTfloatFloat, DataTypes.BooleanType); + spark.udf().register("everEqTemporal", everEqTemporal, DataTypes.BooleanType); + // ever_lt + spark.udf().register("everLtTintInt", everLtTintInt, DataTypes.BooleanType); + spark.udf().register("everLtTfloatFloat", everLtTfloatFloat, DataTypes.BooleanType); + spark.udf().register("everLtTemporal", everLtTemporal, DataTypes.BooleanType); + // ever_le + spark.udf().register("everLeTintInt", everLeTintInt, DataTypes.BooleanType); + spark.udf().register("everLeTfloatFloat", everLeTfloatFloat, DataTypes.BooleanType); + spark.udf().register("everLeTemporal", everLeTemporal, DataTypes.BooleanType); + // ever_gt + spark.udf().register("everGtTintInt", everGtTintInt, DataTypes.BooleanType); + spark.udf().register("everGtTfloatFloat", everGtTfloatFloat, DataTypes.BooleanType); + spark.udf().register("everGtTemporal", everGtTemporal, DataTypes.BooleanType); + // ever_ge + spark.udf().register("everGeTintInt", everGeTintInt, DataTypes.BooleanType); + spark.udf().register("everGeTfloatFloat", everGeTfloatFloat, DataTypes.BooleanType); + spark.udf().register("everGeTemporal", everGeTemporal, DataTypes.BooleanType); + // ever_ne + spark.udf().register("everNeTintInt", everNeTintInt, DataTypes.BooleanType); + spark.udf().register("everNeTfloatFloat", everNeTfloatFloat, DataTypes.BooleanType); + spark.udf().register("everNeTemporal", everNeTemporal, DataTypes.BooleanType); + // always_eq + spark.udf().register("alwaysEqTintInt", alwaysEqTintInt, DataTypes.BooleanType); + spark.udf().register("alwaysEqTfloatFloat", alwaysEqTfloatFloat, DataTypes.BooleanType); + spark.udf().register("alwaysEqTemporal", alwaysEqTemporal, DataTypes.BooleanType); + // always_lt + spark.udf().register("alwaysLtTintInt", alwaysLtTintInt, DataTypes.BooleanType); + spark.udf().register("alwaysLtTfloatFloat", alwaysLtTfloatFloat, DataTypes.BooleanType); + spark.udf().register("alwaysLtTemporal", alwaysLtTemporal, DataTypes.BooleanType); + // always_le + spark.udf().register("alwaysLeTintInt", alwaysLeTintInt, DataTypes.BooleanType); + spark.udf().register("alwaysLeTfloatFloat", alwaysLeTfloatFloat, DataTypes.BooleanType); + spark.udf().register("alwaysLeTemporal", alwaysLeTemporal, DataTypes.BooleanType); + // always_gt + spark.udf().register("alwaysGtTintInt", alwaysGtTintInt, DataTypes.BooleanType); + spark.udf().register("alwaysGtTfloatFloat", alwaysGtTfloatFloat, DataTypes.BooleanType); + spark.udf().register("alwaysGtTemporal", alwaysGtTemporal, DataTypes.BooleanType); + // always_ge + spark.udf().register("alwaysGeTintInt", alwaysGeTintInt, DataTypes.BooleanType); + spark.udf().register("alwaysGeTfloatFloat", alwaysGeTfloatFloat, DataTypes.BooleanType); + spark.udf().register("alwaysGeTemporal", alwaysGeTemporal, DataTypes.BooleanType); + // always_ne + spark.udf().register("alwaysNeTintInt", alwaysNeTintInt, DataTypes.BooleanType); + spark.udf().register("alwaysNeTfloatFloat", alwaysNeTfloatFloat, DataTypes.BooleanType); + spark.udf().register("alwaysNeTemporal", alwaysNeTemporal, DataTypes.BooleanType); + // scalar-first reversed forms + spark.udf().register("alwaysEqIntTint", alwaysEqIntTint, DataTypes.BooleanType); + spark.udf().register("alwaysNeIntTint", alwaysNeIntTint, DataTypes.BooleanType); + spark.udf().register("alwaysLtIntTint", alwaysLtIntTint, DataTypes.BooleanType); + spark.udf().register("alwaysLeIntTint", alwaysLeIntTint, DataTypes.BooleanType); + spark.udf().register("alwaysGtIntTint", alwaysGtIntTint, DataTypes.BooleanType); + spark.udf().register("alwaysGeIntTint", alwaysGeIntTint, DataTypes.BooleanType); + spark.udf().register("alwaysEqFloatTfloat", alwaysEqFloatTfloat, DataTypes.BooleanType); + spark.udf().register("alwaysNeFloatTfloat", alwaysNeFloatTfloat, DataTypes.BooleanType); + spark.udf().register("alwaysLtFloatTfloat", alwaysLtFloatTfloat, DataTypes.BooleanType); + spark.udf().register("alwaysLeFloatTfloat", alwaysLeFloatTfloat, DataTypes.BooleanType); + spark.udf().register("alwaysGtFloatTfloat", alwaysGtFloatTfloat, DataTypes.BooleanType); + spark.udf().register("alwaysGeFloatTfloat", alwaysGeFloatTfloat, DataTypes.BooleanType); + spark.udf().register("everEqIntTint", everEqIntTint, DataTypes.BooleanType); + spark.udf().register("everNeIntTint", everNeIntTint, DataTypes.BooleanType); + spark.udf().register("everLtIntTint", everLtIntTint, DataTypes.BooleanType); + spark.udf().register("everLeIntTint", everLeIntTint, DataTypes.BooleanType); + spark.udf().register("everGtIntTint", everGtIntTint, DataTypes.BooleanType); + spark.udf().register("everGeIntTint", everGeIntTint, DataTypes.BooleanType); + spark.udf().register("everEqFloatTfloat", everEqFloatTfloat, DataTypes.BooleanType); + spark.udf().register("everNeFloatTfloat", everNeFloatTfloat, DataTypes.BooleanType); + spark.udf().register("everLtFloatTfloat", everLtFloatTfloat, DataTypes.BooleanType); + spark.udf().register("everLeFloatTfloat", everLeFloatTfloat, DataTypes.BooleanType); + spark.udf().register("everGtFloatTfloat", everGtFloatTfloat, DataTypes.BooleanType); + spark.udf().register("everGeFloatTfloat", everGeFloatTfloat, DataTypes.BooleanType); + // tbool Γ— bool predicates + spark.udf().register("alwaysEqTboolBool", alwaysEqTboolBool, DataTypes.BooleanType); + spark.udf().register("alwaysNeTboolBool", alwaysNeTboolBool, DataTypes.BooleanType); + spark.udf().register("alwaysEqBoolTbool", alwaysEqBoolTbool, DataTypes.BooleanType); + spark.udf().register("alwaysNeBoolTbool", alwaysNeBoolTbool, DataTypes.BooleanType); + spark.udf().register("everEqTboolBool", everEqTboolBool, DataTypes.BooleanType); + spark.udf().register("everNeTboolBool", everNeTboolBool, DataTypes.BooleanType); + spark.udf().register("everEqBoolTbool", everEqBoolTbool, DataTypes.BooleanType); + spark.udf().register("everNeBoolTbool", everNeBoolTbool, DataTypes.BooleanType); + // text Γ— ttext predicates + spark.udf().register("alwaysEqTextTtext", alwaysEqTextTtext, DataTypes.BooleanType); + spark.udf().register("alwaysNeTextTtext", alwaysNeTextTtext, DataTypes.BooleanType); + spark.udf().register("alwaysLtTextTtext", alwaysLtTextTtext, DataTypes.BooleanType); + spark.udf().register("alwaysLeTextTtext", alwaysLeTextTtext, DataTypes.BooleanType); + spark.udf().register("alwaysGtTextTtext", alwaysGtTextTtext, DataTypes.BooleanType); + spark.udf().register("alwaysGeTextTtext", alwaysGeTextTtext, DataTypes.BooleanType); + spark.udf().register("alwaysEqTtextText", alwaysEqTtextText, DataTypes.BooleanType); + spark.udf().register("alwaysNeTtextText", alwaysNeTtextText, DataTypes.BooleanType); + spark.udf().register("alwaysLtTtextText", alwaysLtTtextText, DataTypes.BooleanType); + spark.udf().register("alwaysLeTtextText", alwaysLeTtextText, DataTypes.BooleanType); + spark.udf().register("alwaysGtTtextText", alwaysGtTtextText, DataTypes.BooleanType); + spark.udf().register("alwaysGeTtextText", alwaysGeTtextText, DataTypes.BooleanType); + spark.udf().register("everEqTextTtext", everEqTextTtext, DataTypes.BooleanType); + spark.udf().register("everNeTextTtext", everNeTextTtext, DataTypes.BooleanType); + spark.udf().register("everLtTextTtext", everLtTextTtext, DataTypes.BooleanType); + spark.udf().register("everLeTextTtext", everLeTextTtext, DataTypes.BooleanType); + spark.udf().register("everGtTextTtext", everGtTextTtext, DataTypes.BooleanType); + spark.udf().register("everGeTextTtext", everGeTextTtext, DataTypes.BooleanType); + spark.udf().register("everEqTtextText", everEqTtextText, DataTypes.BooleanType); + spark.udf().register("everNeTtextText", everNeTtextText, DataTypes.BooleanType); + spark.udf().register("everLtTtextText", everLtTtextText, DataTypes.BooleanType); + spark.udf().register("everLeTtextText", everLeTtextText, DataTypes.BooleanType); + spark.udf().register("everGtTtextText", everGtTtextText, DataTypes.BooleanType); + spark.udf().register("everGeTtextText", everGeTtextText, DataTypes.BooleanType); + // tpoint geometry predicate + spark.udf().register("tpointIsSimple", tpointIsSimple, DataTypes.BooleanType); + } +} diff --git a/src/main/java/org/mobilitydb/spark/temporal/RestrictionUDFs.java b/src/main/java/org/mobilitydb/spark/temporal/RestrictionUDFs.java new file mode 100644 index 00000000..00395600 --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/temporal/RestrictionUDFs.java @@ -0,0 +1,1079 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosThread; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.api.java.UDF1; +import org.apache.spark.sql.api.java.UDF2; +import org.apache.spark.sql.types.DataTypes; + +import java.sql.Timestamp; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import org.mobilitydb.spark.util.TimeUtil; + +/** + * Spark SQL UDFs for restricting temporal values by value or timestamp set. + * + * All temporal values are encoded as hex-WKB Strings. Geometry inputs are + * WKT Strings. Timestamp-set/span/spanset inputs are hex-WKB Strings. + * + * Memory management: every native Pointer allocated by MEOS is freed via + * MeosMemory.free() in a finally block to prevent native heap leakage. + * + * MEOS function authority: meos/include/meos.h, meos/include/meos_geo.h + */ +public final class RestrictionUDFs { + + private RestrictionUDFs() {} + + // ------------------------------------------------------------------ + // Timestamp-set restriction + // ------------------------------------------------------------------ + + // temporalAtTstzspan(s STRING, spanHex STRING) β†’ STRING + // MEOS: temporal_at_tstzspan(const Temporal *, const Span *) β†’ Temporal * + public static final UDF2 temporalAtTstzspan = + (s, spanHex) -> { + if (s == null || spanHex == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (tptr == null) return null; + try { + Pointer spanPtr = GeneratedFunctions.span_from_hexwkb(spanHex); + if (spanPtr == null) return null; + try { + Pointer result = GeneratedFunctions.temporal_at_tstzspan(tptr, spanPtr); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(spanPtr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // temporalAtTstzspanset(s STRING, spansetHex STRING) β†’ STRING + // MEOS: temporal_at_tstzspanset(const Temporal *, const SpanSet *) β†’ Temporal * + public static final UDF2 temporalAtTstzspanset = + (s, spansetHex) -> { + if (s == null || spansetHex == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (tptr == null) return null; + try { + Pointer ssPtr = GeneratedFunctions.spanset_from_hexwkb(spansetHex); + if (ssPtr == null) return null; + try { + Pointer result = GeneratedFunctions.temporal_at_tstzspanset(tptr, ssPtr); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(ssPtr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // temporalAtTstzset(s STRING, tstzsetHex STRING) β†’ STRING + // MEOS: temporal_at_tstzset(const Temporal *, const Set *) β†’ Temporal * + public static final UDF2 temporalAtTstzset = + (s, tstzsetHex) -> { + if (s == null || tstzsetHex == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (tptr == null) return null; + try { + Pointer setptr = GeneratedFunctions.set_from_hexwkb(tstzsetHex); + if (setptr == null) return null; + try { + Pointer result = GeneratedFunctions.temporal_at_tstzset(tptr, setptr); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(setptr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // temporalMinusTstzset(s STRING, tstzsetHex STRING) β†’ STRING + // MEOS: temporal_minus_tstzset(const Temporal *, const Set *) β†’ Temporal * + public static final UDF2 temporalMinusTstzset = + (s, tstzsetHex) -> { + if (s == null || tstzsetHex == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (tptr == null) return null; + try { + Pointer setptr = GeneratedFunctions.set_from_hexwkb(tstzsetHex); + if (setptr == null) return null; + try { + Pointer result = GeneratedFunctions.temporal_minus_tstzset(tptr, setptr); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(setptr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // temporalMinusTstzspan(s STRING, spanHex STRING) β†’ STRING + // MEOS: temporal_minus_tstzspan(const Temporal *, const Span *) β†’ Temporal * + public static final UDF2 temporalMinusTstzspan = + (s, spanHex) -> { + if (s == null || spanHex == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (tptr == null) return null; + try { + Pointer spanptr = GeneratedFunctions.span_from_hexwkb(spanHex); + if (spanptr == null) return null; + try { + Pointer result = GeneratedFunctions.temporal_minus_tstzspan(tptr, spanptr); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(spanptr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // temporalMinusTstzspanset(s STRING, ssHex STRING) β†’ STRING + // MEOS: temporal_minus_tstzspanset(const Temporal *, const SpanSet *) β†’ Temporal * + public static final UDF2 temporalMinusTstzspanset = + (s, ssHex) -> { + if (s == null || ssHex == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (tptr == null) return null; + try { + Pointer ssptr = GeneratedFunctions.spanset_from_hexwkb(ssHex); + if (ssptr == null) return null; + try { + Pointer result = GeneratedFunctions.temporal_minus_tstzspanset(tptr, ssptr); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(ssptr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // ------------------------------------------------------------------ + // Delete operations (gap-free removal) + // ------------------------------------------------------------------ + + // temporalDeleteTstzspan(s STRING, spanHex STRING) β†’ STRING + // MEOS: temporal_delete_tstzspan(const Temporal *, const Span *, bool connect) β†’ Temporal * + public static final UDF2 temporalDeleteTstzspan = + (s, spanHex) -> { + if (s == null || spanHex == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (tptr == null) return null; + try { + Pointer spanptr = GeneratedFunctions.span_from_hexwkb(spanHex); + if (spanptr == null) return null; + try { + Pointer result = GeneratedFunctions.temporal_delete_tstzspan(tptr, spanptr, false); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(spanptr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // temporalDeleteTstzspanset(s STRING, ssHex STRING) β†’ STRING + // MEOS: temporal_delete_tstzspanset(const Temporal *, const SpanSet *, bool connect) β†’ Temporal * + public static final UDF2 temporalDeleteTstzspanset = + (s, ssHex) -> { + if (s == null || ssHex == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (tptr == null) return null; + try { + Pointer ssptr = GeneratedFunctions.spanset_from_hexwkb(ssHex); + if (ssptr == null) return null; + try { + Pointer result = GeneratedFunctions.temporal_delete_tstzspanset(tptr, ssptr, false); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(ssptr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // temporalDeleteTstzset(s STRING, setHex STRING) β†’ STRING + // MEOS: temporal_delete_tstzset(const Temporal *, const Set *, bool connect) β†’ Temporal * + public static final UDF2 temporalDeleteTstzset = + (s, setHex) -> { + if (s == null || setHex == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (tptr == null) return null; + try { + Pointer sptr = GeneratedFunctions.set_from_hexwkb(setHex); + if (sptr == null) return null; + try { + Pointer result = GeneratedFunctions.temporal_delete_tstzset(tptr, sptr, false); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(sptr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // temporalDeleteTimestamptz(s STRING, ts TIMESTAMP) β†’ STRING + // MEOS: temporal_delete_timestamptz(const Temporal *, TimestampTz, bool connect) β†’ Temporal * + public static final UDF2 temporalDeleteTimestamptz = + (s, ts) -> { + if (s == null || ts == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (tptr == null) return null; + try { + long pgEpochMicros = (ts.getTime() - TimeUtil.PG_UNIX_EPOCH_OFFSET_MS) * 1000L; + OffsetDateTime odt = OffsetDateTime.ofInstant( + Instant.ofEpochSecond(pgEpochMicros, 0), ZoneOffset.UTC); + Pointer result = GeneratedFunctions.temporal_delete_timestamptz(tptr, odt, false); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // ------------------------------------------------------------------ + // Timestamp restriction (AT / MINUS for a single timestamptz) + // ------------------------------------------------------------------ + + // temporalAtTimestamptz(s STRING, ts TIMESTAMP) β†’ STRING + // MEOS: temporal_at_timestamptz(const Temporal *, TimestampTz) β†’ Temporal * + public static final UDF2 temporalAtTimestamptz = + (s, ts) -> { + if (s == null || ts == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (tptr == null) return null; + try { + long pgEpochMicros = (ts.getTime() - TimeUtil.PG_UNIX_EPOCH_OFFSET_MS) * 1000L; + OffsetDateTime odt = OffsetDateTime.ofInstant( + Instant.ofEpochSecond(pgEpochMicros, 0), ZoneOffset.UTC); + Pointer result = GeneratedFunctions.temporal_at_timestamptz(tptr, odt); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // temporalMinusTimestamptz(s STRING, ts TIMESTAMP) β†’ STRING + // MEOS: temporal_minus_timestamptz(const Temporal *, TimestampTz) β†’ Temporal * + public static final UDF2 temporalMinusTimestamptz = + (s, ts) -> { + if (s == null || ts == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (tptr == null) return null; + try { + long pgEpochMicros = (ts.getTime() - TimeUtil.PG_UNIX_EPOCH_OFFSET_MS) * 1000L; + OffsetDateTime odt = OffsetDateTime.ofInstant( + Instant.ofEpochSecond(pgEpochMicros, 0), ZoneOffset.UTC); + Pointer result = GeneratedFunctions.temporal_minus_timestamptz(tptr, odt); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // ------------------------------------------------------------------ + // Value restriction: tfloat + // ------------------------------------------------------------------ + + // tfloatAtValue(s STRING, value DOUBLE) β†’ STRING + // MEOS: tfloat_at_value(const Temporal *, double) β†’ Temporal * + public static final UDF2 tfloatAtValue = + (s, value) -> { + if (s == null || value == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (tptr == null) return null; + try { + Pointer result = GeneratedFunctions.tfloat_at_value(tptr, value); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // tfloatMinusValue(s STRING, value DOUBLE) β†’ STRING + // MEOS: tfloat_minus_value(const Temporal *, double) β†’ Temporal * + public static final UDF2 tfloatMinusValue = + (s, value) -> { + if (s == null || value == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (tptr == null) return null; + try { + Pointer result = GeneratedFunctions.tfloat_minus_value(tptr, value); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // tintAtValue(s STRING, value INT) β†’ STRING + // MEOS: tint_at_value(const Temporal *, int) β†’ Temporal * + public static final UDF2 tintAtValue = + (s, value) -> { + if (s == null || value == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (tptr == null) return null; + try { + Pointer result = GeneratedFunctions.tint_at_value(tptr, value); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // tintMinusValue(s STRING, value INT) β†’ STRING + // MEOS: tint_minus_value(const Temporal *, int) β†’ Temporal * + public static final UDF2 tintMinusValue = + (s, value) -> { + if (s == null || value == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (tptr == null) return null; + try { + Pointer result = GeneratedFunctions.tint_minus_value(tptr, value); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // ------------------------------------------------------------------ + // Value-range restriction: tnumber (floatspan / intspan) + // ------------------------------------------------------------------ + + // tnumberAtSpan(s STRING, spanHex STRING) β†’ STRING + // MEOS: tnumber_at_span(const Temporal *, const Span *) β†’ Temporal * + public static final UDF2 tnumberAtSpan = + (s, spanHex) -> { + if (s == null || spanHex == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (tptr == null) return null; + try { + Pointer spanPtr = GeneratedFunctions.span_from_hexwkb(spanHex); + if (spanPtr == null) return null; + try { + Pointer result = GeneratedFunctions.tnumber_at_span(tptr, spanPtr); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(spanPtr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // tnumberMinusSpan(s STRING, spanHex STRING) β†’ STRING + // MEOS: tnumber_minus_span(const Temporal *, const Span *) β†’ Temporal * + public static final UDF2 tnumberMinusSpan = + (s, spanHex) -> { + if (s == null || spanHex == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (tptr == null) return null; + try { + Pointer spanPtr = GeneratedFunctions.span_from_hexwkb(spanHex); + if (spanPtr == null) return null; + try { + Pointer result = GeneratedFunctions.tnumber_minus_span(tptr, spanPtr); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(spanPtr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // tnumberAtSpanset(s STRING, spansetHex STRING) β†’ STRING + // MEOS: tnumber_at_spanset(const Temporal *, const SpanSet *) β†’ Temporal * + public static final UDF2 tnumberAtSpanset = + (s, spansetHex) -> { + if (s == null || spansetHex == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (tptr == null) return null; + try { + Pointer ssPtr = GeneratedFunctions.spanset_from_hexwkb(spansetHex); + if (ssPtr == null) return null; + try { + Pointer result = GeneratedFunctions.tnumber_at_spanset(tptr, ssPtr); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(ssPtr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // tnumberMinusSpanset(s STRING, spansetHex STRING) β†’ STRING + // MEOS: tnumber_minus_spanset(const Temporal *, const SpanSet *) β†’ Temporal * + public static final UDF2 tnumberMinusSpanset = + (s, spansetHex) -> { + if (s == null || spansetHex == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (tptr == null) return null; + try { + Pointer ssPtr = GeneratedFunctions.spanset_from_hexwkb(spansetHex); + if (ssPtr == null) return null; + try { + Pointer result = GeneratedFunctions.tnumber_minus_spanset(tptr, ssPtr); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(ssPtr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // ------------------------------------------------------------------ + // Value restriction: tbool + // ------------------------------------------------------------------ + + // tboolAtValue(s STRING, value BOOLEAN) β†’ STRING + // MEOS: tbool_at_value(const Temporal *, bool) β†’ Temporal * + public static final UDF2 tboolAtValue = + (s, value) -> { + if (s == null || value == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (tptr == null) return null; + try { + Pointer result = GeneratedFunctions.tbool_at_value(tptr, value); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // tboolMinusValue(s STRING, value BOOLEAN) β†’ STRING + // MEOS: tbool_minus_value(const Temporal *, bool) β†’ Temporal * + public static final UDF2 tboolMinusValue = + (s, value) -> { + if (s == null || value == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (tptr == null) return null; + try { + Pointer result = GeneratedFunctions.tbool_minus_value(tptr, value); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // ------------------------------------------------------------------ + // Value restriction: ttext + // ------------------------------------------------------------------ + + // ttextAtValue(s STRING, value STRING) β†’ STRING + // MEOS: ttext_at_value(const Temporal *, text *) β†’ Temporal * + // The value String is converted to a MEOS text* via cstring_to_text. + public static final UDF2 ttextAtValue = + (s, value) -> { + if (s == null || value == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (tptr == null) return null; + try { + Pointer txtptr = GeneratedFunctions.cstring_to_text(value); + if (txtptr == null) return null; + try { + Pointer result = GeneratedFunctions.ttext_at_value(tptr, txtptr); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(txtptr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // ttextMinusValue(s STRING, value STRING) β†’ STRING + // MEOS: ttext_minus_value(const Temporal *, text *) β†’ Temporal * + public static final UDF2 ttextMinusValue = + (s, value) -> { + if (s == null || value == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (tptr == null) return null; + try { + Pointer txtptr = GeneratedFunctions.cstring_to_text(value); + if (txtptr == null) return null; + try { + Pointer result = GeneratedFunctions.ttext_minus_value(tptr, txtptr); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(txtptr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // ------------------------------------------------------------------ + // Value restriction: tpoint + // ------------------------------------------------------------------ + + // tpointAtValue(s STRING, geomWkt STRING) β†’ STRING + // MEOS: tpoint_at_value(const Temporal *, GSERIALIZED *) β†’ Temporal * + // The geometry WKT is parsed via geo_from_text with SRID 0. + public static final UDF2 tpointAtValue = + (s, geomWkt) -> { + if (s == null || geomWkt == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (tptr == null) return null; + try { + Pointer gsptr = GeneratedFunctions.geo_from_text(geomWkt, 0); + if (gsptr == null) return null; + try { + Pointer result = GeneratedFunctions.tpoint_at_value(tptr, gsptr); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(gsptr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // tpointMinusValue(s STRING, geomWkt STRING) β†’ STRING + // MEOS: tpoint_minus_value(const Temporal *, GSERIALIZED *) β†’ Temporal * + public static final UDF2 tpointMinusValue = + (s, geomWkt) -> { + if (s == null || geomWkt == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (tptr == null) return null; + try { + Pointer gsptr = GeneratedFunctions.geo_from_text(geomWkt, 0); + if (gsptr == null) return null; + try { + Pointer result = GeneratedFunctions.tpoint_minus_value(tptr, gsptr); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(gsptr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // ------------------------------------------------------------------ + // STBox and elevation restriction (tpoint) + // ------------------------------------------------------------------ + + // tgeoAtStbox(s STRING, stboxHex STRING) β†’ STRING + // MEOS: tgeo_at_stbox(const Temporal *, const STBox *, bool border_inc) β†’ Temporal * + public static final UDF2 tgeoAtStbox = + (s, stboxHex) -> { + if (s == null || stboxHex == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (tptr == null) return null; + try { + Pointer boxPtr = GeneratedFunctions.stbox_from_hexwkb(stboxHex); + if (boxPtr == null) return null; + try { + Pointer result = GeneratedFunctions.tgeo_at_stbox(tptr, boxPtr, true); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(boxPtr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // tgeoMinusStbox(s STRING, stboxHex STRING) β†’ STRING + // MEOS: tgeo_minus_stbox(const Temporal *, const STBox *, bool border_inc) β†’ Temporal * + public static final UDF2 tgeoMinusStbox = + (s, stboxHex) -> { + if (s == null || stboxHex == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (tptr == null) return null; + try { + Pointer boxPtr = GeneratedFunctions.stbox_from_hexwkb(stboxHex); + if (boxPtr == null) return null; + try { + Pointer result = GeneratedFunctions.tgeo_minus_stbox(tptr, boxPtr, true); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(boxPtr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // tpointAtElevation(s STRING, floatspanHex STRING) β†’ STRING + // MEOS: tpoint_at_elevation(const Temporal *, const Span *) β†’ Temporal * + public static final UDF2 tpointAtElevation = + (s, spanHex) -> { + if (s == null || spanHex == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (tptr == null) return null; + try { + Pointer spanPtr = GeneratedFunctions.span_from_hexwkb(spanHex); + if (spanPtr == null) return null; + try { + Pointer result = GeneratedFunctions.tpoint_at_elevation(tptr, spanPtr); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(spanPtr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // tpointMinusElevation(s STRING, floatspanHex STRING) β†’ STRING + // MEOS: tpoint_minus_elevation(const Temporal *, const Span *) β†’ Temporal * + public static final UDF2 tpointMinusElevation = + (s, spanHex) -> { + if (s == null || spanHex == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (tptr == null) return null; + try { + Pointer spanPtr = GeneratedFunctions.span_from_hexwkb(spanHex); + if (spanPtr == null) return null; + try { + Pointer result = GeneratedFunctions.tpoint_minus_elevation(tptr, spanPtr); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(spanPtr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // ------------------------------------------------------------------ + // Extrema restriction β€” at maximum / minimum value + // ------------------------------------------------------------------ + + // temporalAtMax(s STRING) β†’ STRING (restricts to instants at the maximum value) + // MEOS: temporal_at_max(const Temporal *) β†’ Temporal * + public static final UDF1 temporalAtMax = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + Pointer r = GeneratedFunctions.temporal_at_max(ptr); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // temporalAtMin(s STRING) β†’ STRING (restricts to instants at the minimum value) + // MEOS: temporal_at_min(const Temporal *) β†’ Temporal * + public static final UDF1 temporalAtMin = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + Pointer r = GeneratedFunctions.temporal_at_min(ptr); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // ------------------------------------------------------------------ + // Value-set restriction β€” at/minus a set of values + // ------------------------------------------------------------------ + + // temporalAtValues(s STRING, setHex STRING) β†’ STRING + // MEOS: temporal_at_values(const Temporal *, const Set *) β†’ Temporal * + // setHex is an intset/floatset/textset/etc. in hex-WKB form. + public static final UDF2 temporalAtValues = + (s, setHex) -> { + if (s == null || setHex == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (tptr == null) return null; + try { + Pointer setPtr = GeneratedFunctions.set_from_hexwkb(setHex); + if (setPtr == null) return null; + try { + Pointer result = GeneratedFunctions.temporal_at_values(tptr, setPtr); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(setPtr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // temporalMinusValues(s STRING, setHex STRING) β†’ STRING + // MEOS: temporal_minus_values(const Temporal *, const Set *) β†’ Temporal * + public static final UDF2 temporalMinusValues = + (s, setHex) -> { + if (s == null || setHex == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (tptr == null) return null; + try { + Pointer setPtr = GeneratedFunctions.set_from_hexwkb(setHex); + if (setPtr == null) return null; + try { + Pointer result = GeneratedFunctions.temporal_minus_values(tptr, setPtr); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(setPtr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // ------------------------------------------------------------------ + // Spatial restriction β€” at/minus geometry + // ------------------------------------------------------------------ + + // tgeoAtGeom(s STRING, geomWkt STRING) β†’ STRING + // MEOS: tgeo_at_geom(const Temporal *, const GSERIALIZED *) β†’ Temporal * + public static final UDF2 tgeoAtGeom = + (s, geomWkt) -> { + if (s == null || geomWkt == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (tptr == null) return null; + try { + Pointer gsptr = GeneratedFunctions.geo_from_text(geomWkt, 0); + if (gsptr == null) return null; + try { + Pointer r = GeneratedFunctions.tgeo_at_geom(tptr, gsptr); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(gsptr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // tgeoMinusGeom(s STRING, geomWkt STRING) β†’ STRING + // MEOS: tgeo_minus_geom(const Temporal *, const GSERIALIZED *) β†’ Temporal * + public static final UDF2 tgeoMinusGeom = + (s, geomWkt) -> { + if (s == null || geomWkt == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (tptr == null) return null; + try { + Pointer gsptr = GeneratedFunctions.geo_from_text(geomWkt, 0); + if (gsptr == null) return null; + try { + Pointer r = GeneratedFunctions.tgeo_minus_geom(tptr, gsptr); + if (r == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + } finally { + MeosMemory.free(r); + } + } finally { + MeosMemory.free(gsptr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // ------------------------------------------------------------------ + // Registration + // ------------------------------------------------------------------ + + public static void registerAll(SparkSession spark) { + // Timestamp-span/spanset restriction + spark.udf().register("temporalAtTstzspan", temporalAtTstzspan, DataTypes.StringType); + spark.udf().register("temporalAtTstzspanset", temporalAtTstzspanset, DataTypes.StringType); + // Timestamp-set restriction + spark.udf().register("temporalAtTstzset", temporalAtTstzset, DataTypes.StringType); + spark.udf().register("temporalMinusTstzset", temporalMinusTstzset, DataTypes.StringType); + spark.udf().register("temporalMinusTstzspan", temporalMinusTstzspan, DataTypes.StringType); + spark.udf().register("temporalMinusTstzspanset", temporalMinusTstzspanset, DataTypes.StringType); + // Single-timestamptz restriction + spark.udf().register("temporalAtTimestamptz", temporalAtTimestamptz, DataTypes.StringType); + spark.udf().register("temporalMinusTimestamptz", temporalMinusTimestamptz, DataTypes.StringType); + // Delete operations + spark.udf().register("temporalDeleteTstzspan", temporalDeleteTstzspan, DataTypes.StringType); + spark.udf().register("temporalDeleteTstzspanset", temporalDeleteTstzspanset, DataTypes.StringType); + spark.udf().register("temporalDeleteTstzset", temporalDeleteTstzset, DataTypes.StringType); + spark.udf().register("temporalDeleteTimestamptz", temporalDeleteTimestamptz, DataTypes.StringType); + // Value restriction: tfloat / tint + spark.udf().register("tfloatAtValue", tfloatAtValue, DataTypes.StringType); + spark.udf().register("tfloatMinusValue", tfloatMinusValue, DataTypes.StringType); + spark.udf().register("tintAtValue", tintAtValue, DataTypes.StringType); + spark.udf().register("tintMinusValue", tintMinusValue, DataTypes.StringType); + // Value-range restriction: tnumber + spark.udf().register("tnumberAtSpan", tnumberAtSpan, DataTypes.StringType); + spark.udf().register("tnumberMinusSpan", tnumberMinusSpan, DataTypes.StringType); + spark.udf().register("tnumberAtSpanset", tnumberAtSpanset, DataTypes.StringType); + spark.udf().register("tnumberMinusSpanset", tnumberMinusSpanset, DataTypes.StringType); + // Value restriction: tbool + spark.udf().register("tboolAtValue", tboolAtValue, DataTypes.StringType); + spark.udf().register("tboolMinusValue", tboolMinusValue, DataTypes.StringType); + // Value restriction: ttext + spark.udf().register("ttextAtValue", ttextAtValue, DataTypes.StringType); + spark.udf().register("ttextMinusValue", ttextMinusValue, DataTypes.StringType); + // Value restriction: tpoint + spark.udf().register("tpointAtValue", tpointAtValue, DataTypes.StringType); + spark.udf().register("tpointMinusValue", tpointMinusValue, DataTypes.StringType); + // STBox and elevation restriction + spark.udf().register("tgeoAtStbox", tgeoAtStbox, DataTypes.StringType); + spark.udf().register("tgeoMinusStbox", tgeoMinusStbox, DataTypes.StringType); + spark.udf().register("tpointAtElevation", tpointAtElevation, DataTypes.StringType); + spark.udf().register("tpointMinusElevation", tpointMinusElevation, DataTypes.StringType); + // Extrema restriction + spark.udf().register("temporalAtMax", temporalAtMax, DataTypes.StringType); + spark.udf().register("temporalAtMin", temporalAtMin, DataTypes.StringType); + // Value-set restriction + spark.udf().register("temporalAtValues", temporalAtValues, DataTypes.StringType); + spark.udf().register("temporalMinusValues", temporalMinusValues, DataTypes.StringType); + // Spatial restriction + spark.udf().register("tgeoAtGeom", tgeoAtGeom, DataTypes.StringType); + spark.udf().register("tgeoMinusGeom", tgeoMinusGeom, DataTypes.StringType); + } +} diff --git a/src/main/java/org/mobilitydb/spark/temporal/SeqSetGapsUDFs.java b/src/main/java/org/mobilitydb/spark/temporal/SeqSetGapsUDFs.java new file mode 100644 index 00000000..a3e07a70 --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/temporal/SeqSetGapsUDFs.java @@ -0,0 +1,147 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import jnr.ffi.Runtime; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosThread; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.api.java.UDF1; +import org.apache.spark.sql.api.java.UDF2; +import org.apache.spark.sql.api.java.UDF3; +import org.apache.spark.sql.api.java.UDF4; +import org.apache.spark.sql.types.DataTypes; + +/** + * Spark SQL UDFs for the SeqSetGaps constructor family β€” long-standing + * user request from MobilityDB issue #187. + * + * Constructs a temporal sequence-set from an array of temporal instants + * accounting for gaps: consecutive instants further apart than {@code maxt} + * (in time) or {@code maxdist} (in value space, for tnumber/tpoint) start + * a new sequence within the resulting sequence-set. + * + * MEOS function authority: tsequenceset_make_gaps in meos/include/meos.h. + * + * Per-type variants set the default interpolation: + * - tbool/tint/ttext: STEP (=2) + * - tfloat/tgeompoint/tgeogpoint/tgeometry/tgeography: LINEAR (=3) + */ +public final class SeqSetGapsUDFs { + + private SeqSetGapsUDFs() {} + + private static final int INTERP_STEP = 2; + private static final int INTERP_LINEAR = 3; + + /** + * Core builder: deserialise N temporal-instant hex strings, pack their + * pointers into a native TInstant** buffer, call tsequenceset_make_gaps, + * serialise the result, free everything. + */ + private static String build(String[] instants, String maxtStr, double maxdist, int interp) { + if (instants == null || instants.length == 0) return null; + MeosThread.ensureReady(); + + int n = instants.length; + Pointer[] insts = new Pointer[n]; + try { + for (int i = 0; i < n; i++) { + if (instants[i] == null) return null; + insts[i] = GeneratedFunctions.temporal_from_hexwkb(instants[i]); + if (insts[i] == null) return null; + } + Pointer buf = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8L * n); + for (int i = 0; i < n; i++) { + buf.putAddress(i * 8L, insts[i].address()); + } + Pointer maxt = (maxtStr == null) ? null : GeneratedFunctions.pg_interval_in(maxtStr, -1); + try { + Pointer r = GeneratedFunctions.tsequenceset_make_gaps(buf, n, interp, maxt, maxdist); + if (r == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { + if (maxt != null) MeosMemory.free(maxt); + } + } finally { + for (Pointer p : insts) { + if (p != null) MeosMemory.free(p); + } + } + } + + // tboolSeqSetGaps(tbool[], maxt) β€” STEP interpolation, no maxdist + public static final UDF2 tboolSeqSetGaps = + (instants, maxt) -> build(instants, maxt, -1.0, INTERP_STEP); + + // ttextSeqSetGaps(ttext[], maxt) β€” STEP, no maxdist + public static final UDF2 ttextSeqSetGaps = + (instants, maxt) -> build(instants, maxt, -1.0, INTERP_STEP); + + // tintSeqSetGaps(tint[], maxt, maxdist) β€” STEP, optional maxdist + public static final UDF3 tintSeqSetGaps = + (instants, maxt, maxdist) -> build(instants, maxt, + maxdist == null ? -1.0 : maxdist, INTERP_STEP); + + // tfloatSeqSetGaps(tfloat[], maxt, maxdist, interpStr) β€” LINEAR default + public static final UDF4 tfloatSeqSetGaps = + (instants, maxt, maxdist, interpStr) -> build(instants, maxt, + maxdist == null ? -1.0 : maxdist, parseInterp(interpStr, INTERP_LINEAR)); + + // tgeompointSeqSetGaps / tgeogpointSeqSetGaps / tgeometrySeqSetGaps / + // tgeographySeqSetGaps β€” LINEAR default, optional maxdist + interp string + public static final UDF4 tgeompointSeqSetGaps = + (instants, maxt, maxdist, interpStr) -> build(instants, maxt, + maxdist == null ? -1.0 : maxdist, parseInterp(interpStr, INTERP_LINEAR)); + + public static final UDF4 tgeogpointSeqSetGaps = tgeompointSeqSetGaps; + public static final UDF4 tgeometrySeqSetGaps = tgeompointSeqSetGaps; + public static final UDF4 tgeographySeqSetGaps = tgeompointSeqSetGaps; + + private static int parseInterp(String s, int dflt) { + if (s == null) return dflt; + switch (s.toLowerCase()) { + case "linear": return INTERP_LINEAR; + case "step": return INTERP_STEP; + case "discrete": return 1; + default: return dflt; + } + } + + public static void registerAll(SparkSession spark) { + spark.udf().register("tboolSeqSetGaps", tboolSeqSetGaps, DataTypes.StringType); + spark.udf().register("ttextSeqSetGaps", ttextSeqSetGaps, DataTypes.StringType); + spark.udf().register("tintSeqSetGaps", tintSeqSetGaps, DataTypes.StringType); + spark.udf().register("tfloatSeqSetGaps", tfloatSeqSetGaps, DataTypes.StringType); + spark.udf().register("tgeompointSeqSetGaps", tgeompointSeqSetGaps, DataTypes.StringType); + spark.udf().register("tgeogpointSeqSetGaps", tgeogpointSeqSetGaps, DataTypes.StringType); + spark.udf().register("tgeometrySeqSetGaps", tgeometrySeqSetGaps, DataTypes.StringType); + spark.udf().register("tgeographySeqSetGaps", tgeographySeqSetGaps, DataTypes.StringType); + } +} diff --git a/src/main/java/org/mobilitydb/spark/temporal/SetOpsUDFs.java b/src/main/java/org/mobilitydb/spark/temporal/SetOpsUDFs.java new file mode 100644 index 00000000..3df3fd56 --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/temporal/SetOpsUDFs.java @@ -0,0 +1,130 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosThread; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.api.java.UDF2; +import org.apache.spark.sql.types.DataTypes; + +import java.util.function.BiFunction; + +/** + * Spark SQL UDFs for set Γ— set positional, topological, and distance + * operations. Set hex carries the element type so a single UDF dispatches + * across all set types. + * + * MEOS function authority: meos/include/meos.h + */ +public final class SetOpsUDFs { + + private SetOpsUDFs() {} + + private static UDF2 setSet(BiFunction fn) { + return (h1, h2) -> { + if (h1 == null || h2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.set_from_hexwkb(h1); if (p1 == null) return null; + Pointer p2 = GeneratedFunctions.set_from_hexwkb(h2); + if (p2 == null) { MeosMemory.free(p1); return null; } + try { return fn.apply(p1, p2); } + finally { MeosMemory.free(p1, p2); } + }; + } + + public static final UDF2 setLeft = setSet(GeneratedFunctions::left_set_set); + public static final UDF2 setRight = setSet(GeneratedFunctions::right_set_set); + public static final UDF2 setOverleft = setSet(GeneratedFunctions::overleft_set_set); + public static final UDF2 setOverright = setSet(GeneratedFunctions::overright_set_set); + public static final UDF2 setContains = setSet(GeneratedFunctions::contains_set_set); + public static final UDF2 setContained = setSet(GeneratedFunctions::contained_set_set); + public static final UDF2 setOverlaps = setSet(GeneratedFunctions::overlaps_set_set); + + // Per-type distance (setΓ—set must be of matching element type). + public static final UDF2 distanceIntsetIntset = + (h1, h2) -> { + if (h1 == null || h2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.set_from_hexwkb(h1); if (p1 == null) return null; + Pointer p2 = GeneratedFunctions.set_from_hexwkb(h2); + if (p2 == null) { MeosMemory.free(p1); return null; } + try { return GeneratedFunctions.distance_intset_intset(p1, p2); } + finally { MeosMemory.free(p1, p2); } + }; + + public static final UDF2 distanceBigintsetBigintset = + (h1, h2) -> { + if (h1 == null || h2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.set_from_hexwkb(h1); if (p1 == null) return null; + Pointer p2 = GeneratedFunctions.set_from_hexwkb(h2); + if (p2 == null) { MeosMemory.free(p1); return null; } + try { return GeneratedFunctions.distance_bigintset_bigintset(p1, p2); } + finally { MeosMemory.free(p1, p2); } + }; + + public static final UDF2 distanceFloatsetFloatset = + (h1, h2) -> { + if (h1 == null || h2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.set_from_hexwkb(h1); if (p1 == null) return null; + Pointer p2 = GeneratedFunctions.set_from_hexwkb(h2); + if (p2 == null) { MeosMemory.free(p1); return null; } + try { return GeneratedFunctions.distance_floatset_floatset(p1, p2); } + finally { MeosMemory.free(p1, p2); } + }; + + public static final UDF2 distanceTstzsetTstzset = + (h1, h2) -> { + if (h1 == null || h2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.set_from_hexwkb(h1); if (p1 == null) return null; + Pointer p2 = GeneratedFunctions.set_from_hexwkb(h2); + if (p2 == null) { MeosMemory.free(p1); return null; } + try { return GeneratedFunctions.distance_tstzset_tstzset(p1, p2); } + finally { MeosMemory.free(p1, p2); } + }; + + public static void registerAll(SparkSession spark) { + spark.udf().register("setLeft", setLeft, DataTypes.BooleanType); + spark.udf().register("setRight", setRight, DataTypes.BooleanType); + spark.udf().register("setOverleft", setOverleft, DataTypes.BooleanType); + spark.udf().register("setOverright", setOverright, DataTypes.BooleanType); + spark.udf().register("setContains", setContains, DataTypes.BooleanType); + spark.udf().register("setContained", setContained, DataTypes.BooleanType); + spark.udf().register("setOverlaps", setOverlaps, DataTypes.BooleanType); + + spark.udf().register("distanceIntsetIntset", distanceIntsetIntset, DataTypes.IntegerType); + spark.udf().register("distanceBigintsetBigintset", distanceBigintsetBigintset, DataTypes.LongType); + spark.udf().register("distanceFloatsetFloatset", distanceFloatsetFloatset, DataTypes.DoubleType); + spark.udf().register("distanceTstzsetTstzset", distanceTstzsetTstzset, DataTypes.DoubleType); + // MobilityDB SQL bare-name aliases + spark.udf().register("setDistance", distanceFloatsetFloatset, DataTypes.DoubleType); + } +} diff --git a/src/main/java/org/mobilitydb/spark/temporal/SimilarityUDFs.java b/src/main/java/org/mobilitydb/spark/temporal/SimilarityUDFs.java new file mode 100644 index 00000000..dd4e58be --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/temporal/SimilarityUDFs.java @@ -0,0 +1,191 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosThread; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.api.java.UDF2; +import org.apache.spark.sql.types.DataTypes; + +/** + * Spark SQL UDFs for trajectory similarity measures. + * + * Both measures operate on any pair of temporal values with the same base type + * (tgeompoint Γ— tgeompoint, tfloat Γ— tfloat, etc.) and return a scalar Double. + * + * MEOS function authority: meos/include/meos.h (038_temporal_similarity) + */ +public final class SimilarityUDFs { + + private SimilarityUDFs() {} + + // ------------------------------------------------------------------ + // frechetDistance(t1 STRING, t2 STRING) β†’ DOUBLE + // + // Discrete FrΓ©chet distance between two temporal trajectories. + // Returns null when the inputs have incompatible types or no instants. + // + // MEOS: temporal_frechet_distance(const Temporal *, const Temporal *) β†’ double + // ------------------------------------------------------------------ + public static final UDF2 frechetDistance = + (s1, s2) -> { + if (s1 == null || s2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(s1); + if (p1 == null) return null; + try { + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(s2); + if (p2 == null) return null; + try { + double d = GeneratedFunctions.temporal_frechet_distance(p1, p2); + return (d == Double.MAX_VALUE) ? null : d; + } finally { MeosMemory.free(p2); } + } finally { MeosMemory.free(p1); } + }; + + // ------------------------------------------------------------------ + // dynamicTimeWarp(t1 STRING, t2 STRING) β†’ DOUBLE + // + // Dynamic Time Warping distance between two temporal trajectories. + // Returns null when the inputs have incompatible types or no instants. + // + // MEOS: temporal_dyntimewarp_distance(const Temporal *, const Temporal *) β†’ double + // ------------------------------------------------------------------ + public static final UDF2 dynamicTimeWarp = + (s1, s2) -> { + if (s1 == null || s2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(s1); + if (p1 == null) return null; + try { + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(s2); + if (p2 == null) return null; + try { + double d = GeneratedFunctions.temporal_dyntimewarp_distance(p1, p2); + return (d == Double.MAX_VALUE) ? null : d; + } finally { MeosMemory.free(p2); } + } finally { MeosMemory.free(p1); } + }; + + // ------------------------------------------------------------------ + // hausdorffDistance(t1 STRING, t2 STRING) β†’ DOUBLE + // + // Hausdorff distance between two temporal trajectories. + // Returns null when the inputs have incompatible types or no instants. + // + // MEOS: temporal_hausdorff_distance(const Temporal *, const Temporal *) β†’ double + // ------------------------------------------------------------------ + public static final UDF2 hausdorffDistance = + (s1, s2) -> { + if (s1 == null || s2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(s1); + if (p1 == null) return null; + try { + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(s2); + if (p2 == null) return null; + try { + double d = GeneratedFunctions.temporal_hausdorff_distance(p1, p2); + return (d == Double.MAX_VALUE) ? null : d; + } finally { MeosMemory.free(p2); } + } finally { MeosMemory.free(p1); } + }; + + // ------------------------------------------------------------------ + // REGISTRATION + // ------------------------------------------------------------------ + + public static void registerAll(SparkSession spark) { + spark.udf().register("frechetDistance", frechetDistance, DataTypes.DoubleType); + spark.udf().register("dynamicTimeWarp", dynamicTimeWarp, DataTypes.DoubleType); + spark.udf().register("hausdorffDistance", hausdorffDistance, DataTypes.DoubleType); + // MobilityDB SQL bare-name alias for dynamicTimeWarp + spark.udf().register("dynTimeWarpDistance", dynamicTimeWarp, DataTypes.DoubleType); + // Similarity paths β€” return array of "i,j" pairs as Strings + spark.udf().register("dynTimeWarpPath", dynTimeWarpPath, DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("frechetDistancePath", frechetDistancePath, DataTypes.createArrayType(DataTypes.StringType)); + } + + // dynTimeWarpPath / frechetDistancePath: return array of "i,j" pairs + + public static final org.apache.spark.sql.api.java.UDF2 dynTimeWarpPath = + (h1, h2) -> { + if (h1 == null || h2 == null) return null; + org.mobilitydb.spark.MeosThread.ensureReady(); + jnr.ffi.Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(h1); + if (p1 == null) return null; + jnr.ffi.Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(h2); + if (p2 == null) { org.mobilitydb.spark.MeosMemory.free(p1); return null; } + try { + jnr.ffi.Runtime rt = jnr.ffi.Runtime.getSystemRuntime(); + jnr.ffi.Pointer countOut = rt.getMemoryManager().allocateDirect(4); + jnr.ffi.Pointer arr = org.mobilitydb.spark.MeosNative.INSTANCE + .temporal_dyntimewarp_path(p1, p2, countOut); + if (arr == null) return null; + try { + int n = countOut.getInt(0); + String[] out = new String[n]; + for (int i = 0; i < n; i++) { + // Match = {int i, int j} β€” 8 bytes each + int mi = arr.getInt(i * 8L); + int mj = arr.getInt(i * 8L + 4); + out[i] = mi + "," + mj; + } + return out; + } finally { org.mobilitydb.spark.MeosMemory.free(arr); } + } finally { org.mobilitydb.spark.MeosMemory.free(p1, p2); } + }; + + public static final org.apache.spark.sql.api.java.UDF2 frechetDistancePath = + (h1, h2) -> { + if (h1 == null || h2 == null) return null; + org.mobilitydb.spark.MeosThread.ensureReady(); + jnr.ffi.Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(h1); + if (p1 == null) return null; + jnr.ffi.Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(h2); + if (p2 == null) { org.mobilitydb.spark.MeosMemory.free(p1); return null; } + try { + jnr.ffi.Runtime rt = jnr.ffi.Runtime.getSystemRuntime(); + jnr.ffi.Pointer countOut = rt.getMemoryManager().allocateDirect(4); + jnr.ffi.Pointer arr = org.mobilitydb.spark.MeosNative.INSTANCE + .temporal_frechet_path(p1, p2, countOut); + if (arr == null) return null; + try { + int n = countOut.getInt(0); + String[] out = new String[n]; + for (int i = 0; i < n; i++) { + int mi = arr.getInt(i * 8L); + int mj = arr.getInt(i * 8L + 4); + out[i] = mi + "," + mj; + } + return out; + } finally { org.mobilitydb.spark.MeosMemory.free(arr); } + } finally { org.mobilitydb.spark.MeosMemory.free(p1, p2); } + }; +} diff --git a/src/main/java/org/mobilitydb/spark/temporal/SpanAccessorUDFs.java b/src/main/java/org/mobilitydb/spark/temporal/SpanAccessorUDFs.java new file mode 100644 index 00000000..296b2187 --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/temporal/SpanAccessorUDFs.java @@ -0,0 +1,815 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import jnr.ffi.Runtime; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosThread; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.api.java.UDF1; +import org.apache.spark.sql.api.java.UDF2; +import org.apache.spark.sql.types.DataTypes; + +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; +import org.mobilitydb.spark.util.TimeUtil; + +/** + * Spark SQL UDFs for span, spanset, and set bound/count accessors. + * + * MEOS function authority: meos/include/meos.h + */ +public final class SpanAccessorUDFs { + + private SpanAccessorUDFs() {} + + // milliseconds from Unix epoch (1970-01-01) to PG epoch (2000-01-01) + // days from Unix epoch (1970-01-01) to PG epoch (2000-01-01) + // PG_UNIX_OFFSET_DAYS moved to org.mobilitydb.spark.util.TimeUtil.PG_UNIX_EPOCH_OFFSET_DAYS + + // tstzspan_lower/upper returns OffsetDateTime where toEpochSecond() + // holds raw PG-epoch microseconds; divide by 1000 then add PGβ†’Unix offset + private static java.sql.Timestamp odtToTimestamp(OffsetDateTime odt) { + if (odt == null) return null; + return new java.sql.Timestamp(odt.toEpochSecond() / 1000L + TimeUtil.PG_UNIX_EPOCH_OFFSET_MS); + } + + // ------------------------------------------------------------------ + // intspan accessors + // ------------------------------------------------------------------ + + public static final UDF1 intspanLower = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.span_from_hexwkb(hex); + if (p == null) return null; + return GeneratedFunctions.intspan_lower(p); + }; + + public static final UDF1 intspanUpper = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.span_from_hexwkb(hex); + if (p == null) return null; + return GeneratedFunctions.intspan_upper(p); + }; + + public static final UDF1 intspanWidth = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.span_from_hexwkb(hex); + if (p == null) return null; + return GeneratedFunctions.intspan_width(p); + }; + + // ------------------------------------------------------------------ + // floatspan accessors + // ------------------------------------------------------------------ + + public static final UDF1 floatspanLower = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.span_from_hexwkb(hex); + if (p == null) return null; + return GeneratedFunctions.floatspan_lower(p); + }; + + public static final UDF1 floatspanUpper = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.span_from_hexwkb(hex); + if (p == null) return null; + return GeneratedFunctions.floatspan_upper(p); + }; + + public static final UDF1 floatspanWidth = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.span_from_hexwkb(hex); + if (p == null) return null; + return GeneratedFunctions.floatspan_width(p); + }; + + // ------------------------------------------------------------------ + // bigintspan accessors + // ------------------------------------------------------------------ + + public static final UDF1 bigintspanLower = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.span_from_hexwkb(hex); + if (p == null) return null; + return GeneratedFunctions.bigintspan_lower(p); + }; + + public static final UDF1 bigintspanUpper = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.span_from_hexwkb(hex); + if (p == null) return null; + return GeneratedFunctions.bigintspan_upper(p); + }; + + // ------------------------------------------------------------------ + // datespan accessors (MEOS returns int = days from PG epoch 2000-01-01) + // ------------------------------------------------------------------ + + public static final UDF1 datespanLower = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.span_from_hexwkb(hex); + if (p == null) return null; + int days = GeneratedFunctions.datespan_lower(p); + return new java.sql.Date((days + TimeUtil.PG_UNIX_EPOCH_OFFSET_DAYS) * 86400000L); + }; + + public static final UDF1 datespanUpper = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.span_from_hexwkb(hex); + if (p == null) return null; + int days = GeneratedFunctions.datespan_upper(p); + return new java.sql.Date((days + TimeUtil.PG_UNIX_EPOCH_OFFSET_DAYS) * 86400000L); + }; + + // ------------------------------------------------------------------ + // tstzspan accessors + // ------------------------------------------------------------------ + + public static final UDF1 tstzspanLower = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.span_from_hexwkb(hex); + if (p == null) return null; + return odtToTimestamp(GeneratedFunctions.tstzspan_lower(p)); + }; + + public static final UDF1 tstzspanUpper = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.span_from_hexwkb(hex); + if (p == null) return null; + return odtToTimestamp(GeneratedFunctions.tstzspan_upper(p)); + }; + + // ------------------------------------------------------------------ + // Generic span inclusivity flags + // ------------------------------------------------------------------ + + public static final UDF1 spanLowerInc = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.span_from_hexwkb(hex); + if (p == null) return null; + return GeneratedFunctions.span_lower_inc(p); + }; + + public static final UDF1 spanUpperInc = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.span_from_hexwkb(hex); + if (p == null) return null; + return GeneratedFunctions.span_upper_inc(p); + }; + + // ------------------------------------------------------------------ + // Spanset accessors + // ------------------------------------------------------------------ + + public static final UDF1 spansetNumSpans = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.spanset_from_hexwkb(hex); + if (p == null) return null; + return GeneratedFunctions.spanset_num_spans(p); + }; + + public static final UDF1 spansetStartSpan = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.spanset_from_hexwkb(hex); + if (p == null) return null; + Pointer span = GeneratedFunctions.spanset_start_span(p); + if (span == null) return null; + return GeneratedFunctions.span_as_hexwkb(span, (byte) 0); + }; + + public static final UDF1 spansetEndSpan = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.spanset_from_hexwkb(hex); + if (p == null) return null; + Pointer span = GeneratedFunctions.spanset_end_span(p); + if (span == null) return null; + return GeneratedFunctions.span_as_hexwkb(span, (byte) 0); + }; + + // ------------------------------------------------------------------ + // Spanset inclusivity flags + // ------------------------------------------------------------------ + + // spansetLowerInc(hex STRING) β†’ BOOLEAN (lower bound of the first span) + // MEOS: spanset_lower_inc(const SpanSet *) β†’ bool + public static final UDF1 spansetLowerInc = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.spanset_from_hexwkb(hex); + if (p == null) return null; + return GeneratedFunctions.spanset_lower_inc(p); + }; + + // spansetUpperInc(hex STRING) β†’ BOOLEAN (upper bound of the last span) + // MEOS: spanset_upper_inc(const SpanSet *) β†’ bool + public static final UDF1 spansetUpperInc = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.spanset_from_hexwkb(hex); + if (p == null) return null; + return GeneratedFunctions.spanset_upper_inc(p); + }; + + // ------------------------------------------------------------------ + // Span-to-spanset conversion + // ------------------------------------------------------------------ + + // spanToSpanset(hex STRING) β†’ STRING (wrap a span in a single-element spanset) + // MEOS: span_to_spanset(const Span *) β†’ SpanSet * + public static final UDF1 spanToSpanset = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.span_from_hexwkb(hex); + if (p == null) return null; + Pointer ss = GeneratedFunctions.span_to_spanset(p); + if (ss == null) return null; + return GeneratedFunctions.spanset_as_hexwkb(ss, (byte) 0); + }; + + // ------------------------------------------------------------------ + // TstzSpanSet temporal boundary accessors + // + // MEOS: tstzspanset_lower, tstzspanset_upper, + // tstzspanset_start_timestamptz, tstzspanset_end_timestamptz + // All return OffsetDateTime (PG-epoch microseconds via toEpochSecond()). + // ------------------------------------------------------------------ + + public static final UDF1 tstzspansetLower = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.spanset_from_hexwkb(hex); + if (p == null) return null; + return odtToTimestamp(GeneratedFunctions.tstzspanset_lower(p)); + }; + + public static final UDF1 tstzspansetUpper = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.spanset_from_hexwkb(hex); + if (p == null) return null; + return odtToTimestamp(GeneratedFunctions.tstzspanset_upper(p)); + }; + + public static final UDF1 tstzspansetStartTimestamptz = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.spanset_from_hexwkb(hex); + if (p == null) return null; + return odtToTimestamp(GeneratedFunctions.tstzspanset_start_timestamptz(p)); + }; + + public static final UDF1 tstzspansetEndTimestamptz = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.spanset_from_hexwkb(hex); + if (p == null) return null; + return odtToTimestamp(GeneratedFunctions.tstzspanset_end_timestamptz(p)); + }; + + // ------------------------------------------------------------------ + // Set accessors + // ------------------------------------------------------------------ + + public static final UDF1 setNumValues = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.set_from_hexwkb(hex); + if (p == null) return null; + return GeneratedFunctions.set_num_values(p); + }; + + // ------------------------------------------------------------------ + // intset value accessors + // ------------------------------------------------------------------ + + public static final UDF1 intsetStartValue = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.set_from_hexwkb(hex); + if (p == null) return null; + try { return GeneratedFunctions.intset_start_value(p); } + finally { MeosMemory.free(p); } + }; + + public static final UDF1 intsetEndValue = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.set_from_hexwkb(hex); + if (p == null) return null; + try { return GeneratedFunctions.intset_end_value(p); } + finally { MeosMemory.free(p); } + }; + + // intset_values(Set *) β†’ int * (palloc'd int32 array, count via set_num_values) + public static final UDF1> intsetValues = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.set_from_hexwkb(hex); + if (p == null) return null; + try { + int n = GeneratedFunctions.set_num_values(p); + Pointer arr = GeneratedFunctions.intset_values(p, Runtime.getSystemRuntime().getMemoryManager().allocateDirect(4)); + if (arr == null) return null; + try { + List result = new ArrayList<>(n); + for (int i = 0; i < n; i++) result.add(arr.getInt((long) i * 4)); + return result; + } finally { MeosMemory.free(arr); } + } finally { MeosMemory.free(p); } + }; + + // ------------------------------------------------------------------ + // floatset value accessors + // ------------------------------------------------------------------ + + public static final UDF1 floatsetStartValue = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.set_from_hexwkb(hex); + if (p == null) return null; + try { return GeneratedFunctions.floatset_start_value(p); } + finally { MeosMemory.free(p); } + }; + + public static final UDF1 floatsetEndValue = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.set_from_hexwkb(hex); + if (p == null) return null; + try { return GeneratedFunctions.floatset_end_value(p); } + finally { MeosMemory.free(p); } + }; + + // floatset_values(Set *) β†’ double * + public static final UDF1> floatsetValues = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.set_from_hexwkb(hex); + if (p == null) return null; + try { + int n = GeneratedFunctions.set_num_values(p); + Pointer arr = GeneratedFunctions.floatset_values(p, Runtime.getSystemRuntime().getMemoryManager().allocateDirect(4)); + if (arr == null) return null; + try { + List result = new ArrayList<>(n); + for (int i = 0; i < n; i++) result.add(arr.getDouble((long) i * 8)); + return result; + } finally { MeosMemory.free(arr); } + } finally { MeosMemory.free(p); } + }; + + // ------------------------------------------------------------------ + // dateset value accessors (int32 = days from PG epoch 2000-01-01) + // ------------------------------------------------------------------ + + public static final UDF1 datesetStartValue = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.set_from_hexwkb(hex); + if (p == null) return null; + try { + int days = GeneratedFunctions.dateset_start_value(p); + return new java.sql.Date((days + TimeUtil.PG_UNIX_EPOCH_OFFSET_DAYS) * 86400000L); + } finally { MeosMemory.free(p); } + }; + + public static final UDF1 datesetEndValue = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.set_from_hexwkb(hex); + if (p == null) return null; + try { + int days = GeneratedFunctions.dateset_end_value(p); + return new java.sql.Date((days + TimeUtil.PG_UNIX_EPOCH_OFFSET_DAYS) * 86400000L); + } finally { MeosMemory.free(p); } + }; + + // dateset_values(Set *) β†’ int * (int32 days from PG epoch per element) + public static final UDF1> datesetValues = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.set_from_hexwkb(hex); + if (p == null) return null; + try { + int n = GeneratedFunctions.set_num_values(p); + Pointer arr = GeneratedFunctions.dateset_values(p, Runtime.getSystemRuntime().getMemoryManager().allocateDirect(4)); + if (arr == null) return null; + try { + List result = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + int days = arr.getInt((long) i * 4); + result.add(new java.sql.Date((days + TimeUtil.PG_UNIX_EPOCH_OFFSET_DAYS) * 86400000L)); + } + return result; + } finally { MeosMemory.free(arr); } + } finally { MeosMemory.free(p); } + }; + + // ------------------------------------------------------------------ + // tstzset value accessors + // ------------------------------------------------------------------ + + public static final UDF1 tstzsetStartValue = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.set_from_hexwkb(hex); + if (p == null) return null; + try { return odtToTimestamp(GeneratedFunctions.tstzset_start_value(p)); } + finally { MeosMemory.free(p); } + }; + + public static final UDF1 tstzsetEndValue = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.set_from_hexwkb(hex); + if (p == null) return null; + try { return odtToTimestamp(GeneratedFunctions.tstzset_end_value(p)); } + finally { MeosMemory.free(p); } + }; + + // tstzset_values(Set *) β†’ int64 * (PG-epoch microseconds per element) + public static final UDF1> tstzsetValues = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.set_from_hexwkb(hex); + if (p == null) return null; + try { + int n = GeneratedFunctions.set_num_values(p); + Pointer arr = GeneratedFunctions.tstzset_values(p, Runtime.getSystemRuntime().getMemoryManager().allocateDirect(4)); + if (arr == null) return null; + try { + List result = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + long pgMicros = arr.getLong((long) i * 8); + result.add(new java.sql.Timestamp(pgMicros / 1000L + TimeUtil.PG_UNIX_EPOCH_OFFSET_MS)); + } + return result; + } finally { MeosMemory.free(arr); } + } finally { MeosMemory.free(p); } + }; + + // ------------------------------------------------------------------ + // textset value accessors (text * elements β†’ String via text_out) + // ------------------------------------------------------------------ + + public static final UDF1 textsetStartValue = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.set_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer textPtr = GeneratedFunctions.textset_start_value(p); + if (textPtr == null) return null; + return GeneratedFunctions.text_out(textPtr); + } finally { MeosMemory.free(p); } + }; + + public static final UDF1 textsetEndValue = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.set_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer textPtr = GeneratedFunctions.textset_end_value(p); + if (textPtr == null) return null; + return GeneratedFunctions.text_out(textPtr); + } finally { MeosMemory.free(p); } + }; + + // textset_values(Set *) β†’ text ** (pointer array; elements are views β€” do NOT free) + public static final UDF1> textsetValues = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.set_from_hexwkb(hex); + if (p == null) return null; + try { + int n = GeneratedFunctions.set_num_values(p); + Pointer arr = GeneratedFunctions.textset_values(p, Runtime.getSystemRuntime().getMemoryManager().allocateDirect(4)); + if (arr == null) return null; + try { + List result = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + Pointer textPtr = arr.getPointer((long) i * 8); + if (textPtr != null) result.add(GeneratedFunctions.text_out(textPtr)); + } + return result; + } finally { MeosMemory.free(arr); } + } finally { MeosMemory.free(p); } + }; + + public static void registerAll(SparkSession spark) { + spark.udf().register("intspanLower", intspanLower, DataTypes.IntegerType); + spark.udf().register("intspanUpper", intspanUpper, DataTypes.IntegerType); + spark.udf().register("intspanWidth", intspanWidth, DataTypes.IntegerType); + spark.udf().register("floatspanLower", floatspanLower, DataTypes.DoubleType); + spark.udf().register("floatspanUpper", floatspanUpper, DataTypes.DoubleType); + spark.udf().register("floatspanWidth", floatspanWidth, DataTypes.DoubleType); + spark.udf().register("bigintspanLower", bigintspanLower, DataTypes.LongType); + spark.udf().register("bigintspanUpper", bigintspanUpper, DataTypes.LongType); + spark.udf().register("datespanLower", datespanLower, DataTypes.DateType); + spark.udf().register("datespanUpper", datespanUpper, DataTypes.DateType); + spark.udf().register("tstzspanLower", tstzspanLower, DataTypes.TimestampType); + spark.udf().register("tstzspanUpper", tstzspanUpper, DataTypes.TimestampType); + spark.udf().register("spanLowerInc", spanLowerInc, DataTypes.BooleanType); + spark.udf().register("spanUpperInc", spanUpperInc, DataTypes.BooleanType); + spark.udf().register("spansetNumSpans", spansetNumSpans, DataTypes.IntegerType); + spark.udf().register("spansetStartSpan", spansetStartSpan, DataTypes.StringType); + spark.udf().register("spansetEndSpan", spansetEndSpan, DataTypes.StringType); + spark.udf().register("spansetLowerInc", spansetLowerInc, DataTypes.BooleanType); + spark.udf().register("spansetUpperInc", spansetUpperInc, DataTypes.BooleanType); + spark.udf().register("spanToSpanset", spanToSpanset, DataTypes.StringType); + spark.udf().register("setNumValues", setNumValues, DataTypes.IntegerType); + // intset value accessors + spark.udf().register("intsetStartValue", intsetStartValue, DataTypes.IntegerType); + spark.udf().register("intsetEndValue", intsetEndValue, DataTypes.IntegerType); + spark.udf().register("intsetValues", intsetValues, + DataTypes.createArrayType(DataTypes.IntegerType)); + // floatset value accessors + spark.udf().register("floatsetStartValue", floatsetStartValue, DataTypes.DoubleType); + spark.udf().register("floatsetEndValue", floatsetEndValue, DataTypes.DoubleType); + spark.udf().register("floatsetValues", floatsetValues, + DataTypes.createArrayType(DataTypes.DoubleType)); + // dateset value accessors + spark.udf().register("datesetStartValue", datesetStartValue, DataTypes.DateType); + spark.udf().register("datesetEndValue", datesetEndValue, DataTypes.DateType); + spark.udf().register("datesetValues", datesetValues, + DataTypes.createArrayType(DataTypes.DateType)); + // tstzset value accessors + spark.udf().register("tstzsetStartValue", tstzsetStartValue, DataTypes.TimestampType); + spark.udf().register("tstzsetEndValue", tstzsetEndValue, DataTypes.TimestampType); + spark.udf().register("tstzsetValues", tstzsetValues, + DataTypes.createArrayType(DataTypes.TimestampType)); + // textset value accessors + spark.udf().register("textsetStartValue", textsetStartValue, DataTypes.StringType); + spark.udf().register("textsetEndValue", textsetEndValue, DataTypes.StringType); + spark.udf().register("textsetValues", textsetValues, + DataTypes.createArrayType(DataTypes.StringType)); + // TstzSpanSet temporal boundary accessors + spark.udf().register("tstzspansetLower", tstzspansetLower, DataTypes.TimestampType); + spark.udf().register("tstzspansetUpper", tstzspansetUpper, DataTypes.TimestampType); + spark.udf().register("tstzspansetStartTimestamptz", tstzspansetStartTimestamptz, DataTypes.TimestampType); + spark.udf().register("tstzspansetEndTimestamptz", tstzspansetEndTimestamptz, DataTypes.TimestampType); + // spanset nth-span accessor + spark.udf().register("spansetSpanN", spansetSpanN, DataTypes.StringType); + // intspanset / floatspanset bound accessors + spark.udf().register("intspansetLower", intspansetLower, DataTypes.IntegerType); + spark.udf().register("intspansetUpper", intspansetUpper, DataTypes.IntegerType); + spark.udf().register("intspansetWidth", intspansetWidth, DataTypes.IntegerType); + spark.udf().register("floatspansetLower", floatspansetLower, DataTypes.DoubleType); + spark.udf().register("floatspansetUpper", floatspansetUpper, DataTypes.DoubleType); + spark.udf().register("floatspansetWidth", floatspansetWidth, DataTypes.DoubleType); + // tstzspanset extra accessors + spark.udf().register("tstzspansetNumTimestamps", tstzspansetNumTimestamps, DataTypes.IntegerType); + spark.udf().register("tstzspansetTimestamps", tstzspansetTimestamps, + DataTypes.createArrayType(DataTypes.TimestampType)); + spark.udf().register("tstzspansetDuration", tstzspansetDuration, DataTypes.StringType); + } + + // ------------------------------------------------------------------ + // ------------------------------------------------------------------ + // intspanset / floatspanset bound accessors + // ------------------------------------------------------------------ + + // intspansetLower(hex STRING) β†’ INTEGER + // MEOS: intspanset_lower(const SpanSet *) β†’ int + public static final UDF1 intspansetLower = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.spanset_from_hexwkb(hex); + if (p == null) return null; + try { return GeneratedFunctions.intspanset_lower(p); } + finally { MeosMemory.free(p); } + }; + + // intspansetUpper(hex STRING) β†’ INTEGER + // MEOS: intspanset_upper(const SpanSet *) β†’ int + public static final UDF1 intspansetUpper = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.spanset_from_hexwkb(hex); + if (p == null) return null; + try { return GeneratedFunctions.intspanset_upper(p); } + finally { MeosMemory.free(p); } + }; + + // intspansetWidth(hex STRING, ignoreGaps BOOLEAN) β†’ INTEGER + // MEOS: intspanset_width(const SpanSet *, bool) β†’ int + public static final UDF2 intspansetWidth = + (hex, ignoreGaps) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.spanset_from_hexwkb(hex); + if (p == null) return null; + try { + return GeneratedFunctions.intspanset_width(p, ignoreGaps != null && ignoreGaps); + } finally { MeosMemory.free(p); } + }; + + // floatspansetLower(hex STRING) β†’ DOUBLE + // Workaround: floatspanset_lower in MEOS uses Float8GetDatum (wrong direction), + // so we extract the first span and use floatspan_lower which is correct. + public static final UDF1 floatspansetLower = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.spanset_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer firstSpan = GeneratedFunctions.spanset_start_span(p); + if (firstSpan == null) return null; + return GeneratedFunctions.floatspan_lower(firstSpan); + } finally { MeosMemory.free(p); } + }; + + // floatspansetUpper(hex STRING) β†’ DOUBLE + // Workaround: same Float8GetDatum bug as floatspanset_lower. + // Uses spanset_end_span + floatspan_upper instead. + public static final UDF1 floatspansetUpper = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.spanset_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer lastSpan = GeneratedFunctions.spanset_end_span(p); + if (lastSpan == null) return null; + return GeneratedFunctions.floatspan_upper(lastSpan); + } finally { MeosMemory.free(p); } + }; + + // floatspansetWidth(hex STRING, ignoreGaps BOOLEAN) β†’ DOUBLE + // MEOS: floatspanset_width(const SpanSet *, bool) β†’ double + public static final UDF2 floatspansetWidth = + (hex, ignoreGaps) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.spanset_from_hexwkb(hex); + if (p == null) return null; + try { + return GeneratedFunctions.floatspanset_width(p, ignoreGaps != null && ignoreGaps); + } finally { MeosMemory.free(p); } + }; + + // ------------------------------------------------------------------ + // tstzspanset extra accessors + // ------------------------------------------------------------------ + + // tstzspansetNumTimestamps(hex STRING) β†’ INTEGER + // MEOS: tstzspanset_num_timestamps(const SpanSet *) β†’ int + public static final UDF1 tstzspansetNumTimestamps = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.spanset_from_hexwkb(hex); + if (p == null) return null; + try { return GeneratedFunctions.tstzspanset_num_timestamps(p); } + finally { MeosMemory.free(p); } + }; + + // tstzspansetTimestamps(hex STRING) β†’ ARRAY + // MEOS: tstzspanset_timestamps(const SpanSet *) β†’ TimestampTz * + // Returns int64* array (PG-epoch microseconds); count via tstzspanset_num_timestamps. + public static final UDF1> tstzspansetTimestamps = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.spanset_from_hexwkb(hex); + if (p == null) return null; + try { + int n = GeneratedFunctions.tstzspanset_num_timestamps(p); + Pointer arr = GeneratedFunctions.tstzspanset_timestamps(p); + if (arr == null) return null; + try { + List result = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + long pgMicros = arr.getLong((long) i * 8); + result.add(new java.sql.Timestamp(pgMicros / 1000L + TimeUtil.PG_UNIX_EPOCH_OFFSET_MS)); + } + return result; + } finally { MeosMemory.free(arr); } + } finally { MeosMemory.free(p); } + }; + + // tstzspansetDuration(hex STRING, ignoreGaps BOOLEAN) β†’ STRING (interval) + // MEOS: tstzspanset_duration(const SpanSet *, bool) β†’ Interval * + public static final UDF2 tstzspansetDuration = + (hex, ignoreGaps) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + boolean ignore = (ignoreGaps != null && ignoreGaps); + Pointer p = GeneratedFunctions.spanset_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer iv = GeneratedFunctions.tstzspanset_duration(p, ignore); + if (iv == null) return null; + try { return GeneratedFunctions.pg_interval_out(iv); } + finally { MeosMemory.free(iv); } + } finally { MeosMemory.free(p); } + }; + + // ------------------------------------------------------------------ + // spanset_span_n(spanset, n) β†’ span hex-WKB (1-based index) + // MEOS: spanset_span_n(SpanSet *, int) β†’ Span * (view β€” must NOT free) + // ------------------------------------------------------------------ + + public static final UDF2 spansetSpanN = + (hex, n) -> { + if (hex == null || n == null) return null; + MeosThread.ensureReady(); + Pointer ss = GeneratedFunctions.spanset_from_hexwkb(hex); + if (ss == null) return null; + try { + Pointer span = GeneratedFunctions.spanset_span_n(ss, n); + if (span == null) return null; + return GeneratedFunctions.span_as_hexwkb(span, (byte) 0); + } finally { + MeosMemory.free(ss); + } + }; +} diff --git a/src/main/java/org/mobilitydb/spark/temporal/SpanAlgebraUDFs.java b/src/main/java/org/mobilitydb/spark/temporal/SpanAlgebraUDFs.java new file mode 100644 index 00000000..8ea4ea70 --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/temporal/SpanAlgebraUDFs.java @@ -0,0 +1,816 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosThread; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.api.java.UDF1; +import org.apache.spark.sql.api.java.UDF2; +import org.apache.spark.sql.api.java.UDF3; +import org.apache.spark.sql.types.DataTypes; + +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import org.mobilitydb.spark.util.TimeUtil; + +/** + * Spark SQL UDFs for span and set topology predicates and algebraic operations. + * + * All inputs and outputs use hex-WKB string encoding (the internal MobilitySpark + * storage format). Set-returning operations (union, minus) produce a SpanSet + * hex-WKB; intersection returns a Span hex-WKB (null when disjoint). + * + * Naming convention: camelCase to avoid conflicts with Spark built-ins. + * + * MEOS function authority: meos/include/meos.h + */ +public final class SpanAlgebraUDFs { + + private SpanAlgebraUDFs() {} + + // ------------------------------------------------------------------ + // Helper: parse hex-WKB string β†’ span Pointer + // ------------------------------------------------------------------ + private static Pointer spanPtr(String hex) { + return hex == null ? null : GeneratedFunctions.span_from_hexwkb(hex); + } + + private static Pointer spansetPtr(String hex) { + return hex == null ? null : GeneratedFunctions.spanset_from_hexwkb(hex); + } + + private static Pointer setPtr(String hex) { + return hex == null ? null : GeneratedFunctions.set_from_hexwkb(hex); + } + + // ------------------------------------------------------------------ + // Span topology predicates (span, span) β†’ Boolean + // + // MEOS: contains_span_span / contained_span_span / overlaps_span_span + // adjacent_span_span / left_span_span / right_span_span + // overleft_span_span / overright_span_span + // ------------------------------------------------------------------ + + // spanContains("[1,10)", "[2,5)") β†’ true + public static final UDF2 spanContains = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = spanPtr(s1), p2 = spanPtr(s2); + if (p1 == null || p2 == null) return null; + return GeneratedFunctions.contains_span_span(p1, p2); + }; + + // spanContainedIn("[2,5)", "[1,10)") β†’ true + public static final UDF2 spanContainedIn = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = spanPtr(s1), p2 = spanPtr(s2); + if (p1 == null || p2 == null) return null; + return GeneratedFunctions.contained_span_span(p1, p2); + }; + + // spanOverlaps("[1,5)", "[3,10)") β†’ true + public static final UDF2 spanOverlaps = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = spanPtr(s1), p2 = spanPtr(s2); + if (p1 == null || p2 == null) return null; + return GeneratedFunctions.overlaps_span_span(p1, p2); + }; + + // spanAdjacent("[1,5)", "[5,10)") β†’ true + public static final UDF2 spanAdjacent = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = spanPtr(s1), p2 = spanPtr(s2); + if (p1 == null || p2 == null) return null; + return GeneratedFunctions.adjacent_span_span(p1, p2); + }; + + // spanLeft("[1,5)", "[6,10)") β†’ true + public static final UDF2 spanLeft = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = spanPtr(s1), p2 = spanPtr(s2); + if (p1 == null || p2 == null) return null; + return GeneratedFunctions.left_span_span(p1, p2); + }; + + // spanRight("[6,10)", "[1,5)") β†’ true + public static final UDF2 spanRight = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = spanPtr(s1), p2 = spanPtr(s2); + if (p1 == null || p2 == null) return null; + return GeneratedFunctions.right_span_span(p1, p2); + }; + + // spanOverleft("[1,5)", "[3,10)") β†’ true (s1 does not extend right of s2) + public static final UDF2 spanOverleft = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = spanPtr(s1), p2 = spanPtr(s2); + if (p1 == null || p2 == null) return null; + return GeneratedFunctions.overleft_span_span(p1, p2); + }; + + // spanOverright("[3,10)", "[1,5)") β†’ true (s1 does not extend left of s2) + public static final UDF2 spanOverright = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = spanPtr(s1), p2 = spanPtr(s2); + if (p1 == null || p2 == null) return null; + return GeneratedFunctions.overright_span_span(p1, p2); + }; + + // ------------------------------------------------------------------ + // Span algebraic operations (span, span) β†’ hex-WKB STRING + // + // MEOS: union_span_span β†’ SpanSet * + // intersection_span_span β†’ Span * (null when disjoint) + // minus_span_span β†’ SpanSet * + // ------------------------------------------------------------------ + + // spanUnion("[1,5)", "[3,10)") β†’ "{[1,10)}" (SpanSet hex-WKB) + public static final UDF2 spanUnion = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = spanPtr(s1), p2 = spanPtr(s2); + if (p1 == null || p2 == null) return null; + Pointer ss = GeneratedFunctions.union_span_span(p1, p2); + if (ss == null) return null; + return GeneratedFunctions.spanset_as_hexwkb(ss, (byte) 0); + }; + + // spanIntersection("[1,10)", "[3,7)") β†’ "[3,7)" (Span hex-WKB; null if disjoint) + public static final UDF2 spanIntersection = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = spanPtr(s1), p2 = spanPtr(s2); + if (p1 == null || p2 == null) return null; + Pointer s = GeneratedFunctions.intersection_span_span(p1, p2); + if (s == null) return null; + return GeneratedFunctions.span_as_hexwkb(s, (byte) 0); + }; + + // spanMinus("[1,10)", "[3,7)") β†’ "{[1,3),[7,10)}" (SpanSet hex-WKB) + public static final UDF2 spanMinus = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = spanPtr(s1), p2 = spanPtr(s2); + if (p1 == null || p2 == null) return null; + Pointer ss = GeneratedFunctions.minus_span_span(p1, p2); + if (ss == null) return null; + return GeneratedFunctions.spanset_as_hexwkb(ss, (byte) 0); + }; + + // ------------------------------------------------------------------ + // Tstzspan distance (tstzspan, tstzspan) β†’ Double (seconds) + // + // MEOS: distance_tstzspan_tstzspan β†’ double + // ------------------------------------------------------------------ + + // tstzspanDistance("[2020-01-01, 2020-01-05)", "[2020-01-10, 2020-01-15)") β†’ 432000.0 + public static final UDF2 tstzspanDistance = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = spanPtr(s1), p2 = spanPtr(s2); + if (p1 == null || p2 == null) return null; + return GeneratedFunctions.distance_tstzspan_tstzspan(p1, p2); + }; + + // Per-type span distance (each returns the type's natural distance scalar) + public static final UDF2 intspanDistance = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = spanPtr(s1), p2 = spanPtr(s2); + if (p1 == null || p2 == null) return null; + return GeneratedFunctions.distance_intspan_intspan(p1, p2); + }; + public static final UDF2 bigintspanDistance = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = spanPtr(s1), p2 = spanPtr(s2); + if (p1 == null || p2 == null) return null; + return GeneratedFunctions.distance_bigintspan_bigintspan(p1, p2); + }; + public static final UDF2 floatspanDistance = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = spanPtr(s1), p2 = spanPtr(s2); + if (p1 == null || p2 == null) return null; + return GeneratedFunctions.distance_floatspan_floatspan(p1, p2); + }; + public static final UDF2 datespanDistance = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = spanPtr(s1), p2 = spanPtr(s2); + if (p1 == null || p2 == null) return null; + return GeneratedFunctions.distance_datespan_datespan(p1, p2); + }; + + // Span expand: span + delta β†’ expanded span + public static final UDF2 intspanExpand = + (s, v) -> { + if (s == null || v == null) return null; + MeosThread.ensureReady(); + Pointer p = spanPtr(s); + if (p == null) return null; + try { + Pointer r = org.mobilitydb.spark.MeosNative.INSTANCE.intspan_expand(p, v.intValue()); + if (r == null) return null; + try { return GeneratedFunctions.span_as_hexwkb(r, (byte) 0); } + finally { org.mobilitydb.spark.MeosMemory.free(r); } + } finally { org.mobilitydb.spark.MeosMemory.free(p); } + }; + public static final UDF2 bigintspanExpand = + (s, v) -> { + if (s == null || v == null) return null; + MeosThread.ensureReady(); + Pointer p = spanPtr(s); + if (p == null) return null; + try { + Pointer r = org.mobilitydb.spark.MeosNative.INSTANCE.bigintspan_expand(p, v.longValue()); + if (r == null) return null; + try { return GeneratedFunctions.span_as_hexwkb(r, (byte) 0); } + finally { org.mobilitydb.spark.MeosMemory.free(r); } + } finally { org.mobilitydb.spark.MeosMemory.free(p); } + }; + public static final UDF2 floatspanExpand = + (s, v) -> { + if (s == null || v == null) return null; + MeosThread.ensureReady(); + Pointer p = spanPtr(s); + if (p == null) return null; + try { + Pointer r = org.mobilitydb.spark.MeosNative.INSTANCE.floatspan_expand(p, v.doubleValue()); + if (r == null) return null; + try { return GeneratedFunctions.span_as_hexwkb(r, (byte) 0); } + finally { org.mobilitydb.spark.MeosMemory.free(r); } + } finally { org.mobilitydb.spark.MeosMemory.free(p); } + }; + + // ------------------------------------------------------------------ + // Spanset topology predicates (spanset, span) β†’ Boolean + // + // MEOS: contains_spanset_span / contained_spanset_span + // overlaps_spanset_spanset + // ------------------------------------------------------------------ + + // spansetContainsSpan("{[1,5),[7,10)}", "[2,4)") β†’ true + public static final UDF2 spansetContainsSpan = + (ss, s) -> { + MeosThread.ensureReady(); + Pointer pss = spansetPtr(ss), ps = spanPtr(s); + if (pss == null || ps == null) return null; + return GeneratedFunctions.contains_spanset_span(pss, ps); + }; + + // spanContainedInSpanset("[2,4)", "{[1,5),[7,10)}") β†’ true + public static final UDF2 spanContainedInSpanset = + (s, ss) -> { + MeosThread.ensureReady(); + Pointer ps = spanPtr(s), pss = spansetPtr(ss); + if (ps == null || pss == null) return null; + return GeneratedFunctions.contained_span_spanset(ps, pss); + }; + + // spansetOverlaps("{[1,5)}", "{[3,10)}") β†’ true + public static final UDF2 spansetOverlaps = + (ss1, ss2) -> { + MeosThread.ensureReady(); + Pointer p1 = spansetPtr(ss1), p2 = spansetPtr(ss2); + if (p1 == null || p2 == null) return null; + return GeneratedFunctions.overlaps_spanset_spanset(p1, p2); + }; + + // ------------------------------------------------------------------ + // Spanset algebraic operations (spanset, spanset) β†’ hex-WKB STRING + // + // MEOS: union_spanset_spanset / intersection_spanset_spanset + // minus_spanset_spanset + // ------------------------------------------------------------------ + + // spansetUnion("{[1,5)}", "{[7,10)}") β†’ "{[1,5),[7,10)}" + public static final UDF2 spansetUnion = + (ss1, ss2) -> { + MeosThread.ensureReady(); + Pointer p1 = spansetPtr(ss1), p2 = spansetPtr(ss2); + if (p1 == null || p2 == null) return null; + Pointer r = GeneratedFunctions.union_spanset_spanset(p1, p2); + if (r == null) return null; + return GeneratedFunctions.spanset_as_hexwkb(r, (byte) 0); + }; + + // spansetIntersection("{[1,10)}", "{[3,7)}") β†’ "{[3,7)}" + public static final UDF2 spansetIntersection = + (ss1, ss2) -> { + MeosThread.ensureReady(); + Pointer p1 = spansetPtr(ss1), p2 = spansetPtr(ss2); + if (p1 == null || p2 == null) return null; + Pointer r = GeneratedFunctions.intersection_spanset_spanset(p1, p2); + if (r == null) return null; + return GeneratedFunctions.spanset_as_hexwkb(r, (byte) 0); + }; + + // spansetMinus("{[1,10)}", "{[3,7)}") β†’ "{[1,3),[7,10)}" + public static final UDF2 spansetMinus = + (ss1, ss2) -> { + MeosThread.ensureReady(); + Pointer p1 = spansetPtr(ss1), p2 = spansetPtr(ss2); + if (p1 == null || p2 == null) return null; + Pointer r = GeneratedFunctions.minus_spanset_spanset(p1, p2); + if (r == null) return null; + return GeneratedFunctions.spanset_as_hexwkb(r, (byte) 0); + }; + + // ------------------------------------------------------------------ + // Cross-type spanset Γ— span algebra + // MEOS: intersection_spanset_span(SpanSet *, Span *) β†’ SpanSet * + // union_spanset_span(SpanSet *, Span *) β†’ SpanSet * + // minus_spanset_span(SpanSet *, Span *) β†’ SpanSet * + // ------------------------------------------------------------------ + + // spansetIntersectionSpan("{[1,10)}", "[3,7)") β†’ "{[3,7)}" + public static final UDF2 spansetIntersectionSpan = + (ss, s) -> { + MeosThread.ensureReady(); + Pointer pss = spansetPtr(ss), ps = spanPtr(s); + if (pss == null || ps == null) return null; + Pointer r = GeneratedFunctions.intersection_spanset_span(pss, ps); + if (r == null) return null; + return GeneratedFunctions.spanset_as_hexwkb(r, (byte) 0); + }; + + // spansetUnionSpan("{[1,5)}", "[7,10)") β†’ "{[1,5),[7,10)}" + public static final UDF2 spansetUnionSpan = + (ss, s) -> { + MeosThread.ensureReady(); + Pointer pss = spansetPtr(ss), ps = spanPtr(s); + if (pss == null || ps == null) return null; + Pointer r = GeneratedFunctions.union_spanset_span(pss, ps); + if (r == null) return null; + return GeneratedFunctions.spanset_as_hexwkb(r, (byte) 0); + }; + + // spansetMinusSpan("{[1,10)}", "[3,7)") β†’ "{[1,3),[7,10)}" + public static final UDF2 spansetMinusSpan = + (ss, s) -> { + MeosThread.ensureReady(); + Pointer pss = spansetPtr(ss), ps = spanPtr(s); + if (pss == null || ps == null) return null; + Pointer r = GeneratedFunctions.minus_spanset_span(pss, ps); + if (r == null) return null; + return GeneratedFunctions.spanset_as_hexwkb(r, (byte) 0); + }; + + // ------------------------------------------------------------------ + // Set topology predicates (set, set) β†’ Boolean + // + // MEOS: contains_set_set / overlaps_set_set + // ------------------------------------------------------------------ + + // setContains("{1,2,3,4}", "{2,3}") β†’ true + public static final UDF2 setContains = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = setPtr(s1), p2 = setPtr(s2); + if (p1 == null || p2 == null) return null; + return GeneratedFunctions.contains_set_set(p1, p2); + }; + + // setOverlaps("{1,2,3}", "{3,4,5}") β†’ true + public static final UDF2 setOverlaps = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = setPtr(s1), p2 = setPtr(s2); + if (p1 == null || p2 == null) return null; + return GeneratedFunctions.overlaps_set_set(p1, p2); + }; + + // ------------------------------------------------------------------ + // Set algebraic operations (set, set) β†’ hex-WKB STRING + // + // MEOS: union_set_set / intersection_set_set / minus_set_set β†’ Set * + // ------------------------------------------------------------------ + + // setUnion("{1,2,3}", "{4,5}") β†’ "{1,2,3,4,5}" + public static final UDF2 setUnion = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = setPtr(s1), p2 = setPtr(s2); + if (p1 == null || p2 == null) return null; + Pointer r = GeneratedFunctions.union_set_set(p1, p2); + if (r == null) return null; + return GeneratedFunctions.set_as_hexwkb(r, (byte) 0); + }; + + // setIntersection("{1,2,3,4}", "{3,4,5}") β†’ "{3,4}" + public static final UDF2 setIntersection = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = setPtr(s1), p2 = setPtr(s2); + if (p1 == null || p2 == null) return null; + Pointer r = GeneratedFunctions.intersection_set_set(p1, p2); + if (r == null) return null; + return GeneratedFunctions.set_as_hexwkb(r, (byte) 0); + }; + + // setMinus("{1,2,3,4}", "{3,4,5}") β†’ "{1,2}" + public static final UDF2 setMinus = + (s1, s2) -> { + MeosThread.ensureReady(); + Pointer p1 = setPtr(s1), p2 = setPtr(s2); + if (p1 == null || p2 == null) return null; + Pointer r = GeneratedFunctions.minus_set_set(p1, p2); + if (r == null) return null; + return GeneratedFunctions.set_as_hexwkb(r, (byte) 0); + }; + + // milliseconds from Unix epoch (1970-01-01) to PG epoch (2000-01-01) + // ------------------------------------------------------------------ + // Span type conversions + // + // MEOS: intspan_to_floatspan, floatspan_to_intspan, + // datespan_to_tstzspan, tstzspan_to_datespan + // ------------------------------------------------------------------ + + public static final UDF1 intspanToFloatspan = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = spanPtr(hex); + if (p == null) return null; + Pointer result = GeneratedFunctions.intspan_to_floatspan(p); + if (result == null) return null; + try { return GeneratedFunctions.span_as_hexwkb(result, (byte) 0); } + finally { MeosMemory.free(result); } + }; + + public static final UDF1 floatspanToIntspan = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = spanPtr(hex); + if (p == null) return null; + Pointer result = GeneratedFunctions.floatspan_to_intspan(p); + if (result == null) return null; + try { return GeneratedFunctions.span_as_hexwkb(result, (byte) 0); } + finally { MeosMemory.free(result); } + }; + + public static final UDF1 datespanToTstzspan = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = spanPtr(hex); + if (p == null) return null; + Pointer result = GeneratedFunctions.datespan_to_tstzspan(p); + if (result == null) return null; + try { return GeneratedFunctions.span_as_hexwkb(result, (byte) 0); } + finally { MeosMemory.free(result); } + }; + + public static final UDF1 tstzspanToDatespan = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = spanPtr(hex); + if (p == null) return null; + Pointer result = GeneratedFunctions.tstzspan_to_datespan(p); + if (result == null) return null; + try { return GeneratedFunctions.span_as_hexwkb(result, (byte) 0); } + finally { MeosMemory.free(result); } + }; + + // ------------------------------------------------------------------ + // Set type conversions + // + // MEOS: intset_to_floatset, floatset_to_intset, + // set_to_span, set_to_spanset + // ------------------------------------------------------------------ + + public static final UDF1 intsetToFloatset = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.set_from_hexwkb(hex); + if (p == null) return null; + Pointer result = GeneratedFunctions.intset_to_floatset(p); + if (result == null) return null; + try { return GeneratedFunctions.set_as_hexwkb(result, (byte) 0); } + finally { MeosMemory.free(result); } + }; + + public static final UDF1 floatsetToIntset = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.set_from_hexwkb(hex); + if (p == null) return null; + Pointer result = GeneratedFunctions.floatset_to_intset(p); + if (result == null) return null; + try { return GeneratedFunctions.set_as_hexwkb(result, (byte) 0); } + finally { MeosMemory.free(result); } + }; + + public static final UDF1 setToSpan = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.set_from_hexwkb(hex); + if (p == null) return null; + Pointer result = GeneratedFunctions.set_to_span(p); + if (result == null) return null; + try { return GeneratedFunctions.span_as_hexwkb(result, (byte) 0); } + finally { MeosMemory.free(result); } + }; + + public static final UDF1 setToSpanset = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.set_from_hexwkb(hex); + if (p == null) return null; + Pointer result = GeneratedFunctions.set_to_spanset(p); + if (result == null) return null; + try { return GeneratedFunctions.spanset_as_hexwkb(result, (byte) 0); } + finally { MeosMemory.free(result); } + }; + + // ------------------------------------------------------------------ + // Span duration + // + // MEOS: tstzspan_duration, datespan_duration β†’ Interval * + // Output: PG interval string via pg_interval_out (e.g. "2 days") + // ------------------------------------------------------------------ + + public static final UDF1 tstzspanDuration = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = spanPtr(hex); + if (p == null) return null; + Pointer iv = GeneratedFunctions.tstzspan_duration(p); + if (iv == null) return null; + return GeneratedFunctions.pg_interval_out(iv); + }; + + public static final UDF1 datespanDuration = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = spanPtr(hex); + if (p == null) return null; + Pointer iv = GeneratedFunctions.datespan_duration(p); + if (iv == null) return null; + return GeneratedFunctions.pg_interval_out(iv); + }; + + // ------------------------------------------------------------------ + // Span/spanset shift-and-scale + // + // MEOS: tstzspan_shift_scale, tstzspanset_shift_scale + // Either shift or scale interval may be null. + // ------------------------------------------------------------------ + + public static final UDF3 tstzspanShiftScale = + (hex, shiftStr, scaleStr) -> { + if (hex == null) return null; + if (shiftStr == null && scaleStr == null) return null; + MeosThread.ensureReady(); + Pointer p = spanPtr(hex); + if (p == null) return null; + Pointer shiftIv = shiftStr != null ? GeneratedFunctions.pg_interval_in(shiftStr, -1) : null; + Pointer scaleIv = scaleStr != null ? GeneratedFunctions.pg_interval_in(scaleStr, -1) : null; + Pointer result = GeneratedFunctions.tstzspan_shift_scale(p, shiftIv, scaleIv); + if (result == null) return null; + try { return GeneratedFunctions.span_as_hexwkb(result, (byte) 0); } + finally { + MeosMemory.free(result); + if (shiftIv != null) MeosMemory.free(shiftIv); + if (scaleIv != null) MeosMemory.free(scaleIv); + } + }; + + public static final UDF3 tstzspansetShiftScale = + (hex, shiftStr, scaleStr) -> { + if (hex == null) return null; + if (shiftStr == null && scaleStr == null) return null; + MeosThread.ensureReady(); + Pointer p = spansetPtr(hex); + if (p == null) return null; + Pointer shiftIv = shiftStr != null ? GeneratedFunctions.pg_interval_in(shiftStr, -1) : null; + Pointer scaleIv = scaleStr != null ? GeneratedFunctions.pg_interval_in(scaleStr, -1) : null; + Pointer result = GeneratedFunctions.tstzspanset_shift_scale(p, shiftIv, scaleIv); + if (result == null) return null; + try { return GeneratedFunctions.spanset_as_hexwkb(result, (byte) 0); } + finally { + MeosMemory.free(result); + if (shiftIv != null) MeosMemory.free(shiftIv); + if (scaleIv != null) MeosMemory.free(scaleIv); + } + }; + + // ------------------------------------------------------------------ + // Timestamp β†’ span/set singletons + // + // MEOS: timestamptz_to_span, timestamptz_to_set + // ------------------------------------------------------------------ + + public static final UDF1 timestamptzToSpan = + (ts) -> { + if (ts == null) return null; + MeosThread.ensureReady(); + long pgMicros = (ts.getTime() - TimeUtil.PG_UNIX_EPOCH_OFFSET_MS) * 1000L; + OffsetDateTime odt = OffsetDateTime.ofInstant(Instant.ofEpochSecond(pgMicros, 0), ZoneOffset.UTC); + Pointer result = GeneratedFunctions.timestamptz_to_span(odt); + if (result == null) return null; + try { return GeneratedFunctions.span_as_hexwkb(result, (byte) 0); } + finally { MeosMemory.free(result); } + }; + + public static final UDF1 timestamptzToSet = + (ts) -> { + if (ts == null) return null; + MeosThread.ensureReady(); + long pgMicros = (ts.getTime() - TimeUtil.PG_UNIX_EPOCH_OFFSET_MS) * 1000L; + OffsetDateTime odt = OffsetDateTime.ofInstant(Instant.ofEpochSecond(pgMicros, 0), ZoneOffset.UTC); + Pointer result = GeneratedFunctions.timestamptz_to_set(odt); + if (result == null) return null; + try { return GeneratedFunctions.set_as_hexwkb(result, (byte) 0); } + finally { MeosMemory.free(result); } + }; + + public static void registerAll(SparkSession spark) { + // Span topology predicates + spark.udf().register("spanContains", spanContains, DataTypes.BooleanType); + spark.udf().register("spanContainedIn", spanContainedIn, DataTypes.BooleanType); + spark.udf().register("spanOverlaps", spanOverlaps, DataTypes.BooleanType); + spark.udf().register("spanAdjacent", spanAdjacent, DataTypes.BooleanType); + spark.udf().register("spanLeft", spanLeft, DataTypes.BooleanType); + spark.udf().register("spanRight", spanRight, DataTypes.BooleanType); + spark.udf().register("spanOverleft", spanOverleft, DataTypes.BooleanType); + spark.udf().register("spanOverright", spanOverright, DataTypes.BooleanType); + // Span algebra + spark.udf().register("spanUnion", spanUnion, DataTypes.StringType); + spark.udf().register("spanIntersection", spanIntersection, DataTypes.StringType); + spark.udf().register("spanMinus", spanMinus, DataTypes.StringType); + spark.udf().register("tstzspanDistance", tstzspanDistance, DataTypes.DoubleType); + spark.udf().register("intspanDistance", intspanDistance, DataTypes.IntegerType); + spark.udf().register("bigintspanDistance", bigintspanDistance, DataTypes.LongType); + spark.udf().register("floatspanDistance", floatspanDistance, DataTypes.DoubleType); + spark.udf().register("datespanDistance", datespanDistance, DataTypes.IntegerType); + spark.udf().register("spanDistance", floatspanDistance, DataTypes.DoubleType); + spark.udf().register("timeDistance", tstzspanDistance, DataTypes.DoubleType); + spark.udf().register("intspanExpand", intspanExpand, DataTypes.StringType); + spark.udf().register("bigintspanExpand", bigintspanExpand, DataTypes.StringType); + spark.udf().register("floatspanExpand", floatspanExpand, DataTypes.StringType); + spark.udf().register("expand", floatspanExpand, DataTypes.StringType); + // Spanset predicates + spark.udf().register("spansetContainsSpan", spansetContainsSpan, DataTypes.BooleanType); + spark.udf().register("spanContainedInSpanset", spanContainedInSpanset, DataTypes.BooleanType); + spark.udf().register("spansetOverlaps", spansetOverlaps, DataTypes.BooleanType); + // Spanset algebra + spark.udf().register("spansetUnion", spansetUnion, DataTypes.StringType); + spark.udf().register("spansetIntersection", spansetIntersection, DataTypes.StringType); + spark.udf().register("spansetMinus", spansetMinus, DataTypes.StringType); + // Set predicates + spark.udf().register("setContains", setContains, DataTypes.BooleanType); + spark.udf().register("setOverlaps", setOverlaps, DataTypes.BooleanType); + // Set algebra + spark.udf().register("setUnion", setUnion, DataTypes.StringType); + spark.udf().register("setIntersection", setIntersection, DataTypes.StringType); + spark.udf().register("setMinus", setMinus, DataTypes.StringType); + // Span type conversions + spark.udf().register("intspanToFloatspan", intspanToFloatspan, DataTypes.StringType); + spark.udf().register("floatspanToIntspan", floatspanToIntspan, DataTypes.StringType); + spark.udf().register("datespanToTstzspan", datespanToTstzspan, DataTypes.StringType); + spark.udf().register("tstzspanToDatespan", tstzspanToDatespan, DataTypes.StringType); + // Set type conversions + spark.udf().register("intsetToFloatset", intsetToFloatset, DataTypes.StringType); + spark.udf().register("floatsetToIntset", floatsetToIntset, DataTypes.StringType); + spark.udf().register("setToSpan", setToSpan, DataTypes.StringType); + spark.udf().register("setToSpanset", setToSpanset, DataTypes.StringType); + // Span duration + spark.udf().register("tstzspanDuration", tstzspanDuration, DataTypes.StringType); + spark.udf().register("datespanDuration", datespanDuration, DataTypes.StringType); + // Span/spanset shift-scale + spark.udf().register("tstzspanShiftScale", tstzspanShiftScale, DataTypes.StringType); + spark.udf().register("tstzspansetShiftScale", tstzspansetShiftScale, DataTypes.StringType); + // Timestamp singletons + spark.udf().register("timestamptzToSpan", timestamptzToSpan, DataTypes.StringType); + spark.udf().register("timestamptzToSet", timestamptzToSet, DataTypes.StringType); + // Cross-type spanset Γ— span algebra + spark.udf().register("spansetIntersectionSpan", spansetIntersectionSpan, DataTypes.StringType); + spark.udf().register("spansetUnionSpan", spansetUnionSpan, DataTypes.StringType); + spark.udf().register("spansetMinusSpan", spansetMinusSpan, DataTypes.StringType); + // Scalar singleton constructors + spark.udf().register("intToSpan", intToSpan, DataTypes.StringType); + spark.udf().register("intToSet", intToSet, DataTypes.StringType); + spark.udf().register("intToSpanset", intToSpanset, DataTypes.StringType); + spark.udf().register("floatToSpan", floatToSpan, DataTypes.StringType); + spark.udf().register("floatToSet", floatToSet, DataTypes.StringType); + spark.udf().register("floatToSpanset", floatToSpanset, DataTypes.StringType); + spark.udf().register("intToTbox", intToTbox, DataTypes.StringType); + spark.udf().register("floatToTbox", floatToTbox, DataTypes.StringType); + } + + // ------------------------------------------------------------------ + // Scalar singleton constructors + // MEOS: int_to_span/set/spanset(int) β†’ Span/Set/SpanSet * + // float_to_span/set/spanset(double) β†’ Span/Set/SpanSet * + // int_to_tbox(int) / float_to_tbox(double) β†’ TBox * + // ------------------------------------------------------------------ + + public static final UDF1 intToSpan = + (v) -> { if (v == null) return null; MeosThread.ensureReady(); + Pointer r = GeneratedFunctions.int_to_span(v); + if (r == null) return null; + String h = GeneratedFunctions.span_as_hexwkb(r, (byte) 0); + MeosMemory.free(r); return h; }; + + public static final UDF1 intToSet = + (v) -> { if (v == null) return null; MeosThread.ensureReady(); + Pointer r = GeneratedFunctions.int_to_set(v); + if (r == null) return null; + String h = GeneratedFunctions.set_as_hexwkb(r, (byte) 0); + MeosMemory.free(r); return h; }; + + public static final UDF1 intToSpanset = + (v) -> { if (v == null) return null; MeosThread.ensureReady(); + Pointer r = GeneratedFunctions.int_to_spanset(v); + if (r == null) return null; + String h = GeneratedFunctions.spanset_as_hexwkb(r, (byte) 0); + MeosMemory.free(r); return h; }; + + public static final UDF1 floatToSpan = + (v) -> { if (v == null) return null; MeosThread.ensureReady(); + Pointer r = GeneratedFunctions.float_to_span(v); + if (r == null) return null; + String h = GeneratedFunctions.span_as_hexwkb(r, (byte) 0); + MeosMemory.free(r); return h; }; + + public static final UDF1 floatToSet = + (v) -> { if (v == null) return null; MeosThread.ensureReady(); + Pointer r = GeneratedFunctions.float_to_set(v); + if (r == null) return null; + String h = GeneratedFunctions.set_as_hexwkb(r, (byte) 0); + MeosMemory.free(r); return h; }; + + public static final UDF1 floatToSpanset = + (v) -> { if (v == null) return null; MeosThread.ensureReady(); + Pointer r = GeneratedFunctions.float_to_spanset(v); + if (r == null) return null; + String h = GeneratedFunctions.spanset_as_hexwkb(r, (byte) 0); + MeosMemory.free(r); return h; }; + + public static final UDF1 intToTbox = + (v) -> { + if (v == null) return null; MeosThread.ensureReady(); + Pointer r = GeneratedFunctions.int_to_tbox(v); + if (r == null) return null; + jnr.ffi.Runtime rt = jnr.ffi.Runtime.getSystemRuntime(); + String h = GeneratedFunctions.tbox_as_hexwkb(r, (byte) 0, + rt.getMemoryManager().allocateDirect(8)); + MeosMemory.free(r); return h; + }; + + public static final UDF1 floatToTbox = + (v) -> { + if (v == null) return null; MeosThread.ensureReady(); + Pointer r = GeneratedFunctions.float_to_tbox(v); + if (r == null) return null; + jnr.ffi.Runtime rt = jnr.ffi.Runtime.getSystemRuntime(); + String h = GeneratedFunctions.tbox_as_hexwkb(r, (byte) 0, + rt.getMemoryManager().allocateDirect(8)); + MeosMemory.free(r); return h; + }; +} diff --git a/src/main/java/org/mobilitydb/spark/temporal/SpanUDFs.java b/src/main/java/org/mobilitydb/spark/temporal/SpanUDFs.java new file mode 100644 index 00000000..f455ecac --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/temporal/SpanUDFs.java @@ -0,0 +1,117 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import org.mobilitydb.spark.MeosThread; +import org.apache.spark.sql.api.java.UDF1; +import org.apache.spark.sql.types.DataTypes; + +import java.util.HexFormat; + +/** + * Spark SQL UDFs for span and spanset TemporalParquet readers. + * + * Each xFromBinary UDF converts a Parquet BYTE_ARRAY column (written by + * MobilityDuck's asBinary()) to the internal hex-WKB string used throughout + * MobilitySpark. + * + * Implementation uses the type-agnostic span_from_hexwkb / spanset_from_hexwkb + * MEOS functions β€” the WKB type-code embedded in the byte stream identifies the + * concrete span type. Type-specific names exist for SQL discoverability and to + * match the MobilityDuck surface (tstzspanFromBinary, intspanFromBinary, ...). + * + * Write-back (span β†’ Parquet BINARY) uses the existing TemporalUDFs.asBinary, + * which hex-decodes any hex-WKB string regardless of the underlying MEOS type. + * + * MEOS function authority: meos/include/meos.h + */ +public final class SpanUDFs { + + private SpanUDFs() {} + + // ------------------------------------------------------------------ + // Span fromBinary helpers + // + // MEOS: span_from_hexwkb(const char *) β†’ Span * + // span_as_hexwkb(const Span *, uint8_t variant) β†’ char * + // ------------------------------------------------------------------ + private static String spanFromBinaryImpl(byte[] bytes) throws Exception { + if (bytes == null) return null; + MeosThread.ensureReady(); + String hex = HexFormat.of().formatHex(bytes).toUpperCase(); + Pointer ptr = GeneratedFunctions.span_from_hexwkb(hex); + if (ptr == null) return null; + return GeneratedFunctions.span_as_hexwkb(ptr, (byte) 0); + } + + // ------------------------------------------------------------------ + // Spanset fromBinary helpers + // + // MEOS: spanset_from_hexwkb(const char *) β†’ SpanSet * + // spanset_as_hexwkb(const SpanSet *, uint8_t variant) β†’ char * + // ------------------------------------------------------------------ + private static String spansetFromBinaryImpl(byte[] bytes) throws Exception { + if (bytes == null) return null; + MeosThread.ensureReady(); + String hex = HexFormat.of().formatHex(bytes).toUpperCase(); + Pointer ptr = GeneratedFunctions.spanset_from_hexwkb(hex); + if (ptr == null) return null; + return GeneratedFunctions.spanset_as_hexwkb(ptr, (byte) 0); + } + + // ------------------------------------------------------------------ + // Span fromBinary UDFs + // ------------------------------------------------------------------ + public static final UDF1 tstzspanFromBinary = SpanUDFs::spanFromBinaryImpl; + public static final UDF1 intspanFromBinary = SpanUDFs::spanFromBinaryImpl; + public static final UDF1 floatspanFromBinary = SpanUDFs::spanFromBinaryImpl; + public static final UDF1 bigintspanFromBinary = SpanUDFs::spanFromBinaryImpl; + public static final UDF1 datespanFromBinary = SpanUDFs::spanFromBinaryImpl; + + // ------------------------------------------------------------------ + // Spanset fromBinary UDFs + // ------------------------------------------------------------------ + public static final UDF1 tstzspansetFromBinary = SpanUDFs::spansetFromBinaryImpl; + public static final UDF1 intspansetFromBinary = SpanUDFs::spansetFromBinaryImpl; + public static final UDF1 floatspansetFromBinary = SpanUDFs::spansetFromBinaryImpl; + public static final UDF1 bigintspansetFromBinary = SpanUDFs::spansetFromBinaryImpl; + public static final UDF1 datespansetFromBinary = SpanUDFs::spansetFromBinaryImpl; + + public static void registerAll(org.apache.spark.sql.SparkSession spark) { + spark.udf().register("tstzspanFromBinary", tstzspanFromBinary, DataTypes.StringType); + spark.udf().register("intspanFromBinary", intspanFromBinary, DataTypes.StringType); + spark.udf().register("floatspanFromBinary", floatspanFromBinary, DataTypes.StringType); + spark.udf().register("bigintspanFromBinary", bigintspanFromBinary, DataTypes.StringType); + spark.udf().register("datespanFromBinary", datespanFromBinary, DataTypes.StringType); + spark.udf().register("tstzspansetFromBinary", tstzspansetFromBinary, DataTypes.StringType); + spark.udf().register("intspansetFromBinary", intspansetFromBinary, DataTypes.StringType); + spark.udf().register("floatspansetFromBinary", floatspansetFromBinary, DataTypes.StringType); + spark.udf().register("bigintspansetFromBinary", bigintspansetFromBinary, DataTypes.StringType); + spark.udf().register("datespansetFromBinary", datespansetFromBinary, DataTypes.StringType); + } +} diff --git a/src/main/java/org/mobilitydb/spark/temporal/SpansetOpsUDFs.java b/src/main/java/org/mobilitydb/spark/temporal/SpansetOpsUDFs.java new file mode 100644 index 00000000..ae840219 --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/temporal/SpansetOpsUDFs.java @@ -0,0 +1,155 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosThread; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.api.java.UDF2; +import org.apache.spark.sql.types.DataTypes; + +import java.util.function.BiFunction; + +/** + * Spark SQL UDFs for cross-type positional and topological predicates between + * Span and Spanset types. + * + * Coverage: spanΓ—spanset, spansetΓ—span, spansetΓ—spanset β€” 8 predicates each = 24 UDFs + * Predicates: left, right, overleft, overright, adjacent, contains, contained, overlaps + * + * MEOS function authority: meos/include/meos.h + */ +public final class SpansetOpsUDFs { + + private SpansetOpsUDFs() {} + + private static UDF2 spanSpanset(BiFunction fn) { + return (h1, h2) -> { + if (h1 == null || h2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.span_from_hexwkb(h1); if (p1 == null) return null; + Pointer p2 = GeneratedFunctions.spanset_from_hexwkb(h2); + if (p2 == null) { MeosMemory.free(p1); return null; } + try { return fn.apply(p1, p2); } + finally { MeosMemory.free(p1, p2); } + }; + } + + private static UDF2 spansetSpan(BiFunction fn) { + return (h1, h2) -> { + if (h1 == null || h2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.spanset_from_hexwkb(h1); if (p1 == null) return null; + Pointer p2 = GeneratedFunctions.span_from_hexwkb(h2); + if (p2 == null) { MeosMemory.free(p1); return null; } + try { return fn.apply(p1, p2); } + finally { MeosMemory.free(p1, p2); } + }; + } + + private static UDF2 spansetSpanset(BiFunction fn) { + return (h1, h2) -> { + if (h1 == null || h2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.spanset_from_hexwkb(h1); if (p1 == null) return null; + Pointer p2 = GeneratedFunctions.spanset_from_hexwkb(h2); + if (p2 == null) { MeosMemory.free(p1); return null; } + try { return fn.apply(p1, p2); } + finally { MeosMemory.free(p1, p2); } + }; + } + + // ------------------------------------------------------------------ + // span Γ— spanset + // ------------------------------------------------------------------ + + public static final UDF2 spanLeftSpanset = spanSpanset(GeneratedFunctions::left_span_spanset); + public static final UDF2 spanOverleftSpanset = spanSpanset(GeneratedFunctions::overleft_span_spanset); + public static final UDF2 spanRightSpanset = spanSpanset(GeneratedFunctions::right_span_spanset); + public static final UDF2 spanOverrightSpanset = spanSpanset(GeneratedFunctions::overright_span_spanset); + public static final UDF2 spanAdjacentSpanset = spanSpanset(GeneratedFunctions::adjacent_span_spanset); + public static final UDF2 spanContainsSpanset = spanSpanset(GeneratedFunctions::contains_span_spanset); + public static final UDF2 spanContainedSpanset = spanSpanset(GeneratedFunctions::contained_span_spanset); + public static final UDF2 spanOverlapsSpanset = spanSpanset(GeneratedFunctions::overlaps_span_spanset); + + // ------------------------------------------------------------------ + // spanset Γ— span + // ------------------------------------------------------------------ + + public static final UDF2 spansetLeftSpan = spansetSpan(GeneratedFunctions::left_spanset_span); + public static final UDF2 spansetOverleftSpan = spansetSpan(GeneratedFunctions::overleft_spanset_span); + public static final UDF2 spansetRightSpan = spansetSpan(GeneratedFunctions::right_spanset_span); + public static final UDF2 spansetOverrightSpan = spansetSpan(GeneratedFunctions::overright_spanset_span); + public static final UDF2 spansetAdjacentSpan = spansetSpan(GeneratedFunctions::adjacent_spanset_span); + public static final UDF2 spansetContainedSpan = spansetSpan(GeneratedFunctions::contained_spanset_span); + public static final UDF2 spansetOverlapsSpan = spansetSpan(GeneratedFunctions::overlaps_spanset_span); + // spansetContainsSpan already registered by SpanAlgebraUDFs; omitted here to avoid redundant duplicate. + + // ------------------------------------------------------------------ + // spanset Γ— spanset + // ------------------------------------------------------------------ + + public static final UDF2 spansetLeftSpanset = spansetSpanset(GeneratedFunctions::left_spanset_spanset); + public static final UDF2 spansetOverleftSpanset = spansetSpanset(GeneratedFunctions::overleft_spanset_spanset); + public static final UDF2 spansetRightSpanset = spansetSpanset(GeneratedFunctions::right_spanset_spanset); + public static final UDF2 spansetOverrightSpanset = spansetSpanset(GeneratedFunctions::overright_spanset_spanset); + public static final UDF2 spansetAdjacentSpanset = spansetSpanset(GeneratedFunctions::adjacent_spanset_spanset); + public static final UDF2 spansetContainsSpanset = spansetSpanset(GeneratedFunctions::contains_spanset_spanset); + public static final UDF2 spansetContainedSpanset = spansetSpanset(GeneratedFunctions::contained_spanset_spanset); + // spansetOverlaps(spanset, spanset) already registered by SpanAlgebraUDFs. + + public static void registerAll(SparkSession spark) { + // span Γ— spanset + spark.udf().register("spanLeftSpanset", spanLeftSpanset, DataTypes.BooleanType); + spark.udf().register("spanOverleftSpanset", spanOverleftSpanset, DataTypes.BooleanType); + spark.udf().register("spanRightSpanset", spanRightSpanset, DataTypes.BooleanType); + spark.udf().register("spanOverrightSpanset", spanOverrightSpanset, DataTypes.BooleanType); + spark.udf().register("spanAdjacentSpanset", spanAdjacentSpanset, DataTypes.BooleanType); + spark.udf().register("spanContainsSpanset", spanContainsSpanset, DataTypes.BooleanType); + spark.udf().register("spanContainedSpanset", spanContainedSpanset, DataTypes.BooleanType); + spark.udf().register("spanOverlapsSpanset", spanOverlapsSpanset, DataTypes.BooleanType); + + // spanset Γ— span + spark.udf().register("spansetLeftSpan", spansetLeftSpan, DataTypes.BooleanType); + spark.udf().register("spansetOverleftSpan", spansetOverleftSpan, DataTypes.BooleanType); + spark.udf().register("spansetRightSpan", spansetRightSpan, DataTypes.BooleanType); + spark.udf().register("spansetOverrightSpan", spansetOverrightSpan, DataTypes.BooleanType); + spark.udf().register("spansetAdjacentSpan", spansetAdjacentSpan, DataTypes.BooleanType); + spark.udf().register("spansetContainedSpan", spansetContainedSpan, DataTypes.BooleanType); + spark.udf().register("spansetOverlapsSpan", spansetOverlapsSpan, DataTypes.BooleanType); + + // spanset Γ— spanset + spark.udf().register("spansetLeftSpanset", spansetLeftSpanset, DataTypes.BooleanType); + spark.udf().register("spansetOverleftSpanset", spansetOverleftSpanset, DataTypes.BooleanType); + spark.udf().register("spansetRightSpanset", spansetRightSpanset, DataTypes.BooleanType); + spark.udf().register("spansetOverrightSpanset", spansetOverrightSpanset, DataTypes.BooleanType); + spark.udf().register("spansetAdjacentSpanset", spansetAdjacentSpanset, DataTypes.BooleanType); + spark.udf().register("spansetContainsSpanset", spansetContainsSpanset, DataTypes.BooleanType); + spark.udf().register("spansetContainedSpanset", spansetContainedSpanset, DataTypes.BooleanType); + } +} diff --git a/src/main/java/org/mobilitydb/spark/temporal/SubtypeConstructorUDFs.java b/src/main/java/org/mobilitydb/spark/temporal/SubtypeConstructorUDFs.java new file mode 100644 index 00000000..68fef94d --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/temporal/SubtypeConstructorUDFs.java @@ -0,0 +1,324 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosThread; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.api.java.UDF1; +import org.apache.spark.sql.api.java.UDF2; +import org.apache.spark.sql.types.DataTypes; + +import java.sql.Timestamp; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import org.mobilitydb.spark.util.TimeUtil; + +/** + * Spark SQL UDFs for typed temporal-instant constructors and typed + * subtype-conversion aliases (tboolSeq β†’ temporalToTsequence, etc.). + * + * MEOS function authority: meos/include/meos.h β€” *inst_make, + * temporal_to_tinstant/_to_tsequence/_to_tsequenceset. + */ +public final class SubtypeConstructorUDFs { + + private SubtypeConstructorUDFs() {} + + private static OffsetDateTime toOdt(Timestamp ts) { + return Instant.ofEpochMilli(ts.getTime()).atOffset(ZoneOffset.UTC); + } + + // ------------------------------------------------------------------ + // Per-type Inst constructors β€” (value, timestamp) β†’ temporal instant + // ------------------------------------------------------------------ + + public static final UDF2 tboolInst = + (v, ts) -> { + if (v == null || ts == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.tboolinst_make(v, toOdt(ts)); + if (p == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(p, (byte) 0); } + finally { MeosMemory.free(p); } + }; + + public static final UDF2 tintInst = + (v, ts) -> { + if (v == null || ts == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.tintinst_make(v, toOdt(ts)); + if (p == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(p, (byte) 0); } + finally { MeosMemory.free(p); } + }; + + public static final UDF2 tfloatInst = + (v, ts) -> { + if (v == null || ts == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.tfloatinst_make(v, toOdt(ts)); + if (p == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(p, (byte) 0); } + finally { MeosMemory.free(p); } + }; + + // tpointInst((point WKT), timestamp) β€” used for tgeompoint/tgeogpoint + public static final UDF2 tgeompointInst = + (geomWkt, ts) -> { + if (geomWkt == null || ts == null) return null; + MeosThread.ensureReady(); + Pointer g = GeneratedFunctions.geo_from_text(geomWkt, 0); + if (g == null) return null; + try { + Pointer p = GeneratedFunctions.tpointinst_make(g, toOdt(ts)); + if (p == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(p, (byte) 0); } + finally { MeosMemory.free(p); } + } finally { MeosMemory.free(g); } + }; + + public static final UDF2 tgeogpointInst = tgeompointInst; + + // tgeometryInst((geometry WKT), timestamp) β€” for general geometry, not just points + public static final UDF2 tgeometryInst = + (geomWkt, ts) -> { + if (geomWkt == null || ts == null) return null; + MeosThread.ensureReady(); + Pointer g = GeneratedFunctions.geo_from_text(geomWkt, 0); + if (g == null) return null; + try { + Pointer p = org.mobilitydb.spark.MeosNative.INSTANCE + .tgeoinst_make(g, toPgEpochMicros(ts)); + if (p == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(p, (byte) 0); } + finally { MeosMemory.free(p); } + } finally { MeosMemory.free(g); } + }; + + public static final UDF2 tgeographyInst = tgeometryInst; + + public static final UDF2 ttextInst = + (v, ts) -> { + if (v == null || ts == null) return null; + MeosThread.ensureReady(); + Pointer txt = GeneratedFunctions.cstring_to_text(v); + if (txt == null) return null; + try { + Pointer p = GeneratedFunctions.ttextinst_make(txt, toOdt(ts)); + if (p == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(p, (byte) 0); } + finally { MeosMemory.free(p); } + } finally { MeosMemory.free(txt); } + }; + + // ------------------------------------------------------------------ + // Typed Seq / SeqSet aliases β€” call the generic conversion with a + // default 'linear' interpolation. + // ------------------------------------------------------------------ + + private static UDF1 seqAlias() { + return hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.temporal_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer r = GeneratedFunctions.temporal_to_tsequence(p, 3); + if (r == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(p); } + }; + } + + private static UDF1 seqSetAlias() { + return hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.temporal_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer r = GeneratedFunctions.temporal_to_tsequenceset(p, 3); + if (r == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(p); } + }; + } + + public static final UDF1 tboolSeq = seqAlias(); + public static final UDF1 tintSeq = seqAlias(); + public static final UDF1 tfloatSeq = seqAlias(); + public static final UDF1 ttextSeq = seqAlias(); + public static final UDF1 tgeompointSeq = seqAlias(); + public static final UDF1 tgeogpointSeq = seqAlias(); + public static final UDF1 tgeometrySeq = seqAlias(); + public static final UDF1 tgeographySeq = seqAlias(); + + public static final UDF1 tboolSeqSet = seqSetAlias(); + public static final UDF1 tintSeqSet = seqSetAlias(); + public static final UDF1 tfloatSeqSet = seqSetAlias(); + public static final UDF1 ttextSeqSet = seqSetAlias(); + public static final UDF1 tgeompointSeqSet = seqSetAlias(); + public static final UDF1 tgeogpointSeqSet = seqSetAlias(); + public static final UDF1 tgeometrySeqSet = seqSetAlias(); + public static final UDF1 tgeographySeqSet = seqSetAlias(); + + // ------------------------------------------------------------------ + // Accessor aliases (MobilityDB SQL bare names) + // ------------------------------------------------------------------ + + // temporal subtype label (Inst | Seq | SeqSet) + public static final UDF1 tempSubtype = + hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.temporal_from_hexwkb(hex); + if (p == null) return null; + try { return GeneratedFunctions.temporal_subtype(p); } + finally { MeosMemory.free(p); } + }; + + // memory size of the serialised temporal value + public static final UDF1 memSize = + hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.temporal_from_hexwkb(hex); + if (p == null) return null; + try { return org.mobilitydb.spark.MeosNative.INSTANCE.temporal_mem_size(p); } + finally { MeosMemory.free(p); } + }; + + // getTime β†’ temporal_time β†’ SpanSet hex + public static final UDF1 getTime = + hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.temporal_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer ss = GeneratedFunctions.temporal_time(p); + if (ss == null) return null; + try { return GeneratedFunctions.spanset_as_hexwkb(ss, (byte) 0); } + finally { MeosMemory.free(ss); } + } finally { MeosMemory.free(p); } + }; + + // deleteTime(temporal, ts) β†’ temporal with the timestamp removed (connect=true) + public static final UDF2 deleteTime = + (hex, ts) -> { + if (hex == null || ts == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.temporal_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer r = GeneratedFunctions.temporal_delete_timestamptz(p, toOdt(ts), true); + if (r == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(p); } + }; + + private static long toPgEpochMicros(Timestamp ts) { + return (ts.getTime() - TimeUtil.PG_UNIX_EPOCH_OFFSET_MS) * 1000L; + } + + // beforeTimestamp(temporal, ts) β†’ temporal restricted to before ts + public static final UDF2 beforeTimestamp = + (hex, ts) -> { + if (hex == null || ts == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.temporal_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer r = org.mobilitydb.spark.MeosNative.INSTANCE + .temporal_before_timestamptz(p, toPgEpochMicros(ts)); + if (r == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(p); } + }; + + public static final UDF2 afterTimestamp = + (hex, ts) -> { + if (hex == null || ts == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.temporal_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer r = org.mobilitydb.spark.MeosNative.INSTANCE + .temporal_after_timestamptz(p, toPgEpochMicros(ts)); + if (r == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(p); } + }; + + public static void registerAll(SparkSession spark) { + // Inst constructors + spark.udf().register("tboolInst", tboolInst, DataTypes.StringType); + spark.udf().register("tintInst", tintInst, DataTypes.StringType); + spark.udf().register("tfloatInst", tfloatInst, DataTypes.StringType); + spark.udf().register("ttextInst", ttextInst, DataTypes.StringType); + spark.udf().register("tgeompointInst", tgeompointInst, DataTypes.StringType); + spark.udf().register("tgeogpointInst", tgeogpointInst, DataTypes.StringType); + spark.udf().register("tgeometryInst", tgeometryInst, DataTypes.StringType); + spark.udf().register("tgeographyInst", tgeographyInst, DataTypes.StringType); + + // Seq aliases + spark.udf().register("tboolSeq", tboolSeq, DataTypes.StringType); + spark.udf().register("tintSeq", tintSeq, DataTypes.StringType); + spark.udf().register("tfloatSeq", tfloatSeq, DataTypes.StringType); + spark.udf().register("ttextSeq", ttextSeq, DataTypes.StringType); + spark.udf().register("tgeompointSeq", tgeompointSeq, DataTypes.StringType); + spark.udf().register("tgeogpointSeq", tgeogpointSeq, DataTypes.StringType); + spark.udf().register("tgeometrySeq", tgeometrySeq, DataTypes.StringType); + spark.udf().register("tgeographySeq", tgeographySeq, DataTypes.StringType); + + // SeqSet aliases + spark.udf().register("tboolSeqSet", tboolSeqSet, DataTypes.StringType); + spark.udf().register("tintSeqSet", tintSeqSet, DataTypes.StringType); + spark.udf().register("tfloatSeqSet", tfloatSeqSet, DataTypes.StringType); + spark.udf().register("ttextSeqSet", ttextSeqSet, DataTypes.StringType); + spark.udf().register("tgeompointSeqSet", tgeompointSeqSet, DataTypes.StringType); + spark.udf().register("tgeogpointSeqSet", tgeogpointSeqSet, DataTypes.StringType); + spark.udf().register("tgeometrySeqSet", tgeometrySeqSet, DataTypes.StringType); + spark.udf().register("tgeographySeqSet", tgeographySeqSet, DataTypes.StringType); + + // Accessor aliases + spark.udf().register("tempSubtype", tempSubtype, DataTypes.StringType); + spark.udf().register("memSize", memSize, DataTypes.IntegerType); + spark.udf().register("getTime", getTime, DataTypes.StringType); + spark.udf().register("deleteTime", deleteTime, DataTypes.StringType); + spark.udf().register("beforeTimestamp", beforeTimestamp, DataTypes.StringType); + spark.udf().register("afterTimestamp", afterTimestamp, DataTypes.StringType); + } +} diff --git a/src/main/java/org/mobilitydb/spark/temporal/TBoxOpsUDFs.java b/src/main/java/org/mobilitydb/spark/temporal/TBoxOpsUDFs.java new file mode 100644 index 00000000..535f9b62 --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/temporal/TBoxOpsUDFs.java @@ -0,0 +1,193 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosThread; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.api.java.UDF2; +import org.apache.spark.sql.types.DataTypes; + +import java.util.function.BiFunction; + +/** + * Spark SQL UDFs for cross-type positional, temporal, and topological + * predicates between TBox and TNumber types. + * + * Coverage: tboxΓ—tbox, tboxΓ—tnumber, tnumberΓ—tbox β€” 13 predicates each = 39 UDFs + * Predicates: left, right, overleft, overright (X axis); + * before, after, overbefore, overafter (time axis); + * adjacent, contains, contained, overlaps, same (topological) + * + * MEOS function authority: meos/include/meos.h + */ +public final class TBoxOpsUDFs { + + private TBoxOpsUDFs() {} + + // ------------------------------------------------------------------ + // Helpers β€” reduce 39Γ— boilerplate to one factory per cross-type + // ------------------------------------------------------------------ + + private static UDF2 tboxTbox(BiFunction fn) { + return (h1, h2) -> { + if (h1 == null || h2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.tbox_from_hexwkb(h1); if (p1 == null) return null; + Pointer p2 = GeneratedFunctions.tbox_from_hexwkb(h2); + if (p2 == null) { MeosMemory.free(p1); return null; } + try { return fn.apply(p1, p2); } + finally { MeosMemory.free(p1, p2); } + }; + } + + private static UDF2 tboxTnumber(BiFunction fn) { + return (h1, h2) -> { + if (h1 == null || h2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.tbox_from_hexwkb(h1); if (p1 == null) return null; + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(h2); + if (p2 == null) { MeosMemory.free(p1); return null; } + try { return fn.apply(p1, p2); } + finally { MeosMemory.free(p1, p2); } + }; + } + + private static UDF2 tnumberTbox(BiFunction fn) { + return (h1, h2) -> { + if (h1 == null || h2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(h1); if (p1 == null) return null; + Pointer p2 = GeneratedFunctions.tbox_from_hexwkb(h2); + if (p2 == null) { MeosMemory.free(p1); return null; } + try { return fn.apply(p1, p2); } + finally { MeosMemory.free(p1, p2); } + }; + } + + // ------------------------------------------------------------------ + // tbox Γ— tbox + // ------------------------------------------------------------------ + + public static final UDF2 tboxLeftTbox = tboxTbox(GeneratedFunctions::left_tbox_tbox); + public static final UDF2 tboxOverleftTbox = tboxTbox(GeneratedFunctions::overleft_tbox_tbox); + public static final UDF2 tboxRightTbox = tboxTbox(GeneratedFunctions::right_tbox_tbox); + public static final UDF2 tboxOverrightTbox = tboxTbox(GeneratedFunctions::overright_tbox_tbox); + public static final UDF2 tboxBeforeTbox = tboxTbox(GeneratedFunctions::before_tbox_tbox); + public static final UDF2 tboxOverbeforeTbox = tboxTbox(GeneratedFunctions::overbefore_tbox_tbox); + public static final UDF2 tboxAfterTbox = tboxTbox(GeneratedFunctions::after_tbox_tbox); + public static final UDF2 tboxOverafterTbox = tboxTbox(GeneratedFunctions::overafter_tbox_tbox); + public static final UDF2 tboxAdjacentTbox = tboxTbox(GeneratedFunctions::adjacent_tbox_tbox); + public static final UDF2 tboxContainsTbox = tboxTbox(GeneratedFunctions::contains_tbox_tbox); + public static final UDF2 tboxContainedTbox = tboxTbox(GeneratedFunctions::contained_tbox_tbox); + public static final UDF2 tboxOverlapsTbox = tboxTbox(GeneratedFunctions::overlaps_tbox_tbox); + public static final UDF2 tboxSameTbox = tboxTbox(GeneratedFunctions::same_tbox_tbox); + + // ------------------------------------------------------------------ + // tbox Γ— tnumber + // ------------------------------------------------------------------ + + public static final UDF2 tboxLeftTnumber = tboxTnumber(GeneratedFunctions::left_tbox_tnumber); + public static final UDF2 tboxOverleftTnumber = tboxTnumber(GeneratedFunctions::overleft_tbox_tnumber); + public static final UDF2 tboxRightTnumber = tboxTnumber(GeneratedFunctions::right_tbox_tnumber); + public static final UDF2 tboxOverrightTnumber = tboxTnumber(GeneratedFunctions::overright_tbox_tnumber); + public static final UDF2 tboxBeforeTnumber = tboxTnumber(GeneratedFunctions::before_tbox_tnumber); + public static final UDF2 tboxOverbeforeTnumber = tboxTnumber(GeneratedFunctions::overbefore_tbox_tnumber); + public static final UDF2 tboxAfterTnumber = tboxTnumber(GeneratedFunctions::after_tbox_tnumber); + public static final UDF2 tboxOverafterTnumber = tboxTnumber(GeneratedFunctions::overafter_tbox_tnumber); + public static final UDF2 tboxAdjacentTnumber = tboxTnumber(GeneratedFunctions::adjacent_tbox_tnumber); + public static final UDF2 tboxContainsTnumber = tboxTnumber(GeneratedFunctions::contains_tbox_tnumber); + public static final UDF2 tboxContainedTnumber = tboxTnumber(GeneratedFunctions::contained_tbox_tnumber); + public static final UDF2 tboxOverlapsTnumber = tboxTnumber(GeneratedFunctions::overlaps_tbox_tnumber); + public static final UDF2 tboxSameTnumber = tboxTnumber(GeneratedFunctions::same_tbox_tnumber); + + // ------------------------------------------------------------------ + // tnumber Γ— tbox + // ------------------------------------------------------------------ + + public static final UDF2 tnumberLeftTbox = tnumberTbox(GeneratedFunctions::left_tnumber_tbox); + public static final UDF2 tnumberOverleftTbox = tnumberTbox(GeneratedFunctions::overleft_tnumber_tbox); + public static final UDF2 tnumberRightTbox = tnumberTbox(GeneratedFunctions::right_tnumber_tbox); + public static final UDF2 tnumberOverrightTbox = tnumberTbox(GeneratedFunctions::overright_tnumber_tbox); + public static final UDF2 tnumberBeforeTbox = tnumberTbox(GeneratedFunctions::before_tnumber_tbox); + public static final UDF2 tnumberOverbeforeTbox = tnumberTbox(GeneratedFunctions::overbefore_tnumber_tbox); + public static final UDF2 tnumberAfterTbox = tnumberTbox(GeneratedFunctions::after_tnumber_tbox); + public static final UDF2 tnumberOverafterTbox = tnumberTbox(GeneratedFunctions::overafter_tnumber_tbox); + public static final UDF2 tnumberAdjacentTbox = tnumberTbox(GeneratedFunctions::adjacent_tnumber_tbox); + public static final UDF2 tnumberContainsTbox = tnumberTbox(GeneratedFunctions::contains_tnumber_tbox); + public static final UDF2 tnumberContainedTbox = tnumberTbox(GeneratedFunctions::contained_tnumber_tbox); + public static final UDF2 tnumberOverlapsTbox = tnumberTbox(GeneratedFunctions::overlaps_tnumber_tbox); + public static final UDF2 tnumberSameTbox = tnumberTbox(GeneratedFunctions::same_tnumber_tbox); + + public static void registerAll(SparkSession spark) { + // tbox Γ— tbox + spark.udf().register("tboxLeftTbox", tboxLeftTbox, DataTypes.BooleanType); + spark.udf().register("tboxOverleftTbox", tboxOverleftTbox, DataTypes.BooleanType); + spark.udf().register("tboxRightTbox", tboxRightTbox, DataTypes.BooleanType); + spark.udf().register("tboxOverrightTbox", tboxOverrightTbox, DataTypes.BooleanType); + spark.udf().register("tboxBeforeTbox", tboxBeforeTbox, DataTypes.BooleanType); + spark.udf().register("tboxOverbeforeTbox", tboxOverbeforeTbox, DataTypes.BooleanType); + spark.udf().register("tboxAfterTbox", tboxAfterTbox, DataTypes.BooleanType); + spark.udf().register("tboxOverafterTbox", tboxOverafterTbox, DataTypes.BooleanType); + spark.udf().register("tboxAdjacentTbox", tboxAdjacentTbox, DataTypes.BooleanType); + spark.udf().register("tboxContainsTbox", tboxContainsTbox, DataTypes.BooleanType); + spark.udf().register("tboxContainedTbox", tboxContainedTbox, DataTypes.BooleanType); + spark.udf().register("tboxOverlapsTbox", tboxOverlapsTbox, DataTypes.BooleanType); + spark.udf().register("tboxSameTbox", tboxSameTbox, DataTypes.BooleanType); + + // tbox Γ— tnumber + spark.udf().register("tboxLeftTnumber", tboxLeftTnumber, DataTypes.BooleanType); + spark.udf().register("tboxOverleftTnumber", tboxOverleftTnumber, DataTypes.BooleanType); + spark.udf().register("tboxRightTnumber", tboxRightTnumber, DataTypes.BooleanType); + spark.udf().register("tboxOverrightTnumber", tboxOverrightTnumber, DataTypes.BooleanType); + spark.udf().register("tboxBeforeTnumber", tboxBeforeTnumber, DataTypes.BooleanType); + spark.udf().register("tboxOverbeforeTnumber", tboxOverbeforeTnumber, DataTypes.BooleanType); + spark.udf().register("tboxAfterTnumber", tboxAfterTnumber, DataTypes.BooleanType); + spark.udf().register("tboxOverafterTnumber", tboxOverafterTnumber, DataTypes.BooleanType); + spark.udf().register("tboxAdjacentTnumber", tboxAdjacentTnumber, DataTypes.BooleanType); + spark.udf().register("tboxContainsTnumber", tboxContainsTnumber, DataTypes.BooleanType); + spark.udf().register("tboxContainedTnumber", tboxContainedTnumber, DataTypes.BooleanType); + spark.udf().register("tboxOverlapsTnumber", tboxOverlapsTnumber, DataTypes.BooleanType); + spark.udf().register("tboxSameTnumber", tboxSameTnumber, DataTypes.BooleanType); + + // tnumber Γ— tbox + spark.udf().register("tnumberLeftTbox", tnumberLeftTbox, DataTypes.BooleanType); + spark.udf().register("tnumberOverleftTbox", tnumberOverleftTbox, DataTypes.BooleanType); + spark.udf().register("tnumberRightTbox", tnumberRightTbox, DataTypes.BooleanType); + spark.udf().register("tnumberOverrightTbox", tnumberOverrightTbox, DataTypes.BooleanType); + spark.udf().register("tnumberBeforeTbox", tnumberBeforeTbox, DataTypes.BooleanType); + spark.udf().register("tnumberOverbeforeTbox", tnumberOverbeforeTbox, DataTypes.BooleanType); + spark.udf().register("tnumberAfterTbox", tnumberAfterTbox, DataTypes.BooleanType); + spark.udf().register("tnumberOverafterTbox", tnumberOverafterTbox, DataTypes.BooleanType); + spark.udf().register("tnumberAdjacentTbox", tnumberAdjacentTbox, DataTypes.BooleanType); + spark.udf().register("tnumberContainsTbox", tnumberContainsTbox, DataTypes.BooleanType); + spark.udf().register("tnumberContainedTbox", tnumberContainedTbox, DataTypes.BooleanType); + spark.udf().register("tnumberOverlapsTbox", tnumberOverlapsTbox, DataTypes.BooleanType); + spark.udf().register("tnumberSameTbox", tnumberSameTbox, DataTypes.BooleanType); + } +} diff --git a/src/main/java/org/mobilitydb/spark/temporal/TBoxUDFs.java b/src/main/java/org/mobilitydb/spark/temporal/TBoxUDFs.java new file mode 100644 index 00000000..bf9d36c0 --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/temporal/TBoxUDFs.java @@ -0,0 +1,631 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import jnr.ffi.Runtime; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.api.java.UDF1; +import org.apache.spark.sql.api.java.UDF2; +import org.apache.spark.sql.api.java.UDF3; +import org.apache.spark.sql.types.DataTypes; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosThread; + +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import org.mobilitydb.spark.util.TimeUtil; + +/** + * Spark SQL UDFs for TBox (temporal numeric bounding box) accessor operations. + * + * TBox values are stored as hex-WKB strings (tbox_as_hexwkb output). + * + * Numeric bound accessors (xmin/xmax) use output-pointer pattern: JMEOS + * allocates an 8-byte buffer, passes it as out-pointer, returns null if + * the box has no X component. + * + * Temporal bound accessors (tmin/tmax) use the same pattern with int64 + * PG-epoch microseconds. Inclusivity accessors use a 1-byte output buffer. + * + * MEOS function authority: meos/include/meos.h + */ +public final class TBoxUDFs { + + private TBoxUDFs() {} + + // milliseconds from Unix epoch (1970-01-01) to PG epoch (2000-01-01) + private static Pointer tboxPtr(String hex) { + if (hex == null) return null; + return GeneratedFunctions.tbox_from_hexwkb(hex); + } + + // ------------------------------------------------------------------ + // Has-component flags + // ------------------------------------------------------------------ + + public static final UDF1 tboxHasx = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = tboxPtr(hex); + if (p == null) return null; + return GeneratedFunctions.tbox_hasx(p); + }; + + public static final UDF1 tboxHast = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = tboxPtr(hex); + if (p == null) return null; + return GeneratedFunctions.tbox_hast(p); + }; + + // ------------------------------------------------------------------ + // Numeric (X) bound accessors (Pointer β†’ double at offset 0) + // ------------------------------------------------------------------ + + public static final UDF1 tboxXmin = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = tboxPtr(hex); + if (p == null) return null; + Pointer r = GeneratedFunctions.tbox_xmin(p); + return r == null ? null : r.getDouble(0); + }; + + public static final UDF1 tboxXmax = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = tboxPtr(hex); + if (p == null) return null; + Pointer r = GeneratedFunctions.tbox_xmax(p); + return r == null ? null : r.getDouble(0); + }; + + public static final UDF1 tboxXminInc = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = tboxPtr(hex); + if (p == null) return null; + Pointer r = GeneratedFunctions.tbox_xmin_inc(p); + return r == null ? null : r.getByte(0) != 0; + }; + + public static final UDF1 tboxXmaxInc = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = tboxPtr(hex); + if (p == null) return null; + Pointer r = GeneratedFunctions.tbox_xmax_inc(p); + return r == null ? null : r.getByte(0) != 0; + }; + + // ------------------------------------------------------------------ + // Temporal (T) bound accessors (Pointer β†’ int64 PG-epoch ΞΌs at offset 0) + // ------------------------------------------------------------------ + + public static final UDF1 tboxTmin = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = tboxPtr(hex); + if (p == null) return null; + Pointer r = GeneratedFunctions.tbox_tmin(p); + if (r == null) return null; + return new java.sql.Timestamp(r.getLong(0) / 1000L + TimeUtil.PG_UNIX_EPOCH_OFFSET_MS); + }; + + public static final UDF1 tboxTmax = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = tboxPtr(hex); + if (p == null) return null; + Pointer r = GeneratedFunctions.tbox_tmax(p); + if (r == null) return null; + return new java.sql.Timestamp(r.getLong(0) / 1000L + TimeUtil.PG_UNIX_EPOCH_OFFSET_MS); + }; + + public static final UDF1 tboxTminInc = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = tboxPtr(hex); + if (p == null) return null; + Pointer r = GeneratedFunctions.tbox_tmin_inc(p); + return r == null ? null : r.getByte(0) != 0; + }; + + public static final UDF1 tboxTmaxInc = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = tboxPtr(hex); + if (p == null) return null; + Pointer r = GeneratedFunctions.tbox_tmax_inc(p); + return r == null ? null : r.getByte(0) != 0; + }; + + // ------------------------------------------------------------------ + // Span conversions (tbox_hex β†’ span hex-WKB) + // ------------------------------------------------------------------ + + public static final UDF1 tboxToIntspan = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = tboxPtr(hex); + if (p == null) return null; + Pointer span = GeneratedFunctions.tbox_to_intspan(p); + if (span == null) return null; + return GeneratedFunctions.span_as_hexwkb(span, (byte) 0); + }; + + public static final UDF1 tboxToFloatspan = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = tboxPtr(hex); + if (p == null) return null; + Pointer span = GeneratedFunctions.tbox_to_floatspan(p); + if (span == null) return null; + return GeneratedFunctions.span_as_hexwkb(span, (byte) 0); + }; + + public static final UDF1 tboxToTstzspan = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = tboxPtr(hex); + if (p == null) return null; + Pointer span = GeneratedFunctions.tbox_to_tstzspan(p); + if (span == null) return null; + return GeneratedFunctions.span_as_hexwkb(span, (byte) 0); + }; + + // ------------------------------------------------------------------ + // Conversion from span / spanset / set to TBox + // ------------------------------------------------------------------ + + // spanToTbox(spanHex STRING) β†’ STRING + // MEOS: span_to_tbox(const Span *) β†’ TBox * + public static final UDF1 spanToTbox = + (spanHex) -> { + if (spanHex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.span_from_hexwkb(spanHex); + if (p == null) return null; + Pointer tb = GeneratedFunctions.span_to_tbox(p); + if (tb == null) return null; + try { + jnr.ffi.Runtime rt = jnr.ffi.Runtime.getSystemRuntime(); + return GeneratedFunctions.tbox_as_hexwkb(tb, (byte) 0, rt.getMemoryManager().allocateDirect(8)); + } finally { + MeosMemory.free(tb); + } + }; + + // spansetToTbox(spansetHex STRING) β†’ STRING + // MEOS: spanset_to_tbox(const SpanSet *) β†’ TBox * + public static final UDF1 spansetToTbox = + (spansetHex) -> { + if (spansetHex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.spanset_from_hexwkb(spansetHex); + if (p == null) return null; + Pointer tb = GeneratedFunctions.spanset_to_tbox(p); + if (tb == null) return null; + try { + jnr.ffi.Runtime rt = jnr.ffi.Runtime.getSystemRuntime(); + return GeneratedFunctions.tbox_as_hexwkb(tb, (byte) 0, rt.getMemoryManager().allocateDirect(8)); + } finally { + MeosMemory.free(tb); + } + }; + + // setToTbox(setHex STRING) β†’ STRING + // MEOS: set_to_tbox(const Set *) β†’ TBox * + public static final UDF1 setToTbox = + (setHex) -> { + if (setHex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.set_from_hexwkb(setHex); + if (p == null) return null; + Pointer tb = GeneratedFunctions.set_to_tbox(p); + if (tb == null) return null; + try { + jnr.ffi.Runtime rt = jnr.ffi.Runtime.getSystemRuntime(); + return GeneratedFunctions.tbox_as_hexwkb(tb, (byte) 0, rt.getMemoryManager().allocateDirect(8)); + } finally { + MeosMemory.free(tb); + } + }; + + // ------------------------------------------------------------------ + // Typed X-bound accessors (tboxfloat / tboxint variants) + // + // These differ from the generic tboxXmin/tboxXmax above in that they + // return integer values for tboxint and preserve float precision for + // tboxfloat β€” using the typed MEOS accessors rather than the generic ones. + // + // MEOS: tboxfloat_xmin, tboxfloat_xmax β†’ double * + // tboxint_xmin, tboxint_xmax β†’ int * (MEOS uses int32) + // ------------------------------------------------------------------ + + public static final UDF1 tboxfloatXmin = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = tboxPtr(hex); + if (p == null) return null; + Pointer r = GeneratedFunctions.tboxfloat_xmin(p); + return r == null ? null : r.getDouble(0); + }; + + public static final UDF1 tboxfloatXmax = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = tboxPtr(hex); + if (p == null) return null; + Pointer r = GeneratedFunctions.tboxfloat_xmax(p); + return r == null ? null : r.getDouble(0); + }; + + public static final UDF1 tboxintXmin = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = tboxPtr(hex); + if (p == null) return null; + Pointer r = GeneratedFunctions.tboxint_xmin(p); + return r == null ? null : r.getInt(0); + }; + + public static final UDF1 tboxintXmax = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = tboxPtr(hex); + if (p == null) return null; + Pointer r = GeneratedFunctions.tboxint_xmax(p); + return r == null ? null : r.getInt(0); + }; + + // ------------------------------------------------------------------ + // TBox constructors (make, from numspan+timestamptz, from timestamptz) + // + // MEOS: tbox_make, numspan_timestamptz_to_tbox, timestamptz_to_tbox + // ------------------------------------------------------------------ + + // tboxMake(numspanHex STRING, tstzspanHex STRING) β†’ STRING + // Either argument may be null to produce an X-only or T-only TBox. + public static final UDF2 tboxMake = + (numspanHex, tstzspanHex) -> { + if (numspanHex == null && tstzspanHex == null) return null; + MeosThread.ensureReady(); + Pointer numspan = numspanHex != null ? GeneratedFunctions.span_from_hexwkb(numspanHex) : null; + Pointer tstzspan = tstzspanHex != null ? GeneratedFunctions.span_from_hexwkb(tstzspanHex) : null; + Pointer result = GeneratedFunctions.tbox_make(numspan, tstzspan); + if (result == null) return null; + try { + return GeneratedFunctions.tbox_as_hexwkb(result, (byte) 0, + Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8)); + } finally { + MeosMemory.free(result); + } + }; + + // timestamptzToTbox(ts TIMESTAMP) β†’ STRING + // MEOS: timestamptz_to_tbox(TimestampTz) β†’ TBox * + public static final UDF1 timestamptzToTbox = + (ts) -> { + if (ts == null) return null; + MeosThread.ensureReady(); + long pgMicros = (ts.getTime() - TimeUtil.PG_UNIX_EPOCH_OFFSET_MS) * 1000L; + OffsetDateTime odt = OffsetDateTime.ofInstant(Instant.ofEpochSecond(pgMicros, 0), ZoneOffset.UTC); + Pointer result = GeneratedFunctions.timestamptz_to_tbox(odt); + if (result == null) return null; + try { + return GeneratedFunctions.tbox_as_hexwkb(result, (byte) 0, + Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8)); + } finally { + MeosMemory.free(result); + } + }; + + // numspanTimestamptzToTbox(spanHex STRING, ts TIMESTAMP) β†’ STRING + // MEOS: numspan_timestamptz_to_tbox(const Span *, TimestampTz) β†’ TBox * + public static final UDF2 numspanTimestamptzToTbox = + (spanHex, ts) -> { + if (spanHex == null || ts == null) return null; + MeosThread.ensureReady(); + Pointer span = GeneratedFunctions.span_from_hexwkb(spanHex); + if (span == null) return null; + long pgMicros = (ts.getTime() - TimeUtil.PG_UNIX_EPOCH_OFFSET_MS) * 1000L; + OffsetDateTime odt = OffsetDateTime.ofInstant(Instant.ofEpochSecond(pgMicros, 0), ZoneOffset.UTC); + Pointer result = GeneratedFunctions.numspan_timestamptz_to_tbox(span, odt); + if (result == null) return null; + try { + return GeneratedFunctions.tbox_as_hexwkb(result, (byte) 0, + Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8)); + } finally { + MeosMemory.free(result); + } + }; + + // ------------------------------------------------------------------ + // TBox time-dimension transforms + // + // MEOS: tbox_expand_time, tbox_shift_scale_time + // ------------------------------------------------------------------ + + // tboxExpandTime(hex STRING, intervalStr STRING) β†’ STRING + public static final UDF2 tboxExpandTime = + (hex, intervalStr) -> { + if (hex == null || intervalStr == null) return null; + MeosThread.ensureReady(); + Pointer p = tboxPtr(hex); + if (p == null) return null; + Pointer iv = GeneratedFunctions.pg_interval_in(intervalStr, -1); + if (iv == null) return null; + Pointer result = GeneratedFunctions.tbox_expand_time(p, iv); + if (result == null) return null; + try { + return GeneratedFunctions.tbox_as_hexwkb(result, (byte) 0, + Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8)); + } finally { + MeosMemory.free(result); + } + }; + + // tboxExpandFloat(hex STRING, value DOUBLE) β†’ STRING + // MEOS: tfloatbox_expand (renamed; not in JMEOS-1.4) + public static final UDF2 tboxExpandFloat = + (hex, v) -> { + if (hex == null || v == null) return null; + MeosThread.ensureReady(); + Pointer p = tboxPtr(hex); + if (p == null) return null; + Pointer result = org.mobilitydb.spark.MeosNative.INSTANCE.tfloatbox_expand(p, v); + if (result == null) return null; + try { + return GeneratedFunctions.tbox_as_hexwkb(result, (byte) 0, + Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8)); + } finally { + MeosMemory.free(result); + } + }; + + // tboxExpandInt(hex STRING, value INT) β†’ STRING + // MEOS: tintbox_expand (renamed; not in JMEOS-1.4) + public static final UDF2 tboxExpandInt = + (hex, v) -> { + if (hex == null || v == null) return null; + MeosThread.ensureReady(); + Pointer p = tboxPtr(hex); + if (p == null) return null; + Pointer result = org.mobilitydb.spark.MeosNative.INSTANCE.tintbox_expand(p, v); + if (result == null) return null; + try { + return GeneratedFunctions.tbox_as_hexwkb(result, (byte) 0, + Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8)); + } finally { + MeosMemory.free(result); + } + }; + + // tboxShiftScaleTime(hex STRING, shiftStr STRING, scaleStr STRING) β†’ STRING + // Either shiftStr or scaleStr (but not both) may be null. + public static final UDF3 tboxShiftScaleTime = + (hex, shiftStr, scaleStr) -> { + if (hex == null) return null; + if (shiftStr == null && scaleStr == null) return null; + MeosThread.ensureReady(); + Pointer p = tboxPtr(hex); + if (p == null) return null; + Pointer shiftIv = shiftStr != null ? GeneratedFunctions.pg_interval_in(shiftStr, -1) : null; + Pointer scaleIv = scaleStr != null ? GeneratedFunctions.pg_interval_in(scaleStr, -1) : null; + Pointer result = GeneratedFunctions.tbox_shift_scale_time(p, shiftIv, scaleIv); + if (result == null) return null; + try { + return GeneratedFunctions.tbox_as_hexwkb(result, (byte) 0, + Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8)); + } finally { + MeosMemory.free(result); + } + }; + + // ------------------------------------------------------------------ + // Rounding + // ------------------------------------------------------------------ + + // tboxRound(hex STRING, maxDecimals INT) β†’ STRING + // MEOS: tbox_round(const TBox *, int) β†’ TBox * + public static final UDF2 tboxRound = + (hex, maxDecimals) -> { + if (hex == null || maxDecimals == null) return null; + MeosThread.ensureReady(); + Pointer p = tboxPtr(hex); + if (p == null) return null; + Pointer result = GeneratedFunctions.tbox_round(p, maxDecimals); + if (result == null) return null; + try { + jnr.ffi.Runtime rt = jnr.ffi.Runtime.getSystemRuntime(); + Pointer sizeOut = rt.getMemoryManager().allocateDirect(8); + return GeneratedFunctions.tbox_as_hexwkb(result, (byte) 0, sizeOut); + } finally { + MeosMemory.free(result); + } + }; + + // ------------------------------------------------------------------ + // Registration + // ------------------------------------------------------------------ + + public static void registerAll(SparkSession spark) { + spark.udf().register("tboxHasx", tboxHasx, DataTypes.BooleanType); + spark.udf().register("tboxHast", tboxHast, DataTypes.BooleanType); + spark.udf().register("tboxXmin", tboxXmin, DataTypes.DoubleType); + spark.udf().register("tboxXmax", tboxXmax, DataTypes.DoubleType); + spark.udf().register("tboxXminInc", tboxXminInc, DataTypes.BooleanType); + spark.udf().register("tboxXmaxInc", tboxXmaxInc, DataTypes.BooleanType); + spark.udf().register("tboxTmin", tboxTmin, DataTypes.TimestampType); + spark.udf().register("tboxTmax", tboxTmax, DataTypes.TimestampType); + spark.udf().register("tboxTminInc", tboxTminInc, DataTypes.BooleanType); + spark.udf().register("tboxTmaxInc", tboxTmaxInc, DataTypes.BooleanType); + spark.udf().register("tboxToIntspan", tboxToIntspan, DataTypes.StringType); + spark.udf().register("tboxToFloatspan", tboxToFloatspan, DataTypes.StringType); + spark.udf().register("tboxToTstzspan", tboxToTstzspan, DataTypes.StringType); + spark.udf().register("tboxRound", tboxRound, DataTypes.StringType); + // Conversion from span / spanset / set to TBox + spark.udf().register("spanToTbox", spanToTbox, DataTypes.StringType); + spark.udf().register("spansetToTbox", spansetToTbox, DataTypes.StringType); + spark.udf().register("setToTbox", setToTbox, DataTypes.StringType); + // Typed X-bound accessors + spark.udf().register("tboxfloatXmin", tboxfloatXmin, DataTypes.DoubleType); + spark.udf().register("tboxfloatXmax", tboxfloatXmax, DataTypes.DoubleType); + spark.udf().register("tboxintXmin", tboxintXmin, DataTypes.IntegerType); + spark.udf().register("tboxintXmax", tboxintXmax, DataTypes.IntegerType); + // TBox constructors + spark.udf().register("tboxMake", tboxMake, DataTypes.StringType); + spark.udf().register("timestamptzToTbox", timestamptzToTbox, DataTypes.StringType); + spark.udf().register("numspanTimestamptzToTbox", numspanTimestamptzToTbox, DataTypes.StringType); + // TBox time-dimension transforms + spark.udf().register("tboxExpandTime", tboxExpandTime, DataTypes.StringType); + spark.udf().register("tboxExpandFloat", tboxExpandFloat, DataTypes.StringType); + spark.udf().register("tboxExpandInt", tboxExpandInt, DataTypes.StringType); + spark.udf().register("tboxShiftScaleTime", tboxShiftScaleTime, DataTypes.StringType); + // TBox set operations + spark.udf().register("intersectionTboxTbox", intersectionTboxTbox, DataTypes.StringType); + spark.udf().register("unionTboxTbox", unionTboxTbox, DataTypes.StringType); + // MobilityDB SQL bare-name aliases for the same lambdas + spark.udf().register("tboxIntersection", intersectionTboxTbox, DataTypes.StringType); + spark.udf().register("tboxUnion", unionTboxTbox, DataTypes.StringType); + // expandValue alias β€” covers float/int dispatch via Object input; + // most users will use the typed tboxExpandFloat/tboxExpandInt directly. + spark.udf().register("expandValue", tboxExpandFloat, DataTypes.StringType); + // TBox topology predicates (tbox, tbox) + spark.udf().register("tboxContains", tboxContains, DataTypes.BooleanType); + spark.udf().register("tboxContained", tboxContained, DataTypes.BooleanType); + spark.udf().register("tboxOverlaps", tboxOverlaps, DataTypes.BooleanType); + // TBox positional predicates (tbox, tbox) + spark.udf().register("tboxLeft", tboxLeft, DataTypes.BooleanType); + spark.udf().register("tboxOverleft", tboxOverleft, DataTypes.BooleanType); + spark.udf().register("tboxRight", tboxRight, DataTypes.BooleanType); + spark.udf().register("tboxOverright", tboxOverright, DataTypes.BooleanType); + spark.udf().register("tboxBefore", tboxBefore, DataTypes.BooleanType); + spark.udf().register("tboxOverbefore", tboxOverbefore, DataTypes.BooleanType); + spark.udf().register("tboxAfter", tboxAfter, DataTypes.BooleanType); + spark.udf().register("tboxOverafter", tboxOverafter, DataTypes.BooleanType); + spark.udf().register("tboxAdjacent", tboxAdjacent, DataTypes.BooleanType); + } + + // ------------------------------------------------------------------ + // TBox set operations + // MEOS: intersection_tbox_tbox(TBox *, TBox *) β†’ TBox * (NULL if empty) + // union_tbox_tbox(TBox *, TBox *, bool strict) β†’ TBox * + // ------------------------------------------------------------------ + + private static String tboxBinOp(String h1, String h2, + java.util.function.BiFunction fn) { + if (h1 == null || h2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = tboxPtr(h1), p2 = tboxPtr(h2); + if (p1 == null || p2 == null) return null; + Runtime rt = Runtime.getSystemRuntime(); + try { + Pointer r = fn.apply(p1, p2); + if (r == null) return null; + try { + return GeneratedFunctions.tbox_as_hexwkb(r, (byte) 0, rt.getMemoryManager().allocateDirect(8)); + } finally { MeosMemory.free(r); } + } finally { + MeosMemory.free(p1); + MeosMemory.free(p2); + } + } + + public static final UDF2 intersectionTboxTbox = + (h1, h2) -> tboxBinOp(h1, h2, GeneratedFunctions::intersection_tbox_tbox); + + public static final UDF2 unionTboxTbox = + (h1, h2) -> tboxBinOp(h1, h2, (p1, p2) -> GeneratedFunctions.union_tbox_tbox(p1, p2, false)); + + // ------------------------------------------------------------------ + // TBox positional predicates (tbox, tbox) β†’ Boolean + // MEOS: left/overleft/right/overright/before/overbefore/after/overafter/adjacent + // _tbox_tbox(TBox *, TBox *) β†’ bool + // ------------------------------------------------------------------ + + private static Boolean tboxBoolOp(String h1, String h2, + java.util.function.BiFunction fn) { + if (h1 == null || h2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = tboxPtr(h1), p2 = tboxPtr(h2); + if (p1 == null || p2 == null) return null; + return fn.apply(p1, p2); + } + + // ------------------------------------------------------------------ + // TBox topology predicates (tbox, tbox) β†’ Boolean + // MEOS: contains/contained/overlaps_tbox_tbox β†’ bool + // ------------------------------------------------------------------ + + public static final UDF2 tboxContains = + (h1, h2) -> tboxBoolOp(h1, h2, GeneratedFunctions::contains_tbox_tbox); + public static final UDF2 tboxContained = + (h1, h2) -> tboxBoolOp(h1, h2, GeneratedFunctions::contained_tbox_tbox); + public static final UDF2 tboxOverlaps = + (h1, h2) -> tboxBoolOp(h1, h2, GeneratedFunctions::overlaps_tbox_tbox); + + public static final UDF2 tboxLeft = + (h1, h2) -> tboxBoolOp(h1, h2, GeneratedFunctions::left_tbox_tbox); + public static final UDF2 tboxOverleft = + (h1, h2) -> tboxBoolOp(h1, h2, GeneratedFunctions::overleft_tbox_tbox); + public static final UDF2 tboxRight = + (h1, h2) -> tboxBoolOp(h1, h2, GeneratedFunctions::right_tbox_tbox); + public static final UDF2 tboxOverright = + (h1, h2) -> tboxBoolOp(h1, h2, GeneratedFunctions::overright_tbox_tbox); + public static final UDF2 tboxBefore = + (h1, h2) -> tboxBoolOp(h1, h2, GeneratedFunctions::before_tbox_tbox); + public static final UDF2 tboxOverbefore = + (h1, h2) -> tboxBoolOp(h1, h2, GeneratedFunctions::overbefore_tbox_tbox); + public static final UDF2 tboxAfter = + (h1, h2) -> tboxBoolOp(h1, h2, GeneratedFunctions::after_tbox_tbox); + public static final UDF2 tboxOverafter = + (h1, h2) -> tboxBoolOp(h1, h2, GeneratedFunctions::overafter_tbox_tbox); + public static final UDF2 tboxAdjacent = + (h1, h2) -> tboxBoolOp(h1, h2, GeneratedFunctions::adjacent_tbox_tbox); +} diff --git a/src/main/java/org/mobilitydb/spark/temporal/TTextUDFs.java b/src/main/java/org/mobilitydb/spark/temporal/TTextUDFs.java new file mode 100644 index 00000000..a8c4eafe --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/temporal/TTextUDFs.java @@ -0,0 +1,280 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosThread; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.api.java.UDF1; +import org.apache.spark.sql.api.java.UDF2; +import org.apache.spark.sql.types.DataTypes; + +/** + * Spark SQL UDFs for ttext case-conversion operations. + * + * MEOS function authority: meos/include/meos.h + */ +public final class TTextUDFs { + + private TTextUDFs() {} + + // ttext_upper(ttext hex-WKB) β†’ ttext hex-WKB + public static final UDF1 ttextUpper = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.temporal_from_hexwkb(hex); + if (p == null) return null; + Pointer r = GeneratedFunctions.ttext_upper(p); + if (r == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + }; + + // ttext_lower(ttext hex-WKB) β†’ ttext hex-WKB + public static final UDF1 ttextLower = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.temporal_from_hexwkb(hex); + if (p == null) return null; + Pointer r = GeneratedFunctions.ttext_lower(p); + if (r == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + }; + + // ttext_initcap(ttext hex-WKB) β†’ ttext hex-WKB + public static final UDF1 ttextInitcap = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.temporal_from_hexwkb(hex); + if (p == null) return null; + Pointer r = GeneratedFunctions.ttext_initcap(p); + if (r == null) return null; + return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); + }; + + // ------------------------------------------------------------------ + // ttext comparison operators (scalar text vs ttext) + // + // MEOS: teq/tne/tlt/tle/tgt/tge_text_ttext(text *, Temporal *) β†’ Temporal * + // teq/tne/tlt/tle/tgt/tge_ttext_text(Temporal *, text *) β†’ Temporal * + // + // text * is created from a String via a dummy single-instant ttext and + // ttext_value_n, since text_in is not exposed by JMEOS-1.4. + // + // Both operators return a tbool hex-WKB (temporal boolean). + // ------------------------------------------------------------------ + + // Helper: allocate a MEOS text* from a Java String. + // Returns {textPtr, dummyTtext}; caller must MeosMemory.free() both. + private static Pointer[] makeTextPtr(String val) { + Pointer dummy = GeneratedFunctions.ttext_in(val + "@2000-01-01 00:00:00+00"); + if (dummy == null) return null; + Pointer textPtr = GeneratedFunctions.ttext_value_n(dummy, 1); + if (textPtr == null) { MeosMemory.free(dummy); return null; } + return new Pointer[]{textPtr, dummy}; + } + + private static String ttextCompare(String textVal, String ttextHex, java.util.function.BiFunction fn) { + if (textVal == null || ttextHex == null) return null; + MeosThread.ensureReady(); + Pointer[] tp = makeTextPtr(textVal); + if (tp == null) return null; + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(ttextHex); + if (tptr == null) { MeosMemory.free(tp[0]); MeosMemory.free(tp[1]); return null; } + try { + Pointer result = fn.apply(tp[0], tptr); + if (result == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); } + finally { MeosMemory.free(result); } + } finally { + MeosMemory.free(tptr); + MeosMemory.free(tp[0]); + MeosMemory.free(tp[1]); + } + } + + private static String ttextCompareRev(String ttextHex, String textVal, java.util.function.BiFunction fn) { + if (ttextHex == null || textVal == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(ttextHex); + if (tptr == null) return null; + Pointer[] tp = makeTextPtr(textVal); + if (tp == null) { MeosMemory.free(tptr); return null; } + try { + Pointer result = fn.apply(tptr, tp[0]); + if (result == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); } + finally { MeosMemory.free(result); } + } finally { + MeosMemory.free(tptr); + MeosMemory.free(tp[0]); + MeosMemory.free(tp[1]); + } + } + + // text op ttext + public static final UDF2 teqTextTtext = + (textVal, ttextHex) -> ttextCompare(textVal, ttextHex, GeneratedFunctions::teq_text_ttext); + public static final UDF2 tneTextTtext = + (textVal, ttextHex) -> ttextCompare(textVal, ttextHex, GeneratedFunctions::tne_text_ttext); + public static final UDF2 tltTextTtext = + (textVal, ttextHex) -> ttextCompare(textVal, ttextHex, GeneratedFunctions::tlt_text_ttext); + public static final UDF2 tleTextTtext = + (textVal, ttextHex) -> ttextCompare(textVal, ttextHex, GeneratedFunctions::tle_text_ttext); + public static final UDF2 tgtTextTtext = + (textVal, ttextHex) -> ttextCompare(textVal, ttextHex, GeneratedFunctions::tgt_text_ttext); + public static final UDF2 tgeTextTtext = + (textVal, ttextHex) -> ttextCompare(textVal, ttextHex, GeneratedFunctions::tge_text_ttext); + + // ttext op text + public static final UDF2 teqTtextText = + (ttextHex, textVal) -> ttextCompareRev(ttextHex, textVal, GeneratedFunctions::teq_ttext_text); + public static final UDF2 tneTtextText = + (ttextHex, textVal) -> ttextCompareRev(ttextHex, textVal, GeneratedFunctions::tne_ttext_text); + public static final UDF2 tltTtextText = + (ttextHex, textVal) -> ttextCompareRev(ttextHex, textVal, GeneratedFunctions::tlt_ttext_text); + public static final UDF2 tleTtextText = + (ttextHex, textVal) -> ttextCompareRev(ttextHex, textVal, GeneratedFunctions::tle_ttext_text); + public static final UDF2 tgtTtextText = + (ttextHex, textVal) -> ttextCompareRev(ttextHex, textVal, GeneratedFunctions::tgt_ttext_text); + public static final UDF2 tgeTtextText = + (ttextHex, textVal) -> ttextCompareRev(ttextHex, textVal, GeneratedFunctions::tge_ttext_text); + + public static void registerAll(SparkSession spark) { + spark.udf().register("ttextUpper", ttextUpper, DataTypes.StringType); + spark.udf().register("ttextLower", ttextLower, DataTypes.StringType); + spark.udf().register("ttextInitcap", ttextInitcap, DataTypes.StringType); + // text op ttext comparison operators + spark.udf().register("teqTextTtext", teqTextTtext, DataTypes.StringType); + spark.udf().register("tneTextTtext", tneTextTtext, DataTypes.StringType); + spark.udf().register("tltTextTtext", tltTextTtext, DataTypes.StringType); + spark.udf().register("tleTextTtext", tleTextTtext, DataTypes.StringType); + spark.udf().register("tgtTextTtext", tgtTextTtext, DataTypes.StringType); + spark.udf().register("tgeTextTtext", tgeTextTtext, DataTypes.StringType); + // ttext op text comparison operators + spark.udf().register("teqTtextText", teqTtextText, DataTypes.StringType); + spark.udf().register("tneTtextText", tneTtextText, DataTypes.StringType); + spark.udf().register("tltTtextText", tltTtextText, DataTypes.StringType); + spark.udf().register("tleTtextText", tleTtextText, DataTypes.StringType); + spark.udf().register("tgtTtextText", tgtTtextText, DataTypes.StringType); + spark.udf().register("tgeTtextText", tgeTtextText, DataTypes.StringType); + + // ttext concatenation (MEOS textcat_ttext_*) + spark.udf().register("ttextCatTtextText", ttextCatTtextText, DataTypes.StringType); + spark.udf().register("ttextCatTextTtext", ttextCatTextTtext, DataTypes.StringType); + spark.udf().register("ttextCatTtextTtext", ttextCatTtextTtext, DataTypes.StringType); + // MobilityDB SQL bare-name alias + spark.udf().register("ttextCat", ttextCatTtextTtext, DataTypes.StringType); + // textset concatenation + spark.udf().register("textsetCatTextsetText", textsetCatTextsetText, DataTypes.StringType); + spark.udf().register("textsetCatTextTextset", textsetCatTextTextset, DataTypes.StringType); + spark.udf().register("textsetCat", textsetCatTextsetText, DataTypes.StringType); + } + + public static final UDF2 textsetCatTextsetText = + (setHex, txt) -> { + if (setHex == null || txt == null) return null; + MeosThread.ensureReady(); + Pointer s = GeneratedFunctions.set_from_hexwkb(setHex); + if (s == null) return null; + Pointer t = GeneratedFunctions.cstring_to_text(txt); + if (t == null) { MeosMemory.free(s); return null; } + try { + Pointer r = GeneratedFunctions.textcat_textset_text(s, t); + if (r == null) return null; + try { return GeneratedFunctions.set_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(s, t); } + }; + + public static final UDF2 textsetCatTextTextset = + (txt, setHex) -> { + if (txt == null || setHex == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.cstring_to_text(txt); + if (t == null) return null; + Pointer s = GeneratedFunctions.set_from_hexwkb(setHex); + if (s == null) { MeosMemory.free(t); return null; } + try { + Pointer r = GeneratedFunctions.textcat_text_textset(t, s); + if (r == null) return null; + try { return GeneratedFunctions.set_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(t, s); } + }; + + public static final UDF2 ttextCatTtextText = + (ttextHex, txt) -> { + if (ttextHex == null || txt == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.temporal_from_hexwkb(ttextHex); + if (p == null) return null; + Pointer t = GeneratedFunctions.cstring_to_text(txt); + if (t == null) { MeosMemory.free(p); return null; } + try { + Pointer r = org.mobilitydb.spark.MeosNative.INSTANCE.textcat_ttext_text(p, t); + if (r == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(p, t); } + }; + + public static final UDF2 ttextCatTextTtext = + (txt, ttextHex) -> { + if (txt == null || ttextHex == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.cstring_to_text(txt); + if (t == null) return null; + Pointer p = GeneratedFunctions.temporal_from_hexwkb(ttextHex); + if (p == null) { MeosMemory.free(t); return null; } + try { + Pointer r = org.mobilitydb.spark.MeosNative.INSTANCE.textcat_text_ttext(t, p); + if (r == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(t, p); } + }; + + public static final UDF2 ttextCatTtextTtext = + (h1, h2) -> { + if (h1 == null || h2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(h1); + if (p1 == null) return null; + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(h2); + if (p2 == null) { MeosMemory.free(p1); return null; } + try { + Pointer r = org.mobilitydb.spark.MeosNative.INSTANCE.textcat_ttext_ttext(p1, p2); + if (r == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(p1, p2); } + }; +} diff --git a/src/main/java/org/mobilitydb/spark/temporal/TemporalBoxOpsUDFs.java b/src/main/java/org/mobilitydb/spark/temporal/TemporalBoxOpsUDFs.java new file mode 100644 index 00000000..c26ae56e --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/temporal/TemporalBoxOpsUDFs.java @@ -0,0 +1,190 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosThread; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.api.java.UDF2; +import org.apache.spark.sql.types.DataTypes; + +import java.util.function.BiFunction; + +/** + * Spark SQL UDFs for cross-type box-overlap predicates on temporal types. + * + * Coverage: 30 UDFs across 5 predicates (adjacent, contained, contains, + * overlaps, same) Γ— 6 cross-types: + * tnumber Γ— tnumber β€” value+time bounding-box compare + * numspan Γ— tnumber β€” value-span Γ— tnumber bbox + * tnumber Γ— numspan β€” tnumber Γ— value-span bbox + * tstzspan Γ— temporal β€” time-span Γ— temporal bbox + * temporal Γ— tstzspan β€” temporal Γ— time-span bbox + * temporal Γ— temporal β€” temporal-vs-temporal bbox + * + * tboxΓ—tnumber, tnumberΓ—tbox, tboxΓ—tbox already covered by TBoxOpsUDFs. + * + * MEOS function authority: meos/include/meos.h + */ +public final class TemporalBoxOpsUDFs { + + private TemporalBoxOpsUDFs() {} + + private static UDF2 spanTemporal(BiFunction fn) { + return (h1, h2) -> { + if (h1 == null || h2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.span_from_hexwkb(h1); if (p1 == null) return null; + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(h2); + if (p2 == null) { MeosMemory.free(p1); return null; } + try { return fn.apply(p1, p2); } + finally { MeosMemory.free(p1, p2); } + }; + } + + private static UDF2 temporalSpan(BiFunction fn) { + return (h1, h2) -> { + if (h1 == null || h2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(h1); if (p1 == null) return null; + Pointer p2 = GeneratedFunctions.span_from_hexwkb(h2); + if (p2 == null) { MeosMemory.free(p1); return null; } + try { return fn.apply(p1, p2); } + finally { MeosMemory.free(p1, p2); } + }; + } + + private static UDF2 temporalTemporal(BiFunction fn) { + return (h1, h2) -> { + if (h1 == null || h2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(h1); if (p1 == null) return null; + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(h2); + if (p2 == null) { MeosMemory.free(p1); return null; } + try { return fn.apply(p1, p2); } + finally { MeosMemory.free(p1, p2); } + }; + } + + // ------------------------------------------------------------------ + // tnumber Γ— tnumber (5) + // ------------------------------------------------------------------ + + public static final UDF2 tnumberAdjacentTnumber = temporalTemporal(GeneratedFunctions::adjacent_tnumber_tnumber); + public static final UDF2 tnumberContainsTnumber = temporalTemporal(GeneratedFunctions::contains_tnumber_tnumber); + public static final UDF2 tnumberContainedTnumber = temporalTemporal(GeneratedFunctions::contained_tnumber_tnumber); + public static final UDF2 tnumberOverlapsTnumber = temporalTemporal(GeneratedFunctions::overlaps_tnumber_tnumber); + public static final UDF2 tnumberSameTnumber = temporalTemporal(GeneratedFunctions::same_tnumber_tnumber); + + // ------------------------------------------------------------------ + // numspan Γ— tnumber (5) + // ------------------------------------------------------------------ + + public static final UDF2 numspanAdjacentTnumber = spanTemporal(GeneratedFunctions::adjacent_numspan_tnumber); + public static final UDF2 numspanContainsTnumber = spanTemporal(GeneratedFunctions::contains_numspan_tnumber); + public static final UDF2 numspanContainedTnumber = spanTemporal(GeneratedFunctions::contained_numspan_tnumber); + public static final UDF2 numspanOverlapsTnumber = spanTemporal(GeneratedFunctions::overlaps_numspan_tnumber); + public static final UDF2 numspanSameTnumber = spanTemporal(GeneratedFunctions::same_numspan_tnumber); + + // ------------------------------------------------------------------ + // tnumber Γ— numspan (5) + // ------------------------------------------------------------------ + + public static final UDF2 tnumberAdjacentNumspan = temporalSpan(GeneratedFunctions::adjacent_tnumber_numspan); + public static final UDF2 tnumberContainsNumspan = temporalSpan(GeneratedFunctions::contains_tnumber_numspan); + public static final UDF2 tnumberContainedNumspan = temporalSpan(GeneratedFunctions::contained_tnumber_numspan); + public static final UDF2 tnumberOverlapsNumspan = temporalSpan(GeneratedFunctions::overlaps_tnumber_numspan); + public static final UDF2 tnumberSameNumspan = temporalSpan(GeneratedFunctions::same_tnumber_numspan); + + // ------------------------------------------------------------------ + // tstzspan Γ— temporal (5) + // ------------------------------------------------------------------ + + public static final UDF2 tstzspanAdjacentTemporal = spanTemporal(GeneratedFunctions::adjacent_tstzspan_temporal); + public static final UDF2 tstzspanContainsTemporal = spanTemporal(GeneratedFunctions::contains_tstzspan_temporal); + public static final UDF2 tstzspanContainedTemporal = spanTemporal(GeneratedFunctions::contained_tstzspan_temporal); + public static final UDF2 tstzspanOverlapsTemporal = spanTemporal(GeneratedFunctions::overlaps_tstzspan_temporal); + public static final UDF2 tstzspanSameTemporal = spanTemporal(GeneratedFunctions::same_tstzspan_temporal); + + // ------------------------------------------------------------------ + // temporal Γ— tstzspan (5) + // ------------------------------------------------------------------ + + public static final UDF2 temporalAdjacentTstzspan = temporalSpan(GeneratedFunctions::adjacent_temporal_tstzspan); + public static final UDF2 temporalContainsTstzspan = temporalSpan(GeneratedFunctions::contains_temporal_tstzspan); + public static final UDF2 temporalContainedTstzspan = temporalSpan(GeneratedFunctions::contained_temporal_tstzspan); + public static final UDF2 temporalOverlapsTstzspan = temporalSpan(GeneratedFunctions::overlaps_temporal_tstzspan); + public static final UDF2 temporalSameTstzspan = temporalSpan(GeneratedFunctions::same_temporal_tstzspan); + + // ------------------------------------------------------------------ + // temporal Γ— temporal (5) + // ------------------------------------------------------------------ + + public static final UDF2 temporalAdjacentTemporal = temporalTemporal(GeneratedFunctions::adjacent_temporal_temporal); + public static final UDF2 temporalContainsTemporal = temporalTemporal(GeneratedFunctions::contains_temporal_temporal); + public static final UDF2 temporalContainedTemporal = temporalTemporal(GeneratedFunctions::contained_temporal_temporal); + public static final UDF2 temporalOverlapsTemporal = temporalTemporal(GeneratedFunctions::overlaps_temporal_temporal); + public static final UDF2 temporalSameTemporal = temporalTemporal(GeneratedFunctions::same_temporal_temporal); + + public static void registerAll(SparkSession spark) { + // tnumber Γ— tnumber + spark.udf().register("tnumberAdjacentTnumber", tnumberAdjacentTnumber, DataTypes.BooleanType); + spark.udf().register("tnumberContainsTnumber", tnumberContainsTnumber, DataTypes.BooleanType); + spark.udf().register("tnumberContainedTnumber", tnumberContainedTnumber, DataTypes.BooleanType); + spark.udf().register("tnumberOverlapsTnumber", tnumberOverlapsTnumber, DataTypes.BooleanType); + spark.udf().register("tnumberSameTnumber", tnumberSameTnumber, DataTypes.BooleanType); + // numspan Γ— tnumber + spark.udf().register("numspanAdjacentTnumber", numspanAdjacentTnumber, DataTypes.BooleanType); + spark.udf().register("numspanContainsTnumber", numspanContainsTnumber, DataTypes.BooleanType); + spark.udf().register("numspanContainedTnumber", numspanContainedTnumber, DataTypes.BooleanType); + spark.udf().register("numspanOverlapsTnumber", numspanOverlapsTnumber, DataTypes.BooleanType); + spark.udf().register("numspanSameTnumber", numspanSameTnumber, DataTypes.BooleanType); + // tnumber Γ— numspan + spark.udf().register("tnumberAdjacentNumspan", tnumberAdjacentNumspan, DataTypes.BooleanType); + spark.udf().register("tnumberContainsNumspan", tnumberContainsNumspan, DataTypes.BooleanType); + spark.udf().register("tnumberContainedNumspan", tnumberContainedNumspan, DataTypes.BooleanType); + spark.udf().register("tnumberOverlapsNumspan", tnumberOverlapsNumspan, DataTypes.BooleanType); + spark.udf().register("tnumberSameNumspan", tnumberSameNumspan, DataTypes.BooleanType); + // tstzspan Γ— temporal + spark.udf().register("tstzspanAdjacentTemporal", tstzspanAdjacentTemporal, DataTypes.BooleanType); + spark.udf().register("tstzspanContainsTemporal", tstzspanContainsTemporal, DataTypes.BooleanType); + spark.udf().register("tstzspanContainedTemporal", tstzspanContainedTemporal, DataTypes.BooleanType); + spark.udf().register("tstzspanOverlapsTemporal", tstzspanOverlapsTemporal, DataTypes.BooleanType); + spark.udf().register("tstzspanSameTemporal", tstzspanSameTemporal, DataTypes.BooleanType); + // temporal Γ— tstzspan + spark.udf().register("temporalAdjacentTstzspan", temporalAdjacentTstzspan, DataTypes.BooleanType); + spark.udf().register("temporalContainsTstzspan", temporalContainsTstzspan, DataTypes.BooleanType); + spark.udf().register("temporalContainedTstzspan", temporalContainedTstzspan, DataTypes.BooleanType); + spark.udf().register("temporalOverlapsTstzspan", temporalOverlapsTstzspan, DataTypes.BooleanType); + spark.udf().register("temporalSameTstzspan", temporalSameTstzspan, DataTypes.BooleanType); + // temporal Γ— temporal β€” superseded 1:1 by the portable bare names + // adjacent/contains/contained/overlaps/same, registered by + // org.mobilitydb.spark.portable.PortableOperatorAliasUDFs reusing + // these very backing fields (one bare name, all six families). + } +} diff --git a/src/main/java/org/mobilitydb/spark/temporal/TemporalCompUDFs.java b/src/main/java/org/mobilitydb/spark/temporal/TemporalCompUDFs.java new file mode 100644 index 00000000..ec53057a --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/temporal/TemporalCompUDFs.java @@ -0,0 +1,238 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosThread; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.api.java.UDF2; +import org.apache.spark.sql.types.DataTypes; + +import java.util.function.BiFunction; +import java.util.function.ObjDoubleConsumer; + +/** + * Spark SQL UDFs for temporal comparison operators (`teq`, `tne`, `tlt`, + * `tle`, `tgt`, `tge`) returning a temporal boolean (hex-WKB tbool). + * + * MobilityDB exposes `temporal_teq(value, temporal)` and operators `#=`, + * `#<>`, etc. Spark SQL has no operator extension API, so these are + * registered as named UDFs. Equality/inequality are symmetric so only the + * forward direction (temporal first) is provided. + * + * MEOS function authority: meos/include/meos.h β€” teq_tint_int, teq_tfloat_float, + * teq_ttext_text, teq_tbool_bool, teq_temporal_temporal, and similarly + * for tne/tlt/tle/tgt/tge. + */ +public final class TemporalCompUDFs { + + private TemporalCompUDFs() {} + + // ------------------------------------------------------------------ + // Helpers β€” five families based on right-hand input type. + // ------------------------------------------------------------------ + + private static UDF2 hexInt(BiFunction fn) { + return (hex, v) -> { + if (hex == null || v == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.temporal_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer r = fn.apply(p, v); + if (r == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(p); } + }; + } + + private static UDF2 hexDouble(java.util.function.ToDoubleBiFunction _unused) { + // Not used β€” Java's BiFunction with primitive double is awkward; we + // inline the double calls below instead. + return null; + } + + @FunctionalInterface + private interface PointerDoubleFn { Pointer apply(Pointer a, double b); } + @FunctionalInterface + private interface PointerBoolFn { Pointer apply(Pointer a, boolean b); } + + private static UDF2 hexFloat(PointerDoubleFn fn) { + return (hex, v) -> { + if (hex == null || v == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.temporal_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer r = fn.apply(p, v); + if (r == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(p); } + }; + } + + private static UDF2 hexBool(PointerBoolFn fn) { + return (hex, v) -> { + if (hex == null || v == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.temporal_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer r = fn.apply(p, v); + if (r == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(p); } + }; + } + + private static UDF2 hexText(BiFunction fn) { + return (hex, txt) -> { + if (hex == null || txt == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.temporal_from_hexwkb(hex); + if (p == null) return null; + Pointer t = GeneratedFunctions.cstring_to_text(txt); + if (t == null) { MeosMemory.free(p); return null; } + try { + Pointer r = fn.apply(p, t); + if (r == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(p, t); } + }; + } + + private static UDF2 hexHex(BiFunction fn) { + return (h1, h2) -> { + if (h1 == null || h2 == null) return null; + MeosThread.ensureReady(); + Pointer p1 = GeneratedFunctions.temporal_from_hexwkb(h1); + if (p1 == null) return null; + Pointer p2 = GeneratedFunctions.temporal_from_hexwkb(h2); + if (p2 == null) { MeosMemory.free(p1); return null; } + try { + Pointer r = fn.apply(p1, p2); + if (r == null) return null; + try { return GeneratedFunctions.temporal_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(p1, p2); } + }; + } + + // ------------------------------------------------------------------ + // teq β€” temporal equality + // ------------------------------------------------------------------ + + public static final UDF2 teqTintInt = hexInt(GeneratedFunctions::teq_tint_int); + public static final UDF2 teqTfloatFloat = hexFloat(GeneratedFunctions::teq_tfloat_float); + public static final UDF2 teqTboolBool = hexBool(GeneratedFunctions::teq_tbool_bool); + public static final UDF2 teqTtextText = hexText(GeneratedFunctions::teq_ttext_text); + public static final UDF2 teqTemporal = hexHex(GeneratedFunctions::teq_temporal_temporal); + + // ------------------------------------------------------------------ + // tne β€” temporal inequality + // ------------------------------------------------------------------ + + public static final UDF2 tneTintInt = hexInt(GeneratedFunctions::tne_tint_int); + public static final UDF2 tneTfloatFloat = hexFloat(GeneratedFunctions::tne_tfloat_float); + public static final UDF2 tneTboolBool = hexBool(GeneratedFunctions::tne_tbool_bool); + public static final UDF2 tneTtextText = hexText(GeneratedFunctions::tne_ttext_text); + public static final UDF2 tneTemporal = hexHex(GeneratedFunctions::tne_temporal_temporal); + + // ------------------------------------------------------------------ + // tlt β€” temporal less-than + // ------------------------------------------------------------------ + + public static final UDF2 tltTintInt = hexInt(GeneratedFunctions::tlt_tint_int); + public static final UDF2 tltTfloatFloat = hexFloat(GeneratedFunctions::tlt_tfloat_float); + public static final UDF2 tltTtextText = hexText(GeneratedFunctions::tlt_ttext_text); + public static final UDF2 tltTemporal = hexHex(GeneratedFunctions::tlt_temporal_temporal); + + // ------------------------------------------------------------------ + // tle β€” temporal less-or-equal + // ------------------------------------------------------------------ + + public static final UDF2 tleTintInt = hexInt(GeneratedFunctions::tle_tint_int); + public static final UDF2 tleTfloatFloat = hexFloat(GeneratedFunctions::tle_tfloat_float); + public static final UDF2 tleTtextText = hexText(GeneratedFunctions::tle_ttext_text); + public static final UDF2 tleTemporal = hexHex(GeneratedFunctions::tle_temporal_temporal); + + // ------------------------------------------------------------------ + // tgt β€” temporal greater-than + // ------------------------------------------------------------------ + + public static final UDF2 tgtTintInt = hexInt(GeneratedFunctions::tgt_tint_int); + public static final UDF2 tgtTfloatFloat = hexFloat(GeneratedFunctions::tgt_tfloat_float); + public static final UDF2 tgtTtextText = hexText(GeneratedFunctions::tgt_ttext_text); + public static final UDF2 tgtTemporal = hexHex(GeneratedFunctions::tgt_temporal_temporal); + + // ------------------------------------------------------------------ + // tge β€” temporal greater-or-equal + // ------------------------------------------------------------------ + + public static final UDF2 tgeTintInt = hexInt(GeneratedFunctions::tge_tint_int); + public static final UDF2 tgeTfloatFloat = hexFloat(GeneratedFunctions::tge_tfloat_float); + public static final UDF2 tgeTtextText = hexText(GeneratedFunctions::tge_ttext_text); + public static final UDF2 tgeTemporal = hexHex(GeneratedFunctions::tge_temporal_temporal); + + public static void registerAll(SparkSession spark) { + // teq + spark.udf().register("teqTintInt", teqTintInt, DataTypes.StringType); + spark.udf().register("teqTfloatFloat", teqTfloatFloat, DataTypes.StringType); + spark.udf().register("teqTboolBool", teqTboolBool, DataTypes.StringType); + spark.udf().register("teqTtextText", teqTtextText, DataTypes.StringType); + // tne + spark.udf().register("tneTintInt", tneTintInt, DataTypes.StringType); + spark.udf().register("tneTfloatFloat", tneTfloatFloat, DataTypes.StringType); + spark.udf().register("tneTboolBool", tneTboolBool, DataTypes.StringType); + spark.udf().register("tneTtextText", tneTtextText, DataTypes.StringType); + // tlt + spark.udf().register("tltTintInt", tltTintInt, DataTypes.StringType); + spark.udf().register("tltTfloatFloat", tltTfloatFloat, DataTypes.StringType); + spark.udf().register("tltTtextText", tltTtextText, DataTypes.StringType); + // tle + spark.udf().register("tleTintInt", tleTintInt, DataTypes.StringType); + spark.udf().register("tleTfloatFloat", tleTfloatFloat, DataTypes.StringType); + spark.udf().register("tleTtextText", tleTtextText, DataTypes.StringType); + // tgt + spark.udf().register("tgtTintInt", tgtTintInt, DataTypes.StringType); + spark.udf().register("tgtTfloatFloat", tgtTfloatFloat, DataTypes.StringType); + spark.udf().register("tgtTtextText", tgtTtextText, DataTypes.StringType); + // tge + spark.udf().register("tgeTintInt", tgeTintInt, DataTypes.StringType); + spark.udf().register("tgeTfloatFloat", tgeTfloatFloat, DataTypes.StringType); + spark.udf().register("tgeTtextText", tgeTtextText, DataTypes.StringType); + // The temporal Γ— temporal forms teq/tne/tlt/tle/tgt/tge are + // superseded 1:1 by the portable bare names, registered by + // org.mobilitydb.spark.portable.PortableOperatorAliasUDFs reusing + // these very backing fields (one bare name, all six families). + } +} diff --git a/src/main/java/org/mobilitydb/spark/temporal/TemporalUDFs.java b/src/main/java/org/mobilitydb/spark/temporal/TemporalUDFs.java new file mode 100644 index 00000000..1c7ecda6 --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/temporal/TemporalUDFs.java @@ -0,0 +1,350 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import org.apache.spark.sql.api.java.UDF1; +import org.apache.spark.sql.api.java.UDF2; +import org.apache.spark.sql.types.DataTypes; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosThread; + +import java.sql.Timestamp; +import org.mobilitydb.spark.util.TimeUtil; + +/** + * Spark SQL UDFs for generic temporal operations (type-agnostic). + * + * Storage convention: temporal values are hex-WKB strings produced by + * temporal_as_hexwkb(ptr, (byte) 0) and parsed back with + * temporal_from_hexwkb(hex). + * + * Epoch note: MEOS uses PostgreSQL epoch (Β΅s since 2000-01-01); Spark uses + * UNIX epoch (ms since 1970-01-01). Conversion is done via pg_timestamptz_in() + * which stores the raw PG-epoch value in the OffsetDateTime's seconds field. + * Never call toEpochSecond() on a java.sql.Timestamp and pass it directly. + * + * MEOS function authority: meos/include/meos.h + * JMEOS PR: github.com/MobilityDB/JMEOS/pull/9 + */ +public final class TemporalUDFs { + + private TemporalUDFs() {} + + // PG epoch is 2000-01-01; Unix epoch is 1970-01-01. Difference = 946684800 s. + // JMEOS stores PG-epoch Β΅s in the OffsetDateTime's epoch-seconds field. + /** Convert a JMEOS OffsetDateTime (PG-epoch Β΅s in epoch-seconds field) to Spark Timestamp. */ + static Timestamp fromJmeosTimestamp(java.time.OffsetDateTime odt) { + // odt.toEpochSecond() holds the raw PG-epoch Β΅s (not real seconds). + long unixEpochMillis = odt.toEpochSecond() / 1000L + TimeUtil.PG_UNIX_EPOCH_OFFSET_MS; + return new Timestamp(unixEpochMillis); + } + + // ------------------------------------------------------------------ + // atTime(trip STRING, timeArg STRING|TIMESTAMP) β†’ STRING + // + // timeArg may be: + // - java.sql.Timestamp (Q3: QueryInstants.instant column, Spark TIMESTAMP type) + // - String span literal "[t1,t2]"/"(t1,t2]"/... (Q7: QueryPeriods.period) + // - String instant literal "YYYY-MM-DD HH:MM:SS+TZ" (plain string instant) + // + // MEOS: tstzspan_in + temporal_at_tstzspan (span case) + // temporal_at_timestamptz (instant case) + // ------------------------------------------------------------------ + public static final UDF2 atTime = + (trip, timeArg) -> { + if (trip == null || timeArg == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + try { + Pointer result; + if (timeArg instanceof java.sql.Timestamp) { + long pgEpochMicros = (((java.sql.Timestamp) timeArg).getTime() - TimeUtil.PG_UNIX_EPOCH_OFFSET_MS) * 1000L; + java.time.OffsetDateTime odt = java.time.OffsetDateTime.ofInstant( + java.time.Instant.ofEpochSecond(pgEpochMicros, 0), + java.time.ZoneOffset.UTC); + result = GeneratedFunctions.temporal_at_timestamptz(tptr, odt); + } else { + String s = timeArg.toString().trim(); + if (!s.isEmpty() && (s.charAt(0) == '[' || s.charAt(0) == '(')) { + Pointer spanPtr = GeneratedFunctions.tstzspan_in(s); + if (spanPtr == null) return null; + try { + result = GeneratedFunctions.temporal_at_tstzspan(tptr, spanPtr); + } finally { + MeosMemory.free(spanPtr); + } + } else { + java.time.OffsetDateTime odt = GeneratedFunctions.pg_timestamptz_in(s, -1); + if (odt == null) return null; + result = GeneratedFunctions.temporal_at_timestamptz(tptr, odt); + } + } + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // ------------------------------------------------------------------ + // startTimestamp(trip STRING) β†’ TIMESTAMP + // + // MEOS: temporal_start_timestamptz(const Temporal *) β†’ TimestampTz + // ------------------------------------------------------------------ + public static final UDF1 startTimestamp = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + try { + return fromJmeosTimestamp(GeneratedFunctions.temporal_start_timestamptz(ptr)); + } finally { + MeosMemory.free(ptr); + } + }; + + // ------------------------------------------------------------------ + // endTimestamp(trip STRING) β†’ TIMESTAMP + // + // MEOS: temporal_end_timestamptz(const Temporal *) β†’ TimestampTz + // ------------------------------------------------------------------ + public static final UDF1 endTimestamp = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + try { + return fromJmeosTimestamp(GeneratedFunctions.temporal_end_timestamptz(ptr)); + } finally { + MeosMemory.free(ptr); + } + }; + + // ------------------------------------------------------------------ + // numInstants(trip STRING) β†’ INT + // + // MEOS: temporal_num_instants(const Temporal *) β†’ int + // ------------------------------------------------------------------ + public static final UDF1 numInstants = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + try { + return GeneratedFunctions.temporal_num_instants(ptr); + } finally { + MeosMemory.free(ptr); + } + }; + + // ------------------------------------------------------------------ + // speed(trip STRING) β†’ STRING (hex-WKB of tfloat) + // + // MEOS: tpoint_speed(const Temporal *) β†’ Temporal * (tfloat) + // ------------------------------------------------------------------ + public static final UDF1 speed = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + try { + Pointer result = GeneratedFunctions.tpoint_speed(ptr); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // ------------------------------------------------------------------ + // atGeometry(trip STRING, geomWKT STRING) β†’ STRING + // + // Restricts a tgeompoint to the instants when it was inside geomWkt. + // + // MEOS: geo_from_text(const char *, int32_t) β†’ GSERIALIZED * + // tgeo_at_geom(const Temporal *, const GSERIALIZED *) β†’ Temporal * + // ------------------------------------------------------------------ + public static final UDF2 atGeometry = + (trip, geomWkt) -> { + if (trip == null || geomWkt == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (tptr == null) return null; + try { + Pointer gptr = GeneratedFunctions.geo_from_text(geomWkt, 0); + if (gptr == null) return null; + try { + Pointer result = GeneratedFunctions.tgeo_at_geom(tptr, gptr); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(gptr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // ------------------------------------------------------------------ + // asHexWKB(trip STRING) β†’ STRING + // + // Serializes a temporal value to the canonical MEOS hex-WKB string + // (little-endian, variant 0) β€” byte-for-byte identical across all platforms. + // + // MEOS: temporal_as_hexwkb(const Temporal *, uint8_t variant) β†’ char * + // ------------------------------------------------------------------ + public static final UDF1 asHexWKB = + (trip) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(ptr, (byte) 0); + } finally { + MeosMemory.free(ptr); + } + }; + + // ------------------------------------------------------------------ + // MFJSON output (hex-WKB in β†’ JSON string out) + // + // MEOS: temporal_as_mfjson(temp, withbbox, flags, precision, srs) + // flags=0 β†’ WKT geometry (not EWKT), precision controls decimal places + // ------------------------------------------------------------------ + + public static final UDF2 temporalAsMfjson = + (trip, precision) -> { + if (trip == null) return null; + MeosThread.ensureReady(); + int prec = (precision == null) ? 6 : precision; + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(trip); + if (ptr == null) return null; + try { + return GeneratedFunctions.temporal_as_mfjson(ptr, false, 0, prec, null); + } finally { + MeosMemory.free(ptr); + } + }; + + // ------------------------------------------------------------------ + // Text output (hex-WKB in β†’ WKT-like text string out) + // + // MEOS: tbool_out, tint_out, tfloat_out (with precision), ttext_out + // These mirror the PostgreSQL temporal type output GeneratedFunctions. + // ------------------------------------------------------------------ + + public static final UDF1 tboolOut = + (tbool) -> { + if (tbool == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(tbool); + if (ptr == null) return null; + try { + return GeneratedFunctions.tbool_out(ptr); + } finally { + MeosMemory.free(ptr); + } + }; + + public static final UDF1 tintOut = + (tint) -> { + if (tint == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(tint); + if (ptr == null) return null; + try { + return GeneratedFunctions.tint_out(ptr); + } finally { + MeosMemory.free(ptr); + } + }; + + public static final UDF2 tfloatOut = + (tfloat, precision) -> { + if (tfloat == null) return null; + MeosThread.ensureReady(); + int prec = (precision == null) ? 6 : precision; + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(tfloat); + if (ptr == null) return null; + try { + return GeneratedFunctions.tfloat_out(ptr, prec); + } finally { + MeosMemory.free(ptr); + } + }; + + public static final UDF1 ttextOut = + (ttext) -> { + if (ttext == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(ttext); + if (ptr == null) return null; + try { + return GeneratedFunctions.ttext_out(ptr); + } finally { + MeosMemory.free(ptr); + } + }; + + public static void registerAll(org.apache.spark.sql.SparkSession spark) { + spark.udf().register("atTime", atTime, DataTypes.StringType); + spark.udf().register("startTimestamp", startTimestamp, DataTypes.TimestampType); + spark.udf().register("endTimestamp", endTimestamp, DataTypes.TimestampType); + spark.udf().register("numInstants", numInstants, DataTypes.IntegerType); + spark.udf().register("speed", speed, DataTypes.StringType); + spark.udf().register("atGeometry", atGeometry, DataTypes.StringType); + spark.udf().register("asHexWKB", asHexWKB, DataTypes.StringType); + spark.udf().register("temporalAsMfjson", temporalAsMfjson, DataTypes.StringType); + spark.udf().register("tboolOut", tboolOut, DataTypes.StringType); + spark.udf().register("tintOut", tintOut, DataTypes.StringType); + spark.udf().register("tfloatOut", tfloatOut, DataTypes.StringType); + spark.udf().register("ttextOut", ttextOut, DataTypes.StringType); + + // MobilityDB SQL bare-name aliases for temporal-instant accessors + // (work for any single-instant temporal value) + spark.udf().register("getTimestamp", startTimestamp, DataTypes.TimestampType); + } +} diff --git a/src/main/java/org/mobilitydb/spark/temporal/TileUDFs.java b/src/main/java/org/mobilitydb/spark/temporal/TileUDFs.java new file mode 100644 index 00000000..4f87ce4e --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/temporal/TileUDFs.java @@ -0,0 +1,935 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import jnr.ffi.Runtime; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosNative; +import org.mobilitydb.spark.MeosThread; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.api.java.UDF1; +import org.apache.spark.sql.api.java.UDF2; +import org.apache.spark.sql.api.java.UDF3; +import org.apache.spark.sql.api.java.UDF4; +import org.apache.spark.sql.api.java.UDF5; +import org.apache.spark.sql.api.java.UDF6; +import org.apache.spark.sql.api.java.UDF7; +import org.apache.spark.sql.api.java.UDF8; +import org.apache.spark.sql.api.java.UDF9; +import org.apache.spark.sql.types.DataTypes; + +import java.sql.Timestamp; +import org.mobilitydb.spark.util.TimeUtil; + +/** + * Spark SQL UDFs for multidimensional tiling β€” split a temporal value into + * fixed-size cells (in space, time, value, or combinations) so the resulting + * cells can be processed in parallel and the per-cell results merged. + * + * The "boxes" variants return the bounding STBox/TBox of each cell that the + * temporal value intersects (lighter-weight, preserves no instants). + * The "split" variants (in {@link AccessorAliasUDFs}) return an array of + * sub-temporal values, one per cell. + * + * MEOS function authority: meos/include/meos.h, meos/include/meos_geo.h, + * meos/include/meos_internal.h + */ +public final class TileUDFs { + + private TileUDFs() {} + + private static final int STBOX_SIZE = 80; + private static final int TBOX_SIZE = 56; + + private static long pgEpoch(Timestamp ts) { + return (ts.getTime() - TimeUtil.PG_UNIX_EPOCH_OFFSET_MS) * 1000L; + } + + // ------------------------------------------------------------------ + // Single-tile lookups + // ------------------------------------------------------------------ + + // getTimeTile(timestamptz, intervalStr, originTs) β†’ STBox hex (the cell + // containing this timestamp at the given resolution) + public static final UDF3 getTimeTile = + (t, intervalStr, torigin) -> { + if (t == null || intervalStr == null || torigin == null) return null; + MeosThread.ensureReady(); + Pointer iv = GeneratedFunctions.pg_interval_in(intervalStr, -1); + if (iv == null) return null; + try { + Pointer r = MeosNative.INSTANCE.stbox_get_time_tile(pgEpoch(t), iv, pgEpoch(torigin)); + if (r == null) return null; + try { + Pointer sizeOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + return GeneratedFunctions.stbox_as_hexwkb(r, (byte) 0, sizeOut); + } finally { MeosMemory.free(r); } + } finally { MeosMemory.free(iv); } + }; + + // getSpaceTile(pointWKT, xsize, ysize, zsize, originPointWKT) β†’ STBox hex + public static final UDF5 getSpaceTile = + (pointWkt, xsize, ysize, zsize, originWkt) -> { + if (pointWkt == null || xsize == null || ysize == null || zsize == null) return null; + MeosThread.ensureReady(); + Pointer pt = GeneratedFunctions.geo_from_text(pointWkt, 0); + if (pt == null) return null; + Pointer origin = (originWkt == null) ? null : GeneratedFunctions.geo_from_text(originWkt, 0); + try { + Pointer r = MeosNative.INSTANCE.stbox_get_space_tile(pt, xsize, ysize, zsize, origin); + if (r == null) return null; + try { + Pointer sizeOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + return GeneratedFunctions.stbox_as_hexwkb(r, (byte) 0, sizeOut); + } finally { MeosMemory.free(r); } + } finally { + MeosMemory.free(pt); + if (origin != null) MeosMemory.free(origin); + } + }; + + // getSpaceTimeTile(pointWKT, t, xsize, ysize, zsize, intervalStr, originPointWKT, torigin) β†’ STBox hex + public static final UDF8 + getSpaceTimeTile = (pointWkt, t, xsize, ysize, zsize, intervalStr, originWkt, torigin) -> { + if (pointWkt == null || t == null || xsize == null || ysize == null || zsize == null + || intervalStr == null || torigin == null) return null; + MeosThread.ensureReady(); + Pointer pt = GeneratedFunctions.geo_from_text(pointWkt, 0); + if (pt == null) return null; + Pointer iv = GeneratedFunctions.pg_interval_in(intervalStr, -1); + if (iv == null) { MeosMemory.free(pt); return null; } + Pointer origin = (originWkt == null) ? null : GeneratedFunctions.geo_from_text(originWkt, 0); + try { + Pointer r = MeosNative.INSTANCE.stbox_get_space_time_tile( + pt, pgEpoch(t), xsize, ysize, zsize, iv, origin, pgEpoch(torigin)); + if (r == null) return null; + try { + Pointer sizeOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + return GeneratedFunctions.stbox_as_hexwkb(r, (byte) 0, sizeOut); + } finally { MeosMemory.free(r); } + } finally { + MeosMemory.free(pt, iv); + if (origin != null) MeosMemory.free(origin); + } + }; + + // ------------------------------------------------------------------ + // Multi-tile bounding-box arrays + // ------------------------------------------------------------------ + + // spaceBoxes(tgeo, xsize, ysize, zsize, originPointWKT, bitmatrix, borderInc) β†’ STBox[] + public static final UDF7 + spaceBoxes = (trip, xsize, ysize, zsize, originWkt, bitmatrix, borderInc) -> { + if (trip == null || xsize == null || ysize == null || zsize == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(trip); + if (t == null) return null; + Pointer origin = (originWkt == null) ? null : GeneratedFunctions.geo_from_text(originWkt, 0); + try { + jnr.ffi.Runtime rt = Runtime.getSystemRuntime(); + Pointer countOut = rt.getMemoryManager().allocateDirect(4); + Pointer arr = MeosNative.INSTANCE.tgeo_space_boxes(t, xsize, ysize, zsize, origin, + bitmatrix != null && bitmatrix, borderInc == null ? true : borderInc, countOut); + if (arr == null) return null; + try { + int n = countOut.getInt(0); + String[] out = new String[n]; + Pointer sizeOut = rt.getMemoryManager().allocateDirect(8); + for (int i = 0; i < n; i++) + out[i] = GeneratedFunctions.stbox_as_hexwkb(arr.slice(i * STBOX_SIZE), (byte) 0, sizeOut); + return out; + } finally { MeosMemory.free(arr); } + } finally { + MeosMemory.free(t); + if (origin != null) MeosMemory.free(origin); + } + }; + + // spaceTimeBoxes(tgeo, xsize, ysize, zsize, intervalStr, originPointWKT, torigin, bitmatrix, borderInc) β†’ STBox[] + public static final UDF9 + spaceTimeBoxes = (trip, xsize, ysize, zsize, intervalStr, originWkt, torigin, bitmatrix, borderInc) -> { + if (trip == null || xsize == null || ysize == null || zsize == null + || intervalStr == null || torigin == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(trip); + if (t == null) return null; + Pointer iv = GeneratedFunctions.pg_interval_in(intervalStr, -1); + if (iv == null) { MeosMemory.free(t); return null; } + Pointer origin = (originWkt == null) ? null : GeneratedFunctions.geo_from_text(originWkt, 0); + try { + jnr.ffi.Runtime rt = Runtime.getSystemRuntime(); + Pointer countOut = rt.getMemoryManager().allocateDirect(4); + Pointer arr = MeosNative.INSTANCE.tgeo_space_time_boxes( + t, xsize, ysize, zsize, iv, origin, pgEpoch(torigin), + bitmatrix != null && bitmatrix, borderInc == null ? true : borderInc, countOut); + if (arr == null) return null; + try { + int n = countOut.getInt(0); + String[] out = new String[n]; + Pointer sizeOut = rt.getMemoryManager().allocateDirect(8); + for (int i = 0; i < n; i++) + out[i] = GeneratedFunctions.stbox_as_hexwkb(arr.slice(i * STBOX_SIZE), (byte) 0, sizeOut); + return out; + } finally { MeosMemory.free(arr); } + } finally { + MeosMemory.free(t, iv); + if (origin != null) MeosMemory.free(origin); + } + }; + + // valueTimeBoxesTfloat(tfloat, vsize, intervalStr, vorigin, torigin) β†’ TBox[] + // Datum vsize/vorigin: float Datum is the IEEE 754 bits via doubleToLongBits. + public static final UDF5 + valueTimeBoxesTfloat = (trip, vsize, intervalStr, vorigin, torigin) -> { + if (trip == null || vsize == null || intervalStr == null || vorigin == null || torigin == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(trip); + if (t == null) return null; + Pointer iv = GeneratedFunctions.pg_interval_in(intervalStr, -1); + if (iv == null) { MeosMemory.free(t); return null; } + try { + jnr.ffi.Runtime rt = Runtime.getSystemRuntime(); + Pointer countOut = rt.getMemoryManager().allocateDirect(4); + Pointer arr = MeosNative.INSTANCE.tnumber_value_time_boxes( + t, Double.doubleToLongBits(vsize), iv, + Double.doubleToLongBits(vorigin), pgEpoch(torigin), countOut); + if (arr == null) return null; + try { + int n = countOut.getInt(0); + String[] out = new String[n]; + Pointer sizeOut = rt.getMemoryManager().allocateDirect(8); + for (int i = 0; i < n; i++) + out[i] = GeneratedFunctions.tbox_as_hexwkb(arr.slice(i * TBOX_SIZE), (byte) 0, sizeOut); + return out; + } finally { MeosMemory.free(arr); } + } finally { MeosMemory.free(t, iv); } + }; + + // valueTimeBoxesTint(tint, vsize, intervalStr, vorigin, torigin) β†’ TBox[] + public static final UDF5 + valueTimeBoxesTint = (trip, vsize, intervalStr, vorigin, torigin) -> { + if (trip == null || vsize == null || intervalStr == null || vorigin == null || torigin == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(trip); + if (t == null) return null; + Pointer iv = GeneratedFunctions.pg_interval_in(intervalStr, -1); + if (iv == null) { MeosMemory.free(t); return null; } + try { + jnr.ffi.Runtime rt = Runtime.getSystemRuntime(); + Pointer countOut = rt.getMemoryManager().allocateDirect(4); + Pointer arr = MeosNative.INSTANCE.tnumber_value_time_boxes( + t, vsize.longValue(), iv, vorigin.longValue(), pgEpoch(torigin), countOut); + if (arr == null) return null; + try { + int n = countOut.getInt(0); + String[] out = new String[n]; + Pointer sizeOut = rt.getMemoryManager().allocateDirect(8); + for (int i = 0; i < n; i++) + out[i] = GeneratedFunctions.tbox_as_hexwkb(arr.slice(i * TBOX_SIZE), (byte) 0, sizeOut); + return out; + } finally { MeosMemory.free(arr); } + } finally { MeosMemory.free(t, iv); } + }; + + // ------------------------------------------------------------------ + // Splits β€” return Temporal** array (each element is a pointer to a + // sub-temporal value). Iterate by reading 8-byte pointers and + // dereferencing each. + // ------------------------------------------------------------------ + + // timeSplit(temporal, intervalStr, torigin) β†’ array of temporal hex-WKB + public static final UDF3 + timeSplit = (trip, intervalStr, torigin) -> { + if (trip == null || intervalStr == null || torigin == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(trip); + if (t == null) return null; + Pointer iv = GeneratedFunctions.pg_interval_in(intervalStr, -1); + if (iv == null) { MeosMemory.free(t); return null; } + try { + jnr.ffi.Runtime rt = Runtime.getSystemRuntime(); + Pointer countOut = rt.getMemoryManager().allocateDirect(4); + Pointer binsOut = rt.getMemoryManager().allocateDirect(8); + Pointer arr = MeosNative.INSTANCE.temporal_time_split(t, iv, pgEpoch(torigin), binsOut, countOut); + if (arr == null) return null; + try { + int n = countOut.getInt(0); + String[] out = new String[n]; + for (int i = 0; i < n; i++) { + Pointer p = arr.getPointer(i * 8L); + if (p == null) continue; + out[i] = GeneratedFunctions.temporal_as_hexwkb(p, (byte) 0); + MeosMemory.free(p); + } + return out; + } finally { + MeosMemory.free(arr); + Pointer bins = binsOut.getPointer(0); + if (bins != null) MeosMemory.free(bins); + } + } finally { MeosMemory.free(t, iv); } + }; + + // spaceSplit(tgeo, xsize, ysize, zsize, originPointWKT, bitmatrix, borderInc) β†’ array of temporal hex-WKB + public static final UDF7 + spaceSplit = (trip, xsize, ysize, zsize, originWkt, bitmatrix, borderInc) -> { + if (trip == null || xsize == null || ysize == null || zsize == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(trip); + if (t == null) return null; + Pointer origin = (originWkt == null) ? null : GeneratedFunctions.geo_from_text(originWkt, 0); + try { + jnr.ffi.Runtime rt = Runtime.getSystemRuntime(); + Pointer countOut = rt.getMemoryManager().allocateDirect(4); + Pointer binsOut = rt.getMemoryManager().allocateDirect(8); + Pointer arr = MeosNative.INSTANCE.tgeo_space_split(t, xsize, ysize, zsize, origin, + bitmatrix != null && bitmatrix, borderInc == null ? true : borderInc, binsOut, countOut); + if (arr == null) return null; + try { + int n = countOut.getInt(0); + String[] out = new String[n]; + for (int i = 0; i < n; i++) { + Pointer p = arr.getPointer(i * 8L); + if (p == null) continue; + out[i] = GeneratedFunctions.temporal_as_hexwkb(p, (byte) 0); + MeosMemory.free(p); + } + return out; + } finally { + MeosMemory.free(arr); + Pointer bins = binsOut.getPointer(0); + if (bins != null) MeosMemory.free(bins); + } + } finally { + MeosMemory.free(t); + if (origin != null) MeosMemory.free(origin); + } + }; + + // spaceTimeSplit(tgeo, xsize, ysize, zsize, intervalStr, originPointWKT, torigin, bitmatrix, borderInc) β†’ array + public static final UDF9 + spaceTimeSplit = (trip, xsize, ysize, zsize, intervalStr, originWkt, torigin, bitmatrix, borderInc) -> { + if (trip == null || xsize == null || ysize == null || zsize == null + || intervalStr == null || torigin == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(trip); + if (t == null) return null; + Pointer iv = GeneratedFunctions.pg_interval_in(intervalStr, -1); + if (iv == null) { MeosMemory.free(t); return null; } + Pointer origin = (originWkt == null) ? null : GeneratedFunctions.geo_from_text(originWkt, 0); + try { + jnr.ffi.Runtime rt = Runtime.getSystemRuntime(); + Pointer countOut = rt.getMemoryManager().allocateDirect(4); + Pointer spaceBinsOut = rt.getMemoryManager().allocateDirect(8); + Pointer timeBinsOut = rt.getMemoryManager().allocateDirect(8); + Pointer arr = MeosNative.INSTANCE.tgeo_space_time_split( + t, xsize, ysize, zsize, iv, origin, pgEpoch(torigin), + bitmatrix != null && bitmatrix, borderInc == null ? true : borderInc, + spaceBinsOut, timeBinsOut, countOut); + if (arr == null) return null; + try { + int n = countOut.getInt(0); + String[] out = new String[n]; + for (int i = 0; i < n; i++) { + Pointer p = arr.getPointer(i * 8L); + if (p == null) continue; + out[i] = GeneratedFunctions.temporal_as_hexwkb(p, (byte) 0); + MeosMemory.free(p); + } + return out; + } finally { + MeosMemory.free(arr); + Pointer sb = spaceBinsOut.getPointer(0); + Pointer tb = timeBinsOut.getPointer(0); + if (sb != null) MeosMemory.free(sb); + if (tb != null) MeosMemory.free(tb); + } + } finally { + MeosMemory.free(t, iv); + if (origin != null) MeosMemory.free(origin); + } + }; + + // ------------------------------------------------------------------ + // Bounded tile-set generators β€” given a bounds box (STBox/TBox) and + // tile sizes, enumerate every tile in the bounds. These are the + // primary parallel-partitioning primitives. + // ------------------------------------------------------------------ + + // spaceTiles(boundsStboxHex, xsize, ysize, zsize, originPointWkt, borderInc) β†’ STBox[] + public static final UDF6 + spaceTiles = (boundsHex, xsize, ysize, zsize, originWkt, borderInc) -> { + if (boundsHex == null || xsize == null || ysize == null || zsize == null) return null; + MeosThread.ensureReady(); + Pointer b = GeneratedFunctions.stbox_from_hexwkb(boundsHex); + if (b == null) return null; + Pointer origin = (originWkt == null) ? null : GeneratedFunctions.geo_from_text(originWkt, 0); + try { + jnr.ffi.Runtime rt = Runtime.getSystemRuntime(); + Pointer countOut = rt.getMemoryManager().allocateDirect(4); + Pointer arr = MeosNative.INSTANCE.stbox_space_tiles(b, xsize, ysize, zsize, origin, + borderInc == null ? true : borderInc, countOut); + if (arr == null) return null; + try { + int n = countOut.getInt(0); + String[] out = new String[n]; + Pointer sizeOut = rt.getMemoryManager().allocateDirect(8); + for (int i = 0; i < n; i++) + out[i] = GeneratedFunctions.stbox_as_hexwkb(arr.slice(i * STBOX_SIZE), (byte) 0, sizeOut); + return out; + } finally { MeosMemory.free(arr); } + } finally { + MeosMemory.free(b); + if (origin != null) MeosMemory.free(origin); + } + }; + + // timeTiles(stbox, intervalStr, torigin, borderInc) β†’ STBox[] + public static final UDF4 + stboxTimeTiles = (boundsHex, intervalStr, torigin, borderInc) -> { + if (boundsHex == null || intervalStr == null || torigin == null) return null; + MeosThread.ensureReady(); + Pointer b = GeneratedFunctions.stbox_from_hexwkb(boundsHex); + if (b == null) return null; + Pointer iv = GeneratedFunctions.pg_interval_in(intervalStr, -1); + if (iv == null) { MeosMemory.free(b); return null; } + try { + jnr.ffi.Runtime rt = Runtime.getSystemRuntime(); + Pointer countOut = rt.getMemoryManager().allocateDirect(4); + Pointer arr = MeosNative.INSTANCE.stbox_time_tiles(b, iv, pgEpoch(torigin), + borderInc == null ? true : borderInc, countOut); + if (arr == null) return null; + try { + int n = countOut.getInt(0); + String[] out = new String[n]; + Pointer sizeOut = rt.getMemoryManager().allocateDirect(8); + for (int i = 0; i < n; i++) + out[i] = GeneratedFunctions.stbox_as_hexwkb(arr.slice(i * STBOX_SIZE), (byte) 0, sizeOut); + return out; + } finally { MeosMemory.free(arr); } + } finally { MeosMemory.free(b, iv); } + }; + + // spaceTimeTiles(stbox, xsize, ysize, zsize, intervalStr, originPointWkt, torigin, borderInc) β†’ STBox[] + public static final UDF8 + spaceTimeTiles = (boundsHex, xsize, ysize, zsize, intervalStr, originWkt, torigin, borderInc) -> { + if (boundsHex == null || xsize == null || ysize == null || zsize == null + || intervalStr == null || torigin == null) return null; + MeosThread.ensureReady(); + Pointer b = GeneratedFunctions.stbox_from_hexwkb(boundsHex); + if (b == null) return null; + Pointer iv = GeneratedFunctions.pg_interval_in(intervalStr, -1); + if (iv == null) { MeosMemory.free(b); return null; } + Pointer origin = (originWkt == null) ? null : GeneratedFunctions.geo_from_text(originWkt, 0); + try { + jnr.ffi.Runtime rt = Runtime.getSystemRuntime(); + Pointer countOut = rt.getMemoryManager().allocateDirect(4); + Pointer arr = MeosNative.INSTANCE.stbox_space_time_tiles(b, xsize, ysize, zsize, iv, + origin, pgEpoch(torigin), borderInc == null ? true : borderInc, countOut); + if (arr == null) return null; + try { + int n = countOut.getInt(0); + String[] out = new String[n]; + Pointer sizeOut = rt.getMemoryManager().allocateDirect(8); + for (int i = 0; i < n; i++) + out[i] = GeneratedFunctions.stbox_as_hexwkb(arr.slice(i * STBOX_SIZE), (byte) 0, sizeOut); + return out; + } finally { MeosMemory.free(arr); } + } finally { + MeosMemory.free(b, iv); + if (origin != null) MeosMemory.free(origin); + } + }; + + // tboxTimeTiles(tbox, intervalStr, torigin) β†’ TBox[] β€” value-bounded time tiling + public static final UDF3 + tintboxTimeTiles = (boundsHex, intervalStr, torigin) -> tboxTimeTilesImpl(boundsHex, intervalStr, torigin, false); + + public static final UDF3 + tfloatboxTimeTiles = (boundsHex, intervalStr, torigin) -> tboxTimeTilesImpl(boundsHex, intervalStr, torigin, true); + + private static String[] tboxTimeTilesImpl(String boundsHex, String intervalStr, Timestamp torigin, boolean isFloat) { + if (boundsHex == null || intervalStr == null || torigin == null) return null; + MeosThread.ensureReady(); + Pointer b = GeneratedFunctions.tbox_from_hexwkb(boundsHex); + if (b == null) return null; + Pointer iv = GeneratedFunctions.pg_interval_in(intervalStr, -1); + if (iv == null) { MeosMemory.free(b); return null; } + try { + jnr.ffi.Runtime rt = Runtime.getSystemRuntime(); + Pointer countOut = rt.getMemoryManager().allocateDirect(4); + Pointer arr = isFloat + ? MeosNative.INSTANCE.tfloatbox_time_tiles(b, iv, pgEpoch(torigin), countOut) + : MeosNative.INSTANCE.tintbox_time_tiles(b, iv, pgEpoch(torigin), countOut); + if (arr == null) return null; + try { + int n = countOut.getInt(0); + String[] out = new String[n]; + Pointer sizeOut = rt.getMemoryManager().allocateDirect(8); + for (int i = 0; i < n; i++) + out[i] = GeneratedFunctions.tbox_as_hexwkb(arr.slice(i * TBOX_SIZE), (byte) 0, sizeOut); + return out; + } finally { MeosMemory.free(arr); } + } finally { MeosMemory.free(b, iv); } + } + + // makeSimple(tpoint) β€” returns array of simple sub-tpoints + public static final UDF1 makeSimple = + trip -> { + if (trip == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(trip); + if (t == null) return null; + try { + Pointer countOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(4); + Pointer arr = MeosNative.INSTANCE.tpoint_make_simple(t, countOut); + if (arr == null) return null; + try { + int n = countOut.getInt(0); + String[] out = new String[n]; + for (int i = 0; i < n; i++) { + Pointer p = arr.getPointer(i * 8L); + if (p == null) continue; + out[i] = GeneratedFunctions.temporal_as_hexwkb(p, (byte) 0); + MeosMemory.free(p); + } + return out; + } finally { MeosMemory.free(arr); } + } finally { MeosMemory.free(t); } + }; + + // timeBoxes(tnumber) β†’ TBox[] β€” go through tnumber_to_tbox then tile by time. + // For tfloat default; tint variant uses tintbox. + public static final UDF3 + tfloatTimeBoxes = (trip, intervalStr, torigin) -> { + if (trip == null || intervalStr == null || torigin == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(trip); + if (t == null) return null; + Pointer iv = GeneratedFunctions.pg_interval_in(intervalStr, -1); + if (iv == null) { MeosMemory.free(t); return null; } + try { + Pointer box = MeosNative.INSTANCE.tnumber_to_tbox(t); + if (box == null) return null; + try { + Pointer countOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(4); + Pointer arr = MeosNative.INSTANCE.tfloatbox_time_tiles(box, iv, pgEpoch(torigin), countOut); + if (arr == null) return null; + try { + int n = countOut.getInt(0); + String[] out = new String[n]; + Pointer sizeOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + for (int i = 0; i < n; i++) + out[i] = GeneratedFunctions.tbox_as_hexwkb(arr.slice(i * TBOX_SIZE), (byte) 0, sizeOut); + return out; + } finally { MeosMemory.free(arr); } + } finally { MeosMemory.free(box); } + } finally { MeosMemory.free(t, iv); } + }; + + public static final UDF3 + tintTimeBoxes = (trip, intervalStr, torigin) -> { + if (trip == null || intervalStr == null || torigin == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(trip); + if (t == null) return null; + Pointer iv = GeneratedFunctions.pg_interval_in(intervalStr, -1); + if (iv == null) { MeosMemory.free(t); return null; } + try { + Pointer box = MeosNative.INSTANCE.tnumber_to_tbox(t); + if (box == null) return null; + try { + Pointer countOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(4); + Pointer arr = MeosNative.INSTANCE.tintbox_time_tiles(box, iv, pgEpoch(torigin), countOut); + if (arr == null) return null; + try { + int n = countOut.getInt(0); + String[] out = new String[n]; + Pointer sizeOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + for (int i = 0; i < n; i++) + out[i] = GeneratedFunctions.tbox_as_hexwkb(arr.slice(i * TBOX_SIZE), (byte) 0, sizeOut); + return out; + } finally { MeosMemory.free(arr); } + } finally { MeosMemory.free(box); } + } finally { MeosMemory.free(t, iv); } + }; + + // timeBoxes(tgeo, intervalStr, torigin, ...) β†’ STBox[] β€” extract STBox first + public static final UDF3 + tgeoTimeBoxes = (trip, intervalStr, torigin) -> { + if (trip == null || intervalStr == null || torigin == null) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(trip); + if (t == null) return null; + Pointer iv = GeneratedFunctions.pg_interval_in(intervalStr, -1); + if (iv == null) { MeosMemory.free(t); return null; } + try { + Pointer box = GeneratedFunctions.tspatial_to_stbox(t); + if (box == null) return null; + try { + Pointer countOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(4); + Pointer arr = MeosNative.INSTANCE.stbox_time_tiles(box, iv, pgEpoch(torigin), true, countOut); + if (arr == null) return null; + try { + int n = countOut.getInt(0); + String[] out = new String[n]; + Pointer sizeOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + for (int i = 0; i < n; i++) + out[i] = GeneratedFunctions.stbox_as_hexwkb(arr.slice(i * STBOX_SIZE), (byte) 0, sizeOut); + return out; + } finally { MeosMemory.free(arr); } + } finally { MeosMemory.free(box); } + } finally { MeosMemory.free(t, iv); } + }; + + // tfloatValueTiles / tintValueTiles β€” value-only TBox tiling + public static final UDF3 + tfloatValueTiles = (boxHex, vsize, vorigin) -> { + if (boxHex == null || vsize == null || vorigin == null) return null; + MeosThread.ensureReady(); + Pointer b = GeneratedFunctions.tbox_from_hexwkb(boxHex); + if (b == null) return null; + try { + Pointer countOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(4); + Pointer arr = MeosNative.INSTANCE.tfloatbox_value_tiles(b, vsize, vorigin, countOut); + if (arr == null) return null; + try { + int n = countOut.getInt(0); + String[] out = new String[n]; + Pointer sizeOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + for (int i = 0; i < n; i++) + out[i] = GeneratedFunctions.tbox_as_hexwkb(arr.slice(i * TBOX_SIZE), (byte) 0, sizeOut); + return out; + } finally { MeosMemory.free(arr); } + } finally { MeosMemory.free(b); } + }; + + public static final UDF3 + tintValueTiles = (boxHex, vsize, vorigin) -> { + if (boxHex == null || vsize == null || vorigin == null) return null; + MeosThread.ensureReady(); + Pointer b = GeneratedFunctions.tbox_from_hexwkb(boxHex); + if (b == null) return null; + try { + Pointer countOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(4); + Pointer arr = MeosNative.INSTANCE.tintbox_value_tiles(b, vsize, vorigin, countOut); + if (arr == null) return null; + try { + int n = countOut.getInt(0); + String[] out = new String[n]; + Pointer sizeOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + for (int i = 0; i < n; i++) + out[i] = GeneratedFunctions.tbox_as_hexwkb(arr.slice(i * TBOX_SIZE), (byte) 0, sizeOut); + return out; + } finally { MeosMemory.free(arr); } + } finally { MeosMemory.free(b); } + }; + + // tfloatValueSplit / tintValueSplit β€” Temporal** array of value-tiled sub-tnumbers. + // Datum vsize/vorigin: tfloat passes IEEE bits via doubleToLongBits; tint just .longValue(). + public static final UDF3 + tfloatValueSplit = (trip, vsize, vorigin) -> tnumberValueSplitImpl(trip, + Double.doubleToLongBits(vsize == null ? 0 : vsize), + Double.doubleToLongBits(vorigin == null ? 0 : vorigin), + vsize == null || vorigin == null); + + public static final UDF3 + tintValueSplit = (trip, vsize, vorigin) -> tnumberValueSplitImpl(trip, + vsize == null ? 0 : vsize.longValue(), + vorigin == null ? 0 : vorigin.longValue(), + vsize == null || vorigin == null); + + // tfloatValueTimeSplit / tintValueTimeSplit β€” Temporal** array of value+time-tiled sub-tnumbers + public static final org.apache.spark.sql.api.java.UDF5 + tfloatValueTimeSplit = (trip, vsize, intervalStr, vorigin, torigin) -> tnumberValueTimeSplitImpl(trip, + Double.doubleToLongBits(vsize == null ? 0 : vsize), intervalStr, + Double.doubleToLongBits(vorigin == null ? 0 : vorigin), torigin, + vsize == null || intervalStr == null || vorigin == null || torigin == null); + + public static final org.apache.spark.sql.api.java.UDF5 + tintValueTimeSplit = (trip, vsize, intervalStr, vorigin, torigin) -> tnumberValueTimeSplitImpl(trip, + vsize == null ? 0 : vsize.longValue(), intervalStr, + vorigin == null ? 0 : vorigin.longValue(), torigin, + vsize == null || intervalStr == null || vorigin == null || torigin == null); + + private static String[] tnumberValueTimeSplitImpl(String trip, long vsize, String intervalStr, + long vorigin, Timestamp torigin, boolean nullArgs) { + if (trip == null || nullArgs) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(trip); + if (t == null) return null; + Pointer iv = GeneratedFunctions.pg_interval_in(intervalStr, -1); + if (iv == null) { MeosMemory.free(t); return null; } + try { + jnr.ffi.Runtime rt = Runtime.getSystemRuntime(); + Pointer countOut = rt.getMemoryManager().allocateDirect(4); + Pointer vBinsOut = rt.getMemoryManager().allocateDirect(8); + Pointer tBinsOut = rt.getMemoryManager().allocateDirect(8); + Pointer arr = MeosNative.INSTANCE.tnumber_value_time_split(t, vsize, iv, vorigin, pgEpoch(torigin), + vBinsOut, tBinsOut, countOut); + if (arr == null) return null; + try { + int n = countOut.getInt(0); + String[] out = new String[n]; + for (int i = 0; i < n; i++) { + Pointer p = arr.getPointer(i * 8L); + if (p == null) continue; + out[i] = GeneratedFunctions.temporal_as_hexwkb(p, (byte) 0); + MeosMemory.free(p); + } + return out; + } finally { + MeosMemory.free(arr); + Pointer vb = vBinsOut.getPointer(0); + Pointer tb = tBinsOut.getPointer(0); + if (vb != null) MeosMemory.free(vb); + if (tb != null) MeosMemory.free(tb); + } + } finally { MeosMemory.free(t, iv); } + } + + private static String[] tnumberValueSplitImpl(String trip, long vsize, long vorigin, boolean nullArgs) { + if (trip == null || nullArgs) return null; + MeosThread.ensureReady(); + Pointer t = GeneratedFunctions.temporal_from_hexwkb(trip); + if (t == null) return null; + try { + jnr.ffi.Runtime rt = Runtime.getSystemRuntime(); + Pointer countOut = rt.getMemoryManager().allocateDirect(4); + Pointer binsOut = rt.getMemoryManager().allocateDirect(8); + Pointer arr = MeosNative.INSTANCE.tnumber_value_split(t, vsize, vorigin, binsOut, countOut); + if (arr == null) return null; + try { + int n = countOut.getInt(0); + String[] out = new String[n]; + for (int i = 0; i < n; i++) { + Pointer p = arr.getPointer(i * 8L); + if (p == null) continue; + out[i] = GeneratedFunctions.temporal_as_hexwkb(p, (byte) 0); + MeosMemory.free(p); + } + return out; + } finally { + MeosMemory.free(arr); + Pointer bins = binsOut.getPointer(0); + if (bins != null) MeosMemory.free(bins); + } + } finally { MeosMemory.free(t); } + } + + // ------------------------------------------------------------------ + // Single-tile lookups via tbox_get_value_time_tile + // + // MeosType enum values used: T_FLOAT8=11, T_FLOATSPAN=13, + // T_INT4=15, T_INTSPAN=19. + // Datum vsize/vorigin: tfloat β†’ IEEE bits via doubleToLongBits; + // tint β†’ just longValue(). + // ------------------------------------------------------------------ + + private static final int T_FLOAT8 = 11, T_FLOATSPAN = 13, T_INT4 = 15, T_INTSPAN = 19; + + // getValueTile(v float, vsize float, vorigin float) β†’ tbox hex + public static final UDF3 + getValueTileFloat = (v, vsize, vorigin) -> { + if (v == null || vsize == null || vorigin == null) return null; + MeosThread.ensureReady(); + Pointer r = MeosNative.INSTANCE.tbox_get_value_time_tile( + Double.doubleToLongBits(v), 0L, + Double.doubleToLongBits(vsize), null, + Double.doubleToLongBits(vorigin), 0L, + T_FLOAT8, T_FLOATSPAN); + if (r == null) return null; + try { + Pointer sizeOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + return GeneratedFunctions.tbox_as_hexwkb(r, (byte) 0, sizeOut); + } finally { MeosMemory.free(r); } + }; + + // getTBoxTimeTile(t timestamptz, duration interval, torigin timestamptz) β†’ tbox hex + public static final UDF3 + getTBoxTimeTile = (t, intervalStr, torigin) -> { + if (t == null || intervalStr == null || torigin == null) return null; + MeosThread.ensureReady(); + Pointer iv = GeneratedFunctions.pg_interval_in(intervalStr, -1); + if (iv == null) return null; + try { + Pointer r = MeosNative.INSTANCE.tbox_get_value_time_tile( + 0L, pgEpoch(t), + 0L, iv, + 0L, pgEpoch(torigin), + T_FLOAT8, T_FLOATSPAN); + if (r == null) return null; + try { + Pointer sizeOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + return GeneratedFunctions.tbox_as_hexwkb(r, (byte) 0, sizeOut); + } finally { MeosMemory.free(r); } + } finally { MeosMemory.free(iv); } + }; + + // getValueTimeTile(v float, t timestamptz, vsize float, duration interval, + // vorigin float, torigin timestamptz) β†’ tbox hex + public static final org.apache.spark.sql.api.java.UDF6 + getValueTimeTileFloat = (v, t, vsize, intervalStr, vorigin, torigin) -> { + if (v == null || t == null || vsize == null || intervalStr == null + || vorigin == null || torigin == null) return null; + MeosThread.ensureReady(); + Pointer iv = GeneratedFunctions.pg_interval_in(intervalStr, -1); + if (iv == null) return null; + try { + Pointer r = MeosNative.INSTANCE.tbox_get_value_time_tile( + Double.doubleToLongBits(v), pgEpoch(t), + Double.doubleToLongBits(vsize), iv, + Double.doubleToLongBits(vorigin), pgEpoch(torigin), + T_FLOAT8, T_FLOATSPAN); + if (r == null) return null; + try { + Pointer sizeOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + return GeneratedFunctions.tbox_as_hexwkb(r, (byte) 0, sizeOut); + } finally { MeosMemory.free(r); } + } finally { MeosMemory.free(iv); } + }; + + // ------------------------------------------------------------------ + // Analytics β€” geoMeasure + asMVTGeom + // ------------------------------------------------------------------ + + // geoMeasure(tpoint, measure, segmentize) β†’ geometry hex (geomeasure encoding) + public static final org.apache.spark.sql.api.java.UDF3 + geoMeasure = (tpointHex, measureHex, segmentize) -> { + if (tpointHex == null || measureHex == null) return null; + MeosThread.ensureReady(); + Pointer tp = GeneratedFunctions.temporal_from_hexwkb(tpointHex); + if (tp == null) return null; + Pointer m = GeneratedFunctions.temporal_from_hexwkb(measureHex); + if (m == null) { MeosMemory.free(tp); return null; } + try { + Pointer outBuf = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + boolean ok = MeosNative.INSTANCE.tpoint_tfloat_to_geomeas( + tp, m, segmentize != null && segmentize, outBuf); + if (!ok) return null; + Pointer geo = outBuf.getPointer(0); + if (geo == null) return null; + try { return GeneratedFunctions.geo_as_hexewkb(geo, "NDR"); } + finally { MeosMemory.free(geo); } + } finally { MeosMemory.free(tp, m); } + }; + + // asMVTGeom(tpoint, bounds, extent, buffer, clip_geom) β†’ array of WKT geometries + // (the per-tile clipped tpoint trajectories) + public static final org.apache.spark.sql.api.java.UDF5 + asMVTGeom = (tpointHex, boundsHex, extent, buffer, clipGeom) -> { + if (tpointHex == null || boundsHex == null) return null; + MeosThread.ensureReady(); + Pointer tp = GeneratedFunctions.temporal_from_hexwkb(tpointHex); + if (tp == null) return null; + Pointer b = GeneratedFunctions.stbox_from_hexwkb(boundsHex); + if (b == null) { MeosMemory.free(tp); return null; } + try { + jnr.ffi.Runtime rt = Runtime.getSystemRuntime(); + Pointer gsArrOut = rt.getMemoryManager().allocateDirect(8); + Pointer timesArrOut = rt.getMemoryManager().allocateDirect(8); + Pointer countOut = rt.getMemoryManager().allocateDirect(4); + boolean ok = MeosNative.INSTANCE.tpoint_as_mvtgeom(tp, b, + extent == null ? 4096 : extent, buffer == null ? 256 : buffer, + clipGeom == null || clipGeom, gsArrOut, timesArrOut, countOut); + if (!ok) return null; + Pointer gsArr = gsArrOut.getPointer(0); + Pointer timesArr = timesArrOut.getPointer(0); + if (gsArr == null) return null; + int n = countOut.getInt(0); + String[] out = new String[n]; + try { + for (int i = 0; i < n; i++) { + Pointer gs = gsArr.getPointer(i * 8L); + if (gs == null) continue; + out[i] = GeneratedFunctions.geo_as_text(gs, 6); + // GSERIALIZED ownership: gsArr is allocated by MEOS, each + // entry is owned by the array; do not free entries individually. + } + return out; + } finally { + MeosMemory.free(gsArr); + if (timesArr != null) MeosMemory.free(timesArr); + } + } finally { MeosMemory.free(tp, b); } + }; + + public static void registerAll(SparkSession spark) { + // Single-tile lookups + spark.udf().register("getTimeTile", getTimeTile, DataTypes.StringType); + spark.udf().register("getSpaceTile", getSpaceTile, DataTypes.StringType); + spark.udf().register("getSpaceTimeTile", getSpaceTimeTile, DataTypes.StringType); + // getStboxTimeTile alias for getTimeTile (covers MobilityDB SQL bare name) + spark.udf().register("getStboxTimeTile", getTimeTile, DataTypes.StringType); + // Multi-tile bounding boxes + spark.udf().register("spaceBoxes", spaceBoxes, DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("spaceTimeBoxes", spaceTimeBoxes, DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("valueTimeBoxesTfloat", valueTimeBoxesTfloat, DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("valueTimeBoxesTint", valueTimeBoxesTint, DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("valueTimeBoxes", valueTimeBoxesTfloat, DataTypes.createArrayType(DataTypes.StringType)); + // Splits (return arrays of sub-temporal values) + spark.udf().register("timeSplit", timeSplit, DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("spaceSplit", spaceSplit, DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("spaceTimeSplit", spaceTimeSplit, DataTypes.createArrayType(DataTypes.StringType)); + // Bounded tile-set generators + spark.udf().register("spaceTiles", spaceTiles, DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("spaceTimeTiles", spaceTimeTiles, DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("stboxTimeTiles", stboxTimeTiles, DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("tintboxTimeTiles", tintboxTimeTiles, DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("tfloatboxTimeTiles", tfloatboxTimeTiles, DataTypes.createArrayType(DataTypes.StringType)); + // Bare-name aliases (timeTiles defaults to STBox; users with TBox use stboxTimeTiles vs tboxTimeTiles explicitly) + spark.udf().register("timeTiles", stboxTimeTiles, DataTypes.createArrayType(DataTypes.StringType)); + // makeSimple + timeBoxes (typed dispatch via tnumber/tgeo) + spark.udf().register("makeSimple", makeSimple, DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("tfloatTimeBoxes", tfloatTimeBoxes, DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("tintTimeBoxes", tintTimeBoxes, DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("tgeoTimeBoxes", tgeoTimeBoxes, DataTypes.createArrayType(DataTypes.StringType)); + // timeBoxes default = tgeo (returns STBox[]); for TBox callers use tfloatTimeBoxes/tintTimeBoxes + spark.udf().register("timeBoxes", tgeoTimeBoxes, DataTypes.createArrayType(DataTypes.StringType)); + // Value-only tile generators + spark.udf().register("tfloatValueTiles", tfloatValueTiles, DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("tintValueTiles", tintValueTiles, DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("valueTiles", tfloatValueTiles, DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("valueBoxes", tfloatValueTiles, DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("valueTimeTiles", valueTimeBoxesTfloat, DataTypes.createArrayType(DataTypes.StringType)); + // Value splits (typed Temporal** arrays) + spark.udf().register("tfloatValueSplit", tfloatValueSplit, DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("tintValueSplit", tintValueSplit, DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("valueSplit", tfloatValueSplit, DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("tfloatValueTimeSplit", tfloatValueTimeSplit, DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("tintValueTimeSplit", tintValueTimeSplit, DataTypes.createArrayType(DataTypes.StringType)); + spark.udf().register("valueTimeSplit", tfloatValueTimeSplit, DataTypes.createArrayType(DataTypes.StringType)); + // Single-tile lookups (defaults to float; tint variants would call with T_INT4/T_INTSPAN) + spark.udf().register("getValueTile", getValueTileFloat, DataTypes.StringType); + spark.udf().register("getTBoxTimeTile", getTBoxTimeTile, DataTypes.StringType); + spark.udf().register("getValueTimeTile", getValueTimeTileFloat, DataTypes.StringType); + // Analytics + spark.udf().register("geoMeasure", geoMeasure, DataTypes.StringType); + spark.udf().register("asMVTGeom", asMVTGeom, DataTypes.createArrayType(DataTypes.StringType)); + } +} diff --git a/src/main/java/org/mobilitydb/spark/temporal/TransformUDFs.java b/src/main/java/org/mobilitydb/spark/temporal/TransformUDFs.java new file mode 100644 index 00000000..2e35400d --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/temporal/TransformUDFs.java @@ -0,0 +1,1089 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import functions.GeneratedFunctions; +import jnr.ffi.Pointer; +import org.mobilitydb.spark.MeosMemory; +import org.mobilitydb.spark.MeosThread; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.api.java.UDF1; +import org.apache.spark.sql.api.java.UDF2; +import org.apache.spark.sql.api.java.UDF3; +import org.apache.spark.sql.types.DataTypes; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; + +/** + * Spark SQL UDFs for converting and transforming temporal types. + * + * Covers: subtype conversion (to TInstant/TSequence/TSequenceSet), + * interpolation change, type casting (tfloat↔tint), value-domain + * shifting and scaling, time-domain shifting and scaling, SRID + * assignment, coordinate rounding, and trajectory simplification. + * + * All temporal values are encoded as hex-WKB Strings. Interpolation + * is expressed as a String: "Discrete", "Step", or "Linear". + * Interval arguments use PostgreSQL interval literal syntax, e.g. + * "1 day" or "01:00:00". + * + * Memory management: every native Pointer allocated by MEOS is freed + * via MeosMemory.free() in a finally block to prevent native heap + * leakage across UDF calls. + * + * MEOS function authority: meos/include/meos.h, meos/include/meos_geo.h + */ +public final class TransformUDFs { + + private TransformUDFs() {} + + // interpType constants from meos.h: DISCRETE=1, STEP=2, LINEAR=3 + private static int interpToInt(String interp) { + if ("Discrete".equalsIgnoreCase(interp)) return 1; + if ("Step".equalsIgnoreCase(interp)) return 2; + if ("Linear".equalsIgnoreCase(interp)) return 3; + throw new IllegalArgumentException("Unknown interpolation: " + interp); + } + + // ------------------------------------------------------------------ + // Subtype conversion + // ------------------------------------------------------------------ + + // temporalToTInstant(s STRING) β†’ STRING + // MEOS: temporal_to_tinstant(const Temporal *) β†’ TInstant * + public static final UDF1 temporalToTInstant = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + Pointer result = GeneratedFunctions.temporal_to_tinstant(ptr); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // temporalToTSequence(s STRING, interp STRING) β†’ STRING + // interp: "Discrete" | "Step" | "Linear" + // MEOS: temporal_to_tsequence(const Temporal *, interpType interp) β†’ TSequence * + public static final UDF2 temporalToTSequence = + (s, interp) -> { + if (s == null || interp == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + Pointer result = GeneratedFunctions.temporal_to_tsequence(ptr, interpToInt(interp)); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // temporalToTSequenceSet(s STRING, interp STRING) β†’ STRING + // interp: "Discrete" | "Step" | "Linear" + // MEOS: temporal_to_tsequenceset(const Temporal *, interpType interp) β†’ TSequenceSet * + public static final UDF2 temporalToTSequenceSet = + (s, interp) -> { + if (s == null || interp == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + Pointer result = GeneratedFunctions.temporal_to_tsequenceset(ptr, interpToInt(interp)); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // ------------------------------------------------------------------ + // Interpolation change + // ------------------------------------------------------------------ + + // temporalSetInterp(s STRING, interpStr STRING) β†’ STRING + // interpStr: "Discrete" β†’ 1, "Step" β†’ 2, "Linear" β†’ 3 + // MEOS: temporal_set_interp(const Temporal *, interpType interp) β†’ Temporal * + public static final UDF2 temporalSetInterp = + (s, interpStr) -> { + if (s == null || interpStr == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + int interpInt = interpToInt(interpStr); + Pointer result = GeneratedFunctions.temporal_set_interp(ptr, interpInt); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // ------------------------------------------------------------------ + // Type casting + // ------------------------------------------------------------------ + + // tfloatToTint(s STRING) β†’ STRING + // MEOS: tfloat_to_tint(const Temporal *) β†’ Temporal * + public static final UDF1 tfloatToTint = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + Pointer result = GeneratedFunctions.tfloat_to_tint(ptr); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // tintToTfloat(s STRING) β†’ STRING + // MEOS: tint_to_tfloat(const Temporal *) β†’ Temporal * + public static final UDF1 tintToTfloat = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + Pointer result = GeneratedFunctions.tint_to_tfloat(ptr); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // ------------------------------------------------------------------ + // Value-domain shifting and scaling (tint) + // + // MEOS: tint_shift_value(temp, shift) meos.h + // tint_scale_value(temp, width) meos.h + // tint_shift_scale_value(temp, s, w) meos.h + // ------------------------------------------------------------------ + + public static final UDF2 tintShiftValue = + (s, shift) -> { + if (s == null || shift == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + Pointer result = GeneratedFunctions.tint_shift_value(ptr, shift); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(ptr); + } + }; + + public static final UDF2 tintScaleValue = + (s, width) -> { + if (s == null || width == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + Pointer result = GeneratedFunctions.tint_scale_value(ptr, width); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(ptr); + } + }; + + public static final UDF3 tintShiftScaleValue = + (s, shift, width) -> { + if (s == null || shift == null || width == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + Pointer result = GeneratedFunctions.tint_shift_scale_value(ptr, shift, width); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // ------------------------------------------------------------------ + // Value-domain shifting and scaling (tfloat) + // ------------------------------------------------------------------ + + // tfloatShiftValue(s STRING, shift DOUBLE) β†’ STRING + // MEOS: tfloat_shift_value(const Temporal *, double) β†’ Temporal * + public static final UDF2 tfloatShiftValue = + (s, shift) -> { + if (s == null || shift == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + Pointer result = GeneratedFunctions.tfloat_shift_value(ptr, shift); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // tfloatScaleValue(s STRING, width DOUBLE) β†’ STRING + // MEOS: tfloat_scale_value(const Temporal *, double) β†’ Temporal * + public static final UDF2 tfloatScaleValue = + (s, width) -> { + if (s == null || width == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + Pointer result = GeneratedFunctions.tfloat_scale_value(ptr, width); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // tfloatShiftScaleValue(s STRING, shift DOUBLE, width DOUBLE) β†’ STRING + // MEOS: tfloat_shift_scale_value(const Temporal *, double shift, double width) β†’ Temporal * + public static final UDF3 tfloatShiftScaleValue = + (s, shift, width) -> { + if (s == null || shift == null || width == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + Pointer result = GeneratedFunctions.tfloat_shift_scale_value(ptr, shift, width); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // ------------------------------------------------------------------ + // Time-domain shifting and scaling + // ------------------------------------------------------------------ + + // temporalShiftTime(s STRING, shiftStr STRING) β†’ STRING + // MEOS: temporal_shift_time(const Temporal *, const Interval *) β†’ Temporal * + public static final UDF2 temporalShiftTime = + (s, shiftStr) -> { + if (s == null || shiftStr == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (tptr == null) return null; + try { + Pointer shiftPtr = GeneratedFunctions.pg_interval_in(shiftStr, -1); + if (shiftPtr == null) return null; + try { + Pointer result = GeneratedFunctions.temporal_shift_time(tptr, shiftPtr); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(shiftPtr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // temporalScaleTime(s STRING, scaleStr STRING) β†’ STRING + // MEOS: temporal_scale_time(const Temporal *, const Interval *) β†’ Temporal * + public static final UDF2 temporalScaleTime = + (s, scaleStr) -> { + if (s == null || scaleStr == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (tptr == null) return null; + try { + Pointer scalePtr = GeneratedFunctions.pg_interval_in(scaleStr, -1); + if (scalePtr == null) return null; + try { + Pointer result = GeneratedFunctions.temporal_scale_time(tptr, scalePtr); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(scalePtr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // temporalShiftScaleTime(s STRING, shiftStr STRING, scaleStr STRING) β†’ STRING + // shiftStr and scaleStr are PostgreSQL interval literals, e.g. "1 day". + // MEOS: temporal_shift_scale_time(const Temporal *, const Interval *, const Interval *) β†’ Temporal * + public static final UDF3 temporalShiftScaleTime = + (s, shiftStr, scaleStr) -> { + if (s == null || shiftStr == null || scaleStr == null) return null; + MeosThread.ensureReady(); + Pointer tptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (tptr == null) return null; + try { + Pointer shiftPtr = GeneratedFunctions.pg_interval_in(shiftStr, -1); + if (shiftPtr == null) return null; + try { + Pointer scalePtr = GeneratedFunctions.pg_interval_in(scaleStr, -1); + if (scalePtr == null) return null; + try { + Pointer result = GeneratedFunctions.temporal_shift_scale_time(tptr, shiftPtr, scalePtr); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(scalePtr); + } + } finally { + MeosMemory.free(shiftPtr); + } + } finally { + MeosMemory.free(tptr); + } + }; + + // ------------------------------------------------------------------ + // Spatial transformations (tpoint) + // ------------------------------------------------------------------ + + // tpointSetSrid(s STRING, srid INT) β†’ STRING + // MEOS: tspatial_set_srid(const Temporal *, int32_t srid) β†’ Temporal * + public static final UDF2 tpointSetSrid = + (s, srid) -> { + if (s == null || srid == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + Pointer result = GeneratedFunctions.tspatial_set_srid(ptr, srid); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // tpointRound(s STRING, maxdd INT) β†’ STRING + // MEOS: temporal_round(const Temporal *, int maxdd) β†’ Temporal * + public static final UDF2 tpointRound = + (s, maxdd) -> { + if (s == null || maxdd == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + Pointer result = GeneratedFunctions.temporal_round(ptr, maxdd); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // ------------------------------------------------------------------ + // Trajectory simplification + // ------------------------------------------------------------------ + + // temporalSimplifyDp(s STRING, dist DOUBLE) β†’ STRING + // Uses the Douglas-Peucker algorithm with synchronized=false. + // MEOS: temporal_simplify_dp(const Temporal *, double eps_dist, bool synchronized) β†’ Temporal * + public static final UDF2 temporalSimplifyDp = + (s, dist) -> { + if (s == null || dist == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + Pointer result = GeneratedFunctions.temporal_simplify_dp(ptr, dist, false); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // temporalSimplifyMaxDist(s STRING, dist DOUBLE) β†’ STRING + // Uses maximum-distance simplification with synchronized=false. + // MEOS: temporal_simplify_max_dist(const Temporal *, double eps_dist, bool synchronized) β†’ Temporal * + public static final UDF2 temporalSimplifyMaxDist = + (s, dist) -> { + if (s == null || dist == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + Pointer result = GeneratedFunctions.temporal_simplify_max_dist(ptr, dist, false); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(ptr); + } + }; + + private static final OffsetDateTime PG_EPOCH = OffsetDateTime.of(2000, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC); + + // temporalSimplifyMinDist(s STRING, dist DOUBLE) β†’ STRING + // Removes consecutive instants whose distance is below the threshold. + // MEOS: temporal_simplify_min_dist(const Temporal *, double dist) β†’ Temporal * + public static final UDF2 temporalSimplifyMinDist = + (s, dist) -> { + if (s == null || dist == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + Pointer result = GeneratedFunctions.temporal_simplify_min_dist(ptr, dist); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // temporalSimplifyMinTdelta(s STRING, durationStr STRING) β†’ STRING + // Removes consecutive instants whose time delta is below the threshold. + // MEOS: temporal_simplify_min_tdelta(const Temporal *, const Interval *) β†’ Temporal * + public static final UDF2 temporalSimplifyMinTdelta = + (s, durationStr) -> { + if (s == null || durationStr == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + Pointer ivPtr = GeneratedFunctions.pg_interval_in(durationStr, -1); + if (ivPtr == null) return null; + try { + Pointer result = GeneratedFunctions.temporal_simplify_min_tdelta(ptr, ivPtr); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(ivPtr); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // temporalTPrecision(s STRING, durationStr STRING) β†’ STRING + // Rounds all timestamps to the nearest multiple of the given duration. + // MEOS: temporal_tprecision(const Temporal *, const Interval *, TimestampTz origin) + public static final UDF2 temporalTPrecision = + (s, durationStr) -> { + if (s == null || durationStr == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + Pointer ivPtr = GeneratedFunctions.pg_interval_in(durationStr, -1); + if (ivPtr == null) return null; + try { + Pointer result = GeneratedFunctions.temporal_tprecision(ptr, ivPtr, PG_EPOCH); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(ivPtr); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // temporalTSample(s STRING, durationStr STRING, interpStr STRING) β†’ STRING + // Re-samples a temporal value at regular time intervals. + // origin is fixed at 2000-01-01 00:00:00 UTC (MEOS/PG epoch). + // MEOS: temporal_tsample(const Temporal *, const Interval *, TimestampTz, interpType) β†’ Temporal * + public static final UDF3 temporalTSample = + (s, durationStr, interpStr) -> { + if (s == null || durationStr == null || interpStr == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + Pointer ivPtr = GeneratedFunctions.pg_interval_in(durationStr, -1); + if (ivPtr == null) return null; + try { + int interp; + switch (interpStr) { + case "Discrete": interp = 1; break; + case "Step": interp = 2; break; + default: interp = 3; break; + } + Pointer result = GeneratedFunctions.temporal_tsample(ptr, ivPtr, PG_EPOCH, interp); + if (result == null) return null; + try { + return GeneratedFunctions.temporal_as_hexwkb(result, (byte) 0); + } finally { + MeosMemory.free(result); + } + } finally { + MeosMemory.free(ivPtr); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // tpointTrajectory(s STRING) β†’ STRING (WKT of the trajectory geometry) + // For a tgeompoint sequence, this returns a LINESTRING or POINT geometry. + // MEOS: tpoint_trajectory(const Temporal *, bool unary_union) β†’ GSERIALIZED * + public static final UDF1 tpointTrajectory = + (s) -> { + if (s == null) return null; + MeosThread.ensureReady(); + Pointer ptr = GeneratedFunctions.temporal_from_hexwkb(s); + if (ptr == null) return null; + try { + Pointer gsPtr = GeneratedFunctions.tpoint_trajectory(ptr, false); + if (gsPtr == null) return null; + try { + return GeneratedFunctions.geo_as_text(gsPtr, 6); + } finally { + MeosMemory.free(gsPtr); + } + } finally { + MeosMemory.free(ptr); + } + }; + + // ------------------------------------------------------------------ + // floatset transforms + // ------------------------------------------------------------------ + + // floatsetCeil(setHex STRING) β†’ STRING + // MEOS: floatset_ceil(const Set *) β†’ Set * + public static final UDF1 floatsetCeil = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.set_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer r = GeneratedFunctions.floatset_ceil(p); + if (r == null) return null; + try { return GeneratedFunctions.set_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(p); } + }; + + // floatsetFloor(setHex STRING) β†’ STRING + // MEOS: floatset_floor(const Set *) β†’ Set * + public static final UDF1 floatsetFloor = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.set_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer r = GeneratedFunctions.floatset_floor(p); + if (r == null) return null; + try { return GeneratedFunctions.set_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(p); } + }; + + // floatsetDegrees(setHex STRING) β†’ STRING (radians β†’ degrees) + // MEOS: floatset_degrees(const Set *, bool normalize) β†’ Set * + public static final UDF1 floatsetDegrees = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.set_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer r = GeneratedFunctions.floatset_degrees(p, false); + if (r == null) return null; + try { return GeneratedFunctions.set_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(p); } + }; + + // floatsetRadians(setHex STRING) β†’ STRING (degrees β†’ radians) + // MEOS: floatset_radians(const Set *) β†’ Set * + public static final UDF1 floatsetRadians = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.set_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer r = GeneratedFunctions.floatset_radians(p); + if (r == null) return null; + try { return GeneratedFunctions.set_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(p); } + }; + + // ------------------------------------------------------------------ + // textset transforms (case normalization) + // ------------------------------------------------------------------ + + // textsetLower(setHex STRING) β†’ STRING (all elements lowercased) + // MEOS: textset_lower(const Set *) β†’ Set * + public static final UDF1 textsetLower = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.set_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer r = GeneratedFunctions.textset_lower(p); + if (r == null) return null; + try { return GeneratedFunctions.set_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(p); } + }; + + // textsetUpper(setHex STRING) β†’ STRING (all elements uppercased) + // MEOS: textset_upper(const Set *) β†’ Set * + public static final UDF1 textsetUpper = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.set_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer r = GeneratedFunctions.textset_upper(p); + if (r == null) return null; + try { return GeneratedFunctions.set_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(p); } + }; + + // textsetInitcap(setHex STRING) β†’ STRING (first letter of each element capitalized) + // MEOS: textset_initcap(const Set *) β†’ Set * + public static final UDF1 textsetInitcap = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.set_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer r = GeneratedFunctions.textset_initcap(p); + if (r == null) return null; + try { return GeneratedFunctions.set_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(p); } + }; + + // ------------------------------------------------------------------ + // intspan / floatspan shift-scale + // ------------------------------------------------------------------ + + // intspanShiftScale(spanHex STRING, shift INTEGER, width INTEGER) β†’ STRING + // MEOS: intspan_shift_scale(const Span *, int shift, int width, + // bool hasshift, bool haswidth) β†’ Span * + public static final UDF3 intspanShiftScale = + (hex, shift, width) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.span_from_hexwkb(hex); + if (p == null) return null; + try { + int s = (shift == null) ? 0 : shift; + int w = (width == null) ? 0 : width; + Pointer r = GeneratedFunctions.intspan_shift_scale(p, s, w, + shift != null, width != null); + if (r == null) return null; + try { return GeneratedFunctions.span_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(p); } + }; + + // floatspanShiftScale(spanHex STRING, shift DOUBLE, width DOUBLE) β†’ STRING + // MEOS: floatspan_shift_scale(const Span *, double, double, bool, bool) β†’ Span * + public static final UDF3 floatspanShiftScale = + (hex, shift, width) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.span_from_hexwkb(hex); + if (p == null) return null; + try { + double s = (shift == null) ? 0.0 : shift; + double w = (width == null) ? 0.0 : width; + Pointer r = GeneratedFunctions.floatspan_shift_scale(p, s, w, + shift != null, width != null); + if (r == null) return null; + try { return GeneratedFunctions.span_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(p); } + }; + + // ------------------------------------------------------------------ + // intspanset / floatspanset shift-scale and type conversion + // ------------------------------------------------------------------ + + // intspansetShiftScale(hex STRING, shift INTEGER, width INTEGER) β†’ STRING + // MEOS: intspanset_shift_scale(const SpanSet *, int, int, bool, bool) β†’ SpanSet * + public static final UDF3 intspansetShiftScale = + (hex, shift, width) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.spanset_from_hexwkb(hex); + if (p == null) return null; + try { + int s = (shift == null) ? 0 : shift; + int w = (width == null) ? 0 : width; + Pointer r = GeneratedFunctions.intspanset_shift_scale(p, s, w, + shift != null, width != null); + if (r == null) return null; + try { return GeneratedFunctions.spanset_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(p); } + }; + + // floatspansetShiftScale(hex STRING, shift DOUBLE, width DOUBLE) β†’ STRING + // MEOS: floatspanset_shift_scale(const SpanSet *, double, double, bool, bool) β†’ SpanSet * + public static final UDF3 floatspansetShiftScale = + (hex, shift, width) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.spanset_from_hexwkb(hex); + if (p == null) return null; + try { + double s = (shift == null) ? 0.0 : shift; + double w = (width == null) ? 0.0 : width; + Pointer r = GeneratedFunctions.floatspanset_shift_scale(p, s, w, + shift != null, width != null); + if (r == null) return null; + try { return GeneratedFunctions.spanset_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(p); } + }; + + // floatspansetCeil(hex STRING) β†’ STRING + // MEOS: floatspanset_ceil(const SpanSet *) β†’ SpanSet * + public static final UDF1 floatspansetCeil = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.spanset_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer r = GeneratedFunctions.floatspanset_ceil(p); + if (r == null) return null; + try { return GeneratedFunctions.spanset_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(p); } + }; + + // floatspansetFloor(hex STRING) β†’ STRING + // MEOS: floatspanset_floor(const SpanSet *) β†’ SpanSet * + public static final UDF1 floatspansetFloor = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.spanset_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer r = GeneratedFunctions.floatspanset_floor(p); + if (r == null) return null; + try { return GeneratedFunctions.spanset_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(p); } + }; + + // floatspansetRound(hex STRING, maxDecimals INTEGER) β†’ STRING + // MEOS: floatspanset_round(const SpanSet *, int) β†’ SpanSet * + public static final UDF2 floatspansetRound = + (hex, decimals) -> { + if (hex == null || decimals == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.spanset_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer r = GeneratedFunctions.floatspanset_round(p, decimals); + if (r == null) return null; + try { return GeneratedFunctions.spanset_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(p); } + }; + + // intspansetToFloat(hex STRING) β†’ STRING (intspanset β†’ floatspanset) + // MEOS: intspanset_to_floatspanset(const SpanSet *) β†’ SpanSet * + public static final UDF1 intspansetToFloat = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.spanset_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer r = GeneratedFunctions.intspanset_to_floatspanset(p); + if (r == null) return null; + try { return GeneratedFunctions.spanset_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(p); } + }; + + // floatspansetToInt(hex STRING) β†’ STRING (floatspanset β†’ intspanset) + // MEOS: floatspanset_to_intspanset(const SpanSet *) β†’ SpanSet * + public static final UDF1 floatspansetToInt = + (hex) -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.spanset_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer r = GeneratedFunctions.floatspanset_to_intspanset(p); + if (r == null) return null; + try { return GeneratedFunctions.spanset_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(p); } + }; + + // ------------------------------------------------------------------ + // Registration + // ------------------------------------------------------------------ + + public static void registerAll(SparkSession spark) { + // Subtype conversion + spark.udf().register("temporalToTInstant", temporalToTInstant, DataTypes.StringType); + spark.udf().register("temporalToTSequence", temporalToTSequence, DataTypes.StringType); + spark.udf().register("temporalToTSequenceSet", temporalToTSequenceSet, DataTypes.StringType); + // Interpolation change + spark.udf().register("temporalSetInterp", temporalSetInterp, DataTypes.StringType); + // Type casting + spark.udf().register("tfloatToTint", tfloatToTint, DataTypes.StringType); + spark.udf().register("tintToTfloat", tintToTfloat, DataTypes.StringType); + // tint value-domain shifting and scaling + spark.udf().register("tintShiftValue", tintShiftValue, DataTypes.StringType); + spark.udf().register("tintScaleValue", tintScaleValue, DataTypes.StringType); + spark.udf().register("tintShiftScaleValue", tintShiftScaleValue, DataTypes.StringType); + // Value-domain shifting and scaling + spark.udf().register("tfloatShiftValue", tfloatShiftValue, DataTypes.StringType); + spark.udf().register("tfloatScaleValue", tfloatScaleValue, DataTypes.StringType); + spark.udf().register("tfloatShiftScaleValue", tfloatShiftScaleValue, DataTypes.StringType); + // Time-domain shifting and scaling + spark.udf().register("temporalShiftTime", temporalShiftTime, DataTypes.StringType); + spark.udf().register("temporalScaleTime", temporalScaleTime, DataTypes.StringType); + spark.udf().register("temporalShiftScaleTime", temporalShiftScaleTime, DataTypes.StringType); + // Spatial transformations + spark.udf().register("tpointSetSrid", tpointSetSrid, DataTypes.StringType); + spark.udf().register("tpointRound", tpointRound, DataTypes.StringType); + // Trajectory simplification + spark.udf().register("temporalSimplifyDp", temporalSimplifyDp, DataTypes.StringType); + spark.udf().register("temporalSimplifyMaxDist", temporalSimplifyMaxDist, DataTypes.StringType); + spark.udf().register("temporalSimplifyMinDist", temporalSimplifyMinDist, DataTypes.StringType); + spark.udf().register("temporalSimplifyMinTdelta", temporalSimplifyMinTdelta, DataTypes.StringType); + spark.udf().register("temporalTPrecision", temporalTPrecision, DataTypes.StringType); + // Temporal sampling + spark.udf().register("temporalTSample", temporalTSample, DataTypes.StringType); + // Trajectory extraction + spark.udf().register("tpointTrajectory", tpointTrajectory, DataTypes.StringType); + // floatset transforms + spark.udf().register("floatsetCeil", floatsetCeil, DataTypes.StringType); + spark.udf().register("floatsetFloor", floatsetFloor, DataTypes.StringType); + spark.udf().register("floatsetDegrees", floatsetDegrees, DataTypes.StringType); + spark.udf().register("floatsetRadians", floatsetRadians, DataTypes.StringType); + // textset case normalization + spark.udf().register("textsetLower", textsetLower, DataTypes.StringType); + spark.udf().register("textsetUpper", textsetUpper, DataTypes.StringType); + spark.udf().register("textsetInitcap", textsetInitcap, DataTypes.StringType); + // intspan / floatspan shift-scale + spark.udf().register("intspanShiftScale", intspanShiftScale, DataTypes.StringType); + spark.udf().register("floatspanShiftScale", floatspanShiftScale, DataTypes.StringType); + // intspanset / floatspanset transforms + spark.udf().register("intspansetShiftScale", intspansetShiftScale, DataTypes.StringType); + spark.udf().register("floatspansetShiftScale", floatspansetShiftScale, DataTypes.StringType); + spark.udf().register("floatspansetCeil", floatspansetCeil, DataTypes.StringType); + spark.udf().register("floatspansetFloor", floatspansetFloor, DataTypes.StringType); + spark.udf().register("floatspansetRound", floatspansetRound, DataTypes.StringType); + spark.udf().register("intspansetToFloat", intspansetToFloat, DataTypes.StringType); + spark.udf().register("floatspansetToInt", floatspansetToInt, DataTypes.StringType); + + // MobilityDB SQL bare-name aliases for the simplify family + spark.udf().register("douglasPeuckerSimplify", temporalSimplifyDp, DataTypes.StringType); + spark.udf().register("maxDistSimplify", temporalSimplifyMaxDist, DataTypes.StringType); + spark.udf().register("minDistSimplify", temporalSimplifyMinDist, DataTypes.StringType); + spark.udf().register("minTimeDeltaSimplify", temporalSimplifyMinTdelta, DataTypes.StringType); + // MobilityDB SQL bare-name aliases for span/spanset type conversions + spark.udf().register("intspanset", floatspansetToInt, DataTypes.StringType); + spark.udf().register("floatspanset", intspansetToFloat, DataTypes.StringType); + // shiftScale alias β€” most common case is floatspan + spark.udf().register("shiftScale", floatspanShiftScale, DataTypes.StringType); + // tstzset ↔ dateset conversions + spark.udf().register("dateset", tstzsetToDateset, DataTypes.StringType); + spark.udf().register("tstzset", datesetToTstzset, DataTypes.StringType); + // span / spanset constructor aliases + // (range/multirange NOT registered β€” they wrap PG-specific types + // with no Spark equivalent; see feedback_pg_specific_types_oos memory) + spark.udf().register("span", temporalToTstzspan, DataTypes.StringType); + spark.udf().register("spanset", spanToSpanset, DataTypes.StringType); + } + + public static final UDF1 temporalToTstzspan = + hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.temporal_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer r = GeneratedFunctions.temporal_to_tstzspan(p); + if (r == null) return null; + try { return GeneratedFunctions.span_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(p); } + }; + + public static final UDF1 spanToSpanset = + hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.span_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer r = GeneratedFunctions.span_to_spanset(p); + if (r == null) return null; + try { return GeneratedFunctions.spanset_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(p); } + }; + + public static final UDF1 tstzsetToDateset = + hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.set_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer r = GeneratedFunctions.tstzset_to_dateset(p); + if (r == null) return null; + try { return GeneratedFunctions.set_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(p); } + }; + + public static final UDF1 datesetToTstzset = + hex -> { + if (hex == null) return null; + MeosThread.ensureReady(); + Pointer p = GeneratedFunctions.set_from_hexwkb(hex); + if (p == null) return null; + try { + Pointer r = GeneratedFunctions.dateset_to_tstzset(p); + if (r == null) return null; + try { return GeneratedFunctions.set_as_hexwkb(r, (byte) 0); } + finally { MeosMemory.free(r); } + } finally { MeosMemory.free(p); } + }; +} diff --git a/src/main/java/org/mobilitydb/spark/udfs/TemporalUDFs.java b/src/main/java/org/mobilitydb/spark/udfs/TemporalUDFs.java new file mode 100644 index 00000000..9daa7af3 --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/udfs/TemporalUDFs.java @@ -0,0 +1,46 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.udfs; + +import org.apache.spark.sql.SparkSession; +import org.mobilitydb.spark.geo.GeoUDFs; + +/** + * Convenience facade β€” registers all MobilitySpark UDFs in one call. + * + * Prefer {@link org.mobilitydb.spark.MobilitySparkSession#create(SparkSession)} + * which also initialises MEOS. Use this class only when MEOS is already + * initialised by another mechanism. + */ +public final class TemporalUDFs { + + private TemporalUDFs() {} + + public static void registerAll(SparkSession spark) { + org.mobilitydb.spark.temporal.TemporalUDFs.registerAll(spark); + GeoUDFs.registerAll(spark); + } +} diff --git a/src/main/java/org/mobilitydb/spark/util/TimeUtil.java b/src/main/java/org/mobilitydb/spark/util/TimeUtil.java new file mode 100644 index 00000000..c2162dc5 --- /dev/null +++ b/src/main/java/org/mobilitydb/spark/util/TimeUtil.java @@ -0,0 +1,109 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.util; + +import java.sql.Timestamp; +import java.time.Instant; +import java.time.OffsetDateTime; + +/** + * Centralised boundary conversions between Spark / Java native timestamp + * types and MEOS canonical {@code TimestampTz} (microseconds since the + * PostgreSQL epoch, 2000-01-01 UTC). + * + *

This is the closed-algebra-boundary principle applied to time: + * everywhere in MobilitySpark that crosses the JVM ↔ MEOS boundary, use + * the helpers below. Inlining the magic constant {@code 946684800} + * (seconds between Unix epoch and PG epoch) at individual call-sites + * scatters the conversion logic and risks drift if one site is updated + * and another is missed. + * + *

Companion file: {@code MobilityDuck/src/include/time_util.hpp} + * (same role on the DuckDB side). + */ +public final class TimeUtil { + + private TimeUtil() {} + + /** + * Seconds between the Unix epoch (1970-01-01 UTC) and the PostgreSQL / + * MEOS epoch (2000-01-01 UTC). 10957 days Γ— 86400 seconds. + */ + public static final long PG_UNIX_EPOCH_OFFSET_S = 946684800L; + + /** Milliseconds equivalent of {@link #PG_UNIX_EPOCH_OFFSET_S}. */ + public static final long PG_UNIX_EPOCH_OFFSET_MS = PG_UNIX_EPOCH_OFFSET_S * 1000L; + + /** Microseconds equivalent of {@link #PG_UNIX_EPOCH_OFFSET_S}. */ + public static final long PG_UNIX_EPOCH_OFFSET_US = PG_UNIX_EPOCH_OFFSET_S * 1_000_000L; + + /** Days between the Unix epoch and the PG epoch. Used for date-only conversions. */ + public static final long PG_UNIX_EPOCH_OFFSET_DAYS = 10957L; + + /** + * Spark / JDBC {@link Timestamp} (Unix-epoch milliseconds) β†’ MEOS + * {@code TimestampTz} (PG-epoch microseconds). + */ + public static long toMeosTimestamp(Timestamp ts) { + return (ts.getTime() - PG_UNIX_EPOCH_OFFSET_MS) * 1000L; + } + + /** + * JMEOS-side {@link OffsetDateTime} (which encodes PG-epoch microseconds + * in its epoch-seconds slot β€” a JMEOS internal convention) β†’ + * MEOS-canonical {@code TimestampTz}. + */ + public static long toMeosTimestamp(OffsetDateTime odt) { + return odt.toEpochSecond(); + } + + /** + * MEOS {@code TimestampTz} (PG-epoch microseconds) β†’ Spark / JDBC + * {@link Timestamp} (Unix-epoch milliseconds). + */ + public static Timestamp fromMeosTimestamp(long meosMicros) { + return new Timestamp(meosMicros / 1000L + PG_UNIX_EPOCH_OFFSET_MS); + } + + /** + * MEOS {@code TimestampTz} β†’ {@link Instant} (Unix-epoch). Preserves + * microsecond precision via the nanos slot. + */ + public static Instant fromMeosInstant(long meosMicros) { + long unixMicros = meosMicros + PG_UNIX_EPOCH_OFFSET_US; + long unixSeconds = Math.floorDiv(unixMicros, 1_000_000L); + int nanos = (int) Math.floorMod(unixMicros, 1_000_000L) * 1000; + return Instant.ofEpochSecond(unixSeconds, nanos); + } + + /** + * JMEOS {@link OffsetDateTime} (PG-epoch ΞΌs in epoch-seconds slot) β†’ + * Spark / JDBC {@link Timestamp}. + */ + public static Timestamp jmeosOdtToSparkTimestamp(OffsetDateTime odt) { + return fromMeosTimestamp(toMeosTimestamp(odt)); + } +} diff --git a/src/main/java/org/mobiltydb/Main.java b/src/main/java/org/mobiltydb/Main.java deleted file mode 100644 index 4a080b4b..00000000 --- a/src/main/java/org/mobiltydb/Main.java +++ /dev/null @@ -1,56 +0,0 @@ -package org.mobiltydb; - -import org.apache.spark.sql.*; -import org.apache.spark.sql.types.DataTypes; -import org.mobiltydb.UDF.PowerUDF; -import org.mobiltydb.UDT.classes.TimestampWithValue; - -import static jmeos.functions.functions.meos_finalize; -import static jmeos.functions.functions.meos_initialize; - -public class Main { - public static void main(String[] args) { - SparkSession spark = SparkSession - .builder() - .master("local[*]") - .appName("Java Spark SQL basic example") - .getOrCreate(); - - meos_initialize("UTC"); - // Create an array of TGeomPointInst instances - TimestampWithValue[] pointsArray = new TimestampWithValue[]{ - new TimestampWithValue(java.sql.Timestamp.valueOf("2023-07-20 12:00:00"), 10.5), - new TimestampWithValue(java.sql.Timestamp.valueOf("2023-07-21 15:30:00"), 15.3), - new TimestampWithValue(java.sql.Timestamp.valueOf("2023-07-22 18:45:00"), 20.1) - }; - - spark.udf().register("power", new PowerUDF(), DataTypes.DoubleType); - - - // Convert the array to a Dataset - Dataset pointsDF = spark.createDataFrame(java.util.Arrays.asList(pointsArray), TimestampWithValue.class); - - // Show the DataFrame - pointsDF.show(); - - // Register the DataFrame as a temporary table - pointsDF.createOrReplaceTempView("pointsTable"); - - // Use Spark SQL query to calculate the Euclidean distance and create a new DataFrame - Dataset result = spark.sql( - "SELECT timestamp, power(value) AS distance FROM pointsTable" - ); - - // Show the resulting DataFrame - result.show(); - - // Perform some basic operations on the DataFrame - Dataset filteredPoints = pointsDF.filter("value > 15.0"); - filteredPoints.show(); - - meos_finalize(); - - // Stop the Spark session - spark.stop(); - } -} diff --git a/src/main/java/org/mobiltydb/UDF/PowerUDF.java b/src/main/java/org/mobiltydb/UDF/PowerUDF.java deleted file mode 100644 index 3e018006..00000000 --- a/src/main/java/org/mobiltydb/UDF/PowerUDF.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.mobiltydb.UDF; - -import org.apache.spark.sql.api.java.UDF1; - -public class PowerUDF implements UDF1 { - @Override - public Double call(Double point1) { - // Calculate the distance between the two TemporalPoint objects using their properties. - return Math.abs(point1*point1); - } -} diff --git a/src/main/java/org/mobiltydb/UDT/TimestampWithValueUDT.java b/src/main/java/org/mobiltydb/UDT/TimestampWithValueUDT.java deleted file mode 100644 index 6ec7cafe..00000000 --- a/src/main/java/org/mobiltydb/UDT/TimestampWithValueUDT.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.mobiltydb.UDT; - -import org.apache.spark.sql.catalyst.InternalRow; -import org.apache.spark.sql.catalyst.expressions.GenericInternalRow; -import org.apache.spark.sql.types.*; -import org.mobiltydb.UDT.classes.TimestampWithValue; - - -public class TimestampWithValueUDT extends UserDefinedType { - - @Override - public StructType sqlType() { - // Define the schema of your TemporalPoint class here - return DataTypes.createStructType(new StructField[] { - DataTypes.createStructField("timestamp", DataTypes.TimestampType, false), - DataTypes.createStructField("value", DataTypes.DoubleType, false) - }); - } - - @Override - public TimestampWithValue deserialize(Object datum) { - if (datum instanceof InternalRow) { - InternalRow row = (InternalRow) datum; - Double value = row.getDouble(1); - java.sql.Timestamp timestamp = (java.sql.Timestamp) row.get(0, DataTypes.TimestampType); - return new TimestampWithValue(timestamp, value); - } - return null; - } - @Override - public Object serialize(TimestampWithValue point) { - if (point == null) { - return null; - } - // Convert your TemporalPoint instance to an InternalRow - return new GenericInternalRow(new Object[] {point.getTimestamp(), point.getValue()}); - } - - @Override - public Class userClass() { - return TimestampWithValue.class; - } -} \ No newline at end of file diff --git a/src/main/java/org/mobiltydb/UDT/classes/TimestampWithValue.java b/src/main/java/org/mobiltydb/UDT/classes/TimestampWithValue.java deleted file mode 100644 index ffd90615..00000000 --- a/src/main/java/org/mobiltydb/UDT/classes/TimestampWithValue.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.mobiltydb.UDT.classes; - - -import java.io.Serializable; -import java.sql.Timestamp; - -public class TimestampWithValue implements Serializable { - private Timestamp timestamp; - private double value; - - public TimestampWithValue(Timestamp timestamp, double value) { - this.timestamp = timestamp; - this.value = value; - } - - public Timestamp getTimestamp() { - return timestamp; - } - - public double getValue() { - return value; - } -} diff --git a/src/test/java/org/mobilitydb/spark/NativeMemoryLeakTest.java b/src/test/java/org/mobilitydb/spark/NativeMemoryLeakTest.java new file mode 100644 index 00000000..b5b27025 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/NativeMemoryLeakTest.java @@ -0,0 +1,207 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark; + +import org.junit.jupiter.api.*; +import org.mobilitydb.spark.geo.GeoUDFs; +import org.mobilitydb.spark.temporal.AnalyticsUDFs; +import org.mobilitydb.spark.temporal.TemporalUDFs; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Validates that MeosMemory.free() prevents native heap accumulation. + * + * Each test calls a UDF 5 000 times after a warmup run, then asserts + * that VmRSS (process resident-set size from /proc/self/status) grew + * by less than 10 MB. This is the Java-binding equivalent of running + * MEOS's C smoke tests (geo_test.c, temporal_test.c, setspan_test.c) + * under {@code valgrind --leak-check=full}. + * + * Why VmRSS rather than the Java heap: MEOS allocates objects with the + * system malloc; the JNR-FFI Pointer wrappers are tiny Java objects β€” + * the underlying C memory is invisible to the garbage collector. + * VmRSS is the only observable that reflects native-heap growth. + * + * Threshold rationale: the 10 MB limit accommodates glibc arena + * fragmentation (~0.1 KB/call) plus the structural char* micro-leak + * from JNR-FFI String-returning bindings (~0.4 KB/call Γ— 5 000 = 2 MB). + * Real Temporal* leaks (the Q02 OOM crash root cause) grow at β‰₯100 KB/call + * and would produce β‰₯500 MB growth β€” far above the 10 MB limit. + * + * Tests are Linux-only (reads /proc/self/status). On non-Linux the + * vmRssKb() helper returns -1 and the growth check is skipped. + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class NativeMemoryLeakTest { + + private static final int WARMUP_ITERS = 200; + private static final int TEST_ITERS = 5_000; + // Tolerates glibc arena fragmentation + the structural JNR-FFI char* micro-leak + // (hex strings JMEOS returns as a Java String without freeing the underlying + // C char* β€” unavoidable with the String-return convention). The magnitude of + // both is environment-dependent: on the CI runners (Ubuntu noble glibc) the + // longer hex-EWKB trajectory strings fragment to ~3.6 KB/call (~18 MB), well + // above a developer box. The 50 MB ceiling sits an order of magnitude below the + // real-Temporal*-leak signal β€” those grow at β‰₯100 KB/call (β‰₯500 MB over 5 000 + // calls; 900 KB/call for full BerlinMOD trips) β€” so it still fails loudly on a + // genuine leak while absorbing structural-noise variance across environments. + private static final long MAX_GROWTH_KB = 51_200; + + private static String TRIP_HEX; + private static final String GEOM_WKT = "POINT(0.05 0.0)"; + private static final String PERIOD = "[2020-01-01 00:00:00+00, 2020-01-01 00:30:00+00]"; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + meos_initialize_error_handler(MeosThread.NOEXIT_ERROR_HANDLER); + TRIP_HEX = temporal_as_hexwkb( + tgeompoint_in("[POINT(0.0 0.0)@2020-01-01 00:00:00+00, POINT(0.1 0.0)@2020-01-01 01:00:00+00]"), + (byte) 0); + } + + // Intentionally no @AfterAll meos_finalize: calling it in a surefire + // @AfterAll causes a JVM crash during shutdown hook execution. + + /** Read VmRSS from /proc/self/status in kB; returns -1 on non-Linux. */ + private static long vmRssKb() { + try (BufferedReader br = new BufferedReader(new FileReader("/proc/self/status"))) { + String line; + while ((line = br.readLine()) != null) { + if (line.startsWith("VmRSS:")) { + return Long.parseLong(line.split("\\s+")[1]); + } + } + } catch (IOException ignored) {} + return -1; + } + + private static void forceGc() { + System.gc(); + System.runFinalization(); + System.gc(); + } + + private static void assertNoLeak(long beforeKb, long afterKb, String udfName) { + if (beforeKb < 0 || afterKb < 0) return; // non-Linux: skip + long growthKb = afterKb - beforeKb; + assertTrue(growthKb < MAX_GROWTH_KB, + udfName + " native heap grew " + growthKb + " KB over " + TEST_ITERS + + " calls (limit " + MAX_GROWTH_KB + " KB); check MeosMemory.free() in UDF"); + } + + // ------------------------------------------------------------------ + // eIntersects β€” heaviest leaker in BerlinMOD Q02 before fix. + // Allocates: Temporal* + STBox* + GSERIALIZED* per call. + // ------------------------------------------------------------------ + @Test @Order(1) + void eIntersects_noNativeLeak() throws Exception { + for (int i = 0; i < WARMUP_ITERS; i++) + GeoUDFs.eIntersects.call(TRIP_HEX, GEOM_WKT); + forceGc(); + long before = vmRssKb(); + + for (int i = 0; i < TEST_ITERS; i++) + GeoUDFs.eIntersects.call(TRIP_HEX, GEOM_WKT); + forceGc(); + assertNoLeak(before, vmRssKb(), "eIntersects"); + } + + // ------------------------------------------------------------------ + // atTime(span) β€” used by BerlinMOD Q07. + // Allocates: Temporal* (input) + Span* + Temporal* (result) per call. + // ------------------------------------------------------------------ + @Test @Order(2) + void atTime_span_noNativeLeak() throws Exception { + for (int i = 0; i < WARMUP_ITERS; i++) + TemporalUDFs.atTime.call(TRIP_HEX, PERIOD); + forceGc(); + long before = vmRssKb(); + + for (int i = 0; i < TEST_ITERS; i++) + TemporalUDFs.atTime.call(TRIP_HEX, PERIOD); + forceGc(); + assertNoLeak(before, vmRssKb(), "atTime(span)"); + } + + // ------------------------------------------------------------------ + // tpointSpeed β€” used by BerlinMOD Q08. + // Allocates: Temporal* (input) + Temporal* (tfloat result) per call. + // ------------------------------------------------------------------ + @Test @Order(3) + void tpointSpeed_noNativeLeak() throws Exception { + for (int i = 0; i < WARMUP_ITERS; i++) + AnalyticsUDFs.tpointSpeed.call(TRIP_HEX); + forceGc(); + long before = vmRssKb(); + + for (int i = 0; i < TEST_ITERS; i++) + AnalyticsUDFs.tpointSpeed.call(TRIP_HEX); + forceGc(); + assertNoLeak(before, vmRssKb(), "tpointSpeed"); + } + + // ------------------------------------------------------------------ + // tpointLength β€” used by BerlinMOD QRT. + // Allocates: Temporal* per call; returns primitive double (no result ptr). + // ------------------------------------------------------------------ + @Test @Order(4) + void tpointLength_noNativeLeak() throws Exception { + for (int i = 0; i < WARMUP_ITERS; i++) + AnalyticsUDFs.tpointLength.call(TRIP_HEX); + forceGc(); + long before = vmRssKb(); + + for (int i = 0; i < TEST_ITERS; i++) + AnalyticsUDFs.tpointLength.call(TRIP_HEX); + forceGc(); + assertNoLeak(before, vmRssKb(), "tpointLength"); + } + + // ------------------------------------------------------------------ + // trajectory β€” used by BerlinMOD Q01. + // Allocates: Temporal* + GSERIALIZED* result per call. + // ------------------------------------------------------------------ + @Test @Order(5) + void trajectory_noNativeLeak() throws Exception { + for (int i = 0; i < WARMUP_ITERS; i++) + GeoUDFs.trajectory.call(TRIP_HEX); + forceGc(); + long before = vmRssKb(); + + for (int i = 0; i < TEST_ITERS; i++) + GeoUDFs.trajectory.call(TRIP_HEX); + forceGc(); + assertNoLeak(before, vmRssKb(), "trajectory"); + } +} diff --git a/src/test/java/org/mobilitydb/spark/geo/DistanceUDFsExtTest.java b/src/test/java/org/mobilitydb/spark/geo/DistanceUDFsExtTest.java new file mode 100644 index 00000000..aa5ddecb --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/geo/DistanceUDFsExtTest.java @@ -0,0 +1,223 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.geo; + +import org.junit.jupiter.api.*; +import org.mobilitydb.spark.temporal.ConstructorUDFs; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for nearest approach distance (nadTgeoGeo, nadTgeoTgeo, + * nadTgeoStbox) and nearest approach instant (naiTgeoGeo, naiTgeoTgeo). + * + * MEOS function authority: meos/include/meos_geo.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class DistanceUDFsExtTest { + + private static String TRIP; + private static String STBOX; + + @BeforeAll + static void initMeos() throws Exception { + meos_initialize(); + meos_initialize_timezone("UTC"); + + // Simple 2-instant trip: (0,0)β†’(4,0) + TRIP = temporal_as_hexwkb( + tgeompoint_in("[POINT(0.0 0.0)@2020-01-01 00:00:00+00, " + + "POINT(4.0 0.0)@2020-01-01 01:00:00+00]"), + (byte) 0); + + // STBox covering (0,0)β†’(4,0) with a time span + STBOX = ConstructorUDFs.stbox.call( + "STBOX XT(((0,0),(4,0)),[2020-01-01 00:00:00+00,2020-01-01 01:00:00+00])"); + } + + // ------------------------------------------------------------------ + // nadTgeoGeo + // ------------------------------------------------------------------ + + @Test @Order(1) + void nadTgeoGeo_collocated_returns_zero() throws Exception { + // Trip lies on x-axis; POINT(2,0) is on the trajectory at mid-time. + Double d = DistanceUDFs.nadTgeoGeo.call(TRIP, "POINT(2 0)"); + assertNotNull(d, "NAD must be non-null"); + assertEquals(0.0, d, 1e-9, "NAD to collocated point must be 0"); + } + + @Test @Order(2) + void nadTgeoGeo_perpendicular_offset() throws Exception { + // POINT(2,3) is 3 units above the trajectory. + Double d = DistanceUDFs.nadTgeoGeo.call(TRIP, "POINT(2 3)"); + assertNotNull(d); + assertEquals(3.0, d, 1e-6, "NAD to point 3 units above must be 3"); + } + + @Test @Order(3) + void nadTgeoGeo_null_trip_returns_null() throws Exception { + assertNull(DistanceUDFs.nadTgeoGeo.call(null, "POINT(0 0)")); + } + + @Test @Order(4) + void nadTgeoGeo_null_geom_returns_null() throws Exception { + assertNull(DistanceUDFs.nadTgeoGeo.call(TRIP, null)); + } + + // ------------------------------------------------------------------ + // nadTgeoStbox + // ------------------------------------------------------------------ + + @Test @Order(5) + void nadTgeoStbox_overlapping_returns_zero() throws Exception { + Double d = DistanceUDFs.nadTgeoStbox.call(TRIP, STBOX); + assertNotNull(d, "NAD tgeoΓ—stbox must be non-null"); + assertEquals(0.0, d, 1e-6, "overlapping trip/stbox must give NAD 0"); + } + + @Test @Order(6) + void nadTgeoStbox_null_trip_returns_null() throws Exception { + assertNull(DistanceUDFs.nadTgeoStbox.call(null, STBOX)); + } + + @Test @Order(7) + void nadTgeoStbox_null_stbox_returns_null() throws Exception { + assertNull(DistanceUDFs.nadTgeoStbox.call(TRIP, null)); + } + + // ------------------------------------------------------------------ + // nadTgeoTgeo + // ------------------------------------------------------------------ + + @Test @Order(8) + void nadTgeoTgeo_same_trip_returns_zero() throws Exception { + Double d = DistanceUDFs.nadTgeoTgeo.call(TRIP, TRIP); + assertNotNull(d); + assertEquals(0.0, d, 1e-9, "NAD of a trip to itself must be 0"); + } + + @Test @Order(9) + void nadTgeoTgeo_null_trip1_returns_null() throws Exception { + assertNull(DistanceUDFs.nadTgeoTgeo.call(null, TRIP)); + } + + @Test @Order(10) + void nadTgeoTgeo_null_trip2_returns_null() throws Exception { + assertNull(DistanceUDFs.nadTgeoTgeo.call(TRIP, null)); + } + + // ------------------------------------------------------------------ + // naiTgeoGeo + // ------------------------------------------------------------------ + + @Test @Order(11) + void naiTgeoGeo_returns_nonnull_hex() throws Exception { + String r = DistanceUDFs.naiTgeoGeo.call(TRIP, "POINT(2 3)"); + assertNotNull(r, "NAI must return non-null hex-WKB TInstant"); + assertFalse(r.isBlank()); + assertTrue(r.matches("[0-9A-Fa-f]+"), "result must be hex-WKB"); + } + + @Test @Order(12) + void naiTgeoGeo_null_trip_returns_null() throws Exception { + assertNull(DistanceUDFs.naiTgeoGeo.call(null, "POINT(0 0)")); + } + + @Test @Order(13) + void naiTgeoGeo_null_geom_returns_null() throws Exception { + assertNull(DistanceUDFs.naiTgeoGeo.call(TRIP, null)); + } + + // ------------------------------------------------------------------ + // naiTgeoTgeo + // ------------------------------------------------------------------ + + @Test @Order(14) + void naiTgeoTgeo_returns_nonnull_hex() throws Exception { + String r = DistanceUDFs.naiTgeoTgeo.call(TRIP, TRIP); + assertNotNull(r, "NAI tgeoΓ—tgeo must return non-null hex-WKB TInstant"); + assertFalse(r.isBlank()); + assertTrue(r.matches("[0-9A-Fa-f]+"), "result must be hex-WKB"); + } + + @Test @Order(15) + void naiTgeoTgeo_null_trip1_returns_null() throws Exception { + assertNull(DistanceUDFs.naiTgeoTgeo.call(null, TRIP)); + } + + @Test @Order(16) + void naiTgeoTgeo_null_trip2_returns_null() throws Exception { + assertNull(DistanceUDFs.naiTgeoTgeo.call(TRIP, null)); + } + + // ------------------------------------------------------------------ + // shortestLineTgeoGeo + // ------------------------------------------------------------------ + + @Test @Order(17) + void shortestLineTgeoGeo_returns_wkt_geometry() throws Exception { + String r = DistanceUDFs.shortestLineTgeoGeo.call(TRIP, "POINT(2 3)"); + assertNotNull(r, "shortestLine must return non-null WKT"); + assertFalse(r.isBlank()); + assertTrue(r.toUpperCase().startsWith("LINESTRING") || r.toUpperCase().startsWith("POINT"), + "result must be WKT geometry"); + } + + @Test @Order(18) + void shortestLineTgeoGeo_null_trip_returns_null() throws Exception { + assertNull(DistanceUDFs.shortestLineTgeoGeo.call(null, "POINT(0 0)")); + } + + @Test @Order(19) + void shortestLineTgeoGeo_null_geom_returns_null() throws Exception { + assertNull(DistanceUDFs.shortestLineTgeoGeo.call(TRIP, null)); + } + + // ------------------------------------------------------------------ + // shortestLineTgeoTgeo + // ------------------------------------------------------------------ + + @Test @Order(20) + void shortestLineTgeoTgeo_returns_wkt_geometry() throws Exception { + String r = DistanceUDFs.shortestLineTgeoTgeo.call(TRIP, TRIP); + assertNotNull(r, "shortestLine tgeoΓ—tgeo must return non-null WKT"); + assertFalse(r.isBlank()); + assertTrue(r.toUpperCase().startsWith("LINESTRING") || r.toUpperCase().startsWith("POINT"), + "result must be WKT geometry"); + } + + @Test @Order(21) + void shortestLineTgeoTgeo_null_trip1_returns_null() throws Exception { + assertNull(DistanceUDFs.shortestLineTgeoTgeo.call(null, TRIP)); + } + + @Test @Order(22) + void shortestLineTgeoTgeo_null_trip2_returns_null() throws Exception { + assertNull(DistanceUDFs.shortestLineTgeoTgeo.call(TRIP, null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/geo/DistanceUDFsTest.java b/src/test/java/org/mobilitydb/spark/geo/DistanceUDFsTest.java new file mode 100644 index 00000000..f687f4f4 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/geo/DistanceUDFsTest.java @@ -0,0 +1,117 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.geo; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for DistanceUDFs β€” temporal distance between tgeo/tnumber and + * fixed or temporal counterparts. + * + * MEOS function authority: meos/include/meos.h, meos/include/meos_geo.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class DistanceUDFsTest { + + private static String TRIP; + private static String TRIP2; + private static String TFLOAT_SEQ; + private static String TINT_SEQ; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TRIP = temporal_as_hexwkb( + tgeompoint_in("[POINT(0 0)@2020-01-01 00:00:00+00, POINT(3 4)@2020-01-01 01:00:00+00]"), + (byte) 0); + TRIP2 = temporal_as_hexwkb( + tgeompoint_in("[POINT(1 1)@2020-01-01 00:00:00+00, POINT(4 5)@2020-01-01 01:00:00+00]"), + (byte) 0); + TFLOAT_SEQ = temporal_as_hexwkb( + tfloat_in("[1.0@2020-01-01, 5.0@2020-01-03]"), (byte) 0); + TINT_SEQ = temporal_as_hexwkb( + tint_in("[2@2020-01-01, 6@2020-01-03]"), (byte) 0); + } + + // ------------------------------------------------------------------ + // Spatial distance + // ------------------------------------------------------------------ + + @Test @Order(1) + void tdistanceTgeoGeo_returns_tfloat() throws Exception { + String r = DistanceUDFs.tdistanceTgeoGeo.call(TRIP, "POINT(0 0)"); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(2) + void tdistanceTgeoTgeo_returns_tfloat() throws Exception { + String r = DistanceUDFs.tdistanceTgeoTgeo.call(TRIP, TRIP2); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + // ------------------------------------------------------------------ + // Number distance + // ------------------------------------------------------------------ + + @Test @Order(3) + void tdistanceTfloatFloat_returns_tfloat() throws Exception { + String r = DistanceUDFs.tdistanceTfloatFloat.call(TFLOAT_SEQ, 3.0); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(4) + void tdistanceTintInt_returns_tint() throws Exception { + String r = DistanceUDFs.tdistanceTintInt.call(TINT_SEQ, 4); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(5) + void tdistanceTnumberTnumber_returns_tfloat() throws Exception { + String r = DistanceUDFs.tdistanceTnumberTnumber.call(TFLOAT_SEQ, TFLOAT_SEQ); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + // ------------------------------------------------------------------ + // Null-input guards + // ------------------------------------------------------------------ + + @Test @Order(6) + void null_input_returns_null() throws Exception { + assertNull(DistanceUDFs.tdistanceTgeoGeo.call(null, "POINT(0 0)")); + assertNull(DistanceUDFs.tdistanceTgeoTgeo.call(null, TRIP2)); + assertNull(DistanceUDFs.tdistanceTfloatFloat.call(null, 1.0)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/geo/GeoAnalyticsUDFsExt2Test.java b/src/test/java/org/mobilitydb/spark/geo/GeoAnalyticsUDFsExt2Test.java new file mode 100644 index 00000000..8f035a52 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/geo/GeoAnalyticsUDFsExt2Test.java @@ -0,0 +1,78 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.geo; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for GeoAnalyticsUDFs.geoSame. + * + * MEOS function authority: meos/include/meos_geo.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class GeoAnalyticsUDFsExt2Test { + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + } + + @Test @Order(1) + void geoSame_identical_points_returns_true() throws Exception { + Boolean r = GeoAnalyticsUDFs.geoSame.call("POINT(1 2)", "POINT(1 2)"); + assertNotNull(r, "geoSame must return non-null for identical points"); + assertTrue(r, "identical geometries must be the same"); + } + + @Test @Order(2) + void geoSame_different_points_returns_false() throws Exception { + Boolean r = GeoAnalyticsUDFs.geoSame.call("POINT(1 2)", "POINT(3 4)"); + assertNotNull(r); + assertFalse(r, "different geometries must not be the same"); + } + + @Test @Order(3) + void geoSame_identical_polygons_returns_true() throws Exception { + String poly = "POLYGON((0 0,1 0,1 1,0 1,0 0))"; + Boolean r = GeoAnalyticsUDFs.geoSame.call(poly, poly); + assertNotNull(r); + assertTrue(r, "identical polygons must be the same"); + } + + @Test @Order(4) + void geoSame_null_first_returns_null() throws Exception { + assertNull(GeoAnalyticsUDFs.geoSame.call(null, "POINT(1 2)")); + } + + @Test @Order(5) + void geoSame_null_second_returns_null() throws Exception { + assertNull(GeoAnalyticsUDFs.geoSame.call("POINT(1 2)", null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/geo/GeoAnalyticsUDFsExt3Test.java b/src/test/java/org/mobilitydb/spark/geo/GeoAnalyticsUDFsExt3Test.java new file mode 100644 index 00000000..09e778f7 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/geo/GeoAnalyticsUDFsExt3Test.java @@ -0,0 +1,114 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.geo; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for tpoint spatial analytics UDFs: + * tpointConvexHull, tpointExpandSpace. + * + * MEOS function authority: meos/include/meos_geo.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class GeoAnalyticsUDFsExt3Test { + + private static String TRIP; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TRIP = temporal_as_hexwkb( + tgeompoint_in("[POINT(0.0 0.0)@2020-01-01 00:00:00+00, " + + "POINT(2.0 2.0)@2020-01-01 01:00:00+00, " + + "POINT(4.0 0.0)@2020-01-01 02:00:00+00]"), + (byte) 0); + } + + // ------------------------------------------------------------------ + // tpointConvexHull + // ------------------------------------------------------------------ + + @Test @Order(1) + void tpointConvexHull_returns_nonnull() throws Exception { + String r = GeoAnalyticsUDFs.tpointConvexHull.call(TRIP); + assertNotNull(r, "tpointConvexHull must return non-null hex-EWKB for valid trip"); + assertFalse(r.isBlank()); + } + + @Test @Order(2) + void tpointConvexHull_result_is_parseable_hex() throws Exception { + String r = GeoAnalyticsUDFs.tpointConvexHull.call(TRIP); + assertNotNull(r); + // All hex characters β€” a valid hex-EWKB string has only hex digits. + assertTrue(r.matches("[0-9A-Fa-f]+"), "result must be a hex string"); + } + + @Test @Order(3) + void tpointConvexHull_null_trip_returns_null() throws Exception { + assertNull(GeoAnalyticsUDFs.tpointConvexHull.call(null)); + } + + // ------------------------------------------------------------------ + // tpointExpandSpace + // ------------------------------------------------------------------ + + @Test @Order(4) + void tpointExpandSpace_returns_nonnull() throws Exception { + String r = GeoAnalyticsUDFs.tpointExpandSpace.call(TRIP, 1.0); + assertNotNull(r, "tpointExpandSpace must return non-null hex-WKB STBOX"); + assertFalse(r.isBlank()); + } + + @Test @Order(5) + void tpointExpandSpace_result_is_parseable_hex() throws Exception { + String r = GeoAnalyticsUDFs.tpointExpandSpace.call(TRIP, 0.5); + assertNotNull(r); + assertTrue(r.matches("[0-9A-Fa-f]+"), "result must be a hex string"); + } + + @Test @Order(6) + void tpointExpandSpace_null_trip_returns_null() throws Exception { + assertNull(GeoAnalyticsUDFs.tpointExpandSpace.call(null, 1.0)); + } + + @Test @Order(7) + void tpointExpandSpace_null_distance_returns_null() throws Exception { + assertNull(GeoAnalyticsUDFs.tpointExpandSpace.call(TRIP, null)); + } + + @Test @Order(8) + void tpointExpandSpace_zero_distance_returns_nonnull() throws Exception { + String r = GeoAnalyticsUDFs.tpointExpandSpace.call(TRIP, 0.0); + assertNotNull(r, "zero expansion distance must still produce a valid STBOX"); + assertFalse(r.isBlank()); + } +} diff --git a/src/test/java/org/mobilitydb/spark/geo/GeoUDFsExt2Test.java b/src/test/java/org/mobilitydb/spark/geo/GeoUDFsExt2Test.java new file mode 100644 index 00000000..33663a33 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/geo/GeoUDFsExt2Test.java @@ -0,0 +1,207 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.geo; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for GeoUDFs ever/always predicates: + * eDisjoint, eTouches, eCovers, eDisjointTgeoTgeo, eIntersectsTgeoTgeo, + * aIntersects, aDisjoint, aDwithin, eDwithinGeo, aDwithinGeo. + * + * Fixture: + * TRIP β€” linear from (0,0) to (1,0) in 1 hour + * TRIP2 β€” parallel trip (1,0)β†’(2,0), same time window + * REGION_ON_PATH β€” polygon covering part of trip + * REGION_FAR β€” polygon far from trip + * + * MEOS function authority: meos/include/meos_geo.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class GeoUDFsExt2Test { + + private static String TRIP; + private static String TRIP2; + private static final String REGION_ON_PATH = "POLYGON((-0.1 -1, 0.6 -1, 0.6 1, -0.1 1, -0.1 -1))"; + private static final String REGION_FAR = "POLYGON((10 10, 11 10, 11 11, 10 11, 10 10))"; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TRIP = temporal_as_hexwkb( + tgeompoint_in("[POINT(0.0 0.0)@2020-01-01 00:00:00+00, POINT(1.0 0.0)@2020-01-01 01:00:00+00]"), + (byte) 0); + TRIP2 = temporal_as_hexwkb( + tgeompoint_in("[POINT(1.0 0.0)@2020-01-01 00:00:00+00, POINT(2.0 0.0)@2020-01-01 01:00:00+00]"), + (byte) 0); + } + + // ------------------------------------------------------------------ + // eDisjoint + // ------------------------------------------------------------------ + + @Test @Order(1) + void eDisjoint_trip_with_far_region_returns_true() throws Exception { + assertTrue(GeoUDFs.eDisjoint.call(TRIP, REGION_FAR), + "Trip is always disjoint from far region β€” ever-disjoint must be true"); + } + + @Test @Order(2) + void eDisjoint_trip_with_on_path_region_returns_non_null() throws Exception { + // Trip starts inside the region and exits β€” ever-disjoint is true at the exit end. + // Just verify the UDF completes without exception and returns a Boolean. + Boolean r = GeoUDFs.eDisjoint.call(TRIP, REGION_ON_PATH); + assertNotNull(r); + } + + @Test @Order(3) + void eDisjoint_null_returns_null() throws Exception { + assertNull(GeoUDFs.eDisjoint.call(null, REGION_FAR)); + assertNull(GeoUDFs.eDisjoint.call(TRIP, null)); + } + + // ------------------------------------------------------------------ + // eTouches + // ------------------------------------------------------------------ + + @Test @Order(4) + void eTouches_returns_non_null_for_valid_inputs() throws Exception { + Boolean r = GeoUDFs.eTouches.call(TRIP, REGION_ON_PATH); + // Result is Boolean; just ensure no crash + assertTrue(r == null || r instanceof Boolean); + } + + @Test @Order(5) + void eTouches_null_returns_null() throws Exception { + assertNull(GeoUDFs.eTouches.call(null, REGION_ON_PATH)); + assertNull(GeoUDFs.eTouches.call(TRIP, null)); + } + + // ------------------------------------------------------------------ + // eCovers + // ------------------------------------------------------------------ + + @Test @Order(6) + void eCovers_returns_non_null_boolean() throws Exception { + // ecovers_tgeo_geo semantics: returns true if the moving object ever covers + // the static geometry at any instant. Just verify no exception and non-null. + Boolean r = GeoUDFs.eCovers.call(TRIP, REGION_ON_PATH); + assertNotNull(r); + } + + @Test @Order(7) + void eCovers_null_returns_null() throws Exception { + assertNull(GeoUDFs.eCovers.call(null, REGION_ON_PATH)); + } + + // ------------------------------------------------------------------ + // eDisjointTgeoTgeo / eIntersectsTgeoTgeo + // ------------------------------------------------------------------ + + @Test @Order(8) + void eDisjointTgeoTgeo_non_overlapping_trips_returns_true() throws Exception { + assertTrue(GeoUDFs.eDisjointTgeoTgeo.call(TRIP, TRIP2) != null); + } + + @Test @Order(9) + void eIntersectsTgeoTgeo_same_trip_returns_true() throws Exception { + assertTrue(GeoUDFs.eIntersectsTgeoTgeo.call(TRIP, TRIP), + "A trip always intersects itself"); + } + + @Test @Order(10) + void eDisjointTgeoTgeo_null_returns_null() throws Exception { + assertNull(GeoUDFs.eDisjointTgeoTgeo.call(null, TRIP)); + assertNull(GeoUDFs.eIntersectsTgeoTgeo.call(null, TRIP)); + } + + // ------------------------------------------------------------------ + // aIntersects / aDisjoint + // ------------------------------------------------------------------ + + @Test @Order(11) + void aDisjoint_trip_with_far_region_returns_true() throws Exception { + assertTrue(GeoUDFs.aDisjoint.call(TRIP, REGION_FAR), + "Trip is always disjoint from far region"); + } + + @Test @Order(12) + void aIntersects_trip_with_far_region_returns_false() throws Exception { + assertFalse(GeoUDFs.aIntersects.call(TRIP, REGION_FAR), + "Trip never intersects the far region"); + } + + @Test @Order(13) + void aIntersects_aDisjoint_null_returns_null() throws Exception { + assertNull(GeoUDFs.aIntersects.call(null, REGION_FAR)); + assertNull(GeoUDFs.aDisjoint.call(null, REGION_FAR)); + } + + // ------------------------------------------------------------------ + // aDwithin / eDwithinGeo / aDwithinGeo + // ------------------------------------------------------------------ + + @Test @Order(14) + void aDwithin_same_trip_with_large_dist_returns_true() throws Exception { + assertTrue(GeoUDFs.aDwithin.call(TRIP, TRIP, 1.0), + "A trip is always within distance 1.0 of itself"); + } + + @Test @Order(15) + void aDwithin_null_returns_null() throws Exception { + assertNull(GeoUDFs.aDwithin.call(null, TRIP, 1.0)); + assertNull(GeoUDFs.aDwithin.call(TRIP, null, 1.0)); + assertNull(GeoUDFs.aDwithin.call(TRIP, TRIP, null)); + } + + @Test @Order(16) + void eDwithinGeo_trip_within_large_radius_of_nearby_polygon_returns_true() throws Exception { + assertTrue(GeoUDFs.eDwithinGeo.call(TRIP, REGION_ON_PATH, 100.0), + "Trip is within 100 units of nearby polygon"); + } + + @Test @Order(17) + void eDwithinGeo_null_returns_null() throws Exception { + assertNull(GeoUDFs.eDwithinGeo.call(null, REGION_ON_PATH, 1.0)); + } + + @Test @Order(18) + void aDwithinGeo_trip_far_from_far_region_large_radius_returns_true() throws Exception { + Boolean r = GeoUDFs.aDwithinGeo.call(TRIP, REGION_FAR, 1000.0); + assertNotNull(r); + assertTrue(r, "Trip should be within 1000 units of far region"); + } + + @Test @Order(19) + void aDwithinGeo_null_returns_null() throws Exception { + assertNull(GeoUDFs.aDwithinGeo.call(null, REGION_FAR, 1.0)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/geo/GeoUDFsExt3Test.java b/src/test/java/org/mobilitydb/spark/geo/GeoUDFsExt3Test.java new file mode 100644 index 00000000..19234db2 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/geo/GeoUDFsExt3Test.java @@ -0,0 +1,157 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.geo; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for geo I/O UDFs: + * geoAsEwkt, geoAsGeojson, geoFromGeojson. + * + * MEOS function authority: meos/include/meos_geo.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class GeoUDFsExt3Test { + + private static final String POINT_WKT = "POINT(4.35 50.85)"; + private static final String POLYGON_WKT = "POLYGON((0 0,1 0,1 1,0 1,0 0))"; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + } + + // ------------------------------------------------------------------ + // geoAsEwkt + // ------------------------------------------------------------------ + + @Test @Order(1) + void geoAsEwkt_point_returns_nonnull() throws Exception { + String r = GeoUDFs.geoAsEwkt.call(POINT_WKT, 6); + assertNotNull(r, "geoAsEwkt must return non-null for a valid WKT point"); + assertFalse(r.isBlank()); + } + + @Test @Order(2) + void geoAsEwkt_point_contains_keyword() throws Exception { + String r = GeoUDFs.geoAsEwkt.call(POINT_WKT, 6); + assertNotNull(r); + assertTrue(r.toUpperCase().contains("POINT"), "EWKT must contain POINT keyword"); + } + + @Test @Order(3) + void geoAsEwkt_polygon_returns_nonnull() throws Exception { + String r = GeoUDFs.geoAsEwkt.call(POLYGON_WKT, 6); + assertNotNull(r, "geoAsEwkt must return non-null for a polygon"); + assertFalse(r.isBlank()); + } + + @Test @Order(4) + void geoAsEwkt_null_wkt_returns_null() throws Exception { + assertNull(GeoUDFs.geoAsEwkt.call(null, 6)); + } + + @Test @Order(5) + void geoAsEwkt_null_precision_uses_default() throws Exception { + String r = GeoUDFs.geoAsEwkt.call(POINT_WKT, null); + assertNotNull(r, "null precision must fall back to default (15)"); + assertFalse(r.isBlank()); + } + + // ------------------------------------------------------------------ + // geoAsGeojson + // ------------------------------------------------------------------ + + @Test @Order(6) + void geoAsGeojson_point_returns_nonnull() throws Exception { + String r = GeoUDFs.geoAsGeojson.call(POINT_WKT, 0, 6); + assertNotNull(r, "geoAsGeojson must return non-null for a valid WKT point"); + assertFalse(r.isBlank()); + } + + @Test @Order(7) + void geoAsGeojson_point_contains_type_key() throws Exception { + String r = GeoUDFs.geoAsGeojson.call(POINT_WKT, 0, 6); + assertNotNull(r); + assertTrue(r.contains("\"type\""), "GeoJSON output must contain 'type' key"); + } + + @Test @Order(8) + void geoAsGeojson_polygon_returns_nonnull() throws Exception { + String r = GeoUDFs.geoAsGeojson.call(POLYGON_WKT, 0, 6); + assertNotNull(r, "geoAsGeojson must return non-null for a polygon"); + assertFalse(r.isBlank()); + } + + @Test @Order(9) + void geoAsGeojson_null_returns_null() throws Exception { + assertNull(GeoUDFs.geoAsGeojson.call(null, 0, 6)); + } + + @Test @Order(10) + void geoAsGeojson_null_options_uses_default() throws Exception { + String r = GeoUDFs.geoAsGeojson.call(POINT_WKT, null, 6); + assertNotNull(r, "null options must fall back to default (0)"); + } + + // ------------------------------------------------------------------ + // geoFromGeojson + // ------------------------------------------------------------------ + + @Test @Order(11) + void geoFromGeojson_point_returns_wkt() throws Exception { + String geojson = "{\"type\":\"Point\",\"coordinates\":[4.35,50.85]}"; + String r = GeoUDFs.geoFromGeojson.call(geojson); + assertNotNull(r, "geoFromGeojson must return non-null for valid GeoJSON"); + assertFalse(r.isBlank()); + } + + @Test @Order(12) + void geoFromGeojson_roundtrip_contains_point() throws Exception { + String geojson = "{\"type\":\"Point\",\"coordinates\":[1.0,2.0]}"; + String r = GeoUDFs.geoFromGeojson.call(geojson); + assertNotNull(r); + assertTrue(r.toUpperCase().contains("POINT"), "round-trip WKT must contain POINT"); + } + + @Test @Order(13) + void geoFromGeojson_null_returns_null() throws Exception { + assertNull(GeoUDFs.geoFromGeojson.call(null)); + } + + @Test @Order(14) + void geoAsGeojson_then_fromGeojson_roundtrip_consistent() throws Exception { + String geojson = GeoUDFs.geoAsGeojson.call(POINT_WKT, 0, 9); + assertNotNull(geojson); + String wkt = GeoUDFs.geoFromGeojson.call(geojson); + assertNotNull(wkt, "round-trip WKTβ†’GeoJSONβ†’WKT must not be null"); + assertTrue(wkt.toUpperCase().contains("POINT")); + } +} diff --git a/src/test/java/org/mobilitydb/spark/geo/GeoUDFsExt4Test.java b/src/test/java/org/mobilitydb/spark/geo/GeoUDFsExt4Test.java new file mode 100644 index 00000000..13ed161e --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/geo/GeoUDFsExt4Test.java @@ -0,0 +1,210 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.geo; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for tpoint I/O and transformation UDFs: + * tpointAsText, tpointAsEWKT, tpointSRID, tpointSetSRID, tpointRound, + * tgeomToTgeog, tgeogToTgeom, tpointToStbox. + * + * MEOS function authority: meos/include/meos_geo.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class GeoUDFsExt4Test { + + private static String TRIP; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TRIP = temporal_as_hexwkb( + tgeompoint_in("[POINT(1.0 2.0)@2020-01-01 00:00:00+00, " + + "POINT(3.0 4.0)@2020-01-01 01:00:00+00]"), + (byte) 0); + } + + // ------------------------------------------------------------------ + // tpointAsText + // ------------------------------------------------------------------ + + @Test @Order(1) + void tpointAsText_returns_nonnull() throws Exception { + String r = GeoUDFs.tpointAsText.call(TRIP, 6); + assertNotNull(r, "tpointAsText must return non-null for valid trip"); + assertFalse(r.isBlank()); + } + + @Test @Order(2) + void tpointAsText_contains_point_keyword() throws Exception { + String r = GeoUDFs.tpointAsText.call(TRIP, 6); + assertNotNull(r); + assertTrue(r.toUpperCase().contains("POINT"), "output must contain POINT keyword"); + } + + @Test @Order(3) + void tpointAsText_null_trip_returns_null() throws Exception { + assertNull(GeoUDFs.tpointAsText.call(null, 6)); + } + + @Test @Order(4) + void tpointAsText_null_precision_uses_default() throws Exception { + String r = GeoUDFs.tpointAsText.call(TRIP, null); + assertNotNull(r, "null precision must fall back to 15"); + assertFalse(r.isBlank()); + } + + // ------------------------------------------------------------------ + // tpointAsEWKT + // ------------------------------------------------------------------ + + @Test @Order(5) + void tpointAsEWKT_returns_nonnull() throws Exception { + String r = GeoUDFs.tpointAsEWKT.call(TRIP, 6); + assertNotNull(r, "tpointAsEWKT must return non-null for valid trip"); + assertFalse(r.isBlank()); + } + + @Test @Order(6) + void tpointAsEWKT_contains_point_keyword() throws Exception { + String r = GeoUDFs.tpointAsEWKT.call(TRIP, 6); + assertNotNull(r); + assertTrue(r.toUpperCase().contains("POINT"), "EWKT must contain POINT keyword"); + } + + @Test @Order(7) + void tpointAsEWKT_null_trip_returns_null() throws Exception { + assertNull(GeoUDFs.tpointAsEWKT.call(null, 6)); + } + + @Test @Order(8) + void tpointAsEWKT_null_precision_uses_default() throws Exception { + String r = GeoUDFs.tpointAsEWKT.call(TRIP, null); + assertNotNull(r, "null precision must fall back to 15"); + assertFalse(r.isBlank()); + } + + // ------------------------------------------------------------------ + // tpointSRID + // ------------------------------------------------------------------ + + @Test @Order(9) + void tpointSRID_returns_integer() throws Exception { + Integer r = GeoUDFs.tpointSRID.call(TRIP); + assertNotNull(r, "tpointSRID must return non-null for valid trip"); + } + + @Test @Order(10) + void tpointSRID_null_trip_returns_null() throws Exception { + assertNull(GeoUDFs.tpointSRID.call(null)); + } + + // ------------------------------------------------------------------ + // tpointSetSRID + // ------------------------------------------------------------------ + + @Test @Order(11) + void tpointSetSRID_returns_nonnull() throws Exception { + String r = GeoUDFs.tpointSetSRID.call(TRIP, 4326); + assertNotNull(r, "tpointSetSRID must return non-null hex-WKB"); + assertFalse(r.isBlank()); + } + + @Test @Order(12) + void tpointSetSRID_result_is_valid_temporal() throws Exception { + // ISO WKB (variant 0) does not embed SRID, so a hex-WKB round-trip resets + // SRID to 0. This test verifies the function completes without error and + // the result can be deserialized as a valid temporal. + String r = GeoUDFs.tpointSetSRID.call(TRIP, 4326); + assertNotNull(r); + String text = GeoUDFs.tpointAsText.call(r, 6); + assertNotNull(text, "result of tpointSetSRID must be a valid temporal"); + } + + @Test @Order(13) + void tpointSetSRID_null_trip_returns_null() throws Exception { + assertNull(GeoUDFs.tpointSetSRID.call(null, 4326)); + } + + @Test @Order(14) + void tpointSetSRID_null_srid_returns_null() throws Exception { + assertNull(GeoUDFs.tpointSetSRID.call(TRIP, null)); + } + + // ------------------------------------------------------------------ + // tpointRound + // ------------------------------------------------------------------ + + @Test @Order(15) + void tpointRound_returns_nonnull() throws Exception { + String r = GeoUDFs.tpointRound.call(TRIP, 2); + assertNotNull(r, "tpointRound must return non-null hex-WKB"); + assertFalse(r.isBlank()); + } + + @Test @Order(16) + void tpointRound_null_trip_returns_null() throws Exception { + assertNull(GeoUDFs.tpointRound.call(null, 2)); + } + + @Test @Order(17) + void tpointRound_null_decimals_uses_default() throws Exception { + String r = GeoUDFs.tpointRound.call(TRIP, null); + assertNotNull(r, "null decimals must fall back to 6"); + assertFalse(r.isBlank()); + } + + @Test @Order(18) + void tpointRound_preserves_type() throws Exception { + String r = GeoUDFs.tpointRound.call(TRIP, 3); + assertNotNull(r); + // The result must be a valid temporal: tpointSRID must succeed. + Integer srid = GeoUDFs.tpointSRID.call(r); + assertNotNull(srid, "rounded trip must still be a valid tgeompoint"); + } + + // ------------------------------------------------------------------ + // tpointToStbox + // ------------------------------------------------------------------ + + @Test @Order(19) + void tpointToStbox_returns_nonnull() throws Exception { + String r = GeoUDFs.tpointToStbox.call(TRIP); + assertNotNull(r, "tpointToStbox must return non-null hex-WKB STBOX"); + assertFalse(r.isBlank()); + } + + @Test @Order(20) + void tpointToStbox_null_trip_returns_null() throws Exception { + assertNull(GeoUDFs.tpointToStbox.call(null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/geo/GeoUDFsExt5Test.java b/src/test/java/org/mobilitydb/spark/geo/GeoUDFsExt5Test.java new file mode 100644 index 00000000..020ebf95 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/geo/GeoUDFsExt5Test.java @@ -0,0 +1,85 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.geo; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for tpointTransform. + * + * MEOS function authority: meos/include/meos_geo.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class GeoUDFsExt5Test { + + private static String TRIP_4326; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + // SRID 4326: WGS-84 geographic coordinates + TRIP_4326 = temporal_as_hexwkb( + tgeompoint_in("SRID=4326;[POINT(4.35 50.85)@2020-01-01 00:00:00+00, " + + "POINT(4.40 50.90)@2020-01-01 01:00:00+00]"), + (byte) 0); + } + + // ------------------------------------------------------------------ + // tpointTransform + // + // Note: tspatial_transform requires a spatial_ref_sys catalog loaded via + // meos_set_spatial_ref_sys_csv() (done inside MobilitySparkSession.create()). + // Unit tests that call meos_initialize() directly do not have the catalog, + // so transform calls return null gracefully (MEOS logs an error internally + // but does not crash). Tests here verify the null-handling contract and + // that the UDF does not throw any exception. + // ------------------------------------------------------------------ + + @Test @Order(1) + void tpointTransform_does_not_throw_without_srs_catalog() throws Exception { + // Without spatial_ref_sys.csv, MEOS returns null for any transform. + // Verify the UDF wraps this gracefully (null, not an exception). + String r = GeoUDFs.tpointTransform.call(TRIP_4326, 3857); + // r may be null β€” that is the correct contract when CRS is unavailable + assertTrue(r == null || !r.isBlank(), + "result must be null (no catalog) or a non-empty hex-WKB string"); + } + + @Test @Order(2) + void tpointTransform_null_trip_returns_null() throws Exception { + assertNull(GeoUDFs.tpointTransform.call(null, 3857)); + } + + @Test @Order(3) + void tpointTransform_null_srid_returns_null() throws Exception { + assertNull(GeoUDFs.tpointTransform.call(TRIP_4326, null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/geo/GeoUDFsExtTest.java b/src/test/java/org/mobilitydb/spark/geo/GeoUDFsExtTest.java new file mode 100644 index 00000000..c5f6e3ab --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/geo/GeoUDFsExtTest.java @@ -0,0 +1,163 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.geo; + +import org.junit.jupiter.api.*; +import org.mobilitydb.spark.temporal.AccessorUDFs; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for GeoUDFs Phase 4 extensions β€” getX/Y/Z, cumulativeLength, + * stops, isSimple, shortestLine. + * + * Tests run directly against MEOS via JMEOS without a Spark session. + * MEOS function authority: meos/include/meos_geo.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class GeoUDFsExtTest { + + // Linear trip from (0,0) to (2,0) over 2 hours + private static String TRIP_HEX; + // Second trip offset in Y + private static String TRIP2_HEX; + // Self-intersecting trip (figure-8 approximation) + private static String TRIP_SELF_INTERSECTING; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TRIP_HEX = temporal_as_hexwkb( + tgeompoint_in("[POINT(0 0)@2020-01-01 00:00:00+00, POINT(2 0)@2020-01-01 02:00:00+00]"), + (byte) 0); + TRIP2_HEX = temporal_as_hexwkb( + tgeompoint_in("[POINT(3 1)@2020-01-01 00:00:00+00, POINT(3 2)@2020-01-01 02:00:00+00]"), + (byte) 0); + // A simple sequence (no self-intersection) + TRIP_SELF_INTERSECTING = temporal_as_hexwkb( + tgeompoint_in("[POINT(0 0)@2020-01-01 00:00:00+00, POINT(1 0)@2020-01-01 01:00:00+00, POINT(2 0)@2020-01-01 02:00:00+00]"), + (byte) 0); + } + + @Test @Order(1) + void getX_returns_tfloat_hex() throws Exception { + String x = GeoUDFs.getX.call(TRIP_HEX); + assertNotNull(x, "getX should return tfloat hex-WKB"); + assertFalse(x.isBlank()); + } + + @Test @Order(2) + void getX_start_value_is_zero() throws Exception { + String x = GeoUDFs.getX.call(TRIP_HEX); + assertNotNull(x); + // The start X coordinate should be 0.0 for POINT(0 0)@t1 + // Use tfloatStartValue from AccessorUDFs to check + Double sv = AccessorUDFs.tfloatStartValue.call(x); + assertNotNull(sv); + assertEquals(0.0, sv, 1e-9); + } + + @Test @Order(3) + void getY_returns_tfloat_hex() throws Exception { + String y = GeoUDFs.getY.call(TRIP_HEX); + assertNotNull(y, "getY should return tfloat hex-WKB"); + assertFalse(y.isBlank()); + } + + @Test @Order(4) + void getZ_2d_trip_returns_null() throws Exception { + // 2D tgeompoint has no Z component β†’ MEOS returns null + assertNull(GeoUDFs.getZ.call(TRIP_HEX)); + } + + @Test @Order(5) + void getX_null_returns_null() throws Exception { + assertNull(GeoUDFs.getX.call(null)); + } + + @Test @Order(6) + void cumulativeLength_starts_at_zero() throws Exception { + String cl = GeoUDFs.cumulativeLength.call(TRIP_HEX); + assertNotNull(cl, "cumulativeLength should return a tfloat hex-WKB"); + // The start value of cumulative length for a linear trip should be 0.0 + Double sv = AccessorUDFs.tfloatStartValue.call(cl); + assertNotNull(sv); + assertEquals(0.0, sv, 1e-9); + } + + @Test @Order(7) + void cumulativeLength_null_returns_null() throws Exception { + assertNull(GeoUDFs.cumulativeLength.call(null)); + } + + @Test @Order(8) + void stops_returns_null_for_moving_trip() throws Exception { + // A perfectly linear trip has no stops β€” MEOS returns null for empty stops + // (rather than an empty sequence set) + String s = GeoUDFs.stops.call(TRIP_HEX, 0.001, "1 second"); + if (s != null) assertFalse(s.isBlank()); + } + + @Test @Order(9) + void stops_null_trip_returns_null() throws Exception { + assertNull(GeoUDFs.stops.call(null, 0.001, "1 second")); + } + + @Test @Order(10) + void isSimple_linear_trip_returns_true() throws Exception { + assertTrue(GeoUDFs.isSimple.call(TRIP_HEX), + "A straight linear trip should have no self-intersections"); + } + + @Test @Order(11) + void isSimple_null_returns_null() throws Exception { + assertNull(GeoUDFs.isSimple.call(null)); + } + + @Test @Order(12) + void shortestLine_between_parallel_trips_returns_wkt() throws Exception { + String wkt = GeoUDFs.shortestLine.call(TRIP_HEX, TRIP2_HEX); + assertNotNull(wkt, "shortestLine should return a WKT geometry"); + assertTrue(wkt.startsWith("POINT") || wkt.startsWith("LINESTRING"), + "Expected WKT geometry, got: " + wkt); + } + + @Test @Order(13) + void shortestLine_same_trip_returns_point() throws Exception { + String wkt = GeoUDFs.shortestLine.call(TRIP_HEX, TRIP_HEX); + assertNotNull(wkt); + assertTrue(wkt.startsWith("POINT") || wkt.startsWith("LINESTRING"), + "Expected WKT, got: " + wkt); + } + + @Test @Order(14) + void shortestLine_null_returns_null() throws Exception { + assertNull(GeoUDFs.shortestLine.call(null, TRIP_HEX)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/geo/GeoUDFsTest.java b/src/test/java/org/mobilitydb/spark/geo/GeoUDFsTest.java new file mode 100644 index 00000000..fa5cf754 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/geo/GeoUDFsTest.java @@ -0,0 +1,114 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.geo; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for GeoUDFs β€” spatial relations and distance on tgeompoint. + * + * Tests run directly against MEOS via JMEOS without a Spark session. + * MEOS function authority: meos/include/meos_geo.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class GeoUDFsTest { + + private static String TRIP_HEX; + private static String TRIP2_HEX; + // Geometry passed as WKT text β€” eIntersects parses via geo_from_text internally + private static final String POINT_ON_PATH = "POINT(0.05 0.0)"; + private static final String POINT_FAR = "POINT(10.0 10.0)"; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TRIP_HEX = temporal_as_hexwkb( + tgeompoint_in("[POINT(0.0 0.0)@2020-01-01 00:00:00+00, POINT(0.1 0.0)@2020-01-01 01:00:00+00]"), + (byte) 0); + TRIP2_HEX = temporal_as_hexwkb( + tgeompoint_in("[POINT(0.05 0.001)@2020-01-01 00:00:00+00, POINT(0.15 0.001)@2020-01-01 01:00:00+00]"), + (byte) 0); + } + + @AfterAll + static void finalizeMeos() { + meos_finalize(); + } + + @Test @Order(1) + void eIntersects_point_on_path_returns_true() throws Exception { + assertTrue(GeoUDFs.eIntersects.call(TRIP_HEX, POINT_ON_PATH)); + } + + @Test @Order(2) + void eIntersects_far_point_returns_false() throws Exception { + assertFalse(GeoUDFs.eIntersects.call(TRIP_HEX, POINT_FAR)); + } + + @Test @Order(3) + void eIntersects_null_trip_returns_null() throws Exception { + assertNull(GeoUDFs.eIntersects.call(null, POINT_ON_PATH)); + } + + @Test @Order(4) + void nearestApproachDistance_same_trip_is_zero() throws Exception { + Double d = GeoUDFs.nearestApproachDistance.call(TRIP_HEX, TRIP_HEX); + assertNotNull(d); + assertEquals(0.0, d, 1e-9); + } + + @Test @Order(5) + void nearestApproachDistance_parallel_trips() throws Exception { + Double d = GeoUDFs.nearestApproachDistance.call(TRIP_HEX, TRIP2_HEX); + assertNotNull(d); + assertTrue(d > 0.0 && d < 0.1, "Expected positive distance < 0.1, got " + d); + } + + @Test @Order(6) + void nearestApproachDistance_null_returns_null() throws Exception { + assertNull(GeoUDFs.nearestApproachDistance.call(null, TRIP_HEX)); + } + + @Test @Order(7) + void eDwithin_within_large_distance() throws Exception { + assertTrue(GeoUDFs.eDwithin.call(TRIP_HEX, TRIP2_HEX, 1.0)); + } + + @Test @Order(8) + void eDwithin_outside_tiny_distance() throws Exception { + assertFalse(GeoUDFs.eDwithin.call(TRIP_HEX, TRIP2_HEX, 1e-10)); + } + + @Test @Order(9) + void eDwithin_null_returns_null() throws Exception { + assertNull(GeoUDFs.eDwithin.call(null, TRIP_HEX, 1.0)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/geo/STBoxUDFsTest.java b/src/test/java/org/mobilitydb/spark/geo/STBoxUDFsTest.java new file mode 100644 index 00000000..18378019 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/geo/STBoxUDFsTest.java @@ -0,0 +1,224 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.geo; + +import org.junit.jupiter.api.*; +import org.mobilitydb.spark.temporal.ConstructorUDFs; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for STBoxUDFs β€” STBox accessor and expansion operations. + * + * Tests run directly against MEOS via JMEOS without a Spark session. + * Input STBox values are produced via ConstructorUDFs.stbox (which handles + * the scratch-Pointer allocation required by stbox_as_hexwkb). + * + * MEOS function authority: meos/include/meos_geo.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class STBoxUDFsTest { + + // STBOX XT([-1,1],[-2,2],[2020-01-01,2020-01-02]) + private static String STBOX_XT; + // STBOX T([2020-01-01,2020-01-02]) β€” temporal-only box + private static String STBOX_T; + + @BeforeAll + static void initMeos() throws Exception { + meos_initialize(); + meos_initialize_timezone("UTC"); + + STBOX_XT = ConstructorUDFs.stbox.call( + "STBOX XT(((-1,-2),(1,2)),[2020-01-01 00:00:00+00,2020-01-02 00:00:00+00])"); + STBOX_T = ConstructorUDFs.stbox.call( + "STBOX T([2020-01-01 00:00:00+00,2020-01-02 00:00:00+00])"); + } + + @Test @Order(1) + void stboxHasx_spatial_box_returns_true() throws Exception { + assertTrue(STBoxUDFs.stboxHasx.call(STBOX_XT)); + } + + @Test @Order(2) + void stboxHasx_temporal_only_box_returns_false() throws Exception { + assertFalse(STBoxUDFs.stboxHasx.call(STBOX_T)); + } + + @Test @Order(3) + void stboxHast_spatial_temporal_box_returns_true() throws Exception { + assertTrue(STBoxUDFs.stboxHast.call(STBOX_XT)); + } + + @Test @Order(4) + void stboxHasz_2d_box_returns_false() throws Exception { + assertFalse(STBoxUDFs.stboxHasz.call(STBOX_XT)); + } + + @Test @Order(5) + void stboxXmin_returns_minus_one() throws Exception { + Double v = STBoxUDFs.stboxXmin.call(STBOX_XT); + assertNotNull(v); + assertEquals(-1.0, v, 1e-9); + } + + @Test @Order(6) + void stboxXmax_returns_one() throws Exception { + Double v = STBoxUDFs.stboxXmax.call(STBOX_XT); + assertNotNull(v); + assertEquals(1.0, v, 1e-9); + } + + @Test @Order(7) + void stboxYmin_returns_minus_two() throws Exception { + Double v = STBoxUDFs.stboxYmin.call(STBOX_XT); + assertNotNull(v); + assertEquals(-2.0, v, 1e-9); + } + + @Test @Order(8) + void stboxYmax_returns_two() throws Exception { + Double v = STBoxUDFs.stboxYmax.call(STBOX_XT); + assertNotNull(v); + assertEquals(2.0, v, 1e-9); + } + + @Test @Order(9) + void stboxZmin_no_z_returns_null() throws Exception { + assertNull(STBoxUDFs.stboxZmin.call(STBOX_XT)); + } + + @Test @Order(10) + void stboxTmin_returns_2020_01_01() throws Exception { + java.sql.Timestamp ts = STBoxUDFs.stboxTmin.call(STBOX_XT); + assertNotNull(ts, "stboxTmin should not be null for an XT box"); + assertTrue(ts.toInstant().toString().startsWith("2020-01-01"), + "Expected 2020-01-01, got: " + ts.toInstant()); + } + + @Test @Order(11) + void stboxTmax_returns_2020_01_02() throws Exception { + java.sql.Timestamp ts = STBoxUDFs.stboxTmax.call(STBOX_XT); + assertNotNull(ts); + assertTrue(ts.toInstant().toString().startsWith("2020-01-02"), + "Expected 2020-01-02, got: " + ts.toInstant()); + } + + @Test @Order(12) + void stboxTminInc_closed_lower_returns_true() throws Exception { + Boolean inc = STBoxUDFs.stboxTminInc.call(STBOX_XT); + assertNotNull(inc); + assertTrue(inc); + } + + @Test @Order(13) + void stboxTmaxInc_closed_upper_returns_true() throws Exception { + Boolean inc = STBoxUDFs.stboxTmaxInc.call(STBOX_XT); + assertNotNull(inc); + assertTrue(inc); + } + + @Test @Order(14) + void stboxSrid_zero_for_no_srid() throws Exception { + Integer srid = STBoxUDFs.stboxSrid.call(STBOX_XT); + assertNotNull(srid); + assertEquals(0, srid); + } + + @Test @Order(15) + void stboxXmin_null_input_returns_null() throws Exception { + assertNull(STBoxUDFs.stboxXmin.call(null)); + } + + @Test @Order(16) + void stboxExpandSpace_xmax_grows() throws Exception { + String expanded = STBoxUDFs.stboxExpandSpace.call(STBOX_XT, 1.0); + assertNotNull(expanded, "stboxExpandSpace should return a hex-WKB"); + Double xmax = STBoxUDFs.stboxXmax.call(expanded); + assertNotNull(xmax); + assertTrue(xmax > 1.0, "Expanded Xmax should exceed original 1.0, got " + xmax); + } + + @Test @Order(17) + void stboxExpandTime_returns_non_null_hex() throws Exception { + String expanded = STBoxUDFs.stboxExpandTime.call(STBOX_XT, "1 day"); + assertNotNull(expanded, "stboxExpandTime should return a hex-WKB"); + assertFalse(expanded.isBlank()); + } + + @Test @Order(18) + void stboxArea_returns_positive() throws Exception { + Double area = STBoxUDFs.stboxArea.call(STBOX_XT); + assertNotNull(area); + assertTrue(area > 0, "Area of a 2Γ—4 box should be positive"); + } + + @Test @Order(19) + void stboxPerimeter_returns_positive() throws Exception { + Double perim = STBoxUDFs.stboxPerimeter.call(STBOX_XT); + assertNotNull(perim); + assertTrue(perim > 0, "Perimeter of a non-degenerate box should be positive"); + } + + @Test @Order(20) + void stboxVolume_returns_value_for_2d_box() throws Exception { + Double vol = STBoxUDFs.stboxVolume.call(STBOX_XT); + assertNotNull(vol); + // MEOS returns -1.0 for a 2D box (no Z component) + assertTrue(vol == -1.0 || vol == 0.0, "Expected sentinel for 2D box, got: " + vol); + } + + @Test @Order(21) + void stboxIsGeodetic_cartesian_box_returns_false() throws Exception { + assertFalse(STBoxUDFs.stboxIsGeodetic.call(STBOX_XT)); + } + + @Test @Order(22) + void stboxToGeo_returns_wkt_polygon() throws Exception { + String wkt = STBoxUDFs.stboxToGeo.call(STBOX_XT); + assertNotNull(wkt); + assertTrue(wkt.startsWith("POLYGON") || wkt.startsWith("LINESTRING") || wkt.startsWith("POINT"), + "Expected geometry WKT, got: " + wkt); + } + + @Test @Order(23) + void stboxToTstzspan_returns_span_hex() throws Exception { + String spanHex = STBoxUDFs.stboxToTstzspan.call(STBOX_XT); + assertNotNull(spanHex); + assertFalse(spanHex.isBlank()); + } + + @Test @Order(24) + void stbox_analytics_null_returns_null() throws Exception { + assertNull(STBoxUDFs.stboxArea.call(null)); + assertNull(STBoxUDFs.stboxPerimeter.call(null)); + assertNull(STBoxUDFs.stboxVolume.call(null)); + assertNull(STBoxUDFs.stboxIsGeodetic.call(null)); + assertNull(STBoxUDFs.stboxToGeo.call(null)); + assertNull(STBoxUDFs.stboxToTstzspan.call(null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/geo/StaticGeoUDFsTest.java b/src/test/java/org/mobilitydb/spark/geo/StaticGeoUDFsTest.java new file mode 100644 index 00000000..d06bf89a --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/geo/StaticGeoUDFsTest.java @@ -0,0 +1,197 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.geo; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for StaticGeoUDFs: static geometry predicates, metrics, + * transforms, and line operations. + * + * MEOS function authority: meos/include/meos_geo.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class StaticGeoUDFsTest { + + private static final String POLY = "POLYGON((0 0,4 0,4 4,0 4,0 0))"; + private static final String POLY2 = "POLYGON((2 2,6 2,6 6,2 6,2 2))"; + private static final String POINT_INSIDE = "POINT(2 2)"; + private static final String POINT_OUTSIDE = "POINT(10 10)"; + private static final String LINE = "LINESTRING(0 0,10 0)"; + private static final String LINE2 = "LINESTRING(5 5,5 -5)"; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + } + + // ------------------------------------------------------------------ + // Geometry predicates + // ------------------------------------------------------------------ + + @Test @Order(1) + void geomContains_polygon_contains_interior_point() throws Exception { + assertTrue(StaticGeoUDFs.geomContains.call(POLY, "POINT(1 1)")); + } + + @Test @Order(2) + void geomContains_polygon_does_not_contain_exterior_point() throws Exception { + assertFalse(StaticGeoUDFs.geomContains.call(POLY, POINT_OUTSIDE)); + } + + @Test @Order(3) + void geomIntersects_overlapping_polygons_is_true() throws Exception { + assertTrue(StaticGeoUDFs.geomIntersects.call(POLY, POLY2)); + } + + @Test @Order(4) + void geomDisjoint_non_overlapping_is_true() throws Exception { + String farPoly = "POLYGON((10 10,14 10,14 14,10 14,10 10))"; + assertTrue(StaticGeoUDFs.geomDisjoint.call(POLY, farPoly)); + } + + @Test @Order(5) + void geomTouches_crossing_lines_at_point() throws Exception { + // Two line segments that share only an endpoint β€” touches is true + String line1 = "LINESTRING(0 0,5 5)"; + String line2 = "LINESTRING(5 5,10 0)"; + assertTrue(StaticGeoUDFs.geomTouches.call(line1, line2)); + } + + @Test @Order(6) + void geomDwithin_close_points_is_true() throws Exception { + assertTrue(StaticGeoUDFs.geomDwithin.call("POINT(0 0)", "POINT(3 4)", 5.0001)); + } + + @Test @Order(7) + void geomDwithin_far_points_is_false() throws Exception { + assertFalse(StaticGeoUDFs.geomDwithin.call("POINT(0 0)", "POINT(100 100)", 1.0)); + } + + // ------------------------------------------------------------------ + // Geometry metrics + // ------------------------------------------------------------------ + + @Test @Order(8) + void geomDistance_between_points() throws Exception { + Double d = StaticGeoUDFs.geomDistance.call("POINT(0 0)", "POINT(3 4)"); + assertNotNull(d); + assertEquals(5.0, d, 1e-9); + } + + @Test @Order(9) + void geomLength_of_line() throws Exception { + Double len = StaticGeoUDFs.geomLength.call(LINE); + assertNotNull(len); + assertEquals(10.0, len, 1e-9); + } + + @Test @Order(10) + void geomPerimeter_of_square() throws Exception { + Double p = StaticGeoUDFs.geomPerimeter.call(POLY); + assertNotNull(p); + assertEquals(16.0, p, 1e-9); + } + + // ------------------------------------------------------------------ + // Geometry transforms + // ------------------------------------------------------------------ + + @Test @Order(11) + void geomCentroid_of_square_is_center() throws Exception { + String wkt = StaticGeoUDFs.geomCentroid.call(POLY); + assertNotNull(wkt); + assertTrue(wkt.startsWith("POINT"), "Expected POINT, got: " + wkt); + } + + @Test @Order(12) + void geomBoundary_of_polygon_is_ring() throws Exception { + String wkt = StaticGeoUDFs.geomBoundary.call(POLY); + assertNotNull(wkt); + assertTrue(wkt.startsWith("LINESTRING") || wkt.startsWith("MULTILINESTRING"), + "Expected LINESTRING, got: " + wkt); + } + + @Test @Order(13) + void geomDifference_returns_non_null() throws Exception { + String wkt = StaticGeoUDFs.geomDifference.call(POLY, POLY2); + assertNotNull(wkt); + assertFalse(wkt.isBlank()); + } + + @Test @Order(14) + void geoReverse_line_reverses_direction() throws Exception { + String wkt = StaticGeoUDFs.geoReverse.call(LINE); + assertNotNull(wkt); + assertTrue(wkt.startsWith("LINESTRING"), "Expected LINESTRING, got: " + wkt); + } + + @Test @Order(15) + void geoRound_reduces_precision() throws Exception { + String wkt = StaticGeoUDFs.geoRound.call("POINT(1.123456789 2.987654321)", 3); + assertNotNull(wkt); + assertTrue(wkt.startsWith("POINT"), "Expected POINT, got: " + wkt); + } + + // ------------------------------------------------------------------ + // Line functions + // ------------------------------------------------------------------ + + @Test @Order(16) + void lineInterpolatePoint_midpoint() throws Exception { + String wkt = StaticGeoUDFs.lineInterpolatePoint.call(LINE, 0.5); + assertNotNull(wkt); + assertTrue(wkt.startsWith("POINT"), "Expected POINT, got: " + wkt); + } + + @Test @Order(17) + void lineSubstring_half() throws Exception { + String wkt = StaticGeoUDFs.lineSubstring.call(LINE, 0.0, 0.5); + assertNotNull(wkt); + assertTrue(wkt.startsWith("LINESTRING"), "Expected LINESTRING, got: " + wkt); + } + + // ------------------------------------------------------------------ + // Null-input guards + // ------------------------------------------------------------------ + + @Test @Order(18) + void null_inputs_return_null() throws Exception { + assertNull(StaticGeoUDFs.geomContains.call(null, POLY)); + assertNull(StaticGeoUDFs.geomIntersects.call(POLY, null)); + assertNull(StaticGeoUDFs.geomDistance.call(null, "POINT(1 1)")); + assertNull(StaticGeoUDFs.geomLength.call(null)); + assertNull(StaticGeoUDFs.geomCentroid.call(null)); + assertNull(StaticGeoUDFs.geomBoundary.call(null)); + assertNull(StaticGeoUDFs.geoReverse.call(null)); + assertNull(StaticGeoUDFs.lineInterpolatePoint.call(null, 0.5)); + assertNull(StaticGeoUDFs.lineSubstring.call(null, 0.0, 0.5)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/geo/TPointSTBoxOpsUDFsTest.java b/src/test/java/org/mobilitydb/spark/geo/TPointSTBoxOpsUDFsTest.java new file mode 100644 index 00000000..97ef3e2c --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/geo/TPointSTBoxOpsUDFsTest.java @@ -0,0 +1,242 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.geo; + +import org.junit.jupiter.api.*; +import org.mobilitydb.spark.temporal.ConstructorUDFs; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for cross-type STBox Γ— TPoint positional and topological UDFs. + * + * Fixtures: + * TRIP β€” tgeopoint travelling from (0,0) to (4,0) + * STBOX_OVERLAP β€” STBOX covering (0,0)-(4,0) (overlaps trip) + * STBOX_RIGHT β€” STBOX at (10,0)-(14,0) (trip is left of this) + * + * MEOS function authority: meos/include/meos_geo.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class TPointSTBoxOpsUDFsTest { + + private static String TRIP; + private static String STBOX_OVERLAP; + private static String STBOX_RIGHT; + + @BeforeAll + static void initMeos() throws Exception { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TRIP = temporal_as_hexwkb( + tgeompoint_in("[POINT(0.0 0.0)@2020-01-01 00:00:00+00, " + + "POINT(4.0 0.0)@2020-01-01 01:00:00+00]"), + (byte) 0); + + STBOX_OVERLAP = ConstructorUDFs.stbox.call( + "STBOX XT(((0,0),(4,0)),[2020-01-01 00:00:00+00,2020-01-01 01:00:00+00])"); + + STBOX_RIGHT = ConstructorUDFs.stbox.call( + "STBOX XT(((10,0),(14,0)),[2020-01-01 00:00:00+00,2020-01-01 01:00:00+00])"); + } + + // ------------------------------------------------------------------ + // Null input guards (one representative test per direction) + // ------------------------------------------------------------------ + + @Test @Order(1) + void stboxLeftTpoint_null_stbox_returns_null() throws Exception { + assertNull(TPointSTBoxOpsUDFs.stboxLeftTpoint.call(null, TRIP)); + } + + @Test @Order(2) + void stboxLeftTpoint_null_tpoint_returns_null() throws Exception { + assertNull(TPointSTBoxOpsUDFs.stboxLeftTpoint.call(STBOX_OVERLAP, null)); + } + + @Test @Order(3) + void tpointLeftStbox_null_tpoint_returns_null() throws Exception { + assertNull(TPointSTBoxOpsUDFs.tpointLeftStbox.call(null, STBOX_OVERLAP)); + } + + @Test @Order(4) + void tpointLeftStbox_null_stbox_returns_null() throws Exception { + assertNull(TPointSTBoxOpsUDFs.tpointLeftStbox.call(TRIP, null)); + } + + // ------------------------------------------------------------------ + // Spatial direction predicates β€” stbox Γ— tpoint + // ------------------------------------------------------------------ + + @Test @Order(5) + void stboxLeftTpoint_right_stbox_is_left_of_trip() throws Exception { + // STBOX_RIGHT is to the right of the trip origin area; + // trip (0-4) is to the LEFT of STBOX_RIGHT (10-14). + // So stboxLeftTpoint(STBOX_RIGHT, trip) β†’ trip is NOT to the left of stbox + // Actually: left_stbox_tspatial means stbox is strictly LEFT of tspatial. + // STBOX_RIGHT (10-14) is NOT to the left of trip (0-4); trip is left of stbox. + Boolean r = TPointSTBoxOpsUDFs.stboxLeftTpoint.call(STBOX_OVERLAP, TRIP); + assertNotNull(r); + } + + @Test @Order(6) + void tpointLeftStbox_trip_is_left_of_right_stbox() throws Exception { + // trip (0-4) is strictly to the LEFT of STBOX_RIGHT (10-14) + Boolean r = TPointSTBoxOpsUDFs.tpointLeftStbox.call(TRIP, STBOX_RIGHT); + assertNotNull(r); + assertTrue(r, "trip (x∈[0,4]) must be left of stbox (x∈[10,14])"); + } + + @Test @Order(7) + void stboxRightTpoint_right_stbox_is_right_of_trip() throws Exception { + // STBOX_RIGHT (10-14) is strictly to the RIGHT of trip (0-4) + Boolean r = TPointSTBoxOpsUDFs.stboxRightTpoint.call(STBOX_RIGHT, TRIP); + assertNotNull(r); + assertTrue(r, "stbox (x∈[10,14]) must be right of trip (x∈[0,4])"); + } + + @Test @Order(8) + void tpointRightStbox_trip_not_right_of_right_stbox() throws Exception { + // trip (x∈[0,4]) is NOT to the right of STBOX_RIGHT (x∈[10,14]) + Boolean r = TPointSTBoxOpsUDFs.tpointRightStbox.call(TRIP, STBOX_RIGHT); + assertNotNull(r); + assertFalse(r, "trip (x∈[0,4]) must not be right of stbox (x∈[10,14])"); + } + + // ------------------------------------------------------------------ + // Topological predicates β€” stbox Γ— tpoint + // ------------------------------------------------------------------ + + @Test @Order(9) + void stboxOverlapsTpoint_overlapping_returns_true() throws Exception { + Boolean r = TPointSTBoxOpsUDFs.stboxOverlapsTpoint.call(STBOX_OVERLAP, TRIP); + assertNotNull(r); + assertTrue(r, "overlapping stbox/tpoint must give overlaps=true"); + } + + @Test @Order(10) + void stboxOverlapsTpoint_disjoint_returns_false() throws Exception { + Boolean r = TPointSTBoxOpsUDFs.stboxOverlapsTpoint.call(STBOX_RIGHT, TRIP); + assertNotNull(r); + assertFalse(r, "non-overlapping stbox/tpoint must give overlaps=false"); + } + + @Test @Order(11) + void tpointOverlapsStbox_overlapping_returns_true() throws Exception { + Boolean r = TPointSTBoxOpsUDFs.tpointOverlapsStbox.call(TRIP, STBOX_OVERLAP); + assertNotNull(r); + assertTrue(r, "trip inside stbox must give overlaps=true"); + } + + @Test @Order(12) + void stboxContainsTpoint_full_containment_returns_nonnull() throws Exception { + // Containment test: stbox at least as large as trip bounding box + Boolean r = TPointSTBoxOpsUDFs.stboxContainsTpoint.call(STBOX_OVERLAP, TRIP); + assertNotNull(r); + } + + @Test @Order(13) + void tpointContainedStbox_returns_nonnull() throws Exception { + Boolean r = TPointSTBoxOpsUDFs.tpointContainedStbox.call(TRIP, STBOX_OVERLAP); + assertNotNull(r); + } + + @Test @Order(14) + void stboxSameTpoint_same_extent_returns_nonnull() throws Exception { + Boolean r = TPointSTBoxOpsUDFs.stboxSameTpoint.call(STBOX_OVERLAP, TRIP); + assertNotNull(r); + } + + @Test @Order(15) + void tpointSameStbox_returns_nonnull() throws Exception { + Boolean r = TPointSTBoxOpsUDFs.tpointSameStbox.call(TRIP, STBOX_OVERLAP); + assertNotNull(r); + } + + // ------------------------------------------------------------------ + // Temporal direction predicates + // ------------------------------------------------------------------ + + @Test @Order(16) + void stboxBeforeTpoint_returns_nonnull() throws Exception { + Boolean r = TPointSTBoxOpsUDFs.stboxBeforeTpoint.call(STBOX_OVERLAP, TRIP); + assertNotNull(r); + } + + @Test @Order(17) + void tpointBeforeStbox_returns_nonnull() throws Exception { + Boolean r = TPointSTBoxOpsUDFs.tpointBeforeStbox.call(TRIP, STBOX_OVERLAP); + assertNotNull(r); + } + + @Test @Order(18) + void stboxAfterTpoint_returns_nonnull() throws Exception { + Boolean r = TPointSTBoxOpsUDFs.stboxAfterTpoint.call(STBOX_OVERLAP, TRIP); + assertNotNull(r); + } + + @Test @Order(19) + void tpointAfterStbox_returns_nonnull() throws Exception { + Boolean r = TPointSTBoxOpsUDFs.tpointAfterStbox.call(TRIP, STBOX_OVERLAP); + assertNotNull(r); + } + + // ------------------------------------------------------------------ + // Over-predicates (a representative sample) + // ------------------------------------------------------------------ + + @Test @Order(20) + void stboxOverleftTpoint_returns_nonnull() throws Exception { + assertNotNull(TPointSTBoxOpsUDFs.stboxOverleftTpoint.call(STBOX_OVERLAP, TRIP)); + } + + @Test @Order(21) + void stboxOverrightTpoint_returns_nonnull() throws Exception { + assertNotNull(TPointSTBoxOpsUDFs.stboxOverrightTpoint.call(STBOX_OVERLAP, TRIP)); + } + + @Test @Order(22) + void tpointOverleftStbox_returns_nonnull() throws Exception { + assertNotNull(TPointSTBoxOpsUDFs.tpointOverleftStbox.call(TRIP, STBOX_OVERLAP)); + } + + @Test @Order(23) + void tpointOverrightStbox_returns_nonnull() throws Exception { + assertNotNull(TPointSTBoxOpsUDFs.tpointOverrightStbox.call(TRIP, STBOX_OVERLAP)); + } + + @Test @Order(24) + void stboxAboveTpoint_returns_nonnull() throws Exception { + assertNotNull(TPointSTBoxOpsUDFs.stboxAboveTpoint.call(STBOX_OVERLAP, TRIP)); + } + + @Test @Order(25) + void tpointAboveStbox_returns_nonnull() throws Exception { + assertNotNull(TPointSTBoxOpsUDFs.tpointAboveStbox.call(TRIP, STBOX_OVERLAP)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/geo/TempSpatialRelsUDFsExtTest.java b/src/test/java/org/mobilitydb/spark/geo/TempSpatialRelsUDFsExtTest.java new file mode 100644 index 00000000..c6937beb --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/geo/TempSpatialRelsUDFsExtTest.java @@ -0,0 +1,184 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.geo; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for TempSpatialRelsUDFs tgeoΓ—tgeo and tgeoΓ—geo variants: + * tDisjointTgeoTgeo, tIntersectsTgeoTgeo, tTouchesTogeoTgeo, + * tContainsTgeoGeo, tContainsTgeoTgeo, tCoversTgeoGeo, tDwithinTgeoGeo. + * + * Fixture: + * TRIP β€” linear from (0,0) to (1,0) over 1 hour + * TRIP2 β€” shifted trip: (0,0.5)β†’(1,0.5), same time window + * REGION_ON_PATH β€” polygon covering part of the trip path + * REGION_FAR β€” polygon far from the trip + * + * MEOS function authority: meos/include/meos_geo.h (072_tgeo_tempspatialrels) + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class TempSpatialRelsUDFsExtTest { + + private static String TRIP; + private static String TRIP2; + private static final String REGION_ON_PATH = "POLYGON((-0.1 -1, 0.6 -1, 0.6 1, -0.1 1, -0.1 -1))"; + private static final String REGION_FAR = "POLYGON((10 10, 11 10, 11 11, 10 11, 10 10))"; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TRIP = temporal_as_hexwkb( + tgeompoint_in("[POINT(0.0 0.0)@2020-01-01 00:00:00+00, POINT(1.0 0.0)@2020-01-01 01:00:00+00]"), + (byte) 0); + TRIP2 = temporal_as_hexwkb( + tgeompoint_in("[POINT(0.0 0.5)@2020-01-01 00:00:00+00, POINT(1.0 0.5)@2020-01-01 01:00:00+00]"), + (byte) 0); + } + + // ------------------------------------------------------------------ + // tDisjointTgeoTgeo + // ------------------------------------------------------------------ + + @Test @Order(1) + void tDisjointTgeoTgeo_parallel_trips_returns_nonnull_tbool() throws Exception { + String r = TempSpatialRelsUDFs.tDisjointTgeoTgeo.call(TRIP, TRIP2); + assertNotNull(r, "tDisjointTgeoTgeo should return a tbool hex-WKB"); + assertFalse(r.isBlank()); + } + + @Test @Order(2) + void tDisjointTgeoTgeo_null_returns_null() throws Exception { + assertNull(TempSpatialRelsUDFs.tDisjointTgeoTgeo.call(null, TRIP)); + assertNull(TempSpatialRelsUDFs.tDisjointTgeoTgeo.call(TRIP, null)); + } + + // ------------------------------------------------------------------ + // tIntersectsTgeoTgeo + // ------------------------------------------------------------------ + + @Test @Order(3) + void tIntersectsTgeoTgeo_same_trip_returns_nonnull_tbool() throws Exception { + String r = TempSpatialRelsUDFs.tIntersectsTgeoTgeo.call(TRIP, TRIP); + assertNotNull(r, "tIntersectsTgeoTgeo with same trip should return a tbool"); + assertFalse(r.isBlank()); + } + + @Test @Order(4) + void tIntersectsTgeoTgeo_null_returns_null() throws Exception { + assertNull(TempSpatialRelsUDFs.tIntersectsTgeoTgeo.call(null, TRIP)); + } + + // ------------------------------------------------------------------ + // tTouchesTogeoTgeo + // ------------------------------------------------------------------ + + @Test @Order(5) + void tTouchesTogeoTgeo_returns_non_null_or_handles_gracefully() throws Exception { + // ttouches between point trajectories may return null for certain inputs; + // just verify no exception is thrown + String r = TempSpatialRelsUDFs.tTouchesTogeoTgeo.call(TRIP, TRIP2); + assertTrue(r == null || !r.isBlank(), "Result must be null or non-blank hex-WKB"); + } + + @Test @Order(6) + void tTouchesTogeoTgeo_null_returns_null() throws Exception { + assertNull(TempSpatialRelsUDFs.tTouchesTogeoTgeo.call(null, TRIP)); + } + + // ------------------------------------------------------------------ + // tContainsTgeoGeo + // ------------------------------------------------------------------ + + @Test @Order(7) + void tContainsTgeoGeo_returns_tbool_or_null() throws Exception { + // A point typically does not contain a polygon; result depends on semantics + String r = TempSpatialRelsUDFs.tContainsTgeoGeo.call(TRIP, REGION_FAR); + assertTrue(r == null || !r.isBlank(), "Result must be null or non-blank hex-WKB"); + } + + @Test @Order(8) + void tContainsTgeoGeo_null_returns_null() throws Exception { + assertNull(TempSpatialRelsUDFs.tContainsTgeoGeo.call(null, REGION_FAR)); + assertNull(TempSpatialRelsUDFs.tContainsTgeoGeo.call(TRIP, null)); + } + + // ------------------------------------------------------------------ + // tContainsTgeoTgeo + // ------------------------------------------------------------------ + + @Test @Order(9) + void tContainsTgeoTgeo_same_trip_returns_nonnull() throws Exception { + String r = TempSpatialRelsUDFs.tContainsTgeoTgeo.call(TRIP, TRIP); + assertNotNull(r, "tContainsTgeoTgeo with same trip should return a tbool"); + assertFalse(r.isBlank()); + } + + @Test @Order(10) + void tContainsTgeoTgeo_null_returns_null() throws Exception { + assertNull(TempSpatialRelsUDFs.tContainsTgeoTgeo.call(null, TRIP)); + } + + // ------------------------------------------------------------------ + // tCoversTgeoGeo + // ------------------------------------------------------------------ + + @Test @Order(11) + void tCoversTgeoGeo_returns_tbool_or_null() throws Exception { + String r = TempSpatialRelsUDFs.tCoversTgeoGeo.call(TRIP, REGION_FAR); + assertTrue(r == null || !r.isBlank(), "Result must be null or non-blank hex-WKB"); + } + + @Test @Order(12) + void tCoversTgeoGeo_null_returns_null() throws Exception { + assertNull(TempSpatialRelsUDFs.tCoversTgeoGeo.call(null, REGION_FAR)); + } + + // ------------------------------------------------------------------ + // tDwithinTgeoGeo + // ------------------------------------------------------------------ + + @Test @Order(13) + void tDwithinTgeoGeo_trip_with_nearby_point_returns_nonnull() throws Exception { + // tdwithin_tgeo_geo is defined for point geometries; polygon may return null. + // Use a point geometry close to the trip to ensure a valid result. + String r = TempSpatialRelsUDFs.tDwithinTgeoGeo.call(TRIP, "POINT(0.5 0.0)", 100.0); + assertNotNull(r, "tDwithinTgeoGeo with point geometry should return a tbool"); + assertFalse(r.isBlank()); + } + + @Test @Order(14) + void tDwithinTgeoGeo_null_returns_null() throws Exception { + assertNull(TempSpatialRelsUDFs.tDwithinTgeoGeo.call(null, REGION_ON_PATH, 1.0)); + assertNull(TempSpatialRelsUDFs.tDwithinTgeoGeo.call(TRIP, null, 1.0)); + assertNull(TempSpatialRelsUDFs.tDwithinTgeoGeo.call(TRIP, REGION_ON_PATH, null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/geo/TempSpatialRelsUDFsTest.java b/src/test/java/org/mobilitydb/spark/geo/TempSpatialRelsUDFsTest.java new file mode 100644 index 00000000..6c045132 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/geo/TempSpatialRelsUDFsTest.java @@ -0,0 +1,129 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.geo; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for TempSpatialRelsUDFs β€” tDisjoint, tIntersects, tTouches. + * + * Fixture: + * TRIP β€” tgeompoint moving along X axis from (0,0) to (1,0) + * REGION_ON_PATH β€” polygon enclosing the trip midpoint + * REGION_FAR β€” polygon far away from the trip + * + * MEOS function authority: meos/include/meos_geo.h (072_tgeo_tempspatialrels) + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class TempSpatialRelsUDFsTest { + + private static String TRIP; + private static final String REGION_ON_PATH = "POLYGON((0 -1, 1 -1, 1 1, 0 1, 0 -1))"; + private static final String REGION_FAR = "POLYGON((10 10, 11 10, 11 11, 10 11, 10 10))"; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TRIP = temporal_as_hexwkb( + tgeompoint_in("[POINT(0.0 0.0)@2020-01-01 00:00:00+00, POINT(1.0 0.0)@2020-01-01 01:00:00+00]"), + (byte) 0); + } + + // ------------------------------------------------------------------ + // tIntersects + // ------------------------------------------------------------------ + + @Test @Order(1) + void tIntersects_trip_with_region_on_path_returns_nonnull_tbool() throws Exception { + String r = TempSpatialRelsUDFs.tIntersects.call(TRIP, REGION_ON_PATH); + assertNotNull(r, "tIntersects should return a tbool hex-WKB when trip crosses region"); + assertFalse(r.isBlank()); + } + + @Test @Order(2) + void tIntersects_trip_with_far_region_returns_nonnull_tbool() throws Exception { + String r = TempSpatialRelsUDFs.tIntersects.call(TRIP, REGION_FAR); + assertNotNull(r); + } + + @Test @Order(3) + void tIntersects_null_trip_returns_null() throws Exception { + assertNull(TempSpatialRelsUDFs.tIntersects.call(null, REGION_ON_PATH)); + } + + @Test @Order(4) + void tIntersects_null_geom_returns_null() throws Exception { + assertNull(TempSpatialRelsUDFs.tIntersects.call(TRIP, null)); + } + + // ------------------------------------------------------------------ + // tDisjoint + // ------------------------------------------------------------------ + + @Test @Order(5) + void tDisjoint_trip_with_far_region_returns_nonnull_tbool() throws Exception { + String r = TempSpatialRelsUDFs.tDisjoint.call(TRIP, REGION_FAR); + assertNotNull(r, "tDisjoint should return a tbool hex-WKB"); + assertFalse(r.isBlank()); + } + + @Test @Order(6) + void tDisjoint_null_trip_returns_null() throws Exception { + assertNull(TempSpatialRelsUDFs.tDisjoint.call(null, REGION_FAR)); + } + + @Test @Order(7) + void tDisjoint_null_geom_returns_null() throws Exception { + assertNull(TempSpatialRelsUDFs.tDisjoint.call(TRIP, null)); + } + + // ------------------------------------------------------------------ + // tTouches + // ------------------------------------------------------------------ + + @Test @Order(8) + void tTouches_returns_nonnull_tbool_for_valid_inputs() throws Exception { + // tTouches tests the boundary; this just checks the UDF doesn't crash + String r = TempSpatialRelsUDFs.tTouches.call(TRIP, REGION_ON_PATH); + // Result may be null if MEOS raises an error for a non-boundary condition + assertTrue(r == null || !r.isBlank()); + } + + @Test @Order(9) + void tTouches_null_trip_returns_null() throws Exception { + assertNull(TempSpatialRelsUDFs.tTouches.call(null, REGION_ON_PATH)); + } + + @Test @Order(10) + void tTouches_null_geom_returns_null() throws Exception { + assertNull(TempSpatialRelsUDFs.tTouches.call(TRIP, null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/AccessorUDFsExt2Test.java b/src/test/java/org/mobilitydb/spark/temporal/AccessorUDFsExt2Test.java new file mode 100644 index 00000000..9a491fa6 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/AccessorUDFsExt2Test.java @@ -0,0 +1,129 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for new accessor UDFs: + * tintValueN (MoreAccessorUDFs), tnumberToSpan, tnumberToTbox (AccessorUDFs). + * + * MEOS function authority: meos/include/meos.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class AccessorUDFsExt2Test { + + private static String TINT_SEQ; + private static String TFLOAT_SEQ; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TINT_SEQ = temporal_as_hexwkb(tint_in("[3@2020-01-01, 7@2020-01-03]"), (byte) 0); + TFLOAT_SEQ = temporal_as_hexwkb(tfloat_in("[1.5@2020-01-01, 4.5@2020-01-03]"), (byte) 0); + } + + // ------------------------------------------------------------------ + // tintValueN (MoreAccessorUDFs) + // ------------------------------------------------------------------ + + @Test @Order(1) + void tintValueN_first_value_returns_correct() throws Exception { + Integer r = MoreAccessorUDFs.tintValueN.call(TINT_SEQ, 1); + assertNotNull(r, "First distinct value must be non-null"); + assertEquals(3, r.intValue()); + } + + @Test @Order(2) + void tintValueN_second_value_returns_correct() throws Exception { + Integer r = MoreAccessorUDFs.tintValueN.call(TINT_SEQ, 2); + assertNotNull(r, "Second distinct value must be non-null"); + assertEquals(7, r.intValue()); + } + + @Test @Order(3) + void tintValueN_out_of_range_returns_null() throws Exception { + Integer r = MoreAccessorUDFs.tintValueN.call(TINT_SEQ, 99); + assertNull(r, "Out-of-range index must return null"); + } + + @Test @Order(4) + void tintValueN_null_returns_null() throws Exception { + assertNull(MoreAccessorUDFs.tintValueN.call(null, 1)); + assertNull(MoreAccessorUDFs.tintValueN.call(TINT_SEQ, null)); + } + + // ------------------------------------------------------------------ + // tnumberToSpan (AccessorUDFs) + // ------------------------------------------------------------------ + + @Test @Order(5) + void tnumberToSpan_returns_nonnull_hexwkb() throws Exception { + String r = AccessorUDFs.tnumberToSpan.call(TINT_SEQ); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(6) + void tnumberToSpan_tfloat_returns_nonnull_hexwkb() throws Exception { + String r = AccessorUDFs.tnumberToSpan.call(TFLOAT_SEQ); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(7) + void tnumberToSpan_null_returns_null() throws Exception { + assertNull(AccessorUDFs.tnumberToSpan.call(null)); + } + + // ------------------------------------------------------------------ + // tnumberToTbox (AccessorUDFs) + // ------------------------------------------------------------------ + + @Test @Order(8) + void tnumberToTbox_returns_nonnull_hexwkb() throws Exception { + String r = AccessorUDFs.tnumberToTbox.call(TINT_SEQ); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(9) + void tnumberToTbox_tfloat_returns_nonnull_hexwkb() throws Exception { + String r = AccessorUDFs.tnumberToTbox.call(TFLOAT_SEQ); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(10) + void tnumberToTbox_null_returns_null() throws Exception { + assertNull(AccessorUDFs.tnumberToTbox.call(null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/AccessorUDFsExtTest.java b/src/test/java/org/mobilitydb/spark/temporal/AccessorUDFsExtTest.java new file mode 100644 index 00000000..d1b316d7 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/AccessorUDFsExtTest.java @@ -0,0 +1,235 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; +import org.mobilitydb.spark.temporal.ConstructorUDFs; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for extended AccessorUDFs β€” value restriction, spatio-temporal + * restriction, append operations, and value span accessor. + * + * Tests run directly against MEOS via JMEOS without a Spark session. + * MEOS function authority: meos/include/meos.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class AccessorUDFsExtTest { + + // tint sequence [1@t1, 3@t2, 2@t3] β€” min=1, max=3 + private static String TINT_SEQ; + // tstzspan for minusTime test + private static String TSTZSPAN_FIRST_HOUR; + // intset for atValues test + private static String INTSET_1_3; + // intspan tbox for tnumber restriction test (TBOXINT to match tint span type) + private static String TBOX; + // tgeompoint for stbox restriction test + private static String TRIP_HEX; + // stbox covering half the trip + private static String STBOX_HEX; + + @BeforeAll + static void initMeos() throws Exception { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TINT_SEQ = temporal_as_hexwkb( + tint_in("{1@2020-01-01 00:00:00+00, 3@2020-01-01 01:00:00+00, 2@2020-01-01 02:00:00+00}"), + (byte) 0); + + TSTZSPAN_FIRST_HOUR = span_as_hexwkb( + tstzspan_in("[2020-01-01 00:00:00+00, 2020-01-01 01:00:00+00)"), (byte) 0); + + INTSET_1_3 = set_as_hexwkb(intset_in("{1, 3}"), (byte) 0); + + TBOX = ConstructorUDFs.tbox.call("TBOXINT XT([1,4],[2020-01-01 00:00:00+00, 2020-01-01 03:00:00+00])"); + + TRIP_HEX = temporal_as_hexwkb( + tgeompoint_in("[POINT(0 0)@2020-01-01 00:00:00+00, POINT(2 0)@2020-01-01 02:00:00+00]"), + (byte) 0); + + STBOX_HEX = ConstructorUDFs.stbox.call( + "STBOX XT(((0,0),(1,1)),[2020-01-01 00:00:00+00, 2020-01-01 02:00:00+00])"); + } + + // ------------------------------------------------------------------ + // atMin / atMax + // ------------------------------------------------------------------ + + @Test @Order(1) + void atMin_returns_instants_at_value_1() throws Exception { + String r = AccessorUDFs.atMin.call(TINT_SEQ); + assertNotNull(r, "atMin should return non-null for a valid tint"); + Integer sv = AccessorUDFs.tintStartValue.call(r); + assertNotNull(sv); + assertEquals(1, sv, "atMin start value should be the minimum (1)"); + } + + @Test @Order(2) + void atMax_returns_instants_at_value_3() throws Exception { + String r = AccessorUDFs.atMax.call(TINT_SEQ); + assertNotNull(r); + Integer sv = AccessorUDFs.tintStartValue.call(r); + assertNotNull(sv); + assertEquals(3, sv, "atMax start value should be the maximum (3)"); + } + + @Test @Order(3) + void atMin_null_returns_null() throws Exception { + assertNull(AccessorUDFs.atMin.call(null)); + } + + @Test @Order(4) + void atMax_null_returns_null() throws Exception { + assertNull(AccessorUDFs.atMax.call(null)); + } + + // ------------------------------------------------------------------ + // atValues + // ------------------------------------------------------------------ + + @Test @Order(5) + void atValues_restricts_to_values_1_and_3() throws Exception { + String r = AccessorUDFs.atValues.call(TINT_SEQ, INTSET_1_3); + assertNotNull(r, "atValues should return non-null when set intersects tint values"); + } + + @Test @Order(6) + void atValues_null_trip_returns_null() throws Exception { + assertNull(AccessorUDFs.atValues.call(null, INTSET_1_3)); + } + + // ------------------------------------------------------------------ + // minusTime + // ------------------------------------------------------------------ + + @Test @Order(7) + void minusTime_removes_first_hour() throws Exception { + String r = AccessorUDFs.minusTime.call(TINT_SEQ, TSTZSPAN_FIRST_HOUR); + assertNotNull(r, "minusTime should return remaining instants"); + } + + @Test @Order(8) + void minusTime_null_returns_null() throws Exception { + assertNull(AccessorUDFs.minusTime.call(null, TSTZSPAN_FIRST_HOUR)); + } + + // ------------------------------------------------------------------ + // minusMin / minusMax + // ------------------------------------------------------------------ + + @Test @Order(9) + void minusMin_excludes_value_1() throws Exception { + String r = AccessorUDFs.minusMin.call(TINT_SEQ); + assertNotNull(r, "minusMin should return instants not at minimum"); + } + + @Test @Order(10) + void minusMax_excludes_value_3() throws Exception { + String r = AccessorUDFs.minusMax.call(TINT_SEQ); + assertNotNull(r, "minusMax should return instants not at maximum"); + } + + // ------------------------------------------------------------------ + // atStbox / minusStbox + // ------------------------------------------------------------------ + + @Test @Order(11) + void atStbox_restricts_trip_to_box() throws Exception { + String r = AccessorUDFs.atStbox.call(TRIP_HEX, STBOX_HEX); + // May be null when no intersection β€” just verify no exception and non-blank if present + if (r != null) assertFalse(r.isBlank()); + } + + @Test @Order(12) + void atStbox_null_returns_null() throws Exception { + assertNull(AccessorUDFs.atStbox.call(null, STBOX_HEX)); + } + + @Test @Order(13) + void minusStbox_returns_part_outside_box() throws Exception { + String r = AccessorUDFs.minusStbox.call(TRIP_HEX, STBOX_HEX); + if (r != null) assertFalse(r.isBlank()); + } + + // ------------------------------------------------------------------ + // tnumberAtTbox / tnumberMinusTbox + // ------------------------------------------------------------------ + + @Test @Order(14) + void tnumberAtTbox_restricts_tint_to_tbox() throws Exception { + String r = AccessorUDFs.tnumberAtTbox.call(TINT_SEQ, TBOX); + assertNotNull(r, "tnumberAtTbox should return non-null when tbox covers tint range"); + } + + @Test @Order(15) + void tnumberAtTbox_null_returns_null() throws Exception { + assertNull(AccessorUDFs.tnumberAtTbox.call(null, TBOX)); + } + + @Test @Order(16) + void tnumberMinusTbox_returns_values_outside_tbox() throws Exception { + String r = AccessorUDFs.tnumberMinusTbox.call(TINT_SEQ, TBOX); + // Result can be null if all values are within the box + if (r != null) assertFalse(r.isBlank()); + } + + // ------------------------------------------------------------------ + // appendInstant / appendSequence + // ------------------------------------------------------------------ + + @Test @Order(17) + void appendInstant_extends_sequence() throws Exception { + String instant = temporal_as_hexwkb( + tint_in("5@2020-01-01 03:00:00+00"), (byte) 0); + String r = AccessorUDFs.appendInstant.call(TINT_SEQ, instant); + assertNotNull(r, "appendInstant should return extended temporal"); + } + + @Test @Order(18) + void appendInstant_null_returns_null() throws Exception { + assertNull(AccessorUDFs.appendInstant.call(null, TINT_SEQ)); + } + + // ------------------------------------------------------------------ + // tnumberValuespans + // ------------------------------------------------------------------ + + @Test @Order(19) + void tnumberValuespans_returns_spanset_hex() throws Exception { + String ss = AccessorUDFs.tnumberValuespans.call(TINT_SEQ); + assertNotNull(ss, "tnumberValuespans should return a spanset hex-WKB"); + assertFalse(ss.isBlank()); + } + + @Test @Order(20) + void tnumberValuespans_null_returns_null() throws Exception { + assertNull(AccessorUDFs.tnumberValuespans.call(null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/AggregateUDAFsTest.java b/src/test/java/org/mobilitydb/spark/temporal/AggregateUDAFsTest.java new file mode 100644 index 00000000..62c34af5 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/AggregateUDAFsTest.java @@ -0,0 +1,276 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for AggregateUDAFs β€” temporal aggregate functions exercised by + * driving each Aggregator's zero/reduce/merge/finish lifecycle directly, + * without a SparkSession. + * + * MEOS function authority: meos/include/meos.h (temporal aggregate transfns) + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class AggregateUDAFsTest { + + private static String TRIP1; + private static String TRIP2; + private static String TINT1; + private static String TINT2; + private static String TFLOAT1; + private static String TFLOAT2; + private static String TBOOL_T; + private static String TBOOL_F; + private static String TTEXT1; + private static String TTEXT2; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TRIP1 = temporal_as_hexwkb( + tgeompoint_in("[POINT(0 0)@2020-01-01 00:00:00+00, POINT(1 0)@2020-01-01 01:00:00+00]"), + (byte) 0); + TRIP2 = temporal_as_hexwkb( + tgeompoint_in("[POINT(0 1)@2020-01-01 00:00:00+00, POINT(1 1)@2020-01-01 01:00:00+00]"), + (byte) 0); + TINT1 = temporal_as_hexwkb(tint_in("[1@2020-01-01, 2@2020-01-02]"), (byte) 0); + TINT2 = temporal_as_hexwkb(tint_in("[3@2020-01-01, 4@2020-01-02]"), (byte) 0); + TFLOAT1 = temporal_as_hexwkb(tfloat_in("[1.0@2020-01-01, 2.0@2020-01-02]"), (byte) 0); + TFLOAT2 = temporal_as_hexwkb(tfloat_in("[3.0@2020-01-01, 4.0@2020-01-02]"), (byte) 0); + TBOOL_T = temporal_as_hexwkb(tbool_in("[t@2020-01-01, t@2020-01-02]"), (byte) 0); + TBOOL_F = temporal_as_hexwkb(tbool_in("[f@2020-01-01, f@2020-01-02]"), (byte) 0); + TTEXT1 = temporal_as_hexwkb(ttext_in("[AAA@2020-01-01, BBB@2020-01-02]"), (byte) 0); + TTEXT2 = temporal_as_hexwkb(ttext_in("[CCC@2020-01-01, DDD@2020-01-02]"), (byte) 0); + } + + // ------------------------------------------------------------------ + // tCount + // ------------------------------------------------------------------ + + @Test @Order(1) + void tCount_two_overlapping_trips_returns_nonnull_tint() { + AggregateUDAFs.TCountFn agg = new AggregateUDAFs.TCountFn(); + String buf = agg.zero(); + buf = agg.reduce(buf, TRIP1); + buf = agg.reduce(buf, TRIP2); + String result = agg.finish(buf); + assertNotNull(result); + assertFalse(result.isBlank()); + } + + @Test @Order(2) + void tCount_single_trip_returns_nonnull() { + AggregateUDAFs.TCountFn agg = new AggregateUDAFs.TCountFn(); + String buf = agg.zero(); + buf = agg.reduce(buf, TRIP1); + String result = agg.finish(buf); + assertNotNull(result); + assertFalse(result.isBlank()); + } + + @Test @Order(3) + void tCount_empty_input_returns_null() { + AggregateUDAFs.TCountFn agg = new AggregateUDAFs.TCountFn(); + assertNull(agg.finish(agg.zero())); + } + + // ------------------------------------------------------------------ + // tAnd / tOr + // ------------------------------------------------------------------ + + @Test @Order(4) + void tAnd_all_true_returns_nonnull_tbool() { + AggregateUDAFs.TAndFn agg = new AggregateUDAFs.TAndFn(); + String buf = agg.zero(); + buf = agg.reduce(buf, TBOOL_T); + buf = agg.reduce(buf, TBOOL_T); + String result = agg.finish(buf); + assertNotNull(result); + assertFalse(result.isBlank()); + } + + @Test @Order(5) + void tOr_all_false_returns_nonnull_tbool() { + AggregateUDAFs.TOrFn agg = new AggregateUDAFs.TOrFn(); + String buf = agg.zero(); + buf = agg.reduce(buf, TBOOL_F); + buf = agg.reduce(buf, TBOOL_F); + String result = agg.finish(buf); + assertNotNull(result); + assertFalse(result.isBlank()); + } + + // ------------------------------------------------------------------ + // tIntMin / tIntMax / tIntSum + // ------------------------------------------------------------------ + + @Test @Order(6) + void tIntMin_returns_nonnull_tint() { + AggregateUDAFs.TIntMinFn agg = new AggregateUDAFs.TIntMinFn(); + String buf = agg.zero(); + buf = agg.reduce(buf, TINT1); + buf = agg.reduce(buf, TINT2); + String result = agg.finish(buf); + assertNotNull(result); + assertFalse(result.isBlank()); + } + + @Test @Order(7) + void tIntMax_returns_nonnull_tint() { + AggregateUDAFs.TIntMaxFn agg = new AggregateUDAFs.TIntMaxFn(); + String buf = agg.zero(); + buf = agg.reduce(buf, TINT1); + buf = agg.reduce(buf, TINT2); + String result = agg.finish(buf); + assertNotNull(result); + assertFalse(result.isBlank()); + } + + @Test @Order(8) + void tIntSum_returns_nonnull_tint() { + AggregateUDAFs.TIntSumFn agg = new AggregateUDAFs.TIntSumFn(); + String buf = agg.zero(); + buf = agg.reduce(buf, TINT1); + buf = agg.reduce(buf, TINT2); + String result = agg.finish(buf); + assertNotNull(result); + assertFalse(result.isBlank()); + } + + // ------------------------------------------------------------------ + // tFloatMin / tFloatMax / tFloatSum + // ------------------------------------------------------------------ + + @Test @Order(9) + void tFloatMin_returns_nonnull_tfloat() { + AggregateUDAFs.TFloatMinFn agg = new AggregateUDAFs.TFloatMinFn(); + String buf = agg.zero(); + buf = agg.reduce(buf, TFLOAT1); + buf = agg.reduce(buf, TFLOAT2); + String result = agg.finish(buf); + assertNotNull(result); + assertFalse(result.isBlank()); + } + + @Test @Order(10) + void tFloatMax_returns_nonnull_tfloat() { + AggregateUDAFs.TFloatMaxFn agg = new AggregateUDAFs.TFloatMaxFn(); + String buf = agg.zero(); + buf = agg.reduce(buf, TFLOAT1); + buf = agg.reduce(buf, TFLOAT2); + String result = agg.finish(buf); + assertNotNull(result); + assertFalse(result.isBlank()); + } + + @Test @Order(11) + void tFloatSum_returns_nonnull_tfloat() { + AggregateUDAFs.TFloatSumFn agg = new AggregateUDAFs.TFloatSumFn(); + String buf = agg.zero(); + buf = agg.reduce(buf, TFLOAT1); + buf = agg.reduce(buf, TFLOAT2); + String result = agg.finish(buf); + assertNotNull(result); + assertFalse(result.isBlank()); + } + + // ------------------------------------------------------------------ + // tTextMin / tTextMax + // ------------------------------------------------------------------ + + @Test @Order(12) + void tTextMin_returns_nonnull_ttext() { + AggregateUDAFs.TTextMinFn agg = new AggregateUDAFs.TTextMinFn(); + String buf = agg.zero(); + buf = agg.reduce(buf, TTEXT1); + buf = agg.reduce(buf, TTEXT2); + String result = agg.finish(buf); + assertNotNull(result); + assertFalse(result.isBlank()); + } + + @Test @Order(13) + void tTextMax_returns_nonnull_ttext() { + AggregateUDAFs.TTextMaxFn agg = new AggregateUDAFs.TTextMaxFn(); + String buf = agg.zero(); + buf = agg.reduce(buf, TTEXT1); + buf = agg.reduce(buf, TTEXT2); + String result = agg.finish(buf); + assertNotNull(result); + assertFalse(result.isBlank()); + } + + // ------------------------------------------------------------------ + // tCentroid + // ------------------------------------------------------------------ + + @Test @Order(14) + void tCentroid_two_parallel_trips_returns_nonnull() { + AggregateUDAFs.TCentroidFn agg = new AggregateUDAFs.TCentroidFn(); + String buf = agg.zero(); + buf = agg.reduce(buf, TRIP1); + buf = agg.reduce(buf, TRIP2); + String result = agg.finish(buf); + assertNotNull(result); + assertFalse(result.isBlank()); + } + + // ------------------------------------------------------------------ + // tExtent + // ------------------------------------------------------------------ + + @Test @Order(15) + void tExtent_returns_nonnull_stbox() { + AggregateUDAFs.TExtentFn agg = new AggregateUDAFs.TExtentFn(); + String buf = agg.zero(); + buf = agg.reduce(buf, TRIP1); + buf = agg.reduce(buf, TRIP2); + String result = agg.finish(buf); + assertNotNull(result); + assertFalse(result.isBlank()); + } + + // ------------------------------------------------------------------ + // merge + // ------------------------------------------------------------------ + + @Test @Order(16) + void merge_combines_two_buffers() { + AggregateUDAFs.TCountFn agg = new AggregateUDAFs.TCountFn(); + String b1 = agg.reduce(agg.zero(), TRIP1); + String b2 = agg.reduce(agg.zero(), TRIP2); + String merged = agg.merge(b1, b2); + String result = agg.finish(merged); + assertNotNull(result); + assertFalse(result.isBlank()); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/AnalyticsUDFsExtTest.java b/src/test/java/org/mobilitydb/spark/temporal/AnalyticsUDFsExtTest.java new file mode 100644 index 00000000..b79a589d --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/AnalyticsUDFsExtTest.java @@ -0,0 +1,110 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for new AnalyticsUDFs and TransformUDFs additions: + * tpointCumulativeLength, tgeoTraversedArea, + * temporalShiftTime, temporalScaleTime. + * + * MEOS function authority: meos/include/meos.h, meos/include/meos_geo.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class AnalyticsUDFsExtTest { + + private static String TRIP; + private static String TFLOAT_SEQ; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TRIP = temporal_as_hexwkb( + tgeompoint_in("[POINT(0 0)@2020-01-01 00:00:00+00, POINT(3 4)@2020-01-01 01:00:00+00]"), + (byte) 0); + TFLOAT_SEQ = temporal_as_hexwkb( + tfloat_in("[1.0@2020-01-01, 5.0@2020-01-05]"), (byte) 0); + } + + // ------------------------------------------------------------------ + // tpointCumulativeLength + // ------------------------------------------------------------------ + + @Test @Order(1) + void tpointCumulativeLength_returns_nonnull() throws Exception { + String r = AnalyticsUDFs.tpointCumulativeLength.call(TRIP); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + // ------------------------------------------------------------------ + // tgeoTraversedArea + // ------------------------------------------------------------------ + + @Test @Order(2) + void tgeoTraversedArea_returns_wkt_or_null() throws Exception { + // tgeompoint (moving point) may return null from traversed_area; + // the function is primarily for polygon/body temporal types. + String r = AnalyticsUDFs.tgeoTraversedArea.call(TRIP); + assertTrue(r == null || !r.isBlank()); + } + + // ------------------------------------------------------------------ + // temporalShiftTime / temporalScaleTime + // ------------------------------------------------------------------ + + @Test @Order(3) + void temporalShiftTime_returns_nonnull() throws Exception { + String r = TransformUDFs.temporalShiftTime.call(TFLOAT_SEQ, "1 day"); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(4) + void temporalScaleTime_returns_nonnull() throws Exception { + String r = TransformUDFs.temporalScaleTime.call(TFLOAT_SEQ, "10 days"); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + // ------------------------------------------------------------------ + // Null-input guards + // ------------------------------------------------------------------ + + @Test @Order(5) + void null_input_returns_null() throws Exception { + assertNull(AnalyticsUDFs.tpointCumulativeLength.call(null)); + assertNull(AnalyticsUDFs.tgeoTraversedArea.call(null)); + assertNull(TransformUDFs.temporalShiftTime.call(null, "1 day")); + assertNull(TransformUDFs.temporalScaleTime.call(null, "1 day")); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/BoolOpsUDFsExtTest.java b/src/test/java/org/mobilitydb/spark/temporal/BoolOpsUDFsExtTest.java new file mode 100644 index 00000000..fe6c990b --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/BoolOpsUDFsExtTest.java @@ -0,0 +1,125 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for temporal comparison operator UDFs: + * teqTemporalTemporal, tneTemporalTemporal, tltTemporalTemporal, + * tleTemporalTemporal, tgtTemporalTemporal, tgeTemporalTemporal. + * + * MEOS function authority: meos/include/meos.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class BoolOpsUDFsExtTest { + + private static String TFLOAT_A; + private static String TFLOAT_B; + private static String TBOOL_HEX; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TFLOAT_A = temporal_as_hexwkb( + tfloat_in("[1.0@2020-01-01, 3.0@2020-01-03]"), (byte) 0); + TFLOAT_B = temporal_as_hexwkb( + tfloat_in("[2.0@2020-01-01, 2.0@2020-01-03]"), (byte) 0); + TBOOL_HEX = temporal_as_hexwkb( + tbool_in("[true@2020-01-01 00:00:00+00, false@2020-01-02 00:00:00+00]"), (byte) 0); + } + + @Test @Order(1) + void teqTemporalTemporal_returns_tbool_hex() throws Exception { + String r = BoolOpsUDFs.teqTemporalTemporal.call(TFLOAT_A, TFLOAT_A); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(2) + void tneTemporalTemporal_returns_tbool_hex() throws Exception { + String r = BoolOpsUDFs.tneTemporalTemporal.call(TFLOAT_A, TFLOAT_B); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(3) + void tltTemporalTemporal_returns_tbool_hex() throws Exception { + String r = BoolOpsUDFs.tltTemporalTemporal.call(TFLOAT_A, TFLOAT_B); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(4) + void tleTemporalTemporal_returns_tbool_hex() throws Exception { + String r = BoolOpsUDFs.tleTemporalTemporal.call(TFLOAT_A, TFLOAT_B); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(5) + void tgtTemporalTemporal_returns_tbool_hex() throws Exception { + String r = BoolOpsUDFs.tgtTemporalTemporal.call(TFLOAT_A, TFLOAT_B); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(6) + void tgeTemporalTemporal_returns_tbool_hex() throws Exception { + String r = BoolOpsUDFs.tgeTemporalTemporal.call(TFLOAT_A, TFLOAT_B); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(7) + void null_input_returns_null() throws Exception { + assertNull(BoolOpsUDFs.teqTemporalTemporal.call(null, TFLOAT_A)); + assertNull(BoolOpsUDFs.tneTemporalTemporal.call(TFLOAT_A, null)); + assertNull(BoolOpsUDFs.tltTemporalTemporal.call(null, TFLOAT_B)); + assertNull(BoolOpsUDFs.tgeTemporalTemporal.call(null, null)); + } + + // ------------------------------------------------------------------ + // tnotTbool + // ------------------------------------------------------------------ + + @Test @Order(8) + void tnotTbool_returns_nonnull_hexwkb() throws Exception { + String r = BoolOpsUDFs.tnotTbool.call(TBOOL_HEX); + assertNotNull(r, "tnotTbool must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(9) + void tnotTbool_null_returns_null() throws Exception { + assertNull(BoolOpsUDFs.tnotTbool.call(null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/BoolOpsUDFsTest.java b/src/test/java/org/mobilitydb/spark/temporal/BoolOpsUDFsTest.java new file mode 100644 index 00000000..e8c07726 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/BoolOpsUDFsTest.java @@ -0,0 +1,121 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for BoolOpsUDFs β€” temporal AND/OR on tbool. + * + * MEOS function authority: meos/include/meos.h (028_tbool_boolops) + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class BoolOpsUDFsTest { + + private static String TBOOL_TRUE; + private static String TBOOL_FALSE; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TBOOL_TRUE = temporal_as_hexwkb(tbool_in("t@2020-01-01"), (byte) 0); + TBOOL_FALSE = temporal_as_hexwkb(tbool_in("f@2020-01-01"), (byte) 0); + } + + // ------------------------------------------------------------------ + // tand + // ------------------------------------------------------------------ + + @Test @Order(1) + void tandBool_true_and_true_returns_nonnull() throws Exception { + String r = BoolOpsUDFs.tandBool.call(TBOOL_TRUE, true); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(2) + void tandBool_true_and_false_returns_nonnull() throws Exception { + String r = BoolOpsUDFs.tandBool.call(TBOOL_TRUE, false); + assertNotNull(r); + } + + @Test @Order(3) + void tandBoolTbool_false_and_true_returns_nonnull() throws Exception { + String r = BoolOpsUDFs.tandBoolTbool.call(false, TBOOL_TRUE); + assertNotNull(r); + } + + @Test @Order(4) + void tandTboolTbool_true_and_false_returns_nonnull() throws Exception { + String r = BoolOpsUDFs.tandTboolTbool.call(TBOOL_TRUE, TBOOL_FALSE); + assertNotNull(r); + } + + @Test @Order(5) + void tandBool_null_input_returns_null() throws Exception { + assertNull(BoolOpsUDFs.tandBool.call(null, true)); + assertNull(BoolOpsUDFs.tandBool.call(TBOOL_TRUE, null)); + } + + // ------------------------------------------------------------------ + // tor + // ------------------------------------------------------------------ + + @Test @Order(6) + void torBool_false_or_true_returns_nonnull() throws Exception { + String r = BoolOpsUDFs.torBool.call(TBOOL_FALSE, true); + assertNotNull(r); + } + + @Test @Order(7) + void torBool_false_or_false_returns_nonnull() throws Exception { + String r = BoolOpsUDFs.torBool.call(TBOOL_FALSE, false); + assertNotNull(r); + } + + @Test @Order(8) + void torBoolTbool_true_or_false_returns_nonnull() throws Exception { + String r = BoolOpsUDFs.torBoolTbool.call(true, TBOOL_FALSE); + assertNotNull(r); + } + + @Test @Order(9) + void torTboolTbool_false_or_true_returns_nonnull() throws Exception { + String r = BoolOpsUDFs.torTboolTbool.call(TBOOL_FALSE, TBOOL_TRUE); + assertNotNull(r); + } + + @Test @Order(10) + void torBool_null_input_returns_null() throws Exception { + assertNull(BoolOpsUDFs.torBool.call(null, true)); + assertNull(BoolOpsUDFs.torBool.call(TBOOL_FALSE, null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/BucketUDFsTest.java b/src/test/java/org/mobilitydb/spark/temporal/BucketUDFsTest.java new file mode 100644 index 00000000..3d98aa58 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/BucketUDFsTest.java @@ -0,0 +1,93 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class BucketUDFsTest { + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + } + + @Test @Order(1) + void floatBucket_aligns_to_origin() throws Exception { + // 7.3 with size 1.0, origin 0 β†’ bucket [7.0, 8.0) β†’ 7.0 + assertEquals(7.0, BucketUDFs.floatBucket.call(7.3, 1.0, 0.0), 1e-9); + } + + @Test @Order(2) + void floatBucket_negative_value() throws Exception { + // -0.5 with size 1.0, origin 0 β†’ bucket [-1.0, 0.0) β†’ -1.0 + assertEquals(-1.0, BucketUDFs.floatBucket.call(-0.5, 1.0, 0.0), 1e-9); + } + + @Test @Order(3) + void floatBucket_origin_offset() throws Exception { + // 7.3 with size 1.0, origin 0.5 β†’ bucket [6.5, 7.5) β†’ 6.5 + assertEquals(6.5, BucketUDFs.floatBucket.call(7.3, 1.0, 0.5), 1e-9); + } + + @Test @Order(4) + void floatBucket_null_value_returns_null() throws Exception { + assertNull(BucketUDFs.floatBucket.call(null, 1.0, 0.0)); + } + + @Test @Order(5) + void floatBucket_null_size_returns_null() throws Exception { + assertNull(BucketUDFs.floatBucket.call(7.3, null, 0.0)); + } + + @Test @Order(6) + void intBucket_basic() throws Exception { + // 17 with size 5, origin 0 β†’ bucket [15, 20) β†’ 15 + assertEquals(15, BucketUDFs.intBucket.call(17, 5, 0)); + } + + @Test @Order(7) + void intBucket_exact_boundary() throws Exception { + // 20 with size 5, origin 0 β†’ bucket [20, 25) β†’ 20 + assertEquals(20, BucketUDFs.intBucket.call(20, 5, 0)); + } + + @Test @Order(8) + void intBucket_negative() throws Exception { + assertEquals(-5, BucketUDFs.intBucket.call(-1, 5, 0)); + } + + @Test @Order(9) + void intBucket_null_returns_null() throws Exception { + assertNull(BucketUDFs.intBucket.call(null, 5, 0)); + assertNull(BucketUDFs.intBucket.call(17, null, 0)); + assertNull(BucketUDFs.intBucket.call(17, 5, null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/ConstructorUDFsExt2Test.java b/src/test/java/org/mobilitydb/spark/temporal/ConstructorUDFsExt2Test.java new file mode 100644 index 00000000..57fabe6a --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/ConstructorUDFsExt2Test.java @@ -0,0 +1,155 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for ConstructorUDFs constant-temporal constructors: + * tboolFromBaseTemp, tintFromBaseTemp, tfloatFromBaseTemp, ttextFromBaseTemp. + * + * Each creates a temporal value that is constant at the given scalar over the + * same time structure as a reference temporal. + * + * MEOS function authority: meos/include/meos.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class ConstructorUDFsExt2Test { + + private static String TINT_REF_HEX; + private static String TFLOAT_REF_HEX; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + // Reference temporals whose time structure will be reused + TINT_REF_HEX = temporal_as_hexwkb( + tint_in("[1@2020-01-01 00:00:00+00, 2@2020-01-02 00:00:00+00]"), (byte) 0); + TFLOAT_REF_HEX = temporal_as_hexwkb( + tfloat_in("[1.0@2020-01-01 00:00:00+00, 2.0@2020-01-02 00:00:00+00]"), (byte) 0); + } + + // ------------------------------------------------------------------ + // tboolFromBaseTemp + // ------------------------------------------------------------------ + + @Test @Order(1) + void tboolFromBaseTemp_returns_nonnull_hexwkb() throws Exception { + String r = ConstructorUDFs.tboolFromBaseTemp.call(true, TINT_REF_HEX); + assertNotNull(r, "tboolFromBaseTemp must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(2) + void tboolFromBaseTemp_false_returns_nonnull() throws Exception { + String r = ConstructorUDFs.tboolFromBaseTemp.call(false, TINT_REF_HEX); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(3) + void tboolFromBaseTemp_null_returns_null() throws Exception { + assertNull(ConstructorUDFs.tboolFromBaseTemp.call(null, TINT_REF_HEX)); + assertNull(ConstructorUDFs.tboolFromBaseTemp.call(true, null)); + } + + // ------------------------------------------------------------------ + // tintFromBaseTemp + // ------------------------------------------------------------------ + + @Test @Order(4) + void tintFromBaseTemp_returns_nonnull_hexwkb() throws Exception { + String r = ConstructorUDFs.tintFromBaseTemp.call(42, TINT_REF_HEX); + assertNotNull(r, "tintFromBaseTemp must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(5) + void tintFromBaseTemp_null_returns_null() throws Exception { + assertNull(ConstructorUDFs.tintFromBaseTemp.call(null, TINT_REF_HEX)); + assertNull(ConstructorUDFs.tintFromBaseTemp.call(42, null)); + } + + // ------------------------------------------------------------------ + // tfloatFromBaseTemp + // ------------------------------------------------------------------ + + @Test @Order(6) + void tfloatFromBaseTemp_returns_nonnull_hexwkb() throws Exception { + String r = ConstructorUDFs.tfloatFromBaseTemp.call(3.14, TFLOAT_REF_HEX); + assertNotNull(r, "tfloatFromBaseTemp must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(7) + void tfloatFromBaseTemp_null_returns_null() throws Exception { + assertNull(ConstructorUDFs.tfloatFromBaseTemp.call(null, TFLOAT_REF_HEX)); + assertNull(ConstructorUDFs.tfloatFromBaseTemp.call(3.14, null)); + } + + // ------------------------------------------------------------------ + // ttextFromBaseTemp + // ------------------------------------------------------------------ + + @Test @Order(8) + void ttextFromBaseTemp_returns_nonnull_hexwkb() throws Exception { + String r = ConstructorUDFs.ttextFromBaseTemp.call("hello", TINT_REF_HEX); + assertNotNull(r, "ttextFromBaseTemp must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(9) + void ttextFromBaseTemp_null_returns_null() throws Exception { + assertNull(ConstructorUDFs.ttextFromBaseTemp.call(null, TINT_REF_HEX)); + assertNull(ConstructorUDFs.ttextFromBaseTemp.call("hello", null)); + } + + // ------------------------------------------------------------------ + // tpointFromBaseTemp + // ------------------------------------------------------------------ + + @Test @Order(10) + void tpointFromBaseTemp_returns_nonnull_hexwkb() throws Exception { + String r = ConstructorUDFs.tpointFromBaseTemp.call("POINT(1.0 2.0)", TINT_REF_HEX); + assertNotNull(r, "tpointFromBaseTemp must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(11) + void tpointFromBaseTemp_null_geo_returns_null() throws Exception { + assertNull(ConstructorUDFs.tpointFromBaseTemp.call(null, TINT_REF_HEX)); + } + + @Test @Order(12) + void tpointFromBaseTemp_null_ref_returns_null() throws Exception { + assertNull(ConstructorUDFs.tpointFromBaseTemp.call("POINT(1.0 2.0)", null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/ConstructorUDFsExtTest.java b/src/test/java/org/mobilitydb/spark/temporal/ConstructorUDFsExtTest.java new file mode 100644 index 00000000..60e4c3f8 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/ConstructorUDFsExtTest.java @@ -0,0 +1,181 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import jnr.ffi.Pointer; +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for ConstructorUDFs MFJSON round-trip constructors: + * tboolFromMfjson, tintFromMfjson, tfloatFromMfjson, ttextFromMfjson, + * tgeompointFromMfjson, tgeogpointFromMfjson. + * + * Each test converts a known temporal value to MFJSON then reconstructs it, + * verifying that the round-trip produces a valid non-null hex-WKB. + * + * MEOS function authority: meos/include/meos.h, meos/include/meos_geo.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class ConstructorUDFsExtTest { + + private static String TBOOL_MFJSON; + private static String TINT_MFJSON; + private static String TFLOAT_MFJSON; + private static String TTEXT_MFJSON; + private static String TGEOMPOINT_MFJSON; + private static String TGEOGPOINT_MFJSON; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + Pointer tbool = tbool_in("[true@2020-01-01 00:00:00+00, false@2020-01-02 00:00:00+00]"); + TBOOL_MFJSON = temporal_as_mfjson(tbool, false, 0, 6, null); + + Pointer tint = tint_in("[1@2020-01-01 00:00:00+00, 3@2020-01-03 00:00:00+00]"); + TINT_MFJSON = temporal_as_mfjson(tint, false, 0, 6, null); + + Pointer tfloat = tfloat_in("[1.5@2020-01-01 00:00:00+00, 2.5@2020-01-03 00:00:00+00]"); + TFLOAT_MFJSON = temporal_as_mfjson(tfloat, false, 0, 6, null); + + Pointer ttext = ttext_in("[hello@2020-01-01 00:00:00+00, world@2020-01-02 00:00:00+00]"); + TTEXT_MFJSON = temporal_as_mfjson(ttext, false, 0, 6, null); + + Pointer tgeompoint = tgeompoint_in( + "[POINT(0 0)@2020-01-01 00:00:00+00, POINT(1 0)@2020-01-01 01:00:00+00]"); + TGEOMPOINT_MFJSON = temporal_as_mfjson(tgeompoint, false, 0, 6, null); + + Pointer tgeogpoint = tgeogpoint_in( + "[POINT(4.35 50.85)@2020-01-01 00:00:00+00, POINT(4.36 50.86)@2020-01-01 01:00:00+00]"); + TGEOGPOINT_MFJSON = temporal_as_mfjson(tgeogpoint, false, 0, 6, null); + } + + // ------------------------------------------------------------------ + // tboolFromMfjson + // ------------------------------------------------------------------ + + @Test @Order(1) + void tboolFromMfjson_round_trip_returns_nonnull() throws Exception { + assertNotNull(TBOOL_MFJSON, "MFJSON source must be non-null"); + String r = ConstructorUDFs.tboolFromMfjson.call(TBOOL_MFJSON); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(2) + void tboolFromMfjson_null_returns_null() throws Exception { + assertNull(ConstructorUDFs.tboolFromMfjson.call(null)); + } + + // ------------------------------------------------------------------ + // tintFromMfjson + // ------------------------------------------------------------------ + + @Test @Order(3) + void tintFromMfjson_round_trip_returns_nonnull() throws Exception { + assertNotNull(TINT_MFJSON, "MFJSON source must be non-null"); + String r = ConstructorUDFs.tintFromMfjson.call(TINT_MFJSON); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(4) + void tintFromMfjson_null_returns_null() throws Exception { + assertNull(ConstructorUDFs.tintFromMfjson.call(null)); + } + + // ------------------------------------------------------------------ + // tfloatFromMfjson + // ------------------------------------------------------------------ + + @Test @Order(5) + void tfloatFromMfjson_round_trip_returns_nonnull() throws Exception { + assertNotNull(TFLOAT_MFJSON, "MFJSON source must be non-null"); + String r = ConstructorUDFs.tfloatFromMfjson.call(TFLOAT_MFJSON); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(6) + void tfloatFromMfjson_null_returns_null() throws Exception { + assertNull(ConstructorUDFs.tfloatFromMfjson.call(null)); + } + + // ------------------------------------------------------------------ + // ttextFromMfjson + // ------------------------------------------------------------------ + + @Test @Order(7) + void ttextFromMfjson_round_trip_returns_nonnull() throws Exception { + assertNotNull(TTEXT_MFJSON, "MFJSON source must be non-null"); + String r = ConstructorUDFs.ttextFromMfjson.call(TTEXT_MFJSON); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(8) + void ttextFromMfjson_null_returns_null() throws Exception { + assertNull(ConstructorUDFs.ttextFromMfjson.call(null)); + } + + // ------------------------------------------------------------------ + // tgeompointFromMfjson + // ------------------------------------------------------------------ + + @Test @Order(9) + void tgeompointFromMfjson_round_trip_returns_nonnull() throws Exception { + assertNotNull(TGEOMPOINT_MFJSON, "MFJSON source must be non-null"); + String r = ConstructorUDFs.tgeompointFromMfjson.call(TGEOMPOINT_MFJSON); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(10) + void tgeompointFromMfjson_null_returns_null() throws Exception { + assertNull(ConstructorUDFs.tgeompointFromMfjson.call(null)); + } + + // ------------------------------------------------------------------ + // tgeogpointFromMfjson + // ------------------------------------------------------------------ + + @Test @Order(11) + void tgeogpointFromMfjson_round_trip_returns_nonnull() throws Exception { + assertNotNull(TGEOGPOINT_MFJSON, "MFJSON source must be non-null"); + String r = ConstructorUDFs.tgeogpointFromMfjson.call(TGEOGPOINT_MFJSON); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(12) + void tgeogpointFromMfjson_null_returns_null() throws Exception { + assertNull(ConstructorUDFs.tgeogpointFromMfjson.call(null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/MathUDFsExtTest.java b/src/test/java/org/mobilitydb/spark/temporal/MathUDFsExtTest.java new file mode 100644 index 00000000..56bbb620 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/MathUDFsExtTest.java @@ -0,0 +1,145 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for new MathUDFs β€” transcendental functions (exp, ln, log10) + * and tnumberTrend, plus new BoolOps/Predicate UDFs (tboolWhenTrue, + * tpointIsSimple). + * + * MEOS function authority: meos/include/meos.h, meos/include/meos_geo.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class MathUDFsExtTest { + + private static String TFLOAT_SEQ; + private static String TINT_SEQ; + private static String TBOOL_SEQ; + private static String TRIP; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TFLOAT_SEQ = temporal_as_hexwkb( + tfloat_in("[1.0@2020-01-01, 2.0@2020-01-02, 4.0@2020-01-04]"), (byte) 0); + TINT_SEQ = temporal_as_hexwkb( + tint_in("[1@2020-01-01, 3@2020-01-03]"), (byte) 0); + TBOOL_SEQ = temporal_as_hexwkb( + tbool_in("[true@2020-01-01, false@2020-01-02, true@2020-01-03]"), (byte) 0); + TRIP = temporal_as_hexwkb( + tgeompoint_in("[POINT(0 0)@2020-01-01 00:00:00+00, POINT(3 4)@2020-01-01 01:00:00+00]"), + (byte) 0); + } + + // ------------------------------------------------------------------ + // Transcendental functions + // ------------------------------------------------------------------ + + @Test @Order(1) + void tfloatExp_returns_nonnull() throws Exception { + String r = MathUDFs.tfloatExp.call(TFLOAT_SEQ); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(2) + void tfloatLn_returns_nonnull() throws Exception { + String r = MathUDFs.tfloatLn.call(TFLOAT_SEQ); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(3) + void tfloatLog10_returns_nonnull() throws Exception { + String r = MathUDFs.tfloatLog10.call(TFLOAT_SEQ); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + // ------------------------------------------------------------------ + // tnumberTrend + // ------------------------------------------------------------------ + + @Test @Order(4) + void tnumberTrend_tfloat_returns_nonnull() throws Exception { + String r = AnalyticsUDFs.tnumberTrend.call(TFLOAT_SEQ); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(5) + void tnumberTrend_tint_returns_nonnull() throws Exception { + // At the current MEOS pin tnumber_trend is defined for both step and + // linear interpolation (the prior ensure_linear_interp NULL guard was + // removed), so a step-interpolated tint has a well-defined trend. + String r = AnalyticsUDFs.tnumberTrend.call(TINT_SEQ); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + // ------------------------------------------------------------------ + // tboolWhenTrue + // ------------------------------------------------------------------ + + @Test @Order(6) + void tboolWhenTrue_returns_nonnull() throws Exception { + String r = BoolOpsUDFs.tboolWhenTrue.call(TBOOL_SEQ); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + // ------------------------------------------------------------------ + // tpointIsSimple + // ------------------------------------------------------------------ + + @Test @Order(7) + void tpointIsSimple_simple_trip() throws Exception { + Boolean r = PredicateUDFs.tpointIsSimple.call(TRIP); + assertNotNull(r); + assertTrue(r); + } + + // ------------------------------------------------------------------ + // Null-input guards + // ------------------------------------------------------------------ + + @Test @Order(8) + void null_input_returns_null() throws Exception { + assertNull(MathUDFs.tfloatExp.call(null)); + assertNull(MathUDFs.tfloatLn.call(null)); + assertNull(MathUDFs.tfloatLog10.call(null)); + assertNull(AnalyticsUDFs.tnumberTrend.call(null)); + assertNull(BoolOpsUDFs.tboolWhenTrue.call(null)); + assertNull(PredicateUDFs.tpointIsSimple.call(null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/MathUDFsTest.java b/src/test/java/org/mobilitydb/spark/temporal/MathUDFsTest.java new file mode 100644 index 00000000..98802615 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/MathUDFsTest.java @@ -0,0 +1,192 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for MathUDFs β€” tnumber arithmetic and unary analytics. + * + * MEOS function authority: meos/include/meos.h (026_tnumber_mathfuncs) + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class MathUDFsTest { + + private static String TINT_NEG; + private static String TINT_POS; + private static String TFLOAT_POS; + private static String TRIP; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + // tint with negative value at t1, positive at t2 + TINT_NEG = temporal_as_hexwkb(tint_in("[-5@2020-01-01, 3@2020-01-02]"), (byte) 0); + TINT_POS = temporal_as_hexwkb(tint_in("[2@2020-01-01, 4@2020-01-02]"), (byte) 0); + TFLOAT_POS = temporal_as_hexwkb(tfloat_in("[1.0@2020-01-01, 3.0@2020-01-02]"), (byte) 0); + TRIP = temporal_as_hexwkb( + tgeompoint_in("[POINT(0 0)@2020-01-01, POINT(1 0)@2020-01-02]"), (byte) 0); + } + + // ------------------------------------------------------------------ + // Unary analytics + // ------------------------------------------------------------------ + + @Test @Order(1) + void tnumberAbs_returns_nonnull_hexwkb() throws Exception { + String r = MathUDFs.tnumberAbs.call(TINT_NEG); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(2) + void tnumberDeltaValue_returns_nonnull_hexwkb() throws Exception { + String r = MathUDFs.tnumberDeltaValue.call(TINT_POS); + assertNotNull(r); + } + + @Test @Order(3) + void tnumberAngularDifference_returns_nonnull_or_null_for_step() throws Exception { + // tnumber_angular_difference may return null for step sequences; just check no exception + String r = MathUDFs.tnumberAngularDifference.call(TFLOAT_POS); + // result can be null for non-periodic sequences β€” no assertion on value + assertTrue(r == null || !r.isBlank()); + } + + @Test @Order(4) + void tpointAngularDifference_returns_nonnull_hexwkb() throws Exception { + String r = MathUDFs.tpointAngularDifference.call(TRIP); + // May be null for sequences with only 2 instants (no angular change to compute) + assertTrue(r == null || !r.isBlank()); + } + + @Test @Order(5) + void tnumberAbs_null_returns_null() throws Exception { + assertNull(MathUDFs.tnumberAbs.call(null)); + } + + // ------------------------------------------------------------------ + // tint + scalar + // ------------------------------------------------------------------ + + @Test @Order(6) + void addTintInt_returns_nonnull_hexwkb() throws Exception { + String r = MathUDFs.addTintInt.call(TINT_POS, 10); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(7) + void subTintInt_returns_nonnull_hexwkb() throws Exception { + String r = MathUDFs.subTintInt.call(TINT_POS, 1); + assertNotNull(r); + } + + @Test @Order(8) + void multTintInt_returns_nonnull_hexwkb() throws Exception { + String r = MathUDFs.multTintInt.call(TINT_POS, 3); + assertNotNull(r); + } + + @Test @Order(9) + void divTintInt_returns_nonnull_hexwkb() throws Exception { + String r = MathUDFs.divTintInt.call(TINT_POS, 2); + assertNotNull(r); + } + + @Test @Order(10) + void addTintInt_null_input_returns_null() throws Exception { + assertNull(MathUDFs.addTintInt.call(null, 5)); + assertNull(MathUDFs.addTintInt.call(TINT_POS, null)); + } + + // ------------------------------------------------------------------ + // tfloat + scalar + // ------------------------------------------------------------------ + + @Test @Order(11) + void addTfloatFloat_returns_nonnull_hexwkb() throws Exception { + String r = MathUDFs.addTfloatFloat.call(TFLOAT_POS, 0.5); + assertNotNull(r); + } + + @Test @Order(12) + void subTfloatFloat_returns_nonnull_hexwkb() throws Exception { + String r = MathUDFs.subTfloatFloat.call(TFLOAT_POS, 0.5); + assertNotNull(r); + } + + @Test @Order(13) + void multTfloatFloat_returns_nonnull_hexwkb() throws Exception { + String r = MathUDFs.multTfloatFloat.call(TFLOAT_POS, 2.0); + assertNotNull(r); + } + + @Test @Order(14) + void divTfloatFloat_returns_nonnull_hexwkb() throws Exception { + String r = MathUDFs.divTfloatFloat.call(TFLOAT_POS, 2.0); + assertNotNull(r); + } + + // ------------------------------------------------------------------ + // tnumber + tnumber + // ------------------------------------------------------------------ + + @Test @Order(15) + void addTnumberTnumber_returns_nonnull_hexwkb() throws Exception { + String r = MathUDFs.addTnumberTnumber.call(TINT_POS, TINT_NEG); + assertNotNull(r); + } + + @Test @Order(16) + void subTnumberTnumber_returns_nonnull_hexwkb() throws Exception { + String r = MathUDFs.subTnumberTnumber.call(TINT_POS, TINT_NEG); + assertNotNull(r); + } + + @Test @Order(17) + void multTnumberTnumber_returns_nonnull_hexwkb() throws Exception { + String r = MathUDFs.multTnumberTnumber.call(TINT_POS, TINT_POS); + assertNotNull(r); + } + + @Test @Order(18) + void divTnumberTnumber_returns_nonnull_hexwkb() throws Exception { + String r = MathUDFs.divTnumberTnumber.call(TINT_POS, TINT_POS); + assertNotNull(r); + } + + @Test @Order(19) + void addTnumberTnumber_null_returns_null() throws Exception { + assertNull(MathUDFs.addTnumberTnumber.call(null, TINT_POS)); + assertNull(MathUDFs.addTnumberTnumber.call(TINT_POS, null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/MoreAccessorUDFsExt2Test.java b/src/test/java/org/mobilitydb/spark/temporal/MoreAccessorUDFsExt2Test.java new file mode 100644 index 00000000..050b6abe --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/MoreAccessorUDFsExt2Test.java @@ -0,0 +1,116 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import java.sql.Timestamp; +import java.util.List; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for MoreAccessorUDFs array-returning accessors: + * temporalTimestamps, tboolValues. + * + * MEOS function authority: meos/include/meos.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class MoreAccessorUDFsExt2Test { + + private static String TINT_SEQ_HEX; + private static String TBOOL_SEQ_HEX; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TINT_SEQ_HEX = temporal_as_hexwkb( + tint_in("[1@2020-01-01 00:00:00+00, 2@2020-01-02 00:00:00+00, 3@2020-01-03 00:00:00+00]"), + (byte) 0); + TBOOL_SEQ_HEX = temporal_as_hexwkb( + tbool_in("[true@2020-01-01 00:00:00+00, false@2020-01-02 00:00:00+00]"), + (byte) 0); + } + + // ------------------------------------------------------------------ + // temporalTimestamps + // ------------------------------------------------------------------ + + @Test @Order(1) + void temporalTimestamps_returns_nonnull_list() throws Exception { + List r = MoreAccessorUDFs.temporalTimestamps.call(TINT_SEQ_HEX); + assertNotNull(r, "temporalTimestamps must return non-null"); + assertFalse(r.isEmpty(), "List must not be empty for 3-instant tint sequence"); + } + + @Test @Order(2) + void temporalTimestamps_count_matches_instants() throws Exception { + List r = MoreAccessorUDFs.temporalTimestamps.call(TINT_SEQ_HEX); + assertNotNull(r); + assertEquals(3, r.size(), "3-instant sequence must yield 3 timestamps"); + } + + @Test @Order(3) + void temporalTimestamps_timestamps_are_ordered() throws Exception { + List r = MoreAccessorUDFs.temporalTimestamps.call(TINT_SEQ_HEX); + assertNotNull(r); + assertEquals(3, r.size()); + assertTrue(r.get(0).before(r.get(1)), "timestamps must be in ascending order"); + assertTrue(r.get(1).before(r.get(2))); + } + + @Test @Order(4) + void temporalTimestamps_null_returns_null() throws Exception { + assertNull(MoreAccessorUDFs.temporalTimestamps.call(null)); + } + + // ------------------------------------------------------------------ + // tboolValues + // ------------------------------------------------------------------ + + @Test @Order(5) + void tboolValues_returns_nonnull_list() throws Exception { + List r = MoreAccessorUDFs.tboolValues.call(TBOOL_SEQ_HEX); + assertNotNull(r, "tboolValues must return non-null"); + assertFalse(r.isEmpty()); + } + + @Test @Order(6) + void tboolValues_contains_both_values() throws Exception { + List r = MoreAccessorUDFs.tboolValues.call(TBOOL_SEQ_HEX); + assertNotNull(r); + assertTrue(r.contains(Boolean.TRUE), "Must contain true (appears in sequence)"); + assertTrue(r.contains(Boolean.FALSE), "Must contain false (appears in sequence)"); + } + + @Test @Order(7) + void tboolValues_null_returns_null() throws Exception { + assertNull(MoreAccessorUDFs.tboolValues.call(null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/MoreAccessorUDFsExt3Test.java b/src/test/java/org/mobilitydb/spark/temporal/MoreAccessorUDFsExt3Test.java new file mode 100644 index 00000000..05f7b7ae --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/MoreAccessorUDFsExt3Test.java @@ -0,0 +1,115 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import java.util.List; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for MoreAccessorUDFs array-returning accessors: + * tintValues, tfloatValues. + * + * MEOS function authority: meos/include/meos.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class MoreAccessorUDFsExt3Test { + + private static String TINT_SEQ_HEX; + private static String TFLOAT_SEQ_HEX; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TINT_SEQ_HEX = temporal_as_hexwkb( + tint_in("Interp=Step;[1@2020-01-01 00:00:00+00, 2@2020-01-02 00:00:00+00, 3@2020-01-03 00:00:00+00]"), + (byte) 0); + TFLOAT_SEQ_HEX = temporal_as_hexwkb( + tfloat_in("Interp=Step;[1.5@2020-01-01 00:00:00+00, 2.5@2020-01-02 00:00:00+00]"), + (byte) 0); + } + + // ------------------------------------------------------------------ + // tintValues + // ------------------------------------------------------------------ + + @Test @Order(1) + void tintValues_returns_nonnull_list() throws Exception { + List r = MoreAccessorUDFs.tintValues.call(TINT_SEQ_HEX); + assertNotNull(r, "tintValues must return non-null"); + assertFalse(r.isEmpty()); + } + + @Test @Order(2) + void tintValues_contains_expected_values() throws Exception { + List r = MoreAccessorUDFs.tintValues.call(TINT_SEQ_HEX); + assertNotNull(r); + assertTrue(r.contains(1), "Must contain value 1"); + assertTrue(r.contains(2), "Must contain value 2"); + assertTrue(r.contains(3), "Must contain value 3"); + } + + @Test @Order(3) + void tintValues_count_matches_distinct_instants() throws Exception { + List r = MoreAccessorUDFs.tintValues.call(TINT_SEQ_HEX); + assertNotNull(r); + assertEquals(3, r.size(), "3-instant step tint must yield 3 distinct values"); + } + + @Test @Order(4) + void tintValues_null_returns_null() throws Exception { + assertNull(MoreAccessorUDFs.tintValues.call(null)); + } + + // ------------------------------------------------------------------ + // tfloatValues + // ------------------------------------------------------------------ + + @Test @Order(5) + void tfloatValues_returns_nonnull_list() throws Exception { + List r = MoreAccessorUDFs.tfloatValues.call(TFLOAT_SEQ_HEX); + assertNotNull(r, "tfloatValues must return non-null"); + assertFalse(r.isEmpty()); + } + + @Test @Order(6) + void tfloatValues_contains_expected_values() throws Exception { + List r = MoreAccessorUDFs.tfloatValues.call(TFLOAT_SEQ_HEX); + assertNotNull(r); + assertTrue(r.contains(1.5), "Must contain value 1.5"); + assertTrue(r.contains(2.5), "Must contain value 2.5"); + } + + @Test @Order(7) + void tfloatValues_null_returns_null() throws Exception { + assertNull(MoreAccessorUDFs.tfloatValues.call(null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/MoreAccessorUDFsExt4Test.java b/src/test/java/org/mobilitydb/spark/temporal/MoreAccessorUDFsExt4Test.java new file mode 100644 index 00000000..38bfdd2c --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/MoreAccessorUDFsExt4Test.java @@ -0,0 +1,144 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import java.util.List; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for MoreAccessorUDFs temporal decomposition accessors: + * temporalInstants, temporalSequences, temporalSegments. + * + * MEOS function authority: meos/include/meos.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class MoreAccessorUDFsExt4Test { + + /** 3-instant linear tint sequence. */ + private static String TINT_SEQ_HEX; + /** TSequenceSet: 2 disjoint sequences. */ + private static String TINT_SEQSET_HEX; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TINT_SEQ_HEX = temporal_as_hexwkb( + tint_in("[1@2020-01-01 00:00:00+00, 2@2020-01-02 00:00:00+00, 3@2020-01-03 00:00:00+00]"), + (byte) 0); + TINT_SEQSET_HEX = temporal_as_hexwkb( + tint_in("{[1@2020-01-01 00:00:00+00, 2@2020-01-02 00:00:00+00]," + + "[5@2020-02-01 00:00:00+00, 7@2020-02-03 00:00:00+00]}"), + (byte) 0); + } + + // ------------------------------------------------------------------ + // temporalInstants + // ------------------------------------------------------------------ + + @Test @Order(1) + void temporalInstants_seq_returns_nonnull_list() throws Exception { + List r = MoreAccessorUDFs.temporalInstants.call(TINT_SEQ_HEX); + assertNotNull(r, "temporalInstants must return non-null"); + assertFalse(r.isEmpty()); + } + + @Test @Order(2) + void temporalInstants_seq_count_matches_instants() throws Exception { + List r = MoreAccessorUDFs.temporalInstants.call(TINT_SEQ_HEX); + assertNotNull(r); + assertEquals(3, r.size(), "3-instant sequence must yield 3 instants"); + } + + @Test @Order(3) + void temporalInstants_elements_are_valid_hexwkb() throws Exception { + List r = MoreAccessorUDFs.temporalInstants.call(TINT_SEQ_HEX); + assertNotNull(r); + for (String elem : r) { + assertNotNull(elem, "Each element must be non-null"); + assertFalse(elem.isBlank(), "Each element must be a non-blank hex string"); + } + } + + @Test @Order(4) + void temporalInstants_null_returns_null() throws Exception { + assertNull(MoreAccessorUDFs.temporalInstants.call(null)); + } + + // ------------------------------------------------------------------ + // temporalSequences + // ------------------------------------------------------------------ + + @Test @Order(5) + void temporalSequences_seqset_returns_nonnull_list() throws Exception { + List r = MoreAccessorUDFs.temporalSequences.call(TINT_SEQSET_HEX); + assertNotNull(r, "temporalSequences must return non-null for TSequenceSet"); + assertFalse(r.isEmpty()); + } + + @Test @Order(6) + void temporalSequences_seqset_count_matches_sequences() throws Exception { + List r = MoreAccessorUDFs.temporalSequences.call(TINT_SEQSET_HEX); + assertNotNull(r); + assertEquals(2, r.size(), "2-sequence set must yield 2 sequences"); + } + + @Test @Order(7) + void temporalSequences_elements_are_valid_hexwkb() throws Exception { + List r = MoreAccessorUDFs.temporalSequences.call(TINT_SEQSET_HEX); + assertNotNull(r); + for (String elem : r) { + assertNotNull(elem); + assertFalse(elem.isBlank()); + } + } + + @Test @Order(8) + void temporalSequences_null_returns_null() throws Exception { + assertNull(MoreAccessorUDFs.temporalSequences.call(null)); + } + + // ------------------------------------------------------------------ + // temporalSegments + // ------------------------------------------------------------------ + + @Test @Order(9) + void temporalSegments_seq_returns_nonnull_list() throws Exception { + List r = MoreAccessorUDFs.temporalSegments.call(TINT_SEQ_HEX); + assertNotNull(r, "temporalSegments must return non-null"); + assertFalse(r.isEmpty()); + } + + @Test @Order(10) + void temporalSegments_null_returns_null() throws Exception { + assertNull(MoreAccessorUDFs.temporalSegments.call(null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/MoreAccessorUDFsExt5Test.java b/src/test/java/org/mobilitydb/spark/temporal/MoreAccessorUDFsExt5Test.java new file mode 100644 index 00000000..7b3e9c3f --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/MoreAccessorUDFsExt5Test.java @@ -0,0 +1,95 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import java.util.List; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for MoreAccessorUDFs.ttextValues. + * + * MEOS function authority: meos/include/meos.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class MoreAccessorUDFsExt5Test { + + /** ttext sequence with two distinct values. */ + private static String TTEXT_SEQ_HEX; + /** ttext sequence with repeated value β€” distinct count = 1. */ + private static String TTEXT_CONST_HEX; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TTEXT_SEQ_HEX = temporal_as_hexwkb( + ttext_in("[hello@2020-01-01 00:00:00+00, world@2020-01-02 00:00:00+00]"), + (byte) 0); + TTEXT_CONST_HEX = temporal_as_hexwkb( + ttext_in("[hello@2020-01-01 00:00:00+00, hello@2020-01-03 00:00:00+00]"), + (byte) 0); + } + + @Test @Order(1) + void ttextValues_seq_returns_nonnull() throws Exception { + List vs = MoreAccessorUDFs.ttextValues.call(TTEXT_SEQ_HEX); + assertNotNull(vs, "ttextValues must return non-null for ttext sequence"); + } + + @Test @Order(2) + void ttextValues_seq_count_is_two() throws Exception { + List vs = MoreAccessorUDFs.ttextValues.call(TTEXT_SEQ_HEX); + assertNotNull(vs); + assertEquals(2, vs.size(), "two-value sequence must yield 2 distinct values"); + } + + @Test @Order(3) + void ttextValues_elements_are_non_blank() throws Exception { + List vs = MoreAccessorUDFs.ttextValues.call(TTEXT_SEQ_HEX); + assertNotNull(vs); + for (String s : vs) { + assertNotNull(s); + assertFalse(s.isBlank(), "each value string must be non-blank"); + } + } + + @Test @Order(4) + void ttextValues_constant_seq_returns_one_distinct_value() throws Exception { + List vs = MoreAccessorUDFs.ttextValues.call(TTEXT_CONST_HEX); + assertNotNull(vs); + assertEquals(1, vs.size(), "constant ttext must yield exactly 1 distinct value"); + } + + @Test @Order(5) + void ttextValues_null_returns_null() throws Exception { + assertNull(MoreAccessorUDFs.ttextValues.call(null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/MoreAccessorUDFsExtTest.java b/src/test/java/org/mobilitydb/spark/temporal/MoreAccessorUDFsExtTest.java new file mode 100644 index 00000000..a5eb84d8 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/MoreAccessorUDFsExtTest.java @@ -0,0 +1,172 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import java.sql.Timestamp; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for MoreAccessorUDFs value_at_timestamptz UDFs: + * tboolValueAtTimestamptz, tintValueAtTimestamptz, tfloatValueAtTimestamptz, + * ttextValueAtTimestamptz. + * + * Timestamps are created as Spark java.sql.Timestamp (Unix-epoch ms). + * The UDFs convert them to PG-epoch Β΅s internally before calling MEOS. + * + * MEOS function authority: meos/include/meos.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class MoreAccessorUDFsExtTest { + + private static String TBOOL_SEQ; + private static String TINT_SEQ; + private static String TFLOAT_SEQ; + private static String TTEXT_SEQ; + + // Timestamps inside the sequences (2020-01-01 00:00:00 UTC = Unix 1577836800 s) + private static Timestamp TS_START; + // Timestamp outside the sequences (2020-06-01 00:00:00 UTC = Unix 1590969600 s) + private static Timestamp TS_OUTSIDE; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TBOOL_SEQ = temporal_as_hexwkb( + tbool_in("Interp=Step;[true@2020-01-01 00:00:00+00, true@2020-01-03 00:00:00+00]"), + (byte) 0); + TINT_SEQ = temporal_as_hexwkb( + tint_in("Interp=Step;[7@2020-01-01 00:00:00+00, 7@2020-01-03 00:00:00+00]"), + (byte) 0); + TFLOAT_SEQ = temporal_as_hexwkb( + tfloat_in("[1.0@2020-01-01 00:00:00+00, 3.0@2020-01-03 00:00:00+00]"), + (byte) 0); + TTEXT_SEQ = temporal_as_hexwkb( + ttext_in("Interp=Step;[hello@2020-01-01 00:00:00+00, hello@2020-01-03 00:00:00+00]"), + (byte) 0); + + TS_START = new Timestamp(1577836800L * 1000L); // 2020-01-01 00:00:00 UTC + TS_OUTSIDE = new Timestamp(1590969600L * 1000L); // 2020-06-01 00:00:00 UTC + } + + // ------------------------------------------------------------------ + // tboolValueAtTimestamptz + // ------------------------------------------------------------------ + + @Test @Order(1) + void tboolValueAtTimestamptz_at_start_returns_true() throws Exception { + Boolean r = MoreAccessorUDFs.tboolValueAtTimestamptz.call(TBOOL_SEQ, TS_START); + assertNotNull(r, "Value at start timestamp must be non-null"); + assertTrue(r, "tbool value at t0 must be true"); + } + + @Test @Order(2) + void tboolValueAtTimestamptz_outside_range_returns_null() throws Exception { + Boolean r = MoreAccessorUDFs.tboolValueAtTimestamptz.call(TBOOL_SEQ, TS_OUTSIDE); + assertNull(r, "Value outside sequence range must be null"); + } + + @Test @Order(3) + void tboolValueAtTimestamptz_null_returns_null() throws Exception { + assertNull(MoreAccessorUDFs.tboolValueAtTimestamptz.call(null, TS_START)); + assertNull(MoreAccessorUDFs.tboolValueAtTimestamptz.call(TBOOL_SEQ, null)); + } + + // ------------------------------------------------------------------ + // tintValueAtTimestamptz + // ------------------------------------------------------------------ + + @Test @Order(4) + void tintValueAtTimestamptz_at_start_returns_correct_value() throws Exception { + Integer r = MoreAccessorUDFs.tintValueAtTimestamptz.call(TINT_SEQ, TS_START); + assertNotNull(r, "Value at start timestamp must be non-null"); + assertEquals(7, r.intValue(), "tint value at t0 must be 7"); + } + + @Test @Order(5) + void tintValueAtTimestamptz_outside_range_returns_null() throws Exception { + Integer r = MoreAccessorUDFs.tintValueAtTimestamptz.call(TINT_SEQ, TS_OUTSIDE); + assertNull(r, "Value outside sequence range must be null"); + } + + @Test @Order(6) + void tintValueAtTimestamptz_null_returns_null() throws Exception { + assertNull(MoreAccessorUDFs.tintValueAtTimestamptz.call(null, TS_START)); + assertNull(MoreAccessorUDFs.tintValueAtTimestamptz.call(TINT_SEQ, null)); + } + + // ------------------------------------------------------------------ + // tfloatValueAtTimestamptz + // ------------------------------------------------------------------ + + @Test @Order(7) + void tfloatValueAtTimestamptz_at_start_returns_1_0() throws Exception { + Double r = MoreAccessorUDFs.tfloatValueAtTimestamptz.call(TFLOAT_SEQ, TS_START); + assertNotNull(r, "Value at start timestamp must be non-null"); + assertEquals(1.0, r, 1e-9, "tfloat value at t0 must be 1.0"); + } + + @Test @Order(8) + void tfloatValueAtTimestamptz_outside_range_returns_null() throws Exception { + Double r = MoreAccessorUDFs.tfloatValueAtTimestamptz.call(TFLOAT_SEQ, TS_OUTSIDE); + assertNull(r, "Value outside sequence range must be null"); + } + + @Test @Order(9) + void tfloatValueAtTimestamptz_null_returns_null() throws Exception { + assertNull(MoreAccessorUDFs.tfloatValueAtTimestamptz.call(null, TS_START)); + assertNull(MoreAccessorUDFs.tfloatValueAtTimestamptz.call(TFLOAT_SEQ, null)); + } + + // ------------------------------------------------------------------ + // ttextValueAtTimestamptz + // ------------------------------------------------------------------ + + @Test @Order(10) + void ttextValueAtTimestamptz_at_start_returns_correct_value() throws Exception { + String r = MoreAccessorUDFs.ttextValueAtTimestamptz.call(TTEXT_SEQ, TS_START); + assertNotNull(r, "Value at start timestamp must be non-null"); + // text_out() is PostgreSQL textout(): the raw, unquoted text value. + assertEquals("hello", r, "ttext value at t0 must be 'hello'"); + } + + @Test @Order(11) + void ttextValueAtTimestamptz_outside_range_returns_null() throws Exception { + String r = MoreAccessorUDFs.ttextValueAtTimestamptz.call(TTEXT_SEQ, TS_OUTSIDE); + assertNull(r, "Value outside sequence range must be null"); + } + + @Test @Order(12) + void ttextValueAtTimestamptz_null_returns_null() throws Exception { + assertNull(MoreAccessorUDFs.ttextValueAtTimestamptz.call(null, TS_START)); + assertNull(MoreAccessorUDFs.ttextValueAtTimestamptz.call(TTEXT_SEQ, null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/MoreAccessorUDFsTest.java b/src/test/java/org/mobilitydb/spark/temporal/MoreAccessorUDFsTest.java new file mode 100644 index 00000000..1e48293c --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/MoreAccessorUDFsTest.java @@ -0,0 +1,268 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for MoreAccessorUDFs β€” subtype, instant/sequence navigation, + * timestampN, inclusivity flags, duration, type-specific valueN accessors, + * and tpoint geometry accessors. + * + * MEOS function authority: meos/include/meos.h, meos/include/meos_geo.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class MoreAccessorUDFsTest { + + /** TSequence with 3 instants β€” covers instant/sequence navigation. */ + private static String TRIP; + private static String TINT_SEQ; + private static String TBOOL_SEQ; + private static String TFLOAT_SEQ; + private static String TTEXT_SEQ; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TRIP = temporal_as_hexwkb( + tgeompoint_in("[POINT(0 0)@2020-01-01 00:00:00+00, POINT(1 0)@2020-01-01 01:00:00+00," + + " POINT(2 0)@2020-01-02 00:00:00+00]"), + (byte) 0); + TINT_SEQ = temporal_as_hexwkb(tint_in("[1@2020-01-01, 3@2020-01-03]"), (byte) 0); + TBOOL_SEQ = temporal_as_hexwkb(tbool_in("[t@2020-01-01, t@2020-01-02, f@2020-01-03]"), (byte) 0); + TFLOAT_SEQ = temporal_as_hexwkb(tfloat_in("[1.5@2020-01-01, 3.5@2020-01-02]"), (byte) 0); + TTEXT_SEQ = temporal_as_hexwkb(ttext_in("[AAA@2020-01-01, ZZZ@2020-01-03]"), (byte) 0); + } + + // ------------------------------------------------------------------ + // Subtype + // ------------------------------------------------------------------ + + @Test @Order(1) + void temporalSubtype_tsequence_returns_Sequence() throws Exception { + String r = MoreAccessorUDFs.temporalSubtype.call(TRIP); + assertNotNull(r); + assertTrue(r.contains("Sequence")); + } + + // ------------------------------------------------------------------ + // Instant navigation + // ------------------------------------------------------------------ + + @Test @Order(2) + void startInstant_returns_nonnull_hexwkb() throws Exception { + String r = MoreAccessorUDFs.startInstant.call(TRIP); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(3) + void endInstant_returns_nonnull_hexwkb() throws Exception { + String r = MoreAccessorUDFs.endInstant.call(TRIP); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(4) + void instantN_returns_nonnull_hexwkb() throws Exception { + String r = MoreAccessorUDFs.instantN.call(TRIP, 1); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + // ------------------------------------------------------------------ + // Sequence navigation + // ------------------------------------------------------------------ + + @Test @Order(5) + void startSequence_returns_nonnull_hexwkb() throws Exception { + String r = MoreAccessorUDFs.startSequence.call(TRIP); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(6) + void endSequence_returns_nonnull_hexwkb() throws Exception { + String r = MoreAccessorUDFs.endSequence.call(TRIP); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(7) + void sequenceN_returns_nonnull_hexwkb() throws Exception { + String r = MoreAccessorUDFs.sequenceN.call(TRIP, 1); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + // ------------------------------------------------------------------ + // Min/max instant + // ------------------------------------------------------------------ + + @Test @Order(8) + void minInstant_returns_nonnull_hexwkb() throws Exception { + String r = MoreAccessorUDFs.minInstant.call(TINT_SEQ); + assertNotNull(r); + } + + @Test @Order(9) + void maxInstant_returns_nonnull_hexwkb() throws Exception { + String r = MoreAccessorUDFs.maxInstant.call(TINT_SEQ); + assertNotNull(r); + } + + // ------------------------------------------------------------------ + // Timestamp accessors + // ------------------------------------------------------------------ + + @Test @Order(10) + void numTimestamps_returns_positive_int() throws Exception { + Integer r = MoreAccessorUDFs.numTimestamps.call(TRIP); + assertNotNull(r); + assertTrue(r >= 1); + } + + @Test @Order(11) + void timestampN_returns_nonnull_timestamp() throws Exception { + java.sql.Timestamp r = MoreAccessorUDFs.timestampN.call(TRIP, 1); + assertNotNull(r); + } + + // ------------------------------------------------------------------ + // Inclusivity flags + // ------------------------------------------------------------------ + + @Test @Order(12) + void lowerInc_returns_boolean() throws Exception { + Boolean r = MoreAccessorUDFs.lowerInc.call(TRIP); + assertNotNull(r); + } + + @Test @Order(13) + void upperInc_returns_boolean() throws Exception { + Boolean r = MoreAccessorUDFs.upperInc.call(TRIP); + assertNotNull(r); + } + + // ------------------------------------------------------------------ + // Duration + // ------------------------------------------------------------------ + + @Test @Order(14) + void duration_returns_nonnull_string() throws Exception { + String r = MoreAccessorUDFs.duration.call(TRIP); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + // ------------------------------------------------------------------ + // tbool value accessor + // ------------------------------------------------------------------ + + @Test @Order(15) + void tboolValueN_returns_boolean() throws Exception { + Boolean r = MoreAccessorUDFs.tboolValueN.call(TBOOL_SEQ, 1); + assertNotNull(r); + } + + // ------------------------------------------------------------------ + // tfloat value accessor + // ------------------------------------------------------------------ + + @Test @Order(16) + void tfloatValueN_returns_double() throws Exception { + Double r = MoreAccessorUDFs.tfloatValueN.call(TFLOAT_SEQ, 1); + assertNotNull(r); + assertTrue(r >= 0.0); + } + + // ------------------------------------------------------------------ + // ttext value accessors + // ------------------------------------------------------------------ + + @Test @Order(17) + void ttextMinValue_returns_string() throws Exception { + String r = MoreAccessorUDFs.ttextMinValue.call(TTEXT_SEQ); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(18) + void ttextMaxValue_returns_string() throws Exception { + String r = MoreAccessorUDFs.ttextMaxValue.call(TTEXT_SEQ); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(19) + void ttextValueN_returns_string() throws Exception { + String r = MoreAccessorUDFs.ttextValueN.call(TTEXT_SEQ, 1); + assertNotNull(r); + } + + // ------------------------------------------------------------------ + // tpoint accessors + // ------------------------------------------------------------------ + + @Test @Order(20) + void tpointSrid_returns_int() throws Exception { + Integer r = MoreAccessorUDFs.tpointSrid.call(TRIP); + assertNotNull(r); + } + + @Test @Order(21) + void tpointValueN_returns_wkt() throws Exception { + String r = MoreAccessorUDFs.tpointValueN.call(TRIP, 1); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(22) + void tpointConvexHull_returns_wkt() throws Exception { + String r = MoreAccessorUDFs.tpointConvexHull.call(TRIP); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + // ------------------------------------------------------------------ + // Null-input guards + // ------------------------------------------------------------------ + + @Test @Order(23) + void startInstant_null_returns_null() throws Exception { + assertNull(MoreAccessorUDFs.startInstant.call(null)); + } + + @Test @Order(24) + void timestampN_null_returns_null() throws Exception { + assertNull(MoreAccessorUDFs.timestampN.call(null, 1)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/PosOpsUDFsTest.java b/src/test/java/org/mobilitydb/spark/temporal/PosOpsUDFsTest.java new file mode 100644 index 00000000..4c4dff2e --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/PosOpsUDFsTest.java @@ -0,0 +1,224 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for PosOpsUDFs β€” temporal and spatial positional operators. + * + * Fixtures: + * TRIP_EARLY β€” tgeompoint January 2020 + * TRIP_LATE β€” tgeompoint July 2020 + * TINT_LOW β€” tint with values [1,3] + * TINT_HIGH β€” tint with values [10,20] + * TRIP_LEFT β€” tgeompoint with X in [0,1] + * TRIP_RIGHT β€” tgeompoint with X in [5,6] + * + * MEOS function authority: meos/include/meos.h, meos/include/meos_geo.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class PosOpsUDFsTest { + + private static String TRIP_EARLY; + private static String TRIP_LATE; + private static String TINT_LOW; + private static String TINT_HIGH; + private static String TRIP_LEFT; + private static String TRIP_RIGHT; + private static String SPAN_EARLY; + private static String SPAN_LATE; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TRIP_EARLY = temporal_as_hexwkb( + tgeompoint_in("[POINT(0 0)@2020-01-01, POINT(1 0)@2020-01-02]"), + (byte) 0); + TRIP_LATE = temporal_as_hexwkb( + tgeompoint_in("[POINT(0 0)@2020-07-01, POINT(1 0)@2020-07-02]"), + (byte) 0); + TINT_LOW = temporal_as_hexwkb( + tint_in("[1@2020-01-01, 3@2020-01-02]"), + (byte) 0); + TINT_HIGH = temporal_as_hexwkb( + tint_in("[10@2020-01-01, 20@2020-01-02]"), + (byte) 0); + TRIP_LEFT = temporal_as_hexwkb( + tgeompoint_in("[POINT(0 0)@2020-01-01, POINT(1 0)@2020-01-02]"), + (byte) 0); + TRIP_RIGHT = temporal_as_hexwkb( + tgeompoint_in("[POINT(5 0)@2020-01-01, POINT(6 0)@2020-01-02]"), + (byte) 0); + SPAN_EARLY = span_as_hexwkb(tstzspan_in("[2020-01-01, 2020-02-01]"), (byte) 0); + SPAN_LATE = span_as_hexwkb(tstzspan_in("[2020-07-01, 2020-08-01]"), (byte) 0); + } + + // ------------------------------------------------------------------ + // Time-direction: temporal ↔ temporal + // ------------------------------------------------------------------ + + @Test @Order(1) + void temporalBefore_early_before_late_is_true() throws Exception { + assertTrue(PosOpsUDFs.temporalBefore.call(TRIP_EARLY, TRIP_LATE)); + } + + @Test @Order(2) + void temporalBefore_late_before_early_is_false() throws Exception { + assertFalse(PosOpsUDFs.temporalBefore.call(TRIP_LATE, TRIP_EARLY)); + } + + @Test @Order(3) + void temporalAfter_late_after_early_is_true() throws Exception { + assertTrue(PosOpsUDFs.temporalAfter.call(TRIP_LATE, TRIP_EARLY)); + } + + @Test @Order(4) + void temporalOverbefore_overlapping_start_is_true() throws Exception { + // TRIP_EARLY ends before TRIP_LATE starts β†’ overbefore is true + assertTrue(PosOpsUDFs.temporalOverbefore.call(TRIP_EARLY, TRIP_LATE)); + } + + @Test @Order(5) + void temporalOverafter_late_overafter_early_is_true() throws Exception { + assertTrue(PosOpsUDFs.temporalOverafter.call(TRIP_LATE, TRIP_EARLY)); + } + + @Test @Order(6) + void temporalBefore_null_input_returns_null() throws Exception { + assertNull(PosOpsUDFs.temporalBefore.call(null, TRIP_LATE)); + assertNull(PosOpsUDFs.temporalBefore.call(TRIP_EARLY, null)); + } + + // ------------------------------------------------------------------ + // Time-direction: temporal ↔ tstzspan + // ------------------------------------------------------------------ + + @Test @Order(7) + void temporalBeforeSpan_early_before_late_span_is_true() throws Exception { + assertTrue(PosOpsUDFs.temporalBeforeSpan.call(TRIP_EARLY, SPAN_LATE)); + } + + @Test @Order(8) + void temporalAfterSpan_late_after_early_span_is_true() throws Exception { + assertTrue(PosOpsUDFs.temporalAfterSpan.call(TRIP_LATE, SPAN_EARLY)); + } + + @Test @Order(9) + void temporalOverbeforeSpan_null_returns_null() throws Exception { + assertNull(PosOpsUDFs.temporalOverbeforeSpan.call(null, SPAN_LATE)); + } + + @Test @Order(10) + void temporalOverafterSpan_null_returns_null() throws Exception { + assertNull(PosOpsUDFs.temporalOverafterSpan.call(TRIP_LATE, null)); + } + + // ------------------------------------------------------------------ + // Value-direction: tnumber ↔ tnumber + // ------------------------------------------------------------------ + + @Test @Order(11) + void tnumberLeft_low_left_of_high_is_true() throws Exception { + assertTrue(PosOpsUDFs.tnumberLeft.call(TINT_LOW, TINT_HIGH)); + } + + @Test @Order(12) + void tnumberRight_high_right_of_low_is_true() throws Exception { + assertTrue(PosOpsUDFs.tnumberRight.call(TINT_HIGH, TINT_LOW)); + } + + @Test @Order(13) + void tnumberOverleft_low_overleft_of_high_is_true() throws Exception { + assertTrue(PosOpsUDFs.tnumberOverleft.call(TINT_LOW, TINT_HIGH)); + } + + @Test @Order(14) + void tnumberOverright_high_overright_of_low_is_true() throws Exception { + assertTrue(PosOpsUDFs.tnumberOverright.call(TINT_HIGH, TINT_LOW)); + } + + @Test @Order(15) + void tnumberLeft_null_returns_null() throws Exception { + assertNull(PosOpsUDFs.tnumberLeft.call(null, TINT_HIGH)); + } + + // ------------------------------------------------------------------ + // Spatial x-axis: tpoint ↔ tpoint + // ------------------------------------------------------------------ + + @Test @Order(16) + void tpointLeft_left_trip_is_left_of_right_trip() throws Exception { + assertTrue(PosOpsUDFs.tpointLeft.call(TRIP_LEFT, TRIP_RIGHT)); + } + + @Test @Order(17) + void tpointRight_right_trip_is_right_of_left_trip() throws Exception { + assertTrue(PosOpsUDFs.tpointRight.call(TRIP_RIGHT, TRIP_LEFT)); + } + + @Test @Order(18) + void tpointOverleft_left_overleft_of_right() throws Exception { + assertTrue(PosOpsUDFs.tpointOverleft.call(TRIP_LEFT, TRIP_RIGHT)); + } + + @Test @Order(19) + void tpointOverright_right_overright_of_left() throws Exception { + assertTrue(PosOpsUDFs.tpointOverright.call(TRIP_RIGHT, TRIP_LEFT)); + } + + @Test @Order(20) + void tpointLeft_null_returns_null() throws Exception { + assertNull(PosOpsUDFs.tpointLeft.call(null, TRIP_RIGHT)); + } + + // ------------------------------------------------------------------ + // Spatial y-axis: tpoint ↔ tpoint + // ------------------------------------------------------------------ + + @Test @Order(21) + void tpointBelow_and_above_y_axis() throws Exception { + // TRIP_LEFT has Y=0; TRIP_RIGHT has Y=0 β€” same Y, so neither is strictly below + // Use a trip with Y=5 for the above direction + String tripHigh = temporal_as_hexwkb( + tgeompoint_in("[POINT(0 5)@2020-01-01, POINT(1 5)@2020-01-02]"), (byte) 0); + assertTrue(PosOpsUDFs.tpointBelow.call(TRIP_LEFT, tripHigh)); + assertTrue(PosOpsUDFs.tpointAbove.call(tripHigh, TRIP_LEFT)); + } + + @Test @Order(22) + void tpointOverbelow_and_overabove_y_axis() throws Exception { + String tripHigh = temporal_as_hexwkb( + tgeompoint_in("[POINT(0 5)@2020-01-01, POINT(1 5)@2020-01-02]"), (byte) 0); + assertTrue(PosOpsUDFs.tpointOverbelow.call(TRIP_LEFT, tripHigh)); + assertTrue(PosOpsUDFs.tpointOverabove.call(tripHigh, TRIP_LEFT)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/PredicateUDFsExt2Test.java b/src/test/java/org/mobilitydb/spark/temporal/PredicateUDFsExt2Test.java new file mode 100644 index 00000000..b130c858 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/PredicateUDFsExt2Test.java @@ -0,0 +1,233 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for the new PredicateUDFs: + * - scalar-first reversed forms (int/float OP tint/tfloat) + * - tbool Γ— bool predicates + * - ttext Γ— text predicates (both directions) + * + * Fixtures: + * TINT5_HEX β€” constant tint 5 at a single instant + * TFLOAT2_HEX β€” constant tfloat 2.0 at a single instant + * TBOOL_HEX β€” constant tbool true at a single instant + * TTEXT_HEX β€” constant ttext "hello" at a single instant + * + * MEOS function authority: meos/include/meos.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class PredicateUDFsExt2Test { + + private static String TINT5_HEX; + private static String TFLOAT2_HEX; + private static String TBOOL_HEX; + private static String TTEXT_HEX; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TINT5_HEX = temporal_as_hexwkb(tint_in("5@2020-01-01 00:00:00+00"), (byte) 0); + TFLOAT2_HEX = temporal_as_hexwkb(tfloat_in("2.0@2020-01-01 00:00:00+00"), (byte) 0); + TBOOL_HEX = temporal_as_hexwkb(tbool_in("true@2020-01-01 00:00:00+00"), (byte) 0); + TTEXT_HEX = temporal_as_hexwkb(ttext_in("hello@2020-01-01 00:00:00+00"),(byte) 0); + } + + // ------------------------------------------------------------------ + // scalar-first int OP tint + // ------------------------------------------------------------------ + + @Test @Order(1) + void alwaysEqIntTint_matching_value_returns_true() throws Exception { + assertTrue(PredicateUDFs.alwaysEqIntTint.call(5, TINT5_HEX)); + } + + @Test @Order(2) + void alwaysEqIntTint_non_matching_returns_false() throws Exception { + assertFalse(PredicateUDFs.alwaysEqIntTint.call(9, TINT5_HEX)); + } + + @Test @Order(3) + void everEqIntTint_matching_value_returns_true() throws Exception { + assertTrue(PredicateUDFs.everEqIntTint.call(5, TINT5_HEX)); + } + + @Test @Order(4) + void everLtIntTint_value_less_than_tint_returns_true() throws Exception { + // 3 < tint(5) β†’ true + assertTrue(PredicateUDFs.everLtIntTint.call(3, TINT5_HEX)); + } + + @Test @Order(5) + void alwaysGtIntTint_value_greater_than_tint_returns_true() throws Exception { + // 9 > tint(5) β†’ always true + assertTrue(PredicateUDFs.alwaysGtIntTint.call(9, TINT5_HEX)); + } + + @Test @Order(6) + void alwaysEqIntTint_null_returns_null() throws Exception { + assertNull(PredicateUDFs.alwaysEqIntTint.call(null, TINT5_HEX)); + assertNull(PredicateUDFs.alwaysEqIntTint.call(5, null)); + } + + // ------------------------------------------------------------------ + // scalar-first float OP tfloat + // ------------------------------------------------------------------ + + @Test @Order(7) + void alwaysEqFloatTfloat_matching_value_returns_true() throws Exception { + assertTrue(PredicateUDFs.alwaysEqFloatTfloat.call(2.0, TFLOAT2_HEX)); + } + + @Test @Order(8) + void everLtFloatTfloat_value_less_than_tfloat_returns_true() throws Exception { + // 1.0 < tfloat(2.0) β†’ ever true + assertTrue(PredicateUDFs.everLtFloatTfloat.call(1.0, TFLOAT2_HEX)); + } + + @Test @Order(9) + void alwaysGeFloatTfloat_value_ge_tfloat_returns_true() throws Exception { + // 2.0 >= tfloat(2.0) β†’ always true + assertTrue(PredicateUDFs.alwaysGeFloatTfloat.call(2.0, TFLOAT2_HEX)); + } + + @Test @Order(10) + void alwaysEqFloatTfloat_null_returns_null() throws Exception { + assertNull(PredicateUDFs.alwaysEqFloatTfloat.call(null, TFLOAT2_HEX)); + assertNull(PredicateUDFs.alwaysEqFloatTfloat.call(2.0, null)); + } + + // ------------------------------------------------------------------ + // tbool Γ— bool predicates + // ------------------------------------------------------------------ + + @Test @Order(11) + void alwaysEqTboolBool_true_value_returns_true() throws Exception { + assertTrue(PredicateUDFs.alwaysEqTboolBool.call(TBOOL_HEX, true)); + } + + @Test @Order(12) + void alwaysEqTboolBool_false_value_returns_false() throws Exception { + assertFalse(PredicateUDFs.alwaysEqTboolBool.call(TBOOL_HEX, false)); + } + + @Test @Order(13) + void everEqBoolTbool_true_value_returns_true() throws Exception { + assertTrue(PredicateUDFs.everEqBoolTbool.call(true, TBOOL_HEX)); + } + + @Test @Order(14) + void alwaysNeTboolBool_different_value_returns_true() throws Exception { + assertTrue(PredicateUDFs.alwaysNeTboolBool.call(TBOOL_HEX, false)); + } + + @Test @Order(15) + void everNeBoolTbool_different_value_returns_true() throws Exception { + assertTrue(PredicateUDFs.everNeBoolTbool.call(false, TBOOL_HEX)); + } + + @Test @Order(16) + void alwaysEqTboolBool_null_returns_null() throws Exception { + assertNull(PredicateUDFs.alwaysEqTboolBool.call(null, true)); + assertNull(PredicateUDFs.alwaysEqTboolBool.call(TBOOL_HEX, null)); + } + + // ------------------------------------------------------------------ + // ttext Γ— text predicates (ttext OP text) + // ------------------------------------------------------------------ + + @Test @Order(17) + void alwaysEqTtextText_matching_text_returns_true() throws Exception { + assertTrue(PredicateUDFs.alwaysEqTtextText.call(TTEXT_HEX, "hello")); + } + + @Test @Order(18) + void alwaysEqTtextText_non_matching_returns_false() throws Exception { + assertFalse(PredicateUDFs.alwaysEqTtextText.call(TTEXT_HEX, "world")); + } + + @Test @Order(19) + void everEqTtextText_matching_returns_true() throws Exception { + assertTrue(PredicateUDFs.everEqTtextText.call(TTEXT_HEX, "hello")); + } + + @Test @Order(20) + void alwaysLtTtextText_hello_lt_xyz_returns_true() throws Exception { + // ttext(hello) < "xyz" β†’ always true + assertTrue(PredicateUDFs.alwaysLtTtextText.call(TTEXT_HEX, "xyz")); + } + + @Test @Order(21) + void alwaysGtTtextText_hello_gt_abc_returns_true() throws Exception { + // ttext(hello) > "abc" β†’ always true + assertTrue(PredicateUDFs.alwaysGtTtextText.call(TTEXT_HEX, "abc")); + } + + @Test @Order(22) + void alwaysEqTtextText_null_returns_null() throws Exception { + assertNull(PredicateUDFs.alwaysEqTtextText.call(null, "hello")); + assertNull(PredicateUDFs.alwaysEqTtextText.call(TTEXT_HEX, null)); + } + + // ------------------------------------------------------------------ + // text Γ— ttext predicates (text OP ttext) + // ------------------------------------------------------------------ + + @Test @Order(23) + void alwaysEqTextTtext_matching_returns_true() throws Exception { + assertTrue(PredicateUDFs.alwaysEqTextTtext.call("hello", TTEXT_HEX)); + } + + @Test @Order(24) + void everEqTextTtext_matching_returns_true() throws Exception { + assertTrue(PredicateUDFs.everEqTextTtext.call("hello", TTEXT_HEX)); + } + + @Test @Order(25) + void alwaysLtTextTtext_abc_lt_hello_returns_true() throws Exception { + // "abc" < ttext(hello) β†’ always true + assertTrue(PredicateUDFs.alwaysLtTextTtext.call("abc", TTEXT_HEX)); + } + + @Test @Order(26) + void alwaysGtTextTtext_xyz_gt_hello_returns_true() throws Exception { + // "xyz" > ttext(hello) β†’ always true + assertTrue(PredicateUDFs.alwaysGtTextTtext.call("xyz", TTEXT_HEX)); + } + + @Test @Order(27) + void alwaysEqTextTtext_null_returns_null() throws Exception { + assertNull(PredicateUDFs.alwaysEqTextTtext.call(null, TTEXT_HEX)); + assertNull(PredicateUDFs.alwaysEqTextTtext.call("hello", null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/PredicateUDFsExtTest.java b/src/test/java/org/mobilitydb/spark/temporal/PredicateUDFsExtTest.java new file mode 100644 index 00000000..08b9564b --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/PredicateUDFsExtTest.java @@ -0,0 +1,194 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for PredicateUDFs ever_ne / always_ne predicates: + * everNeTintInt, everNeTfloatFloat, everNeTemporal, + * alwaysNeTintInt, alwaysNeTfloatFloat, alwaysNeTemporal. + * + * Fixture: + * TINT_CONST β€” constant tint [5@t1, 5@t2] + * TINT_VARYING β€” varying tint [1@t1, 3@t3] + * TFLOAT_CONST β€” constant tfloat [2.0@t1, 2.0@t3] + * TFLOAT_VARY β€” varying tfloat [1.0@t1, 3.0@t3] + * + * MEOS function authority: meos/include/meos.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class PredicateUDFsExtTest { + + private static String TINT_CONST; + private static String TINT_VARYING; + private static String TFLOAT_CONST; + private static String TFLOAT_VARY; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TINT_CONST = temporal_as_hexwkb(tint_in("Interp=Step;[5@2020-01-01, 5@2020-01-03]"), (byte) 0); + TINT_VARYING = temporal_as_hexwkb(tint_in("[1@2020-01-01, 3@2020-01-03]"), (byte) 0); + TFLOAT_CONST = temporal_as_hexwkb(tfloat_in("Interp=Step;[2.0@2020-01-01, 2.0@2020-01-03]"), (byte) 0); + TFLOAT_VARY = temporal_as_hexwkb(tfloat_in("[1.0@2020-01-01, 3.0@2020-01-03]"), (byte) 0); + } + + // ------------------------------------------------------------------ + // everNeTintInt + // ------------------------------------------------------------------ + + @Test @Order(1) + void everNeTintInt_const_value_eq_never_ne_returns_false() throws Exception { + assertFalse(PredicateUDFs.everNeTintInt.call(TINT_CONST, 5), + "A constant tint [5,5] is never != 5 β€” ever_ne must be false"); + } + + @Test @Order(2) + void everNeTintInt_varying_always_has_ne_instants_returns_true() throws Exception { + assertTrue(PredicateUDFs.everNeTintInt.call(TINT_VARYING, 5), + "A varying tint [1,3] is always != 5 β€” ever_ne must be true"); + } + + @Test @Order(3) + void everNeTintInt_null_returns_null() throws Exception { + assertNull(PredicateUDFs.everNeTintInt.call(null, 5)); + assertNull(PredicateUDFs.everNeTintInt.call(TINT_VARYING, null)); + } + + // ------------------------------------------------------------------ + // everNeTfloatFloat + // ------------------------------------------------------------------ + + @Test @Order(4) + void everNeTfloatFloat_const_never_ne_returns_false() throws Exception { + assertFalse(PredicateUDFs.everNeTfloatFloat.call(TFLOAT_CONST, 2.0), + "Constant tfloat [2.0,2.0] is never != 2.0 β€” ever_ne must be false"); + } + + @Test @Order(5) + void everNeTfloatFloat_varying_has_ne_returns_true() throws Exception { + assertTrue(PredicateUDFs.everNeTfloatFloat.call(TFLOAT_VARY, 2.0), + "Varying tfloat [1.0,3.0] passes through != 2.0 β€” ever_ne must be true"); + } + + @Test @Order(6) + void everNeTfloatFloat_null_returns_null() throws Exception { + assertNull(PredicateUDFs.everNeTfloatFloat.call(null, 1.0)); + assertNull(PredicateUDFs.everNeTfloatFloat.call(TFLOAT_VARY, null)); + } + + // ------------------------------------------------------------------ + // everNeTemporal + // ------------------------------------------------------------------ + + @Test @Order(7) + void everNeTemporal_same_value_returns_false() throws Exception { + assertFalse(PredicateUDFs.everNeTemporal.call(TINT_CONST, TINT_CONST), + "A sequence compared with itself is never != β€” ever_ne must be false"); + } + + @Test @Order(8) + void everNeTemporal_different_values_returns_true() throws Exception { + assertTrue(PredicateUDFs.everNeTemporal.call(TINT_VARYING, TINT_CONST), + "Varying vs constant with different values β€” ever_ne must be true"); + } + + @Test @Order(9) + void everNeTemporal_null_returns_null() throws Exception { + assertNull(PredicateUDFs.everNeTemporal.call(null, TINT_CONST)); + assertNull(PredicateUDFs.everNeTemporal.call(TINT_CONST, null)); + } + + // ------------------------------------------------------------------ + // alwaysNeTintInt + // ------------------------------------------------------------------ + + @Test @Order(10) + void alwaysNeTintInt_varying_never_equals_target_returns_true() throws Exception { + assertTrue(PredicateUDFs.alwaysNeTintInt.call(TINT_VARYING, 5), + "Varying tint [1,3] is always != 5 β€” always_ne must be true"); + } + + @Test @Order(11) + void alwaysNeTintInt_const_equals_target_returns_false() throws Exception { + assertFalse(PredicateUDFs.alwaysNeTintInt.call(TINT_CONST, 5), + "Constant tint [5,5] is always 5 β€” always_ne(5) must be false"); + } + + @Test @Order(12) + void alwaysNeTintInt_null_returns_null() throws Exception { + assertNull(PredicateUDFs.alwaysNeTintInt.call(null, 5)); + assertNull(PredicateUDFs.alwaysNeTintInt.call(TINT_VARYING, null)); + } + + // ------------------------------------------------------------------ + // alwaysNeTfloatFloat + // ------------------------------------------------------------------ + + @Test @Order(13) + void alwaysNeTfloatFloat_no_instance_returns_true() throws Exception { + assertTrue(PredicateUDFs.alwaysNeTfloatFloat.call(TFLOAT_VARY, 5.0), + "Varying tfloat [1.0,3.0] never equals 5.0 β€” always_ne must be true"); + } + + @Test @Order(14) + void alwaysNeTfloatFloat_equals_target_returns_false() throws Exception { + assertFalse(PredicateUDFs.alwaysNeTfloatFloat.call(TFLOAT_CONST, 2.0), + "Constant tfloat [2.0,2.0] equals 2.0 β€” always_ne(2.0) must be false"); + } + + @Test @Order(15) + void alwaysNeTfloatFloat_null_returns_null() throws Exception { + assertNull(PredicateUDFs.alwaysNeTfloatFloat.call(null, 1.0)); + } + + // ------------------------------------------------------------------ + // alwaysNeTemporal + // ------------------------------------------------------------------ + + @Test @Order(16) + void alwaysNeTemporal_same_value_returns_false() throws Exception { + assertFalse(PredicateUDFs.alwaysNeTemporal.call(TINT_CONST, TINT_CONST), + "Constant compared with itself β€” always_ne must be false"); + } + + @Test @Order(17) + void alwaysNeTemporal_disjoint_values_returns_true() throws Exception { + assertTrue(PredicateUDFs.alwaysNeTemporal.call(TINT_VARYING, TINT_CONST), + "TINT_VARYING [1,3] is always != TINT_CONST [5,5] β€” always_ne must be true"); + } + + @Test @Order(18) + void alwaysNeTemporal_null_returns_null() throws Exception { + assertNull(PredicateUDFs.alwaysNeTemporal.call(null, TINT_CONST)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/RestrictionUDFsExt2Test.java b/src/test/java/org/mobilitydb/spark/temporal/RestrictionUDFsExt2Test.java new file mode 100644 index 00000000..f88cf032 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/RestrictionUDFsExt2Test.java @@ -0,0 +1,139 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import org.mobilitydb.spark.geo.STBoxUDFs; +import org.mobilitydb.spark.temporal.ConstructorUDFs; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for additional RestrictionUDFs batch 2 β€” temporal span/spanset + * restriction (atTstzspan/atTstzspanset) and spatial/elevation restriction + * (tgeoAtStbox, tpointAtElevation, tpointMinusElevation). + * + * MEOS function authority: meos/include/meos.h, meos/include/meos_geo.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class RestrictionUDFsExt2Test { + + private static String TFLOAT_SEQ; + private static String TRIP_3D; + private static String SPAN_HEX; + private static String SPANSET_HEX; + private static String STBOX_HEX; + private static String FLOATSPAN_HEX; + + @BeforeAll + static void initMeos() throws Exception { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TFLOAT_SEQ = temporal_as_hexwkb( + tfloat_in("[1.0@2020-01-01, 5.0@2020-01-05]"), (byte) 0); + + TRIP_3D = temporal_as_hexwkb( + tgeompoint_in("[POINT Z(0 0 1)@2020-01-01 00:00:00+00, " + + "POINT Z(3 4 5)@2020-01-01 01:00:00+00]"), + (byte) 0); + + // tstzspan "[2020-01-02, 2020-01-04]" + SPAN_HEX = span_as_hexwkb( + tstzspan_in("[2020-01-02, 2020-01-04]"), (byte) 0); + + // tstzspanset "{[2020-01-01, 2020-01-02], [2020-01-04, 2020-01-05]}" + SPANSET_HEX = spanset_as_hexwkb( + tstzspanset_in("{[2020-01-01, 2020-01-02],[2020-01-04, 2020-01-05]}"), (byte) 0); + + // STBox enclosing the trip β€” use ConstructorUDFs.stbox to get hex-WKB + STBOX_HEX = ConstructorUDFs.stbox.call( + "STBOX XT(((0,0),(4,5)),[2020-01-01 00:00:00+00,2020-01-01 01:00:00+00])"); + + // floatspan [1.0, 3.0] for elevation restriction + FLOATSPAN_HEX = span_as_hexwkb( + floatspan_make(1.0, 3.0, true, true), (byte) 0); + } + + // ------------------------------------------------------------------ + // Timestamp-span restriction + // ------------------------------------------------------------------ + + @Test @Order(1) + void temporalAtTstzspan_returns_nonnull() throws Exception { + String r = RestrictionUDFs.temporalAtTstzspan.call(TFLOAT_SEQ, SPAN_HEX); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(2) + void temporalAtTstzspanset_returns_nonnull() throws Exception { + String r = RestrictionUDFs.temporalAtTstzspanset.call(TFLOAT_SEQ, SPANSET_HEX); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + // ------------------------------------------------------------------ + // STBox restriction + // ------------------------------------------------------------------ + + @Test @Order(3) + void tgeoAtStbox_returns_nonnull_or_null() throws Exception { + String r = RestrictionUDFs.tgeoAtStbox.call(TRIP_3D, STBOX_HEX); + assertTrue(r == null || !r.isBlank()); + } + + // ------------------------------------------------------------------ + // Elevation restriction (3D tpoint) + // ------------------------------------------------------------------ + + @Test @Order(4) + void tpointAtElevation_returns_nonnull_or_null() throws Exception { + String r = RestrictionUDFs.tpointAtElevation.call(TRIP_3D, FLOATSPAN_HEX); + assertTrue(r == null || !r.isBlank()); + } + + @Test @Order(5) + void tpointMinusElevation_returns_nonnull_or_null() throws Exception { + String r = RestrictionUDFs.tpointMinusElevation.call(TRIP_3D, FLOATSPAN_HEX); + assertTrue(r == null || !r.isBlank()); + } + + // ------------------------------------------------------------------ + // Null-input guards + // ------------------------------------------------------------------ + + @Test @Order(6) + void null_input_returns_null() throws Exception { + assertNull(RestrictionUDFs.temporalAtTstzspan.call(null, SPAN_HEX)); + assertNull(RestrictionUDFs.temporalAtTstzspanset.call(null, SPANSET_HEX)); + assertNull(RestrictionUDFs.tgeoAtStbox.call(null, STBOX_HEX)); + assertNull(RestrictionUDFs.tpointAtElevation.call(null, FLOATSPAN_HEX)); + assertNull(RestrictionUDFs.tpointMinusElevation.call(null, FLOATSPAN_HEX)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/RestrictionUDFsExt3Test.java b/src/test/java/org/mobilitydb/spark/temporal/RestrictionUDFsExt3Test.java new file mode 100644 index 00000000..59d8dd4a --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/RestrictionUDFsExt3Test.java @@ -0,0 +1,135 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for RestrictionUDFs batch 3 β€” tintAtValue, tnumber value-range + * restriction (at/minus span/spanset), and tgeoMinusStbox. + * + * MEOS function authority: meos/include/meos.h, meos/include/meos_geo.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class RestrictionUDFsExt3Test { + + private static String TINT_SEQ; + private static String TFLOAT_SEQ; + private static String TRIP; + private static String INTSPAN_HEX; + private static String FLOATSPAN_HEX; + private static String STBOX_HEX; + + @BeforeAll + static void initMeos() throws Exception { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TINT_SEQ = temporal_as_hexwkb( + tint_in("[1@2020-01-01, 5@2020-01-05]"), (byte) 0); + TFLOAT_SEQ = temporal_as_hexwkb( + tfloat_in("[1.0@2020-01-01, 5.0@2020-01-05]"), (byte) 0); + TRIP = temporal_as_hexwkb( + tgeompoint_in("[POINT(0 0)@2020-01-01 00:00:00+00, POINT(3 4)@2020-01-01 01:00:00+00]"), + (byte) 0); + + // intspan [2, 4] + INTSPAN_HEX = span_as_hexwkb(intspan_make(2, 4, true, true), (byte) 0); + // floatspan [1.0, 3.0] + FLOATSPAN_HEX = span_as_hexwkb(floatspan_make(1.0, 3.0, true, true), (byte) 0); + // STBox outside the trip + STBOX_HEX = ConstructorUDFs.stbox.call( + "STBOX XT(((10,10),(20,20)),[2020-01-01 00:00:00+00,2020-01-01 01:00:00+00])"); + } + + // ------------------------------------------------------------------ + // tintAtValue + // ------------------------------------------------------------------ + + @Test @Order(1) + void tintAtValue_returns_nonnull_or_null() throws Exception { + String r = RestrictionUDFs.tintAtValue.call(TINT_SEQ, 3); + assertTrue(r == null || !r.isBlank()); + } + + // ------------------------------------------------------------------ + // tnumber value-range restriction + // ------------------------------------------------------------------ + + @Test @Order(2) + void tnumberAtSpan_tfloat_returns_nonnull() throws Exception { + String r = RestrictionUDFs.tnumberAtSpan.call(TFLOAT_SEQ, FLOATSPAN_HEX); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(3) + void tnumberMinusSpan_tfloat_returns_nonnull_or_null() throws Exception { + String r = RestrictionUDFs.tnumberMinusSpan.call(TFLOAT_SEQ, FLOATSPAN_HEX); + assertTrue(r == null || !r.isBlank()); + } + + @Test @Order(4) + void tnumberAtSpanset_tint_returns_nonnull_or_null() throws Exception { + String spansetHex = spanset_as_hexwkb(intspanset_in("{[2,4]}"), (byte) 0); + // tint [1,5] restricted to span {[2,4]} may yield null (neither 1 nor 5 is in 2-4) + String r = RestrictionUDFs.tnumberAtSpanset.call(TINT_SEQ, spansetHex); + assertTrue(r == null || !r.isBlank()); + } + + @Test @Order(5) + void tnumberMinusSpanset_returns_nonnull_or_null() throws Exception { + String spansetHex = spanset_as_hexwkb(intspanset_in("{[2,4]}"), (byte) 0); + String r = RestrictionUDFs.tnumberMinusSpanset.call(TINT_SEQ, spansetHex); + assertTrue(r == null || !r.isBlank()); + } + + // ------------------------------------------------------------------ + // tgeoMinusStbox + // ------------------------------------------------------------------ + + @Test @Order(6) + void tgeoMinusStbox_outside_box_returns_original() throws Exception { + // Subtracting a box that doesn't overlap β†’ result is the full trip + String r = RestrictionUDFs.tgeoMinusStbox.call(TRIP, STBOX_HEX); + assertTrue(r == null || !r.isBlank()); + } + + // ------------------------------------------------------------------ + // Null-input guards + // ------------------------------------------------------------------ + + @Test @Order(7) + void null_input_returns_null() throws Exception { + assertNull(RestrictionUDFs.tintAtValue.call(null, 1)); + assertNull(RestrictionUDFs.tnumberAtSpan.call(null, FLOATSPAN_HEX)); + assertNull(RestrictionUDFs.tnumberMinusSpan.call(null, FLOATSPAN_HEX)); + assertNull(RestrictionUDFs.tgeoMinusStbox.call(null, STBOX_HEX)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/RestrictionUDFsExt4Test.java b/src/test/java/org/mobilitydb/spark/temporal/RestrictionUDFsExt4Test.java new file mode 100644 index 00000000..ced62a4f --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/RestrictionUDFsExt4Test.java @@ -0,0 +1,108 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import java.sql.Timestamp; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for new restriction UDFs: + * tintMinusValue, temporalDeleteTimestamptz. + * + * MEOS function authority: meos/include/meos.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class RestrictionUDFsExt4Test { + + private static String TINT_SEQ; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TINT_SEQ = temporal_as_hexwkb( + tint_in("Interp=Step;[1@2020-01-01 00:00:00+00, 1@2020-01-02 00:00:00+00, 2@2020-01-03 00:00:00+00, 2@2020-01-04 00:00:00+00]"), + (byte) 0); + } + + // ------------------------------------------------------------------ + // tintMinusValue + // ------------------------------------------------------------------ + + @Test @Order(1) + void tintMinusValue_removes_instants_with_value() throws Exception { + String r = RestrictionUDFs.tintMinusValue.call(TINT_SEQ, 1); + assertTrue(r == null || !r.isBlank(), + "Minus value 1 must return null (fully removed) or a valid temporal"); + } + + @Test @Order(2) + void tintMinusValue_nonexistent_value_returns_original_or_nonnull() throws Exception { + String r = RestrictionUDFs.tintMinusValue.call(TINT_SEQ, 99); + assertNotNull(r, "Minus value not in sequence must return original sequence"); + assertFalse(r.isBlank()); + } + + @Test @Order(3) + void tintMinusValue_null_returns_null() throws Exception { + assertNull(RestrictionUDFs.tintMinusValue.call(null, 1)); + assertNull(RestrictionUDFs.tintMinusValue.call(TINT_SEQ, null)); + } + + // ------------------------------------------------------------------ + // temporalDeleteTimestamptz + // ------------------------------------------------------------------ + + @Test @Order(4) + void temporalDeleteTimestamptz_at_known_instant_returns_shorter_seq() throws Exception { + // 2020-01-02 00:00:00 UTC = Unix 1577923200 + Timestamp ts = new Timestamp(1577923200L * 1000L); + String r = RestrictionUDFs.temporalDeleteTimestamptz.call(TINT_SEQ, ts); + assertTrue(r == null || !r.isBlank(), + "Delete at known instant must return null or a valid temporal"); + } + + @Test @Order(5) + void temporalDeleteTimestamptz_outside_range_returns_original_or_nonnull() throws Exception { + // 2020-06-01 00:00:00 UTC = Unix 1590969600 + Timestamp ts = new Timestamp(1590969600L * 1000L); + String r = RestrictionUDFs.temporalDeleteTimestamptz.call(TINT_SEQ, ts); + assertNotNull(r, "Delete outside range must return original sequence unchanged"); + assertFalse(r.isBlank()); + } + + @Test @Order(6) + void temporalDeleteTimestamptz_null_returns_null() throws Exception { + Timestamp ts = new Timestamp(1577836800L * 1000L); + assertNull(RestrictionUDFs.temporalDeleteTimestamptz.call(null, ts)); + assertNull(RestrictionUDFs.temporalDeleteTimestamptz.call(TINT_SEQ, null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/RestrictionUDFsExt5Test.java b/src/test/java/org/mobilitydb/spark/temporal/RestrictionUDFsExt5Test.java new file mode 100644 index 00000000..a51f5354 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/RestrictionUDFsExt5Test.java @@ -0,0 +1,110 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import java.sql.Timestamp; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for new restriction UDFs: + * temporalAtTimestamptz, temporalMinusTimestamptz. + * + * MEOS function authority: meos/include/meos.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class RestrictionUDFsExt5Test { + + private static String TINT_SEQ; + + // 2020-01-02 00:00:00 UTC = Unix 1577923200 s (inside sequence) + private static Timestamp TS_INSIDE; + // 2020-06-01 00:00:00 UTC = Unix 1590969600 s (outside sequence) + private static Timestamp TS_OUTSIDE; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TINT_SEQ = temporal_as_hexwkb( + tint_in("Interp=Step;[1@2020-01-01 00:00:00+00, 2@2020-01-03 00:00:00+00]"), + (byte) 0); + + TS_INSIDE = new Timestamp(1577923200L * 1000L); // 2020-01-02 00:00:00 UTC + TS_OUTSIDE = new Timestamp(1590969600L * 1000L); // 2020-06-01 00:00:00 UTC + } + + // ------------------------------------------------------------------ + // temporalAtTimestamptz + // ------------------------------------------------------------------ + + @Test @Order(1) + void temporalAtTimestamptz_inside_returns_instant() throws Exception { + String r = RestrictionUDFs.temporalAtTimestamptz.call(TINT_SEQ, TS_INSIDE); + assertNotNull(r, "AT inside-range timestamp must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(2) + void temporalAtTimestamptz_outside_returns_null() throws Exception { + String r = RestrictionUDFs.temporalAtTimestamptz.call(TINT_SEQ, TS_OUTSIDE); + assertNull(r, "AT outside-range timestamp must return null"); + } + + @Test @Order(3) + void temporalAtTimestamptz_null_returns_null() throws Exception { + assertNull(RestrictionUDFs.temporalAtTimestamptz.call(null, TS_INSIDE)); + assertNull(RestrictionUDFs.temporalAtTimestamptz.call(TINT_SEQ, null)); + } + + // ------------------------------------------------------------------ + // temporalMinusTimestamptz + // ------------------------------------------------------------------ + + @Test @Order(4) + void temporalMinusTimestamptz_inside_returns_shorter() throws Exception { + String r = RestrictionUDFs.temporalMinusTimestamptz.call(TINT_SEQ, TS_INSIDE); + assertTrue(r == null || !r.isBlank(), + "MINUS inside-range timestamp must return null or a valid shorter sequence"); + } + + @Test @Order(5) + void temporalMinusTimestamptz_outside_returns_original() throws Exception { + String r = RestrictionUDFs.temporalMinusTimestamptz.call(TINT_SEQ, TS_OUTSIDE); + assertNotNull(r, "MINUS outside-range timestamp must return original sequence"); + assertFalse(r.isBlank()); + } + + @Test @Order(6) + void temporalMinusTimestamptz_null_returns_null() throws Exception { + assertNull(RestrictionUDFs.temporalMinusTimestamptz.call(null, TS_INSIDE)); + assertNull(RestrictionUDFs.temporalMinusTimestamptz.call(TINT_SEQ, null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/RestrictionUDFsExt6Test.java b/src/test/java/org/mobilitydb/spark/temporal/RestrictionUDFsExt6Test.java new file mode 100644 index 00000000..bf3637d2 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/RestrictionUDFsExt6Test.java @@ -0,0 +1,110 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for RestrictionUDFs value-set operations: + * temporalAtValues, temporalMinusValues. + * + * MEOS function authority: meos/include/meos.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class RestrictionUDFsExt6Test { + + private static String TINT_SEQ; + private static String INTSET_HEX; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + // Sequence with values 1, 2, 3 + TINT_SEQ = temporal_as_hexwkb( + tint_in("Interp=Step;[1@2020-01-01 00:00:00+00," + + " 2@2020-01-02 00:00:00+00," + + " 3@2020-01-03 00:00:00+00, 3@2020-01-04 00:00:00+00]"), + (byte) 0); + + // intset {1, 3} + INTSET_HEX = set_as_hexwkb(intset_in("{1, 3}"), (byte) 0); + } + + // ------------------------------------------------------------------ + // temporalAtValues + // ------------------------------------------------------------------ + + @Test @Order(1) + void temporalAtValues_restricts_to_matching_instants() throws Exception { + String r = RestrictionUDFs.temporalAtValues.call(TINT_SEQ, INTSET_HEX); + assertNotNull(r, "AT values {1,3} must return non-null for sequence with those values"); + assertFalse(r.isBlank()); + } + + @Test @Order(2) + void temporalAtValues_empty_set_returns_null_or_empty() throws Exception { + String emptyHex = set_as_hexwkb(intset_in("{99}"), (byte) 0); + String r = RestrictionUDFs.temporalAtValues.call(TINT_SEQ, emptyHex); + assertTrue(r == null || !r.isBlank(), + "AT nonexistent value must return null or valid empty temporal"); + } + + @Test @Order(3) + void temporalAtValues_null_returns_null() throws Exception { + assertNull(RestrictionUDFs.temporalAtValues.call(null, INTSET_HEX)); + assertNull(RestrictionUDFs.temporalAtValues.call(TINT_SEQ, null)); + } + + // ------------------------------------------------------------------ + // temporalMinusValues + // ------------------------------------------------------------------ + + @Test @Order(4) + void temporalMinusValues_removes_matching_instants() throws Exception { + String r = RestrictionUDFs.temporalMinusValues.call(TINT_SEQ, INTSET_HEX); + assertTrue(r == null || !r.isBlank(), + "MINUS values {1,3} must return null or a shorter temporal"); + } + + @Test @Order(5) + void temporalMinusValues_nonexistent_value_returns_original() throws Exception { + String emptyHex = set_as_hexwkb(intset_in("{99}"), (byte) 0); + String r = RestrictionUDFs.temporalMinusValues.call(TINT_SEQ, emptyHex); + assertNotNull(r, "MINUS nonexistent value must return original sequence"); + assertFalse(r.isBlank()); + } + + @Test @Order(6) + void temporalMinusValues_null_returns_null() throws Exception { + assertNull(RestrictionUDFs.temporalMinusValues.call(null, INTSET_HEX)); + assertNull(RestrictionUDFs.temporalMinusValues.call(TINT_SEQ, null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/RestrictionUDFsExtTest.java b/src/test/java/org/mobilitydb/spark/temporal/RestrictionUDFsExtTest.java new file mode 100644 index 00000000..f0057a5e --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/RestrictionUDFsExtTest.java @@ -0,0 +1,105 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for additional RestrictionUDFs β€” extrema restriction + * (temporalAtMax/Min) and spatial restriction (tgeoAtGeom/tgeoMinusGeom). + * + * MEOS function authority: meos/include/meos.h, meos/include/meos_geo.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class RestrictionUDFsExtTest { + + private static String TFLOAT_SEQ; + private static String TRIP; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TFLOAT_SEQ = temporal_as_hexwkb( + tfloat_in("[1.0@2020-01-01, 5.0@2020-01-02, 2.0@2020-01-03]"), (byte) 0); + TRIP = temporal_as_hexwkb( + tgeompoint_in("[POINT(0 0)@2020-01-01 00:00:00+00, POINT(3 4)@2020-01-01 01:00:00+00]"), + (byte) 0); + } + + // ------------------------------------------------------------------ + // Extrema restriction + // ------------------------------------------------------------------ + + @Test @Order(1) + void temporalAtMax_returns_nonnull() throws Exception { + String r = RestrictionUDFs.temporalAtMax.call(TFLOAT_SEQ); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(2) + void temporalAtMin_returns_nonnull() throws Exception { + String r = RestrictionUDFs.temporalAtMin.call(TFLOAT_SEQ); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + // ------------------------------------------------------------------ + // Spatial restriction + // ------------------------------------------------------------------ + + @Test @Order(3) + void tgeoAtGeom_returns_nonnull_or_null() throws Exception { + // The trip passes through the polygon β€” result may be non-null + String r = RestrictionUDFs.tgeoAtGeom.call( + TRIP, "POLYGON((0 0, 2 0, 2 2, 0 2, 0 0))"); + assertTrue(r == null || !r.isBlank()); + } + + @Test @Order(4) + void tgeoMinusGeom_returns_nonnull_or_null() throws Exception { + String r = RestrictionUDFs.tgeoMinusGeom.call( + TRIP, "POLYGON((10 10, 20 10, 20 20, 10 20, 10 10))"); + assertTrue(r == null || !r.isBlank()); + } + + // ------------------------------------------------------------------ + // Null-input guards + // ------------------------------------------------------------------ + + @Test @Order(5) + void null_input_returns_null() throws Exception { + assertNull(RestrictionUDFs.temporalAtMax.call(null)); + assertNull(RestrictionUDFs.temporalAtMin.call(null)); + assertNull(RestrictionUDFs.tgeoAtGeom.call(null, "POINT(0 0)")); + assertNull(RestrictionUDFs.tgeoMinusGeom.call(null, "POINT(0 0)")); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/RestrictionUDFsTest.java b/src/test/java/org/mobilitydb/spark/temporal/RestrictionUDFsTest.java new file mode 100644 index 00000000..beaf77c6 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/RestrictionUDFsTest.java @@ -0,0 +1,206 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for RestrictionUDFs β€” at/minus restrictions by value and + * timestamp set/span/spanset, and delete operations. + * + * For cases where MEOS may legitimately return null (value absent from the + * sequence, or a span covers the entire input), the assertion is + * {@code assertTrue(r == null || !r.isBlank())} to verify no exception is + * thrown and no empty string is produced. + * + * MEOS function authority: meos/include/meos.h, meos/include/meos_geo.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class RestrictionUDFsTest { + + private static String TRIP; + private static String TINT_SEQ; + private static String TBOOL_SEQ; + private static String TFLOAT_SEQ; + private static String TTEXT_SEQ; + /** Hex-WKB of tstzspan [2020-01-01, 2020-01-02]. */ + private static String SPAN_HEX; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TRIP = temporal_as_hexwkb( + tgeompoint_in("[POINT(0 0)@2020-01-01 00:00:00+00, POINT(2 0)@2020-01-01 02:00:00+00]"), + (byte) 0); + TINT_SEQ = temporal_as_hexwkb(tint_in("[1@2020-01-01, 3@2020-01-03]"), (byte) 0); + TBOOL_SEQ = temporal_as_hexwkb(tbool_in("[t@2020-01-01, t@2020-01-02, f@2020-01-03]"), (byte) 0); + TFLOAT_SEQ = temporal_as_hexwkb(tfloat_in("[1.0@2020-01-01, 3.0@2020-01-03]"), (byte) 0); + TTEXT_SEQ = temporal_as_hexwkb(ttext_in("[Hello@2020-01-01, World@2020-01-03]"), (byte) 0); + SPAN_HEX = span_as_hexwkb( + tstzspan_in("[2020-01-01 00:00:00+00, 2020-01-02 00:00:00+00]"), + (byte) 0); + } + + // ------------------------------------------------------------------ + // Timestamp-set restriction + // ------------------------------------------------------------------ + + @Test @Order(1) + void temporalAtTstzset_returns_nonnull_or_null() throws Exception { + String setHex = set_as_hexwkb(tstzset_in("{2020-01-01 00:30:00+00}"), (byte) 0); + String r = RestrictionUDFs.temporalAtTstzset.call(TRIP, setHex); + // MEOS returns null when the instant does not coincide with a stored value. + assertTrue(r == null || !r.isBlank()); + } + + @Test @Order(2) + void temporalMinusTstzset_returns_nonnull_or_null() throws Exception { + String setHex = set_as_hexwkb(tstzset_in("{2020-01-01 00:30:00+00}"), (byte) 0); + String r = RestrictionUDFs.temporalMinusTstzset.call(TRIP, setHex); + assertTrue(r == null || !r.isBlank()); + } + + // ------------------------------------------------------------------ + // Span / spanset restriction + // ------------------------------------------------------------------ + + @Test @Order(3) + void temporalMinusTstzspan_returns_nonnull_or_null() throws Exception { + String r = RestrictionUDFs.temporalMinusTstzspan.call(TRIP, SPAN_HEX); + assertTrue(r == null || !r.isBlank()); + } + + @Test @Order(4) + void temporalMinusTstzspanset_returns_nonnull_or_null() throws Exception { + String ssHex = spanset_as_hexwkb( + tstzspanset_in("{[2020-01-01 00:00:00+00, 2020-01-02 00:00:00+00]}"), + (byte) 0); + String r = RestrictionUDFs.temporalMinusTstzspanset.call(TRIP, ssHex); + assertTrue(r == null || !r.isBlank()); + } + + // ------------------------------------------------------------------ + // Delete operations + // ------------------------------------------------------------------ + + @Test @Order(5) + void temporalDeleteTstzspan_returns_nonnull_or_null() throws Exception { + String r = RestrictionUDFs.temporalDeleteTstzspan.call(TRIP, SPAN_HEX); + assertTrue(r == null || !r.isBlank()); + } + + // ------------------------------------------------------------------ + // Value restriction: tfloat + // ------------------------------------------------------------------ + + @Test @Order(6) + void tfloatAtValue_returns_nonnull_or_null() throws Exception { + // 2.0 lies between 1.0 and 3.0 in a linear sequence, so MEOS returns the + // crossing instant; result may be null for step sequences but not here. + String r = RestrictionUDFs.tfloatAtValue.call(TFLOAT_SEQ, 2.0); + assertTrue(r == null || !r.isBlank()); + } + + @Test @Order(7) + void tfloatMinusValue_returns_nonnull() throws Exception { + // 99.0 never appears in the sequence; the full sequence is returned. + String r = RestrictionUDFs.tfloatMinusValue.call(TFLOAT_SEQ, 99.0); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + // ------------------------------------------------------------------ + // Value restriction: tbool + // ------------------------------------------------------------------ + + @Test @Order(8) + void tboolAtValue_true_returns_nonnull() throws Exception { + // TBOOL_SEQ is [t@..., t@..., f@...]; restricting to true yields a non-null result. + String r = RestrictionUDFs.tboolAtValue.call(TBOOL_SEQ, true); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(9) + void tboolMinusValue_false_returns_nonnull() throws Exception { + // Removing the false portion of TBOOL_SEQ leaves the true portion. + String r = RestrictionUDFs.tboolMinusValue.call(TBOOL_SEQ, false); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + // ------------------------------------------------------------------ + // Value restriction: ttext + // ------------------------------------------------------------------ + + @Test @Order(10) + void ttextAtValue_matching_returns_nonnull() throws Exception { + String r = RestrictionUDFs.ttextAtValue.call(TTEXT_SEQ, "Hello"); + assertNotNull(r); + } + + @Test @Order(11) + void ttextMinusValue_absent_returns_nonnull() throws Exception { + // "NotInSeq" is not in TTEXT_SEQ; the full sequence is returned. + String r = RestrictionUDFs.ttextMinusValue.call(TTEXT_SEQ, "NotInSeq"); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + // ------------------------------------------------------------------ + // Value restriction: tpoint + // ------------------------------------------------------------------ + + @Test @Order(12) + void tpointAtValue_start_point_returns_nonnull() throws Exception { + // POINT(0 0) is the exact start of TRIP. + String r = RestrictionUDFs.tpointAtValue.call(TRIP, "POINT(0 0)"); + assertNotNull(r); + } + + @Test @Order(13) + void tpointMinusValue_absent_point_returns_nonnull() throws Exception { + // POINT(999 999) never appears; the full trip is returned. + String r = RestrictionUDFs.tpointMinusValue.call(TRIP, "POINT(999 999)"); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + // ------------------------------------------------------------------ + // Null-input guards + // ------------------------------------------------------------------ + + @Test @Order(14) + void null_input_returns_null() throws Exception { + assertNull(RestrictionUDFs.tfloatAtValue.call(null, 1.0)); + assertNull(RestrictionUDFs.tfloatAtValue.call(TFLOAT_SEQ, null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/STBoxUDFsExt2Test.java b/src/test/java/org/mobilitydb/spark/temporal/STBoxUDFsExt2Test.java new file mode 100644 index 00000000..953d42c8 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/STBoxUDFsExt2Test.java @@ -0,0 +1,204 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import jnr.ffi.Pointer; +import jnr.ffi.Runtime; +import org.junit.jupiter.api.*; +import org.mobilitydb.spark.geo.STBoxUDFs; + +import java.sql.Timestamp; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for new STBoxUDFs constructors: + * geoToStbox, tstzspanToStbox, timestamptzToStbox. + * + * MEOS function authority: meos/include/meos_geo.h, meos/include/meos.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class STBoxUDFsExt2Test { + + private static String TSTZSPAN_HEX; + private static String STBOX_A_HEX; // STBox with spatial + time component + private static String STBOX_B_HEX; // STBox strictly right of STBOX_A in X + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TSTZSPAN_HEX = span_as_hexwkb( + tstzspan_in("[2020-01-01 00:00:00+00, 2020-01-03 00:00:00+00]"), (byte) 0); + + // Build spatial-only STBoxes from point geometries. + // STBOX_A: centred at (0,0), STBOX_B: centred at (20,0) β€” strictly right + Pointer sizeOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + STBOX_A_HEX = stbox_as_hexwkb(geo_to_stbox(geo_from_text("POINT(0 0)", 0)), (byte) 0, sizeOut); + sizeOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + STBOX_B_HEX = stbox_as_hexwkb(geo_to_stbox(geo_from_text("POINT(20 0)", 0)), (byte) 0, sizeOut); + } + + // ------------------------------------------------------------------ + // geoToStbox + // ------------------------------------------------------------------ + + @Test @Order(1) + void geoToStbox_returns_nonnull_hexwkb() throws Exception { + String r = STBoxUDFs.geoToStbox.call("POINT(4.35 50.85)"); + assertNotNull(r, "geoToStbox must return non-null for valid WKT"); + assertFalse(r.isBlank()); + } + + @Test @Order(2) + void geoToStbox_polygon_returns_nonnull() throws Exception { + String r = STBoxUDFs.geoToStbox.call("POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))"); + assertNotNull(r, "geoToStbox must return non-null for polygon WKT"); + assertFalse(r.isBlank()); + } + + @Test @Order(3) + void geoToStbox_null_returns_null() throws Exception { + assertNull(STBoxUDFs.geoToStbox.call(null)); + } + + // ------------------------------------------------------------------ + // tstzspanToStbox + // ------------------------------------------------------------------ + + @Test @Order(4) + void tstzspanToStbox_returns_nonnull_hexwkb() throws Exception { + String r = STBoxUDFs.tstzspanToStbox.call(TSTZSPAN_HEX); + assertNotNull(r, "tstzspanToStbox must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(5) + void tstzspanToStbox_null_returns_null() throws Exception { + assertNull(STBoxUDFs.tstzspanToStbox.call(null)); + } + + // ------------------------------------------------------------------ + // timestamptzToStbox + // ------------------------------------------------------------------ + + @Test @Order(6) + void timestamptzToStbox_returns_nonnull_hexwkb() throws Exception { + Timestamp ts = new Timestamp(1577836800000L); // 2020-01-01 00:00:00 UTC + String r = STBoxUDFs.timestamptzToStbox.call(ts); + assertNotNull(r, "timestamptzToStbox must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(7) + void timestamptzToStbox_null_returns_null() throws Exception { + assertNull(STBoxUDFs.timestamptzToStbox.call((Timestamp) null)); + } + + // ------------------------------------------------------------------ + // intersectionStboxStbox / unionStboxStbox + // ------------------------------------------------------------------ + + @Test @Order(8) + void intersectionStboxStbox_same_box_returns_nonnull() throws Exception { + String box = STBoxUDFs.tstzspanToStbox.call(TSTZSPAN_HEX); + assertNotNull(box); + String r = STBoxUDFs.intersectionStboxStbox.call(box, box); + assertNotNull(r, "intersection of a box with itself must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(9) + void unionStboxStbox_returns_nonnull() throws Exception { + String box = STBoxUDFs.tstzspanToStbox.call(TSTZSPAN_HEX); + assertNotNull(box); + String r = STBoxUDFs.unionStboxStbox.call(box, box); + assertNotNull(r, "union of a box with itself must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(10) + void intersectionStboxStbox_null_returns_null() throws Exception { + String box = STBoxUDFs.tstzspanToStbox.call(TSTZSPAN_HEX); + assertNull(STBoxUDFs.intersectionStboxStbox.call(null, box)); + assertNull(STBoxUDFs.intersectionStboxStbox.call(box, null)); + } + + // ------------------------------------------------------------------ + // STBox positional predicates + // ------------------------------------------------------------------ + + @Test @Order(11) + void stboxLeft_a_is_left_of_b() throws Exception { + assertNotNull(STBOX_A_HEX, "STBOX_A_HEX setup required"); + assertNotNull(STBOX_B_HEX, "STBOX_B_HEX setup required"); + assertTrue(STBoxUDFs.stboxLeft.call(STBOX_A_HEX, STBOX_B_HEX), + "stbox at (0,0) must be left of stbox at (20,0)"); + } + + @Test @Order(12) + void stboxRight_b_is_right_of_a() throws Exception { + assertTrue(STBoxUDFs.stboxRight.call(STBOX_B_HEX, STBOX_A_HEX), + "stbox at (20,0) must be right of stbox at (0,0)"); + } + + @Test @Order(13) + void stboxLeft_null_returns_null() throws Exception { + assertNull(STBoxUDFs.stboxLeft.call(null, STBOX_B_HEX)); + assertNull(STBoxUDFs.stboxLeft.call(STBOX_A_HEX, null)); + } + + // ------------------------------------------------------------------ + // STBox topology predicates + // ------------------------------------------------------------------ + + @Test @Order(14) + void stboxContains_same_box_returns_true() throws Exception { + assertNotNull(STBOX_A_HEX); + assertTrue(STBoxUDFs.stboxContains.call(STBOX_A_HEX, STBOX_A_HEX), + "an stbox contains itself"); + } + + @Test @Order(15) + void stboxContained_same_box_returns_true() throws Exception { + assertTrue(STBoxUDFs.stboxContained.call(STBOX_A_HEX, STBOX_A_HEX), + "an stbox is contained in itself"); + } + + @Test @Order(16) + void stboxOverlaps_same_box_returns_true() throws Exception { + assertTrue(STBoxUDFs.stboxOverlaps.call(STBOX_A_HEX, STBOX_A_HEX), + "an stbox overlaps itself"); + } + + @Test @Order(17) + void stboxContains_null_returns_null() throws Exception { + assertNull(STBoxUDFs.stboxContains.call(null, STBOX_A_HEX)); + assertNull(STBoxUDFs.stboxContains.call(STBOX_A_HEX, null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/STBoxUDFsTest.java b/src/test/java/org/mobilitydb/spark/temporal/STBoxUDFsTest.java new file mode 100644 index 00000000..e5826a18 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/STBoxUDFsTest.java @@ -0,0 +1,150 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import jnr.ffi.Pointer; +import jnr.ffi.Runtime; +import org.junit.jupiter.api.*; +import org.mobilitydb.spark.geo.STBoxUDFs; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for STBoxUDFs: + * stboxRound, stboxExpandSpace, stboxSetSrid, stboxShiftScaleTime. + * + * MEOS function authority: meos/include/meos.h, meos/include/meos_geo.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class STBoxUDFsTest { + + private static String STBOX_HEX; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + // Build stbox hex: STBOX XT(((1.1234,2.5678),(3.9876,4.1111)),[2020-01-01,2020-01-03]) + Pointer sb = stbox_in( + "STBOX XT(((1.1234,2.5678),(3.9876,4.1111))," + + "[2020-01-01 00:00:00+00,2020-01-03 00:00:00+00])"); + assertNotNull(sb, "stbox_in must succeed"); + Pointer sizeOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + STBOX_HEX = stbox_as_hexwkb(sb, (byte) 0, sizeOut); + } + + // ------------------------------------------------------------------ + // stboxRound + // ------------------------------------------------------------------ + + @Test @Order(1) + void stboxRound_returns_nonnull_hexwkb() throws Exception { + String r = STBoxUDFs.stboxRound.call(STBOX_HEX, 2); + assertNotNull(r, "stboxRound must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(2) + void stboxRound_null_returns_null() throws Exception { + assertNull(STBoxUDFs.stboxRound.call(null, 2)); + assertNull(STBoxUDFs.stboxRound.call(STBOX_HEX, null)); + } + + // ------------------------------------------------------------------ + // stboxExpandSpace + // ------------------------------------------------------------------ + + @Test @Order(3) + void stboxExpandSpace_returns_nonnull_hexwkb() throws Exception { + String r = STBoxUDFs.stboxExpandSpace.call(STBOX_HEX, 1.0); + assertNotNull(r, "stboxExpandSpace must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(4) + void stboxExpandSpace_zero_radius_is_identity() throws Exception { + String r = STBoxUDFs.stboxExpandSpace.call(STBOX_HEX, 0.0); + assertNotNull(r, "Expanding by 0 must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(5) + void stboxExpandSpace_null_returns_null() throws Exception { + assertNull(STBoxUDFs.stboxExpandSpace.call(null, 1.0)); + assertNull(STBoxUDFs.stboxExpandSpace.call(STBOX_HEX, null)); + } + + // ------------------------------------------------------------------ + // stboxSetSrid + // ------------------------------------------------------------------ + + @Test @Order(6) + void stboxSetSrid_returns_nonnull_hexwkb() throws Exception { + String r = STBoxUDFs.stboxSetSrid.call(STBOX_HEX, 4326); + assertNotNull(r, "stboxSetSrid must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(7) + void stboxSetSrid_null_returns_null() throws Exception { + assertNull(STBoxUDFs.stboxSetSrid.call(null, 4326)); + assertNull(STBoxUDFs.stboxSetSrid.call(STBOX_HEX, null)); + } + + // ------------------------------------------------------------------ + // stboxShiftScaleTime + // ------------------------------------------------------------------ + + @Test @Order(8) + void stboxShiftScaleTime_shift_returns_nonnull() throws Exception { + String r = STBoxUDFs.stboxShiftScaleTime.call(STBOX_HEX, "1 day", null); + assertNotNull(r, "Shift by 1 day must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(9) + void stboxShiftScaleTime_null_stbox_returns_null() throws Exception { + assertNull(STBoxUDFs.stboxShiftScaleTime.call(null, "1 day", null)); + } + + // ------------------------------------------------------------------ + // stboxGetSpace + // ------------------------------------------------------------------ + + @Test @Order(10) + void stboxGetSpace_returns_nonnull_hexwkb() throws Exception { + String r = STBoxUDFs.stboxGetSpace.call(STBOX_HEX); + assertNotNull(r, "stboxGetSpace must return non-null for XT stbox"); + assertFalse(r.isBlank()); + } + + @Test @Order(11) + void stboxGetSpace_null_returns_null() throws Exception { + assertNull(STBoxUDFs.stboxGetSpace.call(null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/SetAccessorUDFsExtTest.java b/src/test/java/org/mobilitydb/spark/temporal/SetAccessorUDFsExtTest.java new file mode 100644 index 00000000..ddc0ad36 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/SetAccessorUDFsExtTest.java @@ -0,0 +1,223 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import java.sql.Date; +import java.sql.Timestamp; +import java.util.List; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for set value accessor UDFs: + * intset, floatset, dateset, tstzset, textset β€” start/end/values. + * + * MEOS function authority: meos/include/meos.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class SetAccessorUDFsExtTest { + + private static String INTSET_HEX; + private static String FLOATSET_HEX; + private static String DATESET_HEX; + private static String TSTZSET_HEX; + private static String TEXTSET_HEX; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + INTSET_HEX = set_as_hexwkb(intset_in("{1, 5, 10}"), (byte) 0); + FLOATSET_HEX = set_as_hexwkb(floatset_in("{1.5, 3.0, 7.25}"), (byte) 0); + DATESET_HEX = set_as_hexwkb(dateset_in("{2020-01-01, 2020-06-15, 2021-12-31}"), (byte) 0); + TSTZSET_HEX = set_as_hexwkb( + tstzset_in("{2020-01-01 00:00:00+00, 2020-06-01 00:00:00+00}"), (byte) 0); + TEXTSET_HEX = set_as_hexwkb(textset_in("{\"apple\", \"banana\", \"cherry\"}"), (byte) 0); + } + + // ------------------------------------------------------------------ + // intset + // ------------------------------------------------------------------ + + @Test @Order(1) + void intsetStartValue_returns_minimum() throws Exception { + Integer v = SpanAccessorUDFs.intsetStartValue.call(INTSET_HEX); + assertNotNull(v); + assertEquals(1, v.intValue()); + } + + @Test @Order(2) + void intsetEndValue_returns_maximum() throws Exception { + Integer v = SpanAccessorUDFs.intsetEndValue.call(INTSET_HEX); + assertNotNull(v); + assertEquals(10, v.intValue()); + } + + @Test @Order(3) + void intsetValues_returns_all_elements() throws Exception { + List vs = SpanAccessorUDFs.intsetValues.call(INTSET_HEX); + assertNotNull(vs); + assertEquals(3, vs.size()); + assertTrue(vs.contains(1)); + assertTrue(vs.contains(5)); + assertTrue(vs.contains(10)); + } + + @Test @Order(4) + void intsetStartValue_null_returns_null() throws Exception { + assertNull(SpanAccessorUDFs.intsetStartValue.call(null)); + } + + // ------------------------------------------------------------------ + // floatset + // ------------------------------------------------------------------ + + @Test @Order(5) + void floatsetStartValue_returns_minimum() throws Exception { + Double v = SpanAccessorUDFs.floatsetStartValue.call(FLOATSET_HEX); + assertNotNull(v); + assertEquals(1.5, v, 1e-9); + } + + @Test @Order(6) + void floatsetEndValue_returns_maximum() throws Exception { + Double v = SpanAccessorUDFs.floatsetEndValue.call(FLOATSET_HEX); + assertNotNull(v); + assertEquals(7.25, v, 1e-9); + } + + @Test @Order(7) + void floatsetValues_returns_all_elements() throws Exception { + List vs = SpanAccessorUDFs.floatsetValues.call(FLOATSET_HEX); + assertNotNull(vs); + assertEquals(3, vs.size()); + } + + @Test @Order(8) + void floatsetEndValue_null_returns_null() throws Exception { + assertNull(SpanAccessorUDFs.floatsetEndValue.call(null)); + } + + // ------------------------------------------------------------------ + // dateset + // ------------------------------------------------------------------ + + @Test @Order(9) + void datesetStartValue_returns_first_date() throws Exception { + Date d = SpanAccessorUDFs.datesetStartValue.call(DATESET_HEX); + assertNotNull(d, "datesetStartValue must return non-null"); + } + + @Test @Order(10) + void datesetEndValue_returns_last_date() throws Exception { + Date d = SpanAccessorUDFs.datesetEndValue.call(DATESET_HEX); + assertNotNull(d, "datesetEndValue must return non-null"); + } + + @Test @Order(11) + void datesetValues_returns_all_elements() throws Exception { + List ds = SpanAccessorUDFs.datesetValues.call(DATESET_HEX); + assertNotNull(ds); + assertEquals(3, ds.size()); + } + + @Test @Order(12) + void datesetStartValue_null_returns_null() throws Exception { + assertNull(SpanAccessorUDFs.datesetStartValue.call(null)); + } + + // ------------------------------------------------------------------ + // tstzset + // ------------------------------------------------------------------ + + @Test @Order(13) + void tstzsetStartValue_returns_nonnull_timestamp() throws Exception { + Timestamp ts = SpanAccessorUDFs.tstzsetStartValue.call(TSTZSET_HEX); + assertNotNull(ts, "tstzsetStartValue must return non-null"); + } + + @Test @Order(14) + void tstzsetEndValue_returns_nonnull_timestamp() throws Exception { + Timestamp ts = SpanAccessorUDFs.tstzsetEndValue.call(TSTZSET_HEX); + assertNotNull(ts, "tstzsetEndValue must return non-null"); + } + + @Test @Order(15) + void tstzsetStartValue_before_endValue() throws Exception { + Timestamp start = SpanAccessorUDFs.tstzsetStartValue.call(TSTZSET_HEX); + Timestamp end = SpanAccessorUDFs.tstzsetEndValue.call(TSTZSET_HEX); + assertNotNull(start); + assertNotNull(end); + assertTrue(start.before(end), "start value must be before end value"); + } + + @Test @Order(16) + void tstzsetValues_returns_two_elements() throws Exception { + List tss = SpanAccessorUDFs.tstzsetValues.call(TSTZSET_HEX); + assertNotNull(tss); + assertEquals(2, tss.size()); + } + + @Test @Order(17) + void tstzsetStartValue_null_returns_null() throws Exception { + assertNull(SpanAccessorUDFs.tstzsetStartValue.call(null)); + } + + // ------------------------------------------------------------------ + // textset + // ------------------------------------------------------------------ + + @Test @Order(18) + void textsetStartValue_returns_first_string() throws Exception { + String s = SpanAccessorUDFs.textsetStartValue.call(TEXTSET_HEX); + assertNotNull(s, "textsetStartValue must return non-null"); + assertFalse(s.isBlank()); + } + + @Test @Order(19) + void textsetEndValue_returns_last_string() throws Exception { + String s = SpanAccessorUDFs.textsetEndValue.call(TEXTSET_HEX); + assertNotNull(s, "textsetEndValue must return non-null"); + assertFalse(s.isBlank()); + } + + @Test @Order(20) + void textsetValues_returns_three_strings() throws Exception { + List vs = SpanAccessorUDFs.textsetValues.call(TEXTSET_HEX); + assertNotNull(vs); + assertEquals(3, vs.size()); + vs.forEach(s -> assertFalse(s == null || s.isBlank(), "Each element must be non-blank")); + } + + @Test @Order(21) + void textsetStartValue_null_returns_null() throws Exception { + assertNull(SpanAccessorUDFs.textsetStartValue.call(null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/SimilarityUDFsExtTest.java b/src/test/java/org/mobilitydb/spark/temporal/SimilarityUDFsExtTest.java new file mode 100644 index 00000000..a2d2e18a --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/SimilarityUDFsExtTest.java @@ -0,0 +1,76 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for SimilarityUDFs.hausdorffDistance. + * + * MEOS function authority: meos/include/meos.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class SimilarityUDFsExtTest { + + private static String TRIP1; + private static String TRIP2; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TRIP1 = temporal_as_hexwkb( + tgeompoint_in("[POINT(0 0)@2020-01-01, POINT(1 0)@2020-01-02, POINT(2 0)@2020-01-03]"), + (byte) 0); + TRIP2 = temporal_as_hexwkb( + tgeompoint_in("[POINT(0 1)@2020-01-01, POINT(1 1)@2020-01-02, POINT(2 1)@2020-01-03]"), + (byte) 0); + } + + @Test @Order(1) + void hausdorffDistance_returns_positive_double() throws Exception { + Double d = SimilarityUDFs.hausdorffDistance.call(TRIP1, TRIP2); + assertNotNull(d, "Hausdorff distance must not be null for valid trips"); + assertTrue(d > 0, "Hausdorff distance must be positive for non-identical trips"); + } + + @Test @Order(2) + void hausdorffDistance_same_trip_returns_zero() throws Exception { + Double d = SimilarityUDFs.hausdorffDistance.call(TRIP1, TRIP1); + assertNotNull(d); + assertEquals(0.0, d, 1e-9, "Hausdorff distance of a trip with itself must be 0"); + } + + @Test @Order(3) + void hausdorffDistance_null_returns_null() throws Exception { + assertNull(SimilarityUDFs.hausdorffDistance.call(null, TRIP1)); + assertNull(SimilarityUDFs.hausdorffDistance.call(TRIP1, null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/SimilarityUDFsTest.java b/src/test/java/org/mobilitydb/spark/temporal/SimilarityUDFsTest.java new file mode 100644 index 00000000..cce3954b --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/SimilarityUDFsTest.java @@ -0,0 +1,96 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for SimilarityUDFs β€” FrΓ©chet and DTW trajectory distances. + * + * MEOS function authority: meos/include/meos.h (038_temporal_similarity) + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class SimilarityUDFsTest { + + private static String TRIP1; + private static String TRIP2; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TRIP1 = temporal_as_hexwkb( + tgeompoint_in("[POINT(0 0)@2020-01-01, POINT(1 0)@2020-01-02, POINT(2 0)@2020-01-03]"), + (byte) 0); + TRIP2 = temporal_as_hexwkb( + tgeompoint_in("[POINT(0 1)@2020-01-01, POINT(1 1)@2020-01-02, POINT(2 1)@2020-01-03]"), + (byte) 0); + } + + @Test @Order(1) + void frechetDistance_returns_positive_double() throws Exception { + Double d = SimilarityUDFs.frechetDistance.call(TRIP1, TRIP2); + assertNotNull(d, "FrΓ©chet distance should not be null for valid trips"); + assertTrue(d >= 0.0, "FrΓ©chet distance must be non-negative"); + } + + @Test @Order(2) + void frechetDistance_identical_trips_is_zero() throws Exception { + Double d = SimilarityUDFs.frechetDistance.call(TRIP1, TRIP1); + assertNotNull(d); + assertEquals(0.0, d, 1e-9); + } + + @Test @Order(3) + void dynamicTimeWarp_returns_positive_double() throws Exception { + Double d = SimilarityUDFs.dynamicTimeWarp.call(TRIP1, TRIP2); + assertNotNull(d, "DTW distance should not be null for valid trips"); + assertTrue(d >= 0.0, "DTW distance must be non-negative"); + } + + @Test @Order(4) + void dynamicTimeWarp_identical_trips_is_zero() throws Exception { + Double d = SimilarityUDFs.dynamicTimeWarp.call(TRIP1, TRIP1); + assertNotNull(d); + assertEquals(0.0, d, 1e-9); + } + + @Test @Order(5) + void frechetDistance_null_input_returns_null() throws Exception { + assertNull(SimilarityUDFs.frechetDistance.call(null, TRIP2)); + assertNull(SimilarityUDFs.frechetDistance.call(TRIP1, null)); + } + + @Test @Order(6) + void dynamicTimeWarp_null_input_returns_null() throws Exception { + assertNull(SimilarityUDFs.dynamicTimeWarp.call(null, TRIP2)); + assertNull(SimilarityUDFs.dynamicTimeWarp.call(TRIP1, null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/SpanAccessorUDFsExt2Test.java b/src/test/java/org/mobilitydb/spark/temporal/SpanAccessorUDFsExt2Test.java new file mode 100644 index 00000000..fa9bba82 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/SpanAccessorUDFsExt2Test.java @@ -0,0 +1,144 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import java.sql.Timestamp; +import java.util.List; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for tstzspanset extra accessors and tpointFromBaseTemp constructor: + * tstzspansetNumTimestamps, tstzspansetTimestamps, tstzspansetDuration, + * tpointFromBaseTemp. + * + * MEOS function authority: meos/include/meos.h, meos/include/meos_geo.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class SpanAccessorUDFsExt2Test { + + /** TstzSpanSet: 2 disjoint 1-day spans with a gap in between. */ + private static String TSTZSPANSET_HEX; + /** Single tgeompoint instant β€” used as template for tpointFromBaseTemp. */ + private static String TPOINT_INST_HEX; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TSTZSPANSET_HEX = spanset_as_hexwkb( + tstzspanset_in("{[2020-01-01 00:00:00+00, 2020-01-02 00:00:00+00]," + + "[2020-02-01 00:00:00+00, 2020-02-02 00:00:00+00]}"), + (byte) 0); + + TPOINT_INST_HEX = temporal_as_hexwkb( + tgeompoint_in("POINT(1 2)@2020-01-01 00:00:00+00"), + (byte) 0); + } + + // ------------------------------------------------------------------ + // tstzspansetNumTimestamps + // ------------------------------------------------------------------ + + @Test @Order(1) + void tstzspansetNumTimestamps_returns_nonnull() throws Exception { + Integer n = SpanAccessorUDFs.tstzspansetNumTimestamps.call(TSTZSPANSET_HEX); + assertNotNull(n, "tstzspansetNumTimestamps must return non-null"); + } + + @Test @Order(2) + void tstzspansetNumTimestamps_two_spans_returns_four() throws Exception { + Integer n = SpanAccessorUDFs.tstzspansetNumTimestamps.call(TSTZSPANSET_HEX); + assertNotNull(n); + assertEquals(4, n.intValue(), + "2 closed spans β†’ 4 boundary timestamps"); + } + + @Test @Order(3) + void tstzspansetNumTimestamps_null_returns_null() throws Exception { + assertNull(SpanAccessorUDFs.tstzspansetNumTimestamps.call(null)); + } + + // ------------------------------------------------------------------ + // tstzspansetTimestamps + // ------------------------------------------------------------------ + + @Test @Order(4) + void tstzspansetTimestamps_returns_nonnull_list() throws Exception { + List ts = SpanAccessorUDFs.tstzspansetTimestamps.call(TSTZSPANSET_HEX); + assertNotNull(ts, "tstzspansetTimestamps must return non-null"); + assertFalse(ts.isEmpty()); + } + + @Test @Order(5) + void tstzspansetTimestamps_count_matches_numTimestamps() throws Exception { + Integer n = SpanAccessorUDFs.tstzspansetNumTimestamps.call(TSTZSPANSET_HEX); + List ts = SpanAccessorUDFs.tstzspansetTimestamps.call(TSTZSPANSET_HEX); + assertNotNull(n); + assertNotNull(ts); + assertEquals(n.intValue(), ts.size(), + "timestamp list size must match tstzspansetNumTimestamps"); + } + + @Test @Order(6) + void tstzspansetTimestamps_elements_are_nonnull() throws Exception { + List ts = SpanAccessorUDFs.tstzspansetTimestamps.call(TSTZSPANSET_HEX); + assertNotNull(ts); + ts.forEach(t -> assertNotNull(t, "each timestamp must be non-null")); + } + + @Test @Order(7) + void tstzspansetTimestamps_null_returns_null() throws Exception { + assertNull(SpanAccessorUDFs.tstzspansetTimestamps.call(null)); + } + + // ------------------------------------------------------------------ + // tstzspansetDuration + // ------------------------------------------------------------------ + + @Test @Order(8) + void tstzspansetDuration_returns_nonnull_interval_string() throws Exception { + String dur = SpanAccessorUDFs.tstzspansetDuration.call(TSTZSPANSET_HEX, false); + assertNotNull(dur, "tstzspansetDuration must return non-null"); + assertFalse(dur.isBlank()); + } + + @Test @Order(9) + void tstzspansetDuration_ignoreGaps_true_returns_nonnull() throws Exception { + String dur = SpanAccessorUDFs.tstzspansetDuration.call(TSTZSPANSET_HEX, true); + assertNotNull(dur, "tstzspansetDuration with ignoreGaps=true must return non-null"); + assertFalse(dur.isBlank()); + } + + @Test @Order(10) + void tstzspansetDuration_null_returns_null() throws Exception { + assertNull(SpanAccessorUDFs.tstzspansetDuration.call(null, false)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/SpanAccessorUDFsExt3Test.java b/src/test/java/org/mobilitydb/spark/temporal/SpanAccessorUDFsExt3Test.java new file mode 100644 index 00000000..0063fa18 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/SpanAccessorUDFsExt3Test.java @@ -0,0 +1,118 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for intspanset/floatspanset bound accessors: + * intspansetLower/Upper/Width, floatspansetLower/Upper/Width. + * + * MEOS function authority: meos/include/meos.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class SpanAccessorUDFsExt3Test { + + private static String INTSPANSET_HEX; + private static String FLOATSPANSET_HEX; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + INTSPANSET_HEX = spanset_as_hexwkb(intspanset_in("{[1, 5], [10, 20]}"), (byte) 0); + FLOATSPANSET_HEX = spanset_as_hexwkb(floatspanset_in("{[1.0, 5.0], [10.0, 20.0]}"), (byte) 0); + } + + // ------------------------------------------------------------------ + // intspanset bounds + // ------------------------------------------------------------------ + + @Test @Order(1) + void intspansetLower_returns_first_lower_bound() throws Exception { + Integer v = SpanAccessorUDFs.intspansetLower.call(INTSPANSET_HEX); + assertNotNull(v); + assertEquals(1, v.intValue()); + } + + @Test @Order(2) + void intspansetUpper_returns_nonnull() throws Exception { + // Integer spans store exclusive upper bound internally; + // [10, 20] is stored as upper = 21. + Integer v = SpanAccessorUDFs.intspansetUpper.call(INTSPANSET_HEX); + assertNotNull(v, "intspansetUpper must return non-null"); + } + + @Test @Order(3) + void intspansetWidth_ignoreGaps_false_returns_nonnull() throws Exception { + Integer w = SpanAccessorUDFs.intspansetWidth.call(INTSPANSET_HEX, false); + assertNotNull(w, "intspansetWidth must return non-null"); + } + + @Test @Order(4) + void intspansetWidth_ignoreGaps_true_returns_nonnull() throws Exception { + Integer w = SpanAccessorUDFs.intspansetWidth.call(INTSPANSET_HEX, true); + assertNotNull(w, "intspansetWidth(ignoreGaps=true) must return non-null"); + } + + @Test @Order(5) + void intspansetLower_null_returns_null() throws Exception { + assertNull(SpanAccessorUDFs.intspansetLower.call(null)); + } + + // ------------------------------------------------------------------ + // floatspanset bounds + // ------------------------------------------------------------------ + + @Test @Order(6) + void floatspansetLower_returns_first_lower_bound() throws Exception { + Double v = SpanAccessorUDFs.floatspansetLower.call(FLOATSPANSET_HEX); + assertNotNull(v); + assertEquals(1.0, v, 1e-9); + } + + @Test @Order(7) + void floatspansetUpper_returns_last_upper_bound() throws Exception { + Double v = SpanAccessorUDFs.floatspansetUpper.call(FLOATSPANSET_HEX); + assertNotNull(v); + assertEquals(20.0, v, 1e-9); + } + + @Test @Order(8) + void floatspansetWidth_returns_nonnull() throws Exception { + Double w = SpanAccessorUDFs.floatspansetWidth.call(FLOATSPANSET_HEX, false); + assertNotNull(w, "floatspansetWidth must return non-null"); + } + + @Test @Order(9) + void floatspansetLower_null_returns_null() throws Exception { + assertNull(SpanAccessorUDFs.floatspansetLower.call(null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/SpanAccessorUDFsExtTest.java b/src/test/java/org/mobilitydb/spark/temporal/SpanAccessorUDFsExtTest.java new file mode 100644 index 00000000..e00aa988 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/SpanAccessorUDFsExtTest.java @@ -0,0 +1,246 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for SpanAccessorUDFs new accessors: + * spansetLowerInc, spansetUpperInc, spanToSpanset. + * + * And TBoxUDFs new constructors: + * spanToTbox, spansetToTbox, setToTbox. + * + * MEOS function authority: meos/include/meos.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class SpanAccessorUDFsExtTest { + + private static String INTSPAN_HEX; + private static String INTSPANSET_HEX; + private static String INTSET_HEX; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + // intspan [1,10] + INTSPAN_HEX = span_as_hexwkb(intspan_in("[1, 10]"), (byte) 0); + // intspanset {[1,5], [8,10]} + INTSPANSET_HEX = spanset_as_hexwkb(intspanset_in("{[1, 5], [8, 10]}"), (byte) 0); + // intset {2, 5, 8} + INTSET_HEX = set_as_hexwkb(intset_in("{2, 5, 8}"), (byte) 0); + } + + // ------------------------------------------------------------------ + // spansetLowerInc + // ------------------------------------------------------------------ + + @Test @Order(1) + void spansetLowerInc_closed_lower_returns_true() throws Exception { + Boolean r = SpanAccessorUDFs.spansetLowerInc.call(INTSPANSET_HEX); + assertNotNull(r); + assertTrue(r, "Lower bound of {[1,5],[8,10]} must be inclusive"); + } + + @Test @Order(2) + void spansetLowerInc_null_returns_null() throws Exception { + assertNull(SpanAccessorUDFs.spansetLowerInc.call(null)); + } + + // ------------------------------------------------------------------ + // spansetUpperInc + // ------------------------------------------------------------------ + + @Test @Order(3) + void spansetUpperInc_returns_nonnull() throws Exception { + // Integer spansets use exclusive canonical upper bound; just verify the call succeeds. + Boolean r = SpanAccessorUDFs.spansetUpperInc.call(INTSPANSET_HEX); + assertNotNull(r, "spansetUpperInc must return non-null for a valid spanset"); + } + + @Test @Order(4) + void spansetUpperInc_null_returns_null() throws Exception { + assertNull(SpanAccessorUDFs.spansetUpperInc.call(null)); + } + + // ------------------------------------------------------------------ + // spanToSpanset + // ------------------------------------------------------------------ + + @Test @Order(5) + void spanToSpanset_returns_nonnull_hexwkb() throws Exception { + String r = SpanAccessorUDFs.spanToSpanset.call(INTSPAN_HEX); + assertNotNull(r, "spanToSpanset must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(6) + void spanToSpanset_null_returns_null() throws Exception { + assertNull(SpanAccessorUDFs.spanToSpanset.call(null)); + } + + // ------------------------------------------------------------------ + // spanToTbox (TBoxUDFs) + // ------------------------------------------------------------------ + + @Test @Order(7) + void spanToTbox_returns_nonnull_hexwkb() throws Exception { + String r = TBoxUDFs.spanToTbox.call(INTSPAN_HEX); + assertNotNull(r, "spanToTbox must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(8) + void spanToTbox_null_returns_null() throws Exception { + assertNull(TBoxUDFs.spanToTbox.call(null)); + } + + // ------------------------------------------------------------------ + // spansetToTbox (TBoxUDFs) + // ------------------------------------------------------------------ + + @Test @Order(9) + void spansetToTbox_returns_nonnull_hexwkb() throws Exception { + String r = TBoxUDFs.spansetToTbox.call(INTSPANSET_HEX); + assertNotNull(r, "spansetToTbox must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(10) + void spansetToTbox_null_returns_null() throws Exception { + assertNull(TBoxUDFs.spansetToTbox.call(null)); + } + + // ------------------------------------------------------------------ + // setToTbox (TBoxUDFs) + // ------------------------------------------------------------------ + + @Test @Order(11) + void setToTbox_returns_nonnull_hexwkb() throws Exception { + String r = TBoxUDFs.setToTbox.call(INTSET_HEX); + assertNotNull(r, "setToTbox must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(12) + void setToTbox_null_returns_null() throws Exception { + assertNull(TBoxUDFs.setToTbox.call(null)); + } + + // ------------------------------------------------------------------ + // tstzspanset boundary accessors (SpanAccessorUDFs) + // ------------------------------------------------------------------ + + private static String TSTZSPANSET_HEX; + + // Note: @BeforeAll already called meos_initialize() above; this + // additional fixture is safe to initialise here. + // TSTZSPANSET_HEX is set at the end of the single @BeforeAll. + + @Test @Order(13) + void tstzspansetLower_returns_nonnull_timestamp() throws Exception { + String ss = spanset_as_hexwkb(tstzspanset_in( + "{[2020-01-01 00:00:00+00, 2020-01-02 00:00:00+00]," + + "[2020-03-01 00:00:00+00, 2020-03-03 00:00:00+00]}"), (byte) 0); + java.sql.Timestamp r = SpanAccessorUDFs.tstzspansetLower.call(ss); + assertNotNull(r, "tstzspansetLower must return non-null"); + } + + @Test @Order(14) + void tstzspansetLower_null_returns_null() throws Exception { + assertNull(SpanAccessorUDFs.tstzspansetLower.call(null)); + } + + @Test @Order(15) + void tstzspansetUpper_returns_nonnull_timestamp() throws Exception { + String ss = spanset_as_hexwkb(tstzspanset_in( + "{[2020-01-01 00:00:00+00, 2020-01-02 00:00:00+00]," + + "[2020-03-01 00:00:00+00, 2020-03-03 00:00:00+00]}"), (byte) 0); + java.sql.Timestamp r = SpanAccessorUDFs.tstzspansetUpper.call(ss); + assertNotNull(r, "tstzspansetUpper must return non-null"); + } + + @Test @Order(16) + void tstzspansetUpper_null_returns_null() throws Exception { + assertNull(SpanAccessorUDFs.tstzspansetUpper.call(null)); + } + + @Test @Order(17) + void tstzspansetStartTimestamptz_returns_nonnull() throws Exception { + String ss = spanset_as_hexwkb(tstzspanset_in( + "{[2020-01-01 00:00:00+00, 2020-01-02 00:00:00+00]," + + "[2020-03-01 00:00:00+00, 2020-03-03 00:00:00+00]}"), (byte) 0); + java.sql.Timestamp r = SpanAccessorUDFs.tstzspansetStartTimestamptz.call(ss); + assertNotNull(r, "tstzspansetStartTimestamptz must return non-null"); + } + + @Test @Order(18) + void tstzspansetEndTimestamptz_returns_nonnull() throws Exception { + String ss = spanset_as_hexwkb(tstzspanset_in( + "{[2020-01-01 00:00:00+00, 2020-01-02 00:00:00+00]," + + "[2020-03-01 00:00:00+00, 2020-03-03 00:00:00+00]}"), (byte) 0); + java.sql.Timestamp r = SpanAccessorUDFs.tstzspansetEndTimestamptz.call(ss); + assertNotNull(r, "tstzspansetEndTimestamptz must return non-null"); + } + + // ------------------------------------------------------------------ + // spansetSpanN + // ------------------------------------------------------------------ + + @Test @Order(19) + void spansetSpanN_first_span_returns_nonnull() throws Exception { + String r = SpanAccessorUDFs.spansetSpanN.call(INTSPANSET_HEX, 1); + assertNotNull(r, "spansetSpanN(1) must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(20) + void spansetSpanN_second_span_returns_nonnull() throws Exception { + String r = SpanAccessorUDFs.spansetSpanN.call(INTSPANSET_HEX, 2); + assertNotNull(r, "spansetSpanN(2) must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(21) + void spansetSpanN_two_spans_differ() throws Exception { + String s1 = SpanAccessorUDFs.spansetSpanN.call(INTSPANSET_HEX, 1); + String s2 = SpanAccessorUDFs.spansetSpanN.call(INTSPANSET_HEX, 2); + assertNotNull(s1); + assertNotNull(s2); + assertNotEquals(s1, s2, "Span 1 and span 2 of a 2-span spanset must differ"); + } + + @Test @Order(22) + void spansetSpanN_null_returns_null() throws Exception { + assertNull(SpanAccessorUDFs.spansetSpanN.call(null, 1)); + assertNull(SpanAccessorUDFs.spansetSpanN.call(INTSPANSET_HEX, null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/SpanAccessorUDFsTest.java b/src/test/java/org/mobilitydb/spark/temporal/SpanAccessorUDFsTest.java new file mode 100644 index 00000000..ddc0999d --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/SpanAccessorUDFsTest.java @@ -0,0 +1,185 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for SpanAccessorUDFs β€” span/spanset/set bound and count accessors. + * + * Tests run directly against MEOS via JMEOS without a Spark session. + * MEOS function authority: meos/include/meos.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class SpanAccessorUDFsTest { + + private static String INTSPAN_1_10; + private static String INTSPAN_5_15; + private static String FLOATSPAN; + private static String BIGINTSPAN; + private static String DATESPAN; + private static String TSTZSPAN; + private static String INTSPANSET; + private static String INTSET; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + INTSPAN_1_10 = span_as_hexwkb(intspan_in("[1,10)"), (byte) 0); + INTSPAN_5_15 = span_as_hexwkb(intspan_in("[5,15)"), (byte) 0); + FLOATSPAN = span_as_hexwkb(floatspan_in("[1.5,4.5)"), (byte) 0); + BIGINTSPAN = span_as_hexwkb(bigintspan_in("[100,200)"), (byte) 0); + DATESPAN = span_as_hexwkb(datespan_in("[2020-01-01,2020-02-01)"), (byte) 0); + TSTZSPAN = span_as_hexwkb(tstzspan_in("[2020-01-01 00:00:00+00,2020-01-02 00:00:00+00)"), (byte) 0); + INTSPANSET = spanset_as_hexwkb(intspanset_in("{[1,5),[10,20)}"), (byte) 0); + INTSET = set_as_hexwkb(intset_in("{2,4,6,8}"), (byte) 0); + } + + @Test @Order(1) + void intspanLower_returns_one() throws Exception { + assertEquals(1, SpanAccessorUDFs.intspanLower.call(INTSPAN_1_10)); + } + + @Test @Order(2) + void intspanUpper_returns_ten() throws Exception { + assertEquals(10, SpanAccessorUDFs.intspanUpper.call(INTSPAN_1_10)); + } + + @Test @Order(3) + void intspanWidth_returns_nine() throws Exception { + assertEquals(9, SpanAccessorUDFs.intspanWidth.call(INTSPAN_1_10)); + } + + @Test @Order(4) + void intspanLower_null_input_returns_null() throws Exception { + assertNull(SpanAccessorUDFs.intspanLower.call(null)); + } + + @Test @Order(5) + void floatspanLower_returns_correct_value() throws Exception { + Double lo = SpanAccessorUDFs.floatspanLower.call(FLOATSPAN); + assertNotNull(lo); + assertEquals(1.5, lo, 1e-9); + } + + @Test @Order(6) + void floatspanUpper_returns_correct_value() throws Exception { + Double hi = SpanAccessorUDFs.floatspanUpper.call(FLOATSPAN); + assertNotNull(hi); + assertEquals(4.5, hi, 1e-9); + } + + @Test @Order(7) + void floatspanWidth_returns_three() throws Exception { + Double w = SpanAccessorUDFs.floatspanWidth.call(FLOATSPAN); + assertNotNull(w); + assertEquals(3.0, w, 1e-9); + } + + @Test @Order(8) + void bigintspanLower_returns_100() throws Exception { + assertEquals(100L, SpanAccessorUDFs.bigintspanLower.call(BIGINTSPAN)); + } + + @Test @Order(9) + void bigintspanUpper_returns_200() throws Exception { + assertEquals(200L, SpanAccessorUDFs.bigintspanUpper.call(BIGINTSPAN)); + } + + @Test @Order(10) + void datespanLower_returns_2020_01_01() throws Exception { + java.sql.Date d = SpanAccessorUDFs.datespanLower.call(DATESPAN); + assertNotNull(d); + assertEquals("2020-01-01", d.toString()); + } + + @Test @Order(11) + void datespanUpper_returns_2020_02_01() throws Exception { + java.sql.Date d = SpanAccessorUDFs.datespanUpper.call(DATESPAN); + assertNotNull(d); + assertEquals("2020-02-01", d.toString()); + } + + @Test @Order(12) + void tstzspanLower_returns_2020_01_01() throws Exception { + java.sql.Timestamp ts = SpanAccessorUDFs.tstzspanLower.call(TSTZSPAN); + assertNotNull(ts); + assertTrue(ts.toInstant().toString().startsWith("2020-01-01"), + "Expected 2020-01-01, got: " + ts.toInstant()); + } + + @Test @Order(13) + void tstzspanUpper_returns_2020_01_02() throws Exception { + java.sql.Timestamp ts = SpanAccessorUDFs.tstzspanUpper.call(TSTZSPAN); + assertNotNull(ts); + assertTrue(ts.toInstant().toString().startsWith("2020-01-02"), + "Expected 2020-01-02, got: " + ts.toInstant()); + } + + @Test @Order(14) + void spanLowerInc_closed_lower() throws Exception { + assertTrue(SpanAccessorUDFs.spanLowerInc.call(INTSPAN_1_10)); + } + + @Test @Order(15) + void spanUpperInc_open_upper_returns_false() throws Exception { + assertFalse(SpanAccessorUDFs.spanUpperInc.call(INTSPAN_1_10)); + } + + @Test @Order(16) + void spansetNumSpans_returns_two() throws Exception { + assertEquals(2, SpanAccessorUDFs.spansetNumSpans.call(INTSPANSET)); + } + + @Test @Order(17) + void spansetStartSpan_returns_non_null_hex() throws Exception { + String start = SpanAccessorUDFs.spansetStartSpan.call(INTSPANSET); + assertNotNull(start, "spansetStartSpan should return a span hex-WKB"); + assertFalse(start.isBlank()); + } + + @Test @Order(18) + void spansetEndSpan_lower_bound_is_10() throws Exception { + String end = SpanAccessorUDFs.spansetEndSpan.call(INTSPANSET); + assertNotNull(end); + assertEquals(10, SpanAccessorUDFs.intspanLower.call(end)); + } + + @Test @Order(19) + void setNumValues_returns_four() throws Exception { + assertEquals(4, SpanAccessorUDFs.setNumValues.call(INTSET)); + } + + @Test @Order(20) + void setNumValues_null_returns_null() throws Exception { + assertNull(SpanAccessorUDFs.setNumValues.call(null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/SpanAlgebraUDFsExtTest.java b/src/test/java/org/mobilitydb/spark/temporal/SpanAlgebraUDFsExtTest.java new file mode 100644 index 00000000..f7282cc4 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/SpanAlgebraUDFsExtTest.java @@ -0,0 +1,378 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import java.sql.Timestamp; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for SpanAlgebraUDFs new functions: + * intspanToFloatspan, floatspanToIntspan, + * datespanToTstzspan, tstzspanToDatespan, + * intsetToFloatset, floatsetToIntset, + * setToSpan, setToSpanset, + * tstzspanDuration, datespanDuration, + * tstzspanShiftScale, tstzspansetShiftScale, + * timestamptzToSpan, timestamptzToSet. + * + * MEOS function authority: meos/include/meos.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class SpanAlgebraUDFsExtTest { + + private static String INTSPAN_HEX; + private static String FLOATSPAN_HEX; + private static String TSTZSPAN_HEX; + private static String DATESPAN_HEX; + private static String INTSET_HEX; + private static String FLOATSET_HEX; + private static String TSTZSPANSET_HEX; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + INTSPAN_HEX = span_as_hexwkb(intspan_in("[1, 10]"), (byte) 0); + FLOATSPAN_HEX = span_as_hexwkb(floatspan_in("[1.0, 10.0]"), (byte) 0); + TSTZSPAN_HEX = span_as_hexwkb(tstzspan_in( + "[2020-01-01 00:00:00+00, 2020-01-03 00:00:00+00]"), (byte) 0); + DATESPAN_HEX = span_as_hexwkb(datespan_in("[2020-01-01, 2020-01-03]"), (byte) 0); + INTSET_HEX = set_as_hexwkb(intset_in("{1, 2, 3, 5}"), (byte) 0); + FLOATSET_HEX = set_as_hexwkb(floatset_in("{1.0, 2.0, 3.0}"), (byte) 0); + TSTZSPANSET_HEX = spanset_as_hexwkb(tstzspanset_in( + "{[2020-01-01 00:00:00+00, 2020-01-02 00:00:00+00]," + + "[2020-02-01 00:00:00+00, 2020-02-03 00:00:00+00]}"), (byte) 0); + } + + // ------------------------------------------------------------------ + // intspanToFloatspan / floatspanToIntspan + // ------------------------------------------------------------------ + + @Test @Order(1) + void intspanToFloatspan_returns_nonnull() throws Exception { + String r = SpanAlgebraUDFs.intspanToFloatspan.call(INTSPAN_HEX); + assertNotNull(r, "intspanToFloatspan must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(2) + void intspanToFloatspan_null_returns_null() throws Exception { + assertNull(SpanAlgebraUDFs.intspanToFloatspan.call(null)); + } + + @Test @Order(3) + void floatspanToIntspan_returns_nonnull() throws Exception { + String r = SpanAlgebraUDFs.floatspanToIntspan.call(FLOATSPAN_HEX); + assertNotNull(r, "floatspanToIntspan must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(4) + void floatspanToIntspan_null_returns_null() throws Exception { + assertNull(SpanAlgebraUDFs.floatspanToIntspan.call(null)); + } + + // ------------------------------------------------------------------ + // datespanToTstzspan / tstzspanToDatespan + // ------------------------------------------------------------------ + + @Test @Order(5) + void datespanToTstzspan_returns_nonnull() throws Exception { + String r = SpanAlgebraUDFs.datespanToTstzspan.call(DATESPAN_HEX); + assertNotNull(r, "datespanToTstzspan must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(6) + void datespanToTstzspan_null_returns_null() throws Exception { + assertNull(SpanAlgebraUDFs.datespanToTstzspan.call(null)); + } + + @Test @Order(7) + void tstzspanToDatespan_returns_nonnull() throws Exception { + String r = SpanAlgebraUDFs.tstzspanToDatespan.call(TSTZSPAN_HEX); + assertNotNull(r, "tstzspanToDatespan must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(8) + void tstzspanToDatespan_null_returns_null() throws Exception { + assertNull(SpanAlgebraUDFs.tstzspanToDatespan.call(null)); + } + + // ------------------------------------------------------------------ + // intsetToFloatset / floatsetToIntset + // ------------------------------------------------------------------ + + @Test @Order(9) + void intsetToFloatset_returns_nonnull() throws Exception { + String r = SpanAlgebraUDFs.intsetToFloatset.call(INTSET_HEX); + assertNotNull(r, "intsetToFloatset must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(10) + void intsetToFloatset_null_returns_null() throws Exception { + assertNull(SpanAlgebraUDFs.intsetToFloatset.call(null)); + } + + @Test @Order(11) + void floatsetToIntset_returns_nonnull() throws Exception { + String r = SpanAlgebraUDFs.floatsetToIntset.call(FLOATSET_HEX); + assertNotNull(r, "floatsetToIntset must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(12) + void floatsetToIntset_null_returns_null() throws Exception { + assertNull(SpanAlgebraUDFs.floatsetToIntset.call(null)); + } + + // ------------------------------------------------------------------ + // setToSpan / setToSpanset + // ------------------------------------------------------------------ + + @Test @Order(13) + void setToSpan_returns_nonnull() throws Exception { + String r = SpanAlgebraUDFs.setToSpan.call(INTSET_HEX); + assertNotNull(r, "setToSpan must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(14) + void setToSpan_null_returns_null() throws Exception { + assertNull(SpanAlgebraUDFs.setToSpan.call(null)); + } + + @Test @Order(15) + void setToSpanset_returns_nonnull() throws Exception { + String r = SpanAlgebraUDFs.setToSpanset.call(INTSET_HEX); + assertNotNull(r, "setToSpanset must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(16) + void setToSpanset_null_returns_null() throws Exception { + assertNull(SpanAlgebraUDFs.setToSpanset.call(null)); + } + + // ------------------------------------------------------------------ + // tstzspanDuration / datespanDuration + // ------------------------------------------------------------------ + + @Test @Order(17) + void tstzspanDuration_returns_nonnull_string() throws Exception { + String r = SpanAlgebraUDFs.tstzspanDuration.call(TSTZSPAN_HEX); + assertNotNull(r, "tstzspanDuration must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(18) + void tstzspanDuration_null_returns_null() throws Exception { + assertNull(SpanAlgebraUDFs.tstzspanDuration.call(null)); + } + + @Test @Order(19) + void datespanDuration_returns_nonnull_string() throws Exception { + String r = SpanAlgebraUDFs.datespanDuration.call(DATESPAN_HEX); + assertNotNull(r, "datespanDuration must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(20) + void datespanDuration_null_returns_null() throws Exception { + assertNull(SpanAlgebraUDFs.datespanDuration.call(null)); + } + + // ------------------------------------------------------------------ + // tstzspanShiftScale / tstzspansetShiftScale + // ------------------------------------------------------------------ + + @Test @Order(21) + void tstzspanShiftScale_shift_returns_nonnull() throws Exception { + String r = SpanAlgebraUDFs.tstzspanShiftScale.call(TSTZSPAN_HEX, "1 day", null); + assertNotNull(r, "tstzspanShiftScale(shift) must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(22) + void tstzspanShiftScale_null_returns_null() throws Exception { + assertNull(SpanAlgebraUDFs.tstzspanShiftScale.call(null, "1 day", null)); + assertNull(SpanAlgebraUDFs.tstzspanShiftScale.call(TSTZSPAN_HEX, null, null)); + } + + @Test @Order(23) + void tstzspansetShiftScale_shift_returns_nonnull() throws Exception { + String r = SpanAlgebraUDFs.tstzspansetShiftScale.call(TSTZSPANSET_HEX, "1 day", null); + assertNotNull(r, "tstzspansetShiftScale(shift) must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(24) + void tstzspansetShiftScale_null_returns_null() throws Exception { + assertNull(SpanAlgebraUDFs.tstzspansetShiftScale.call(null, "1 day", null)); + } + + // ------------------------------------------------------------------ + // timestamptzToSpan / timestamptzToSet + // ------------------------------------------------------------------ + + @Test @Order(25) + void timestamptzToSpan_returns_nonnull() throws Exception { + Timestamp ts = new Timestamp(1577836800000L); // 2020-01-01 00:00:00 UTC + String r = SpanAlgebraUDFs.timestamptzToSpan.call(ts); + assertNotNull(r, "timestamptzToSpan must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(26) + void timestamptzToSpan_null_returns_null() throws Exception { + assertNull(SpanAlgebraUDFs.timestamptzToSpan.call(null)); + } + + @Test @Order(27) + void timestamptzToSet_returns_nonnull() throws Exception { + Timestamp ts = new Timestamp(1577836800000L); // 2020-01-01 00:00:00 UTC + String r = SpanAlgebraUDFs.timestamptzToSet.call(ts); + assertNotNull(r, "timestamptzToSet must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(28) + void timestamptzToSet_null_returns_null() throws Exception { + assertNull(SpanAlgebraUDFs.timestamptzToSet.call(null)); + } + + // ------------------------------------------------------------------ + // Cross-type spanset Γ— span algebra + // ------------------------------------------------------------------ + + @Test @Order(29) + void spansetIntersectionSpan_overlapping_returns_nonnull() throws Exception { + // TSTZSPANSET_HEX has two periods; intersect with the first span + String overlapSpan = span_as_hexwkb(tstzspan_in( + "[2020-01-01 06:00:00+00, 2020-01-01 18:00:00+00]"), (byte) 0); + String r = SpanAlgebraUDFs.spansetIntersectionSpan.call(TSTZSPANSET_HEX, overlapSpan); + assertNotNull(r, "spansetIntersectionSpan must return non-null for overlap"); + assertFalse(r.isBlank()); + } + + @Test @Order(30) + void spansetUnionSpan_returns_nonnull() throws Exception { + String addSpan = span_as_hexwkb(tstzspan_in( + "[2020-03-01 00:00:00+00, 2020-03-03 00:00:00+00]"), (byte) 0); + String r = SpanAlgebraUDFs.spansetUnionSpan.call(TSTZSPANSET_HEX, addSpan); + assertNotNull(r, "spansetUnionSpan must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(31) + void spansetMinusSpan_removes_overlap() throws Exception { + // remove a chunk from within the first period + String removeSpan = span_as_hexwkb(tstzspan_in( + "[2020-01-01 06:00:00+00, 2020-01-01 18:00:00+00]"), (byte) 0); + String r = SpanAlgebraUDFs.spansetMinusSpan.call(TSTZSPANSET_HEX, removeSpan); + assertNotNull(r, "spansetMinusSpan must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(32) + void spansetIntersectionSpan_null_returns_null() throws Exception { + assertNull(SpanAlgebraUDFs.spansetIntersectionSpan.call(null, TSTZSPAN_HEX)); + assertNull(SpanAlgebraUDFs.spansetIntersectionSpan.call(TSTZSPANSET_HEX, null)); + } + + // ------------------------------------------------------------------ + // Scalar singleton constructors + // ------------------------------------------------------------------ + + @Test @Order(33) + void intToSpan_returns_nonnull() throws Exception { + String r = SpanAlgebraUDFs.intToSpan.call(5); + assertNotNull(r, "intToSpan(5) must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(34) + void intToSet_returns_nonnull() throws Exception { + String r = SpanAlgebraUDFs.intToSet.call(5); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(35) + void intToSpanset_returns_nonnull() throws Exception { + String r = SpanAlgebraUDFs.intToSpanset.call(5); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(36) + void floatToSpan_returns_nonnull() throws Exception { + String r = SpanAlgebraUDFs.floatToSpan.call(3.14); + assertNotNull(r, "floatToSpan(3.14) must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(37) + void floatToSet_returns_nonnull() throws Exception { + String r = SpanAlgebraUDFs.floatToSet.call(3.14); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(38) + void floatToSpanset_returns_nonnull() throws Exception { + String r = SpanAlgebraUDFs.floatToSpanset.call(3.14); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(39) + void intToTbox_returns_nonnull() throws Exception { + String r = SpanAlgebraUDFs.intToTbox.call(42); + assertNotNull(r, "intToTbox(42) must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(40) + void floatToTbox_returns_nonnull() throws Exception { + String r = SpanAlgebraUDFs.floatToTbox.call(2.5); + assertNotNull(r, "floatToTbox(2.5) must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(41) + void intToSpan_null_returns_null() throws Exception { + assertNull(SpanAlgebraUDFs.intToSpan.call(null)); + assertNull(SpanAlgebraUDFs.floatToSpan.call(null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/SpansetOpsUDFsTest.java b/src/test/java/org/mobilitydb/spark/temporal/SpansetOpsUDFsTest.java new file mode 100644 index 00000000..d760edf3 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/SpansetOpsUDFsTest.java @@ -0,0 +1,217 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for cross-type Span Γ— Spanset positional/topological UDFs. + * + * Fixtures (all floatspan / floatspanset for arithmetic semantics): + * SPAN_LO β€” floatspan [0, 5) + * SPAN_HI β€” floatspan [10, 15) + * SPANSET_LO β€” floatspanset { [0,5), [6,8) } + * SPANSET_HI β€” floatspanset { [10,12), [13,15) } + * + * MEOS function authority: meos/include/meos.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class SpansetOpsUDFsTest { + + private static String SPAN_LO; + private static String SPAN_HI; + private static String SPANSET_LO; + private static String SPANSET_HI; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + SPAN_LO = span_as_hexwkb(floatspan_in("[0, 5)"), (byte) 0); + SPAN_HI = span_as_hexwkb(floatspan_in("[10, 15)"), (byte) 0); + SPANSET_LO = spanset_as_hexwkb(floatspanset_in("{[0,5), [6,8)}"), (byte) 0); + SPANSET_HI = spanset_as_hexwkb(floatspanset_in("{[10,12), [13,15)}"), (byte) 0); + } + + // ------------------------------------------------------------------ + // Null guards + // ------------------------------------------------------------------ + + @Test @Order(1) + void spanLeftSpanset_null_returns_null() throws Exception { + assertNull(SpansetOpsUDFs.spanLeftSpanset.call(null, SPANSET_HI)); + assertNull(SpansetOpsUDFs.spanLeftSpanset.call(SPAN_LO, null)); + } + + @Test @Order(2) + void spansetLeftSpan_null_returns_null() throws Exception { + assertNull(SpansetOpsUDFs.spansetLeftSpan.call(null, SPAN_HI)); + assertNull(SpansetOpsUDFs.spansetLeftSpan.call(SPANSET_LO, null)); + } + + @Test @Order(3) + void spansetLeftSpanset_null_returns_null() throws Exception { + assertNull(SpansetOpsUDFs.spansetLeftSpanset.call(null, SPANSET_HI)); + assertNull(SpansetOpsUDFs.spansetLeftSpanset.call(SPANSET_LO, null)); + } + + // ------------------------------------------------------------------ + // span Γ— spanset + // ------------------------------------------------------------------ + + @Test @Order(4) + void spanLeftSpanset_low_left_of_high() throws Exception { + assertTrue(SpansetOpsUDFs.spanLeftSpanset.call(SPAN_LO, SPANSET_HI)); + } + + @Test @Order(5) + void spanLeftSpanset_high_not_left_of_low() throws Exception { + assertFalse(SpansetOpsUDFs.spanLeftSpanset.call(SPAN_HI, SPANSET_LO)); + } + + @Test @Order(6) + void spanRightSpanset_high_right_of_low() throws Exception { + assertTrue(SpansetOpsUDFs.spanRightSpanset.call(SPAN_HI, SPANSET_LO)); + } + + @Test @Order(7) + void spanOverlapsSpanset_overlap_returns_true() throws Exception { + assertTrue(SpansetOpsUDFs.spanOverlapsSpanset.call(SPAN_LO, SPANSET_LO)); + } + + @Test @Order(8) + void spanOverlapsSpanset_disjoint_returns_false() throws Exception { + assertFalse(SpansetOpsUDFs.spanOverlapsSpanset.call(SPAN_LO, SPANSET_HI)); + } + + @Test @Order(9) + void spanContainsSpanset_returns_nonnull() throws Exception { + assertNotNull(SpansetOpsUDFs.spanContainsSpanset.call(SPAN_LO, SPANSET_LO)); + } + + @Test @Order(10) + void spanContainedSpanset_self_returns_true() throws Exception { + assertTrue(SpansetOpsUDFs.spanContainedSpanset.call(SPAN_LO, SPANSET_LO), + "[0,5) is contained in {[0,5), [6,8)}"); + } + + @Test @Order(11) + void spanAdjacentSpanset_returns_nonnull() throws Exception { + assertNotNull(SpansetOpsUDFs.spanAdjacentSpanset.call(SPAN_LO, SPANSET_HI)); + } + + @Test @Order(12) + void spanOverleftSpanset_low_overleft_of_high() throws Exception { + assertTrue(SpansetOpsUDFs.spanOverleftSpanset.call(SPAN_LO, SPANSET_HI)); + } + + // ------------------------------------------------------------------ + // spanset Γ— span + // ------------------------------------------------------------------ + + @Test @Order(13) + void spansetLeftSpan_low_left_of_high() throws Exception { + assertTrue(SpansetOpsUDFs.spansetLeftSpan.call(SPANSET_LO, SPAN_HI)); + } + + @Test @Order(14) + void spansetRightSpan_high_right_of_low() throws Exception { + assertTrue(SpansetOpsUDFs.spansetRightSpan.call(SPANSET_HI, SPAN_LO)); + } + + @Test @Order(15) + void spansetOverlapsSpan_overlap_returns_true() throws Exception { + assertTrue(SpansetOpsUDFs.spansetOverlapsSpan.call(SPANSET_LO, SPAN_LO)); + } + + @Test @Order(16) + void spansetOverlapsSpan_disjoint_returns_false() throws Exception { + assertFalse(SpansetOpsUDFs.spansetOverlapsSpan.call(SPANSET_LO, SPAN_HI)); + } + + @Test @Order(17) + void spansetContainedSpan_returns_nonnull() throws Exception { + assertNotNull(SpansetOpsUDFs.spansetContainedSpan.call(SPANSET_LO, SPAN_LO)); + } + + @Test @Order(18) + void spansetAdjacentSpan_returns_nonnull() throws Exception { + assertNotNull(SpansetOpsUDFs.spansetAdjacentSpan.call(SPANSET_LO, SPAN_HI)); + } + + @Test @Order(19) + void spansetOverleftSpan_returns_nonnull() throws Exception { + assertNotNull(SpansetOpsUDFs.spansetOverleftSpan.call(SPANSET_LO, SPAN_HI)); + } + + @Test @Order(20) + void spansetOverrightSpan_returns_nonnull() throws Exception { + assertNotNull(SpansetOpsUDFs.spansetOverrightSpan.call(SPANSET_HI, SPAN_LO)); + } + + // ------------------------------------------------------------------ + // spanset Γ— spanset + // ------------------------------------------------------------------ + + @Test @Order(21) + void spansetLeftSpanset_low_left_of_high() throws Exception { + assertTrue(SpansetOpsUDFs.spansetLeftSpanset.call(SPANSET_LO, SPANSET_HI)); + } + + @Test @Order(22) + void spansetRightSpanset_high_right_of_low() throws Exception { + assertTrue(SpansetOpsUDFs.spansetRightSpanset.call(SPANSET_HI, SPANSET_LO)); + } + + @Test @Order(23) + void spansetContainsSpanset_self_returns_true() throws Exception { + assertTrue(SpansetOpsUDFs.spansetContainsSpanset.call(SPANSET_LO, SPANSET_LO)); + } + + @Test @Order(24) + void spansetContainedSpanset_self_returns_true() throws Exception { + assertTrue(SpansetOpsUDFs.spansetContainedSpanset.call(SPANSET_LO, SPANSET_LO)); + } + + @Test @Order(25) + void spansetAdjacentSpanset_returns_nonnull() throws Exception { + assertNotNull(SpansetOpsUDFs.spansetAdjacentSpanset.call(SPANSET_LO, SPANSET_HI)); + } + + @Test @Order(26) + void spansetOverleftSpanset_low_overleft_of_high() throws Exception { + assertTrue(SpansetOpsUDFs.spansetOverleftSpanset.call(SPANSET_LO, SPANSET_HI)); + } + + @Test @Order(27) + void spansetOverrightSpanset_high_overright_of_low() throws Exception { + assertTrue(SpansetOpsUDFs.spansetOverrightSpanset.call(SPANSET_HI, SPANSET_LO)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/TBoxOpsUDFsTest.java b/src/test/java/org/mobilitydb/spark/temporal/TBoxOpsUDFsTest.java new file mode 100644 index 00000000..97a206dc --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/TBoxOpsUDFsTest.java @@ -0,0 +1,258 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for cross-type TBox Γ— TNumber positional/topological UDFs. + * + * Fixtures: + * TBOX_LO β€” TBOXFLOAT XT([0,5],[2020-01-01,2020-01-02]) β€” value range [0,5] + * TBOX_HI β€” TBOXFLOAT XT([10,15],[2020-01-03,2020-01-04]) β€” value range [10,15] + * TFLOAT_LO β€” tfloat trip 0β†’5 over 2020-01-01 β†’ 2020-01-02 + * + * MEOS function authority: meos/include/meos.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class TBoxOpsUDFsTest { + + private static String TBOX_LO; + private static String TBOX_HI; + private static String TFLOAT_LO; + + @BeforeAll + static void initMeos() throws Exception { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TBOX_LO = ConstructorUDFs.tbox.call( + "TBOXFLOAT XT([0,5],[2020-01-01 00:00:00+00,2020-01-02 00:00:00+00])"); + TBOX_HI = ConstructorUDFs.tbox.call( + "TBOXFLOAT XT([10,15],[2020-01-03 00:00:00+00,2020-01-04 00:00:00+00])"); + TFLOAT_LO = temporal_as_hexwkb( + tfloat_in("[0.0@2020-01-01 00:00:00+00, 5.0@2020-01-02 00:00:00+00]"), + (byte) 0); + } + + // ------------------------------------------------------------------ + // Null guards + // ------------------------------------------------------------------ + + @Test @Order(1) + void tboxLeftTbox_null_returns_null() throws Exception { + assertNull(TBoxOpsUDFs.tboxLeftTbox.call(null, TBOX_HI)); + assertNull(TBoxOpsUDFs.tboxLeftTbox.call(TBOX_LO, null)); + } + + @Test @Order(2) + void tboxLeftTnumber_null_returns_null() throws Exception { + assertNull(TBoxOpsUDFs.tboxLeftTnumber.call(null, TFLOAT_LO)); + assertNull(TBoxOpsUDFs.tboxLeftTnumber.call(TBOX_LO, null)); + } + + @Test @Order(3) + void tnumberLeftTbox_null_returns_null() throws Exception { + assertNull(TBoxOpsUDFs.tnumberLeftTbox.call(null, TBOX_HI)); + assertNull(TBoxOpsUDFs.tnumberLeftTbox.call(TFLOAT_LO, null)); + } + + // ------------------------------------------------------------------ + // tbox Γ— tbox β€” semantic correctness + // ------------------------------------------------------------------ + + @Test @Order(4) + void tboxLeftTbox_low_is_left_of_high() throws Exception { + assertTrue(TBoxOpsUDFs.tboxLeftTbox.call(TBOX_LO, TBOX_HI), + "TBOX_LO (val [0,5]) must be left of TBOX_HI (val [10,15])"); + } + + @Test @Order(5) + void tboxLeftTbox_high_not_left_of_low() throws Exception { + assertFalse(TBoxOpsUDFs.tboxLeftTbox.call(TBOX_HI, TBOX_LO)); + } + + @Test @Order(6) + void tboxRightTbox_high_is_right_of_low() throws Exception { + assertTrue(TBoxOpsUDFs.tboxRightTbox.call(TBOX_HI, TBOX_LO)); + } + + @Test @Order(7) + void tboxBeforeTbox_low_is_before_high() throws Exception { + assertTrue(TBoxOpsUDFs.tboxBeforeTbox.call(TBOX_LO, TBOX_HI), + "TBOX_LO time precedes TBOX_HI time"); + } + + @Test @Order(8) + void tboxAfterTbox_high_is_after_low() throws Exception { + assertTrue(TBoxOpsUDFs.tboxAfterTbox.call(TBOX_HI, TBOX_LO)); + } + + @Test @Order(9) + void tboxOverlapsTbox_disjoint_returns_false() throws Exception { + assertFalse(TBoxOpsUDFs.tboxOverlapsTbox.call(TBOX_LO, TBOX_HI)); + } + + @Test @Order(10) + void tboxOverlapsTbox_self_returns_true() throws Exception { + assertTrue(TBoxOpsUDFs.tboxOverlapsTbox.call(TBOX_LO, TBOX_LO)); + } + + @Test @Order(11) + void tboxSameTbox_self_returns_true() throws Exception { + assertTrue(TBoxOpsUDFs.tboxSameTbox.call(TBOX_LO, TBOX_LO)); + } + + @Test @Order(12) + void tboxContainsTbox_self_returns_true() throws Exception { + assertTrue(TBoxOpsUDFs.tboxContainsTbox.call(TBOX_LO, TBOX_LO)); + } + + @Test @Order(13) + void tboxContainedTbox_self_returns_true() throws Exception { + assertTrue(TBoxOpsUDFs.tboxContainedTbox.call(TBOX_LO, TBOX_LO)); + } + + @Test @Order(14) + void tboxAdjacentTbox_disjoint_in_value_returns_nonnull() throws Exception { + // Disjoint boxes are not adjacent (gap between [0,5] and [10,15]) + assertNotNull(TBoxOpsUDFs.tboxAdjacentTbox.call(TBOX_LO, TBOX_HI)); + } + + @Test @Order(15) + void tboxOverleftTbox_low_is_overleft_of_high() throws Exception { + // [0,5] is overleft (i.e., does not extend right) of [10,15] + assertTrue(TBoxOpsUDFs.tboxOverleftTbox.call(TBOX_LO, TBOX_HI)); + } + + @Test @Order(16) + void tboxOverbeforeTbox_low_is_overbefore_high() throws Exception { + assertTrue(TBoxOpsUDFs.tboxOverbeforeTbox.call(TBOX_LO, TBOX_HI)); + } + + // ------------------------------------------------------------------ + // tbox Γ— tnumber β€” semantic correctness + // ------------------------------------------------------------------ + + @Test @Order(17) + void tboxOverlapsTnumber_self_returns_true() throws Exception { + // TBOX_LO covers tfloat 0β†’5 over the same time window + assertTrue(TBoxOpsUDFs.tboxOverlapsTnumber.call(TBOX_LO, TFLOAT_LO)); + } + + @Test @Order(18) + void tboxLeftTnumber_high_not_left_of_low_tnumber() throws Exception { + // TBOX_HI (val [10,15]) is NOT left of TFLOAT_LO (val [0,5]) + assertFalse(TBoxOpsUDFs.tboxLeftTnumber.call(TBOX_HI, TFLOAT_LO)); + } + + @Test @Order(19) + void tboxContainsTnumber_overlap_returns_nonnull() throws Exception { + assertNotNull(TBoxOpsUDFs.tboxContainsTnumber.call(TBOX_LO, TFLOAT_LO)); + } + + @Test @Order(20) + void tboxBeforeTnumber_high_not_before_low() throws Exception { + // TBOX_HI starts 2020-01-03; TFLOAT_LO ends 2020-01-02. So TBOX_HI is AFTER, not BEFORE. + assertFalse(TBoxOpsUDFs.tboxBeforeTnumber.call(TBOX_HI, TFLOAT_LO)); + } + + @Test @Order(21) + void tboxAfterTnumber_high_is_after_low_tnumber() throws Exception { + assertTrue(TBoxOpsUDFs.tboxAfterTnumber.call(TBOX_HI, TFLOAT_LO)); + } + + // ------------------------------------------------------------------ + // tnumber Γ— tbox β€” semantic correctness + // ------------------------------------------------------------------ + + @Test @Order(22) + void tnumberOverlapsTbox_self_returns_true() throws Exception { + assertTrue(TBoxOpsUDFs.tnumberOverlapsTbox.call(TFLOAT_LO, TBOX_LO)); + } + + @Test @Order(23) + void tnumberLeftTbox_low_is_left_of_high() throws Exception { + // TFLOAT_LO (val [0,5]) is left of TBOX_HI (val [10,15]) + assertTrue(TBoxOpsUDFs.tnumberLeftTbox.call(TFLOAT_LO, TBOX_HI)); + } + + @Test @Order(24) + void tnumberBeforeTbox_low_is_before_high() throws Exception { + assertTrue(TBoxOpsUDFs.tnumberBeforeTbox.call(TFLOAT_LO, TBOX_HI)); + } + + @Test @Order(25) + void tnumberContainedTbox_self_returns_nonnull() throws Exception { + assertNotNull(TBoxOpsUDFs.tnumberContainedTbox.call(TFLOAT_LO, TBOX_LO)); + } + + @Test @Order(26) + void tnumberOverrightTbox_low_not_overright_of_high() throws Exception { + // TFLOAT_LO (val [0,5]) does NOT extend past the right of TBOX_HI (val [10,15]) + assertFalse(TBoxOpsUDFs.tnumberOverrightTbox.call(TFLOAT_LO, TBOX_HI)); + } + + @Test @Order(27) + void tnumberSameTbox_self_returns_nonnull() throws Exception { + assertNotNull(TBoxOpsUDFs.tnumberSameTbox.call(TFLOAT_LO, TBOX_LO)); + } + + // ------------------------------------------------------------------ + // tboxExpandFloat / tboxExpandInt β€” wired via MeosNative + // ------------------------------------------------------------------ + + @Test @Order(28) + void tboxExpandFloat_returns_nonnull() throws Exception { + String r = TBoxUDFs.tboxExpandFloat.call(TBOX_LO, 2.5); + assertNotNull(r, "tboxExpandFloat must return non-null hex-WKB"); + assertFalse(r.isBlank()); + } + + @Test @Order(29) + void tboxExpandFloat_null_returns_null() throws Exception { + assertNull(TBoxUDFs.tboxExpandFloat.call(null, 2.5)); + assertNull(TBoxUDFs.tboxExpandFloat.call(TBOX_LO, null)); + } + + @Test @Order(30) + void tboxExpandInt_returns_nonnull() throws Exception { + // Need an integer TBox for tintbox_expand + String tboxInt = ConstructorUDFs.tbox.call( + "TBOXINT XT([0,5],[2020-01-01 00:00:00+00,2020-01-02 00:00:00+00])"); + String r = TBoxUDFs.tboxExpandInt.call(tboxInt, 3); + assertNotNull(r, "tboxExpandInt must return non-null hex-WKB"); + assertFalse(r.isBlank()); + } + + @Test @Order(31) + void tboxExpandInt_null_returns_null() throws Exception { + assertNull(TBoxUDFs.tboxExpandInt.call(null, 3)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/TBoxUDFsExt2Test.java b/src/test/java/org/mobilitydb/spark/temporal/TBoxUDFsExt2Test.java new file mode 100644 index 00000000..e023f1da --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/TBoxUDFsExt2Test.java @@ -0,0 +1,326 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import jnr.ffi.Pointer; +import jnr.ffi.Runtime; +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for new TBoxUDFs: + * tboxMake, timestamptzToTbox, numspanTimestamptzToTbox, + * tboxExpandTime, tboxShiftScaleTime, + * tboxExpandFloat, tboxExpandInt, + * tboxfloatXmin, tboxfloatXmax, tboxintXmin, tboxintXmax. + * + * MEOS function authority: meos/include/meos.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class TBoxUDFsExt2Test { + + private static String TBOX_XT_HEX; // TBox with X and T dimensions (floatspan) + private static String TBOX_T_HEX; // TBox with T dimension only + private static String FLOATSPAN_HEX; + private static String TSTZSPAN_HEX; + private static String TBOX_INT_HEX; // TBox with X and T dimensions (intspan) + private static String INTSPAN_HEX; + private static String TBOX_FUTURE_HEX; // TBox strictly after TBOX_XT_HEX in time + private static String TBOX_HIGH_HEX; // TBox strictly right of TBOX_XT_HEX in X + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + // Build a tbox with float X + time via tnumber_to_tbox on a tfloat + Pointer tfloatPtr = tfloat_in("[1.5@2020-01-01 00:00:00+00, 9.5@2020-01-03 00:00:00+00]"); + Pointer sizeOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + TBOX_XT_HEX = tbox_as_hexwkb(tnumber_to_tbox(tfloatPtr), (byte) 0, sizeOut); + + // Build a tbox with integer X + time via tnumber_to_tbox on a tint + Pointer tintPtr = tint_in("[1@2020-01-01 00:00:00+00, 9@2020-01-03 00:00:00+00]"); + sizeOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + TBOX_INT_HEX = tbox_as_hexwkb(tnumber_to_tbox(tintPtr), (byte) 0, sizeOut); + + // T-only TBox from tstzspan + Pointer tstzspanPtr = tstzspan_in("[2020-01-01 00:00:00+00, 2020-01-03 00:00:00+00]"); + TSTZSPAN_HEX = span_as_hexwkb(tstzspanPtr, (byte) 0); + sizeOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + TBOX_T_HEX = tbox_as_hexwkb(span_to_tbox(tstzspanPtr), (byte) 0, sizeOut); + + FLOATSPAN_HEX = span_as_hexwkb(floatspan_in("[1.0, 10.0]"), (byte) 0); + INTSPAN_HEX = span_as_hexwkb(intspan_in("[1, 10]"), (byte) 0); + + // TBox strictly after TBOX_XT_HEX in time (2020-01-10 onwards) + Pointer futurePtr = tfloat_in("[1.5@2020-01-10 00:00:00+00, 1.5@2020-01-12 00:00:00+00]"); + sizeOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + TBOX_FUTURE_HEX = tbox_as_hexwkb(tnumber_to_tbox(futurePtr), (byte) 0, sizeOut); + + // TBox strictly right of TBOX_XT_HEX in X (X range [100, 200]) + Pointer highPtr = tfloat_in("[100.0@2020-01-01 00:00:00+00, 200.0@2020-01-03 00:00:00+00]"); + sizeOut = Runtime.getSystemRuntime().getMemoryManager().allocateDirect(8); + TBOX_HIGH_HEX = tbox_as_hexwkb(tnumber_to_tbox(highPtr), (byte) 0, sizeOut); + } + + // ------------------------------------------------------------------ + // tboxMake + // ------------------------------------------------------------------ + + @Test @Order(1) + void tboxMake_xt_returns_nonnull() throws Exception { + String r = TBoxUDFs.tboxMake.call(FLOATSPAN_HEX, TSTZSPAN_HEX); + assertNotNull(r, "tboxMake(floatspan, tstzspan) must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(2) + void tboxMake_t_only_returns_nonnull() throws Exception { + String r = TBoxUDFs.tboxMake.call(null, TSTZSPAN_HEX); + assertNotNull(r, "tboxMake(null, tstzspan) must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(3) + void tboxMake_both_null_returns_null() throws Exception { + assertNull(TBoxUDFs.tboxMake.call(null, null)); + } + + // ------------------------------------------------------------------ + // timestamptzToTbox + // ------------------------------------------------------------------ + + @Test @Order(4) + void timestamptzToTbox_returns_nonnull() throws Exception { + java.sql.Timestamp ts = java.sql.Timestamp.valueOf("2020-01-01 00:00:00"); + String r = TBoxUDFs.timestamptzToTbox.call(ts); + assertNotNull(r, "timestamptzToTbox must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(5) + void timestamptzToTbox_null_returns_null() throws Exception { + assertNull(TBoxUDFs.timestamptzToTbox.call((java.sql.Timestamp) null)); + } + + // ------------------------------------------------------------------ + // numspanTimestamptzToTbox + // ------------------------------------------------------------------ + + @Test @Order(6) + void numspanTimestamptzToTbox_returns_nonnull() throws Exception { + java.sql.Timestamp ts = java.sql.Timestamp.valueOf("2020-01-01 00:00:00"); + String r = TBoxUDFs.numspanTimestamptzToTbox.call(FLOATSPAN_HEX, ts); + assertNotNull(r, "numspanTimestamptzToTbox must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(7) + void numspanTimestamptzToTbox_null_returns_null() throws Exception { + java.sql.Timestamp ts = java.sql.Timestamp.valueOf("2020-01-01 00:00:00"); + assertNull(TBoxUDFs.numspanTimestamptzToTbox.call(null, ts)); + assertNull(TBoxUDFs.numspanTimestamptzToTbox.call(FLOATSPAN_HEX, null)); + } + + // ------------------------------------------------------------------ + // tboxExpandTime + // ------------------------------------------------------------------ + + @Test @Order(8) + void tboxExpandTime_returns_nonnull() throws Exception { + String r = TBoxUDFs.tboxExpandTime.call(TBOX_XT_HEX, "1 day"); + assertNotNull(r, "tboxExpandTime must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(9) + void tboxExpandTime_null_returns_null() throws Exception { + assertNull(TBoxUDFs.tboxExpandTime.call(null, "1 day")); + assertNull(TBoxUDFs.tboxExpandTime.call(TBOX_XT_HEX, null)); + } + + // ------------------------------------------------------------------ + // tboxShiftScaleTime + // ------------------------------------------------------------------ + + @Test @Order(10) + void tboxShiftScaleTime_shift_returns_nonnull() throws Exception { + String r = TBoxUDFs.tboxShiftScaleTime.call(TBOX_XT_HEX, "1 day", null); + assertNotNull(r, "tboxShiftScaleTime(shift) must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(11) + void tboxShiftScaleTime_scale_returns_nonnull() throws Exception { + String r = TBoxUDFs.tboxShiftScaleTime.call(TBOX_XT_HEX, null, "2 days"); + assertNotNull(r, "tboxShiftScaleTime(scale) must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(12) + void tboxShiftScaleTime_null_stbox_returns_null() throws Exception { + assertNull(TBoxUDFs.tboxShiftScaleTime.call(null, "1 day", null)); + } + + // ------------------------------------------------------------------ + // tboxfloatXmin / tboxfloatXmax + // ------------------------------------------------------------------ + + @Test @Order(13) + void tboxfloatXmin_returns_correct_value() throws Exception { + Double r = TBoxUDFs.tboxfloatXmin.call(TBOX_XT_HEX); + assertNotNull(r, "tboxfloatXmin must return non-null for XT tbox"); + assertEquals(1.5, r, 1e-9); + } + + @Test @Order(14) + void tboxfloatXmax_returns_correct_value() throws Exception { + Double r = TBoxUDFs.tboxfloatXmax.call(TBOX_XT_HEX); + assertNotNull(r, "tboxfloatXmax must return non-null for XT tbox"); + assertEquals(9.5, r, 1e-9); + } + + @Test @Order(15) + void tboxfloatXmin_null_returns_null() throws Exception { + assertNull(TBoxUDFs.tboxfloatXmin.call(null)); + } + + // ------------------------------------------------------------------ + // tboxintXmin / tboxintXmax + // ------------------------------------------------------------------ + + @Test @Order(16) + void tboxintXmin_returns_correct_value() throws Exception { + Integer r = TBoxUDFs.tboxintXmin.call(TBOX_INT_HEX); + assertNotNull(r, "tboxintXmin must return non-null for integer tbox"); + assertEquals(1, r); + } + + @Test @Order(17) + void tboxintXmax_returns_correct_value() throws Exception { + Integer r = TBoxUDFs.tboxintXmax.call(TBOX_INT_HEX); + assertNotNull(r, "tboxintXmax must return non-null for integer tbox"); + assertEquals(9, r); + } + + @Test @Order(18) + void tboxintXmin_null_returns_null() throws Exception { + assertNull(TBoxUDFs.tboxintXmin.call(null)); + } + + // ------------------------------------------------------------------ + // intersectionTboxTbox / unionTboxTbox + // ------------------------------------------------------------------ + + @Test @Order(19) + void intersectionTboxTbox_overlapping_boxes_returns_nonnull() throws Exception { + String r = TBoxUDFs.intersectionTboxTbox.call(TBOX_XT_HEX, TBOX_XT_HEX); + assertNotNull(r, "intersection of a box with itself must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(20) + void unionTboxTbox_returns_nonnull() throws Exception { + String r = TBoxUDFs.unionTboxTbox.call(TBOX_XT_HEX, TBOX_XT_HEX); + assertNotNull(r, "union of a box with itself must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(21) + void intersectionTboxTbox_null_returns_null() throws Exception { + assertNull(TBoxUDFs.intersectionTboxTbox.call(null, TBOX_XT_HEX)); + assertNull(TBoxUDFs.intersectionTboxTbox.call(TBOX_XT_HEX, null)); + } + + @Test @Order(22) + void unionTboxTbox_null_returns_null() throws Exception { + assertNull(TBoxUDFs.unionTboxTbox.call(null, TBOX_XT_HEX)); + } + + // ------------------------------------------------------------------ + // TBox positional predicates + // ------------------------------------------------------------------ + + @Test @Order(23) + void tboxLeft_x_left_of_high_box_returns_true() throws Exception { + assertTrue(TBoxUDFs.tboxLeft.call(TBOX_XT_HEX, TBOX_HIGH_HEX), + "tbox([1.5,9.5]) is strictly left of tbox([100,200])"); + } + + @Test @Order(24) + void tboxRight_high_box_right_of_x_returns_true() throws Exception { + assertTrue(TBoxUDFs.tboxRight.call(TBOX_HIGH_HEX, TBOX_XT_HEX), + "tbox([100,200]) is strictly right of tbox([1.5,9.5])"); + } + + @Test @Order(25) + void tboxBefore_xt_before_future_returns_true() throws Exception { + assertTrue(TBoxUDFs.tboxBefore.call(TBOX_XT_HEX, TBOX_FUTURE_HEX), + "tbox ending 2020-01-03 is before tbox starting 2020-01-10"); + } + + @Test @Order(26) + void tboxAfter_future_after_xt_returns_true() throws Exception { + assertTrue(TBoxUDFs.tboxAfter.call(TBOX_FUTURE_HEX, TBOX_XT_HEX), + "tbox starting 2020-01-10 is after tbox ending 2020-01-03"); + } + + @Test @Order(27) + void tboxLeft_null_returns_null() throws Exception { + assertNull(TBoxUDFs.tboxLeft.call(null, TBOX_XT_HEX)); + assertNull(TBoxUDFs.tboxLeft.call(TBOX_XT_HEX, null)); + } + + // ------------------------------------------------------------------ + // TBox topology predicates + // ------------------------------------------------------------------ + + @Test @Order(28) + void tboxContains_same_box_returns_true() throws Exception { + assertTrue(TBoxUDFs.tboxContains.call(TBOX_XT_HEX, TBOX_XT_HEX), + "a tbox contains itself"); + } + + @Test @Order(29) + void tboxContained_same_box_returns_true() throws Exception { + assertTrue(TBoxUDFs.tboxContained.call(TBOX_XT_HEX, TBOX_XT_HEX), + "a tbox is contained in itself"); + } + + @Test @Order(30) + void tboxOverlaps_same_box_returns_true() throws Exception { + assertTrue(TBoxUDFs.tboxOverlaps.call(TBOX_XT_HEX, TBOX_XT_HEX), + "a tbox overlaps itself"); + } + + @Test @Order(31) + void tboxContains_null_returns_null() throws Exception { + assertNull(TBoxUDFs.tboxContains.call(null, TBOX_XT_HEX)); + assertNull(TBoxUDFs.tboxContains.call(TBOX_XT_HEX, null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/TBoxUDFsExtTest.java b/src/test/java/org/mobilitydb/spark/temporal/TBoxUDFsExtTest.java new file mode 100644 index 00000000..39b86c9e --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/TBoxUDFsExtTest.java @@ -0,0 +1,84 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for TBoxUDFs rounding: tboxRound. + * + * MEOS function authority: meos/include/meos.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class TBoxUDFsExtTest { + + private static String TBOX_HEX; + + @BeforeAll + static void initMeos() throws Exception { + meos_initialize(); + meos_initialize_timezone("UTC"); + + // Build a tbox via tnumber_to_tbox from a tfloat sequence with non-trivial values + TBOX_HEX = AccessorUDFs.tnumberToTbox.call( + temporal_as_hexwkb(tfloat_in("[1.123456@2020-01-01, 9.987654@2020-01-03]"), (byte) 0)); + } + + @Test @Order(1) + void tboxRound_returns_nonnull_hexwkb() throws Exception { + assertNotNull(TBOX_HEX, "TBOX must be buildable"); + String r = TBoxUDFs.tboxRound.call(TBOX_HEX, 2); + assertNotNull(r, "tboxRound must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(2) + void tboxRound_xmin_is_rounded() throws Exception { + String rounded = TBoxUDFs.tboxRound.call(TBOX_HEX, 2); + assertNotNull(rounded); + Double xmin = TBoxUDFs.tboxXmin.call(rounded); + assertNotNull(xmin); + assertEquals(1.12, xmin, 1e-9, "xmin rounded to 2 decimals must be 1.12"); + } + + @Test @Order(3) + void tboxRound_xmax_is_rounded() throws Exception { + String rounded = TBoxUDFs.tboxRound.call(TBOX_HEX, 2); + assertNotNull(rounded); + Double xmax = TBoxUDFs.tboxXmax.call(rounded); + assertNotNull(xmax); + assertEquals(9.99, xmax, 1e-9, "xmax rounded to 2 decimals must be 9.99"); + } + + @Test @Order(4) + void tboxRound_null_returns_null() throws Exception { + assertNull(TBoxUDFs.tboxRound.call(null, 2)); + assertNull(TBoxUDFs.tboxRound.call(TBOX_HEX, null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/TBoxUDFsTest.java b/src/test/java/org/mobilitydb/spark/temporal/TBoxUDFsTest.java new file mode 100644 index 00000000..f005d244 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/TBoxUDFsTest.java @@ -0,0 +1,177 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for TBoxUDFs β€” TBox accessor and span-conversion operations. + * + * MEOS function authority: meos/include/meos.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class TBoxUDFsTest { + + // TBOX XT([1,10],[2020-01-01,2020-01-10]) + private static String TBOX_XT; + // TBOX T([2020-01-01,2020-01-10]) β€” temporal only + private static String TBOX_T; + + @BeforeAll + static void initMeos() throws Exception { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TBOX_XT = ConstructorUDFs.tbox.call( + "TBOX XT([1,10],[2020-01-01 00:00:00+00,2020-01-10 00:00:00+00])"); + TBOX_T = ConstructorUDFs.tbox.call( + "TBOX T([2020-01-01 00:00:00+00,2020-01-10 00:00:00+00])"); + } + + // ------------------------------------------------------------------ + // Has-component flags + // ------------------------------------------------------------------ + + @Test @Order(1) + void tboxHasx_xt_box_returns_true() throws Exception { + assertTrue(TBoxUDFs.tboxHasx.call(TBOX_XT)); + } + + @Test @Order(2) + void tboxHasx_t_only_box_returns_false() throws Exception { + assertFalse(TBoxUDFs.tboxHasx.call(TBOX_T)); + } + + @Test @Order(3) + void tboxHast_xt_box_returns_true() throws Exception { + assertTrue(TBoxUDFs.tboxHast.call(TBOX_XT)); + } + + // ------------------------------------------------------------------ + // Numeric bound accessors + // ------------------------------------------------------------------ + + @Test @Order(4) + void tboxXmin_returns_one() throws Exception { + Double xmin = TBoxUDFs.tboxXmin.call(TBOX_XT); + assertNotNull(xmin); + assertEquals(1.0, xmin, 1e-9); + } + + @Test @Order(5) + void tboxXmax_returns_ten() throws Exception { + Double xmax = TBoxUDFs.tboxXmax.call(TBOX_XT); + assertNotNull(xmax); + assertEquals(10.0, xmax, 1e-9); + } + + @Test @Order(6) + void tboxXmin_t_only_returns_null() throws Exception { + assertNull(TBoxUDFs.tboxXmin.call(TBOX_T)); + } + + @Test @Order(7) + void tboxXminInc_closed_returns_true() throws Exception { + assertTrue(TBoxUDFs.tboxXminInc.call(TBOX_XT)); + } + + @Test @Order(8) + void tboxXmaxInc_closed_returns_true() throws Exception { + assertTrue(TBoxUDFs.tboxXmaxInc.call(TBOX_XT)); + } + + // ------------------------------------------------------------------ + // Temporal bound accessors + // ------------------------------------------------------------------ + + @Test @Order(9) + void tboxTmin_returns_2020_01_01() throws Exception { + java.sql.Timestamp ts = TBoxUDFs.tboxTmin.call(TBOX_XT); + assertNotNull(ts); + assertTrue(ts.toString().startsWith("2020-01-01"), + "Expected 2020-01-01, got: " + ts); + } + + @Test @Order(10) + void tboxTmax_returns_2020_01_10() throws Exception { + java.sql.Timestamp ts = TBoxUDFs.tboxTmax.call(TBOX_XT); + assertNotNull(ts); + assertTrue(ts.toString().startsWith("2020-01-10"), + "Expected 2020-01-10, got: " + ts); + } + + @Test @Order(11) + void tboxTminInc_closed_returns_true() throws Exception { + assertTrue(TBoxUDFs.tboxTminInc.call(TBOX_XT)); + } + + @Test @Order(12) + void tboxTmaxInc_closed_returns_true() throws Exception { + assertTrue(TBoxUDFs.tboxTmaxInc.call(TBOX_XT)); + } + + // ------------------------------------------------------------------ + // Span conversions + // ------------------------------------------------------------------ + + @Test @Order(13) + void tboxToFloatspan_returns_hex() throws Exception { + String hex = TBoxUDFs.tboxToFloatspan.call(TBOX_XT); + assertNotNull(hex); + assertFalse(hex.isBlank()); + } + + @Test @Order(14) + void tboxToTstzspan_returns_hex() throws Exception { + String hex = TBoxUDFs.tboxToTstzspan.call(TBOX_XT); + assertNotNull(hex); + assertFalse(hex.isBlank()); + } + + @Test @Order(15) + void tboxToFloatspan_t_only_returns_null() throws Exception { + assertNull(TBoxUDFs.tboxToFloatspan.call(TBOX_T)); + } + + // ------------------------------------------------------------------ + // Null-input guards + // ------------------------------------------------------------------ + + @Test @Order(16) + void null_input_returns_null() throws Exception { + assertNull(TBoxUDFs.tboxHasx.call(null)); + assertNull(TBoxUDFs.tboxHast.call(null)); + assertNull(TBoxUDFs.tboxXmin.call(null)); + assertNull(TBoxUDFs.tboxXmax.call(null)); + assertNull(TBoxUDFs.tboxTmin.call(null)); + assertNull(TBoxUDFs.tboxTmax.call(null)); + assertNull(TBoxUDFs.tboxToFloatspan.call(null)); + assertNull(TBoxUDFs.tboxToTstzspan.call(null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/TTextUDFsExt2Test.java b/src/test/java/org/mobilitydb/spark/temporal/TTextUDFsExt2Test.java new file mode 100644 index 00000000..a9998897 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/TTextUDFsExt2Test.java @@ -0,0 +1,183 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for TTextUDFs ttext comparison operators. + * + * Each comparison UDF returns a tbool hex-WKB; the start value (decoded via + * AccessorUDFs.tboolStartValue) is used to verify correctness. + * + * MEOS function authority: meos/include/meos.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class TTextUDFsExt2Test { + + /** ttext "hello" as a single instant: used as the right-hand operand. */ + private static String TTEXT_HELLO_HEX; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TTEXT_HELLO_HEX = temporal_as_hexwkb( + ttext_in("hello@2020-01-01 00:00:00+00"), (byte) 0); + } + + // ------------------------------------------------------------------ + // text op ttext + // ------------------------------------------------------------------ + + @Test @Order(1) + void teqTextTtext_equal_text_returns_true_tbool() throws Exception { + String r = TTextUDFs.teqTextTtext.call("hello", TTEXT_HELLO_HEX); + assertNotNull(r, "teqTextTtext must return non-null"); + Boolean sv = AccessorUDFs.tboolStartValue.call(r); + assertTrue(sv, "\"hello\" = ttext(hello) must be true"); + } + + @Test @Order(2) + void teqTextTtext_different_text_returns_false_tbool() throws Exception { + String r = TTextUDFs.teqTextTtext.call("world", TTEXT_HELLO_HEX); + assertNotNull(r); + Boolean sv = AccessorUDFs.tboolStartValue.call(r); + assertFalse(sv, "\"world\" = ttext(hello) must be false"); + } + + @Test @Order(3) + void tneTextTtext_different_text_returns_true_tbool() throws Exception { + String r = TTextUDFs.tneTextTtext.call("world", TTEXT_HELLO_HEX); + assertNotNull(r); + Boolean sv = AccessorUDFs.tboolStartValue.call(r); + assertTrue(sv, "\"world\" <> ttext(hello) must be true"); + } + + @Test @Order(4) + void tltTextTtext_lesser_text_returns_true_tbool() throws Exception { + // "abc" < "hello" lexicographically + String r = TTextUDFs.tltTextTtext.call("abc", TTEXT_HELLO_HEX); + assertNotNull(r); + Boolean sv = AccessorUDFs.tboolStartValue.call(r); + assertTrue(sv, "\"abc\" < ttext(hello) must be true"); + } + + @Test @Order(5) + void tleTextTtext_equal_text_returns_true_tbool() throws Exception { + String r = TTextUDFs.tleTextTtext.call("hello", TTEXT_HELLO_HEX); + assertNotNull(r); + Boolean sv = AccessorUDFs.tboolStartValue.call(r); + assertTrue(sv, "\"hello\" <= ttext(hello) must be true"); + } + + @Test @Order(6) + void tgtTextTtext_greater_text_returns_true_tbool() throws Exception { + // "xyz" > "hello" lexicographically + String r = TTextUDFs.tgtTextTtext.call("xyz", TTEXT_HELLO_HEX); + assertNotNull(r); + Boolean sv = AccessorUDFs.tboolStartValue.call(r); + assertTrue(sv, "\"xyz\" > ttext(hello) must be true"); + } + + @Test @Order(7) + void tgeTextTtext_equal_text_returns_true_tbool() throws Exception { + String r = TTextUDFs.tgeTextTtext.call("hello", TTEXT_HELLO_HEX); + assertNotNull(r); + Boolean sv = AccessorUDFs.tboolStartValue.call(r); + assertTrue(sv, "\"hello\" >= ttext(hello) must be true"); + } + + @Test @Order(8) + void teqTextTtext_null_returns_null() throws Exception { + assertNull(TTextUDFs.teqTextTtext.call(null, TTEXT_HELLO_HEX)); + assertNull(TTextUDFs.teqTextTtext.call("hello", null)); + } + + // ------------------------------------------------------------------ + // ttext op text + // ------------------------------------------------------------------ + + @Test @Order(9) + void teqTtextText_equal_text_returns_true_tbool() throws Exception { + String r = TTextUDFs.teqTtextText.call(TTEXT_HELLO_HEX, "hello"); + assertNotNull(r, "teqTtextText must return non-null"); + Boolean sv = AccessorUDFs.tboolStartValue.call(r); + assertTrue(sv, "ttext(hello) = \"hello\" must be true"); + } + + @Test @Order(10) + void tneTtextText_different_text_returns_true_tbool() throws Exception { + String r = TTextUDFs.tneTtextText.call(TTEXT_HELLO_HEX, "world"); + assertNotNull(r); + Boolean sv = AccessorUDFs.tboolStartValue.call(r); + assertTrue(sv, "ttext(hello) <> \"world\" must be true"); + } + + @Test @Order(11) + void tltTtextText_ttext_less_than_text_returns_true_tbool() throws Exception { + // "hello" < "xyz" + String r = TTextUDFs.tltTtextText.call(TTEXT_HELLO_HEX, "xyz"); + assertNotNull(r); + Boolean sv = AccessorUDFs.tboolStartValue.call(r); + assertTrue(sv, "ttext(hello) < \"xyz\" must be true"); + } + + @Test @Order(12) + void tleTtextText_equal_returns_true_tbool() throws Exception { + String r = TTextUDFs.tleTtextText.call(TTEXT_HELLO_HEX, "hello"); + assertNotNull(r); + Boolean sv = AccessorUDFs.tboolStartValue.call(r); + assertTrue(sv, "ttext(hello) <= \"hello\" must be true"); + } + + @Test @Order(13) + void tgtTtextText_ttext_greater_than_text_returns_true_tbool() throws Exception { + // "hello" > "abc" + String r = TTextUDFs.tgtTtextText.call(TTEXT_HELLO_HEX, "abc"); + assertNotNull(r); + Boolean sv = AccessorUDFs.tboolStartValue.call(r); + assertTrue(sv, "ttext(hello) > \"abc\" must be true"); + } + + @Test @Order(14) + void tgeTtextText_equal_returns_true_tbool() throws Exception { + String r = TTextUDFs.tgeTtextText.call(TTEXT_HELLO_HEX, "hello"); + assertNotNull(r); + Boolean sv = AccessorUDFs.tboolStartValue.call(r); + assertTrue(sv, "ttext(hello) >= \"hello\" must be true"); + } + + @Test @Order(15) + void teqTtextText_null_returns_null() throws Exception { + assertNull(TTextUDFs.teqTtextText.call(null, "hello")); + assertNull(TTextUDFs.teqTtextText.call(TTEXT_HELLO_HEX, null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/TTextUDFsTest.java b/src/test/java/org/mobilitydb/spark/temporal/TTextUDFsTest.java new file mode 100644 index 00000000..df36fcdd --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/TTextUDFsTest.java @@ -0,0 +1,99 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for TTextUDFs β€” ttext case-conversion operations. + * + * Tests run directly against MEOS via JMEOS without a Spark session. + * MEOS function authority: meos/include/meos.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class TTextUDFsTest { + + private static String TTEXT_HELLO; + private static String TTEXT_WORLD; + + @BeforeAll + static void initMeos() throws Exception { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TTEXT_HELLO = temporal_as_hexwkb(ttext_in("hello@2020-01-01"), (byte) 0); + TTEXT_WORLD = temporal_as_hexwkb(ttext_in("World@2020-01-01"), (byte) 0); + } + + @Test @Order(1) + void ttextUpper_converts_to_uppercase() throws Exception { + String upper = TTextUDFs.ttextUpper.call(TTEXT_HELLO); + assertNotNull(upper); + // Decode back and check the start value + String sv = AccessorUDFs.ttextStartValue.call(upper); + assertNotNull(sv); + // text_out() is PostgreSQL textout() (pg_text_to_cstring): the raw, + // unquoted text value. (Pre-pin libmeos quoted it β€” an input/output + // asymmetry the centralised escape fix corrected.) + assertEquals("HELLO", sv); + } + + @Test @Order(2) + void ttextLower_converts_to_lowercase() throws Exception { + String lower = TTextUDFs.ttextLower.call(TTEXT_WORLD); + assertNotNull(lower); + String sv = AccessorUDFs.ttextStartValue.call(lower); + assertNotNull(sv); + assertEquals("world", sv); + } + + @Test @Order(3) + void ttextInitcap_capitalises_first_letter() throws Exception { + String init = TTextUDFs.ttextInitcap.call(TTEXT_HELLO); + assertNotNull(init); + String sv = AccessorUDFs.ttextStartValue.call(init); + assertNotNull(sv); + assertEquals("Hello", sv); + } + + @Test @Order(4) + void ttextUpper_null_input_returns_null() throws Exception { + assertNull(TTextUDFs.ttextUpper.call(null)); + } + + @Test @Order(5) + void ttextLower_null_input_returns_null() throws Exception { + assertNull(TTextUDFs.ttextLower.call(null)); + } + + @Test @Order(6) + void ttextInitcap_null_input_returns_null() throws Exception { + assertNull(TTextUDFs.ttextInitcap.call(null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/TemporalCompUDFsTest.java b/src/test/java/org/mobilitydb/spark/temporal/TemporalCompUDFsTest.java new file mode 100644 index 00000000..b67b36e9 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/TemporalCompUDFsTest.java @@ -0,0 +1,112 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class TemporalCompUDFsTest { + + private static String TINT; + private static String TFLOAT; + private static String TBOOL; + private static String TTEXT; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TINT = temporal_as_hexwkb( + tint_in("[1@2020-01-01 00:00:00+00, 5@2020-01-02 00:00:00+00]"), (byte) 0); + TFLOAT = temporal_as_hexwkb( + tfloat_in("[1.0@2020-01-01 00:00:00+00, 5.0@2020-01-02 00:00:00+00]"), (byte) 0); + TBOOL = temporal_as_hexwkb( + tbool_in("[true@2020-01-01 00:00:00+00, false@2020-01-02 00:00:00+00]"), (byte) 0); + TTEXT = temporal_as_hexwkb( + ttext_in("[\"a\"@2020-01-01 00:00:00+00, \"b\"@2020-01-02 00:00:00+00]"), (byte) 0); + } + + // teq β€” one test per type combo + + @Test @Order(1) void teqTintInt() throws Exception { + String r = TemporalCompUDFs.teqTintInt.call(TINT, 3); + assertNotNull(r); assertFalse(r.isBlank()); + } + + @Test @Order(2) void teqTfloatFloat() throws Exception { + assertNotNull(TemporalCompUDFs.teqTfloatFloat.call(TFLOAT, 3.0)); + } + + @Test @Order(3) void teqTboolBool() throws Exception { + assertNotNull(TemporalCompUDFs.teqTboolBool.call(TBOOL, true)); + } + + @Test @Order(4) void teqTtextText() throws Exception { + assertNotNull(TemporalCompUDFs.teqTtextText.call(TTEXT, "a")); + } + + @Test @Order(5) void teqTemporal() throws Exception { + assertNotNull(TemporalCompUDFs.teqTemporal.call(TINT, TINT)); + } + + // tne / tlt / tle / tgt / tge β€” one representative each + + @Test @Order(6) void tneTintInt() throws Exception { + assertNotNull(TemporalCompUDFs.tneTintInt.call(TINT, 3)); + } + + @Test @Order(7) void tltTfloatFloat() throws Exception { + assertNotNull(TemporalCompUDFs.tltTfloatFloat.call(TFLOAT, 3.0)); + } + + @Test @Order(8) void tleTtextText() throws Exception { + assertNotNull(TemporalCompUDFs.tleTtextText.call(TTEXT, "b")); + } + + @Test @Order(9) void tgtTintInt() throws Exception { + assertNotNull(TemporalCompUDFs.tgtTintInt.call(TINT, 3)); + } + + @Test @Order(10) void tgeTemporal() throws Exception { + assertNotNull(TemporalCompUDFs.tgeTemporal.call(TINT, TINT)); + } + + // null guards + + @Test @Order(11) void teqTintInt_null_returns_null() throws Exception { + assertNull(TemporalCompUDFs.teqTintInt.call(null, 3)); + assertNull(TemporalCompUDFs.teqTintInt.call(TINT, null)); + } + + @Test @Order(12) void teqTemporal_null_returns_null() throws Exception { + assertNull(TemporalCompUDFs.teqTemporal.call(null, TINT)); + assertNull(TemporalCompUDFs.teqTemporal.call(TINT, null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/TemporalUDFsExtTest.java b/src/test/java/org/mobilitydb/spark/temporal/TemporalUDFsExtTest.java new file mode 100644 index 00000000..db14e1f8 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/TemporalUDFsExtTest.java @@ -0,0 +1,160 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for TemporalUDFs MFJSON output and text-output UDFs: + * temporalAsMfjson, tboolOut, tintOut, tfloatOut, ttextOut. + * + * MEOS function authority: meos/include/meos.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class TemporalUDFsExtTest { + + private static String TRIP_HEX; + private static String TBOOL_HEX; + private static String TINT_HEX; + private static String TFLOAT_HEX; + private static String TTEXT_HEX; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TRIP_HEX = temporal_as_hexwkb( + tgeompoint_in("[POINT(0 0)@2020-01-01 00:00:00+00, POINT(1 0)@2020-01-01 01:00:00+00]"), + (byte) 0); + TBOOL_HEX = temporal_as_hexwkb(tbool_in("[true@2020-01-01, false@2020-01-03]"), (byte) 0); + TINT_HEX = temporal_as_hexwkb(tint_in("[1@2020-01-01, 3@2020-01-03]"), (byte) 0); + TFLOAT_HEX = temporal_as_hexwkb(tfloat_in("[1.5@2020-01-01, 2.5@2020-01-03]"), (byte) 0); + TTEXT_HEX = temporal_as_hexwkb(ttext_in("[hello@2020-01-01, world@2020-01-03]"), (byte) 0); + } + + // ------------------------------------------------------------------ + // temporalAsMfjson + // ------------------------------------------------------------------ + + @Test @Order(1) + void temporalAsMfjson_returns_json_string() throws Exception { + String r = TemporalUDFs.temporalAsMfjson.call(TRIP_HEX, 6); + assertNotNull(r); + assertFalse(r.isBlank()); + assertTrue(r.contains("\"type\""), "MFJSON output must be a JSON object"); + } + + @Test @Order(2) + void temporalAsMfjson_null_precision_uses_default() throws Exception { + String r = TemporalUDFs.temporalAsMfjson.call(TRIP_HEX, null); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(3) + void temporalAsMfjson_null_trip_returns_null() throws Exception { + assertNull(TemporalUDFs.temporalAsMfjson.call(null, 6)); + } + + // ------------------------------------------------------------------ + // tboolOut + // ------------------------------------------------------------------ + + @Test @Order(4) + void tboolOut_returns_text_representation() throws Exception { + String r = TemporalUDFs.tboolOut.call(TBOOL_HEX); + assertNotNull(r); + assertFalse(r.isBlank()); + assertTrue(r.contains("t") || r.contains("f"), "tbool text must contain t or f"); + } + + @Test @Order(5) + void tboolOut_null_returns_null() throws Exception { + assertNull(TemporalUDFs.tboolOut.call(null)); + } + + // ------------------------------------------------------------------ + // tintOut + // ------------------------------------------------------------------ + + @Test @Order(6) + void tintOut_returns_text_representation() throws Exception { + String r = TemporalUDFs.tintOut.call(TINT_HEX); + assertNotNull(r); + assertFalse(r.isBlank()); + assertTrue(r.contains("1"), "tintOut must contain the value 1"); + } + + @Test @Order(7) + void tintOut_null_returns_null() throws Exception { + assertNull(TemporalUDFs.tintOut.call(null)); + } + + // ------------------------------------------------------------------ + // tfloatOut + // ------------------------------------------------------------------ + + @Test @Order(8) + void tfloatOut_returns_text_with_decimal_value() throws Exception { + String r = TemporalUDFs.tfloatOut.call(TFLOAT_HEX, 2); + assertNotNull(r); + assertFalse(r.isBlank()); + assertTrue(r.contains("1.5") || r.contains("1.50"), "tfloatOut must contain 1.5"); + } + + @Test @Order(9) + void tfloatOut_null_precision_uses_default() throws Exception { + String r = TemporalUDFs.tfloatOut.call(TFLOAT_HEX, null); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(10) + void tfloatOut_null_returns_null() throws Exception { + assertNull(TemporalUDFs.tfloatOut.call(null, 6)); + } + + // ------------------------------------------------------------------ + // ttextOut + // ------------------------------------------------------------------ + + @Test @Order(11) + void ttextOut_returns_text_with_value() throws Exception { + String r = TemporalUDFs.ttextOut.call(TTEXT_HEX); + assertNotNull(r); + assertFalse(r.isBlank()); + assertTrue(r.contains("hello"), "ttextOut must contain the literal value 'hello'"); + } + + @Test @Order(12) + void ttextOut_null_returns_null() throws Exception { + assertNull(TemporalUDFs.ttextOut.call(null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/TransformUDFsExt2Test.java b/src/test/java/org/mobilitydb/spark/temporal/TransformUDFsExt2Test.java new file mode 100644 index 00000000..27a8d8ae --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/TransformUDFsExt2Test.java @@ -0,0 +1,126 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for second batch of TransformUDF extensions: + * tintToTfloat, temporalSimplifyMinTdelta, temporalTPrecision, + * and temporalDeleteTstzset (restriction extension). + * + * MEOS function authority: meos/include/meos.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class TransformUDFsExt2Test { + + private static String TINT_SEQ; + private static String TFLOAT_SEQ; + private static String TSTZSET_HEX; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TINT_SEQ = temporal_as_hexwkb( + tint_in("[1@2020-01-01, 2@2020-01-02, 3@2020-01-03, 4@2020-01-04, 5@2020-01-05]"), + (byte) 0); + TFLOAT_SEQ = temporal_as_hexwkb( + tfloat_in("[1.5@2020-01-01, 2.5@2020-01-03, 3.5@2020-01-05]"), (byte) 0); + TSTZSET_HEX = set_as_hexwkb( + tstzset_in("{2020-01-02 00:00:00+00, 2020-01-04 00:00:00+00}"), (byte) 0); + } + + // ------------------------------------------------------------------ + // tintToTfloat + // ------------------------------------------------------------------ + + @Test @Order(1) + void tintToTfloat_returns_tfloat_hex() throws Exception { + String r = TransformUDFs.tintToTfloat.call(TINT_SEQ); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(2) + void tintToTfloat_value_preserved() throws Exception { + String r = TransformUDFs.tintToTfloat.call(TINT_SEQ); + assertNotNull(r); + Double sv = AccessorUDFs.tfloatStartValue.call(r); + assertNotNull(sv); + assertEquals(1.0, sv, 1e-9); + } + + // ------------------------------------------------------------------ + // temporalSimplifyMinTdelta + // ------------------------------------------------------------------ + + @Test @Order(3) + void temporalSimplifyMinTdelta_returns_nonnull() throws Exception { + String r = TransformUDFs.temporalSimplifyMinTdelta.call(TFLOAT_SEQ, "1 day"); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + // ------------------------------------------------------------------ + // temporalTPrecision + // ------------------------------------------------------------------ + + @Test @Order(4) + void temporalTPrecision_returns_nonnull() throws Exception { + String r = TransformUDFs.temporalTPrecision.call(TFLOAT_SEQ, "1 day"); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + // ------------------------------------------------------------------ + // temporalDeleteTstzset (RestrictionUDFs) + // ------------------------------------------------------------------ + + @Test @Order(5) + void temporalDeleteTstzset_removes_instants() throws Exception { + String r = RestrictionUDFs.temporalDeleteTstzset.call(TINT_SEQ, TSTZSET_HEX); + assertTrue(r == null || !r.isBlank()); + } + + // ------------------------------------------------------------------ + // Null-input guards + // ------------------------------------------------------------------ + + @Test @Order(6) + void null_inputs_return_null() throws Exception { + assertNull(TransformUDFs.tintToTfloat.call(null)); + assertNull(TransformUDFs.temporalSimplifyMinTdelta.call(null, "1 day")); + assertNull(TransformUDFs.temporalSimplifyMinTdelta.call(TFLOAT_SEQ, null)); + assertNull(TransformUDFs.temporalTPrecision.call(null, "1 day")); + assertNull(RestrictionUDFs.temporalDeleteTstzset.call(null, TSTZSET_HEX)); + assertNull(RestrictionUDFs.temporalDeleteTstzset.call(TINT_SEQ, null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/TransformUDFsExt3Test.java b/src/test/java/org/mobilitydb/spark/temporal/TransformUDFsExt3Test.java new file mode 100644 index 00000000..af9446ba --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/TransformUDFsExt3Test.java @@ -0,0 +1,129 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for tint value-domain shift/scale UDFs: + * tintShiftValue, tintScaleValue, tintShiftScaleValue. + * + * MEOS function authority: meos/include/meos.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class TransformUDFsExt3Test { + + private static String TINT_SEQ; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TINT_SEQ = temporal_as_hexwkb( + tint_in("[1@2020-01-01, 3@2020-01-03]"), (byte) 0); + } + + // ------------------------------------------------------------------ + // tintShiftValue + // ------------------------------------------------------------------ + + @Test @Order(1) + void tintShiftValue_positive_shift_returns_nonnull() throws Exception { + String r = TransformUDFs.tintShiftValue.call(TINT_SEQ, 10); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(2) + void tintShiftValue_changes_start_value() throws Exception { + String r = TransformUDFs.tintShiftValue.call(TINT_SEQ, 5); + assertNotNull(r); + Integer sv = AccessorUDFs.tintStartValue.call(r); + assertNotNull(sv); + assertEquals(6, sv.intValue()); + } + + @Test @Order(3) + void tintShiftValue_negative_shift_returns_nonnull() throws Exception { + String r = TransformUDFs.tintShiftValue.call(TINT_SEQ, -1); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(4) + void tintShiftValue_null_returns_null() throws Exception { + assertNull(TransformUDFs.tintShiftValue.call(null, 5)); + assertNull(TransformUDFs.tintShiftValue.call(TINT_SEQ, null)); + } + + // ------------------------------------------------------------------ + // tintScaleValue + // ------------------------------------------------------------------ + + @Test @Order(5) + void tintScaleValue_doubles_start_value() throws Exception { + String r = TransformUDFs.tintScaleValue.call(TINT_SEQ, 2); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(6) + void tintScaleValue_by_one_is_identity() throws Exception { + String r = TransformUDFs.tintScaleValue.call(TINT_SEQ, 1); + assertNotNull(r); + Integer sv = AccessorUDFs.tintStartValue.call(r); + assertNotNull(sv); + assertEquals(1, sv.intValue()); + } + + @Test @Order(7) + void tintScaleValue_null_returns_null() throws Exception { + assertNull(TransformUDFs.tintScaleValue.call(null, 2)); + assertNull(TransformUDFs.tintScaleValue.call(TINT_SEQ, null)); + } + + // ------------------------------------------------------------------ + // tintShiftScaleValue + // ------------------------------------------------------------------ + + @Test @Order(8) + void tintShiftScaleValue_shift_then_scale_returns_nonnull() throws Exception { + String r = TransformUDFs.tintShiftScaleValue.call(TINT_SEQ, 1, 3); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(9) + void tintShiftScaleValue_null_returns_null() throws Exception { + assertNull(TransformUDFs.tintShiftScaleValue.call(null, 1, 2)); + assertNull(TransformUDFs.tintShiftScaleValue.call(TINT_SEQ, null, 2)); + assertNull(TransformUDFs.tintShiftScaleValue.call(TINT_SEQ, 1, null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/TransformUDFsExt4Test.java b/src/test/java/org/mobilitydb/spark/temporal/TransformUDFsExt4Test.java new file mode 100644 index 00000000..8087bec6 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/TransformUDFsExt4Test.java @@ -0,0 +1,224 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for TransformUDFs set and span transforms: + * floatset (ceil/floor/round/degrees/radians), + * textset (lower/upper/initcap), + * intspan/floatspan shift-scale, + * intspanset/floatspanset shift-scale, ceil, floor, round, type conversions. + * + * MEOS function authority: meos/include/meos.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class TransformUDFsExt4Test { + + private static String FLOATSET_HEX; + private static String TEXTSET_HEX; + private static String INTSPAN_HEX; + private static String FLOATSPAN_HEX; + private static String INTSPANSET_HEX; + private static String FLOATSPANSET_HEX; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + FLOATSET_HEX = set_as_hexwkb(floatset_in("{1.1, 2.2, 3.3}"), (byte) 0); + TEXTSET_HEX = set_as_hexwkb(textset_in("{\"apple\", \"BANANA\", \"Cherry\"}"), (byte) 0); + INTSPAN_HEX = span_as_hexwkb(intspan_in("[1, 10]"), (byte) 0); + FLOATSPAN_HEX = span_as_hexwkb(floatspan_in("[1.5, 10.5]"), (byte) 0); + INTSPANSET_HEX = spanset_as_hexwkb(intspanset_in("{[1, 5], [10, 20]}"), (byte) 0); + FLOATSPANSET_HEX = spanset_as_hexwkb(floatspanset_in("{[1.5, 5.5], [10.0, 20.0]}"), (byte) 0); + } + + // ------------------------------------------------------------------ + // floatset transforms + // ------------------------------------------------------------------ + + @Test @Order(1) + void floatsetCeil_returns_nonnull() throws Exception { + String r = TransformUDFs.floatsetCeil.call(FLOATSET_HEX); + assertNotNull(r, "floatsetCeil must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(2) + void floatsetFloor_returns_nonnull() throws Exception { + String r = TransformUDFs.floatsetFloor.call(FLOATSET_HEX); + assertNotNull(r, "floatsetFloor must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(4) + void floatsetDegrees_returns_nonnull() throws Exception { + String r = TransformUDFs.floatsetDegrees.call(FLOATSET_HEX); + assertNotNull(r, "floatsetDegrees must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(5) + void floatsetRadians_returns_nonnull() throws Exception { + String r = TransformUDFs.floatsetRadians.call(FLOATSET_HEX); + assertNotNull(r, "floatsetRadians must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(6) + void floatsetCeil_null_returns_null() throws Exception { + assertNull(TransformUDFs.floatsetCeil.call(null)); + } + + // ------------------------------------------------------------------ + // textset case normalization + // ------------------------------------------------------------------ + + @Test @Order(7) + void textsetLower_returns_nonnull() throws Exception { + String r = TransformUDFs.textsetLower.call(TEXTSET_HEX); + assertNotNull(r, "textsetLower must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(8) + void textsetUpper_returns_nonnull() throws Exception { + String r = TransformUDFs.textsetUpper.call(TEXTSET_HEX); + assertNotNull(r, "textsetUpper must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(9) + void textsetInitcap_returns_nonnull() throws Exception { + String r = TransformUDFs.textsetInitcap.call(TEXTSET_HEX); + assertNotNull(r, "textsetInitcap must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(10) + void textsetLower_null_returns_null() throws Exception { + assertNull(TransformUDFs.textsetLower.call(null)); + } + + // ------------------------------------------------------------------ + // intspan / floatspan shift-scale + // ------------------------------------------------------------------ + + @Test @Order(11) + void intspanShiftScale_shift_only_returns_nonnull() throws Exception { + String r = TransformUDFs.intspanShiftScale.call(INTSPAN_HEX, 5, null); + assertNotNull(r, "intspanShiftScale (shift only) must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(12) + void intspanShiftScale_scale_only_returns_nonnull() throws Exception { + String r = TransformUDFs.intspanShiftScale.call(INTSPAN_HEX, null, 20); + assertNotNull(r, "intspanShiftScale (scale only) must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(13) + void intspanShiftScale_null_span_returns_null() throws Exception { + assertNull(TransformUDFs.intspanShiftScale.call(null, 5, 10)); + } + + @Test @Order(14) + void floatspanShiftScale_shift_only_returns_nonnull() throws Exception { + String r = TransformUDFs.floatspanShiftScale.call(FLOATSPAN_HEX, 2.5, null); + assertNotNull(r, "floatspanShiftScale (shift only) must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(15) + void floatspanShiftScale_null_span_returns_null() throws Exception { + assertNull(TransformUDFs.floatspanShiftScale.call(null, 1.0, 5.0)); + } + + // ------------------------------------------------------------------ + // intspanset / floatspanset transforms + // ------------------------------------------------------------------ + + @Test @Order(16) + void intspansetShiftScale_returns_nonnull() throws Exception { + String r = TransformUDFs.intspansetShiftScale.call(INTSPANSET_HEX, 10, null); + assertNotNull(r, "intspansetShiftScale must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(17) + void floatspansetShiftScale_returns_nonnull() throws Exception { + String r = TransformUDFs.floatspansetShiftScale.call(FLOATSPANSET_HEX, 1.0, null); + assertNotNull(r, "floatspansetShiftScale must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(18) + void floatspansetCeil_returns_nonnull() throws Exception { + String r = TransformUDFs.floatspansetCeil.call(FLOATSPANSET_HEX); + assertNotNull(r, "floatspansetCeil must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(19) + void floatspansetFloor_returns_nonnull() throws Exception { + String r = TransformUDFs.floatspansetFloor.call(FLOATSPANSET_HEX); + assertNotNull(r, "floatspansetFloor must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(20) + void floatspansetRound_returns_nonnull() throws Exception { + String r = TransformUDFs.floatspansetRound.call(FLOATSPANSET_HEX, 1); + assertNotNull(r, "floatspansetRound must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(21) + void intspansetToFloat_returns_nonnull() throws Exception { + String r = TransformUDFs.intspansetToFloat.call(INTSPANSET_HEX); + assertNotNull(r, "intspansetToFloat must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(22) + void floatspansetToInt_returns_nonnull() throws Exception { + String r = TransformUDFs.floatspansetToInt.call(FLOATSPANSET_HEX); + assertNotNull(r, "floatspansetToInt must return non-null"); + assertFalse(r.isBlank()); + } + + @Test @Order(23) + void floatspansetCeil_null_returns_null() throws Exception { + assertNull(TransformUDFs.floatspansetCeil.call(null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/TransformUDFsExtTest.java b/src/test/java/org/mobilitydb/spark/temporal/TransformUDFsExtTest.java new file mode 100644 index 00000000..fd447c90 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/TransformUDFsExtTest.java @@ -0,0 +1,119 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for new TransformUDFs β€” min-distance simplification, + * temporal re-sampling (tSample), and trajectory extraction. + * + * MEOS function authority: meos/include/meos.h, meos/include/meos_geo.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class TransformUDFsExtTest { + + private static String TRIP; + private static String TFLOAT_SEQ; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TRIP = temporal_as_hexwkb( + tgeompoint_in("[POINT(0 0)@2020-01-01 00:00:00+00, " + + "POINT(1 0)@2020-01-01 00:15:00+00, " + + "POINT(3 4)@2020-01-01 01:00:00+00]"), + (byte) 0); + TFLOAT_SEQ = temporal_as_hexwkb( + tfloat_in("[1.0@2020-01-01, 2.0@2020-01-02, 5.0@2020-01-05]"), (byte) 0); + } + + // ------------------------------------------------------------------ + // Min-distance simplification + // ------------------------------------------------------------------ + + @Test @Order(1) + void temporalSimplifyMinDist_returns_nonnull() throws Exception { + String r = TransformUDFs.temporalSimplifyMinDist.call(TRIP, 0.1); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(2) + void temporalSimplifyMinDist_large_threshold_reduces() throws Exception { + // A very large threshold should collapse the sequence to fewer instants + String r = TransformUDFs.temporalSimplifyMinDist.call(TRIP, 100.0); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + // ------------------------------------------------------------------ + // Temporal re-sampling + // ------------------------------------------------------------------ + + @Test @Order(3) + void temporalTSample_linear_returns_nonnull() throws Exception { + String r = TransformUDFs.temporalTSample.call(TFLOAT_SEQ, "1 day", "Linear"); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(4) + void temporalTSample_step_returns_nonnull() throws Exception { + String r = TransformUDFs.temporalTSample.call(TFLOAT_SEQ, "1 day", "Step"); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + // ------------------------------------------------------------------ + // Trajectory extraction + // ------------------------------------------------------------------ + + @Test @Order(5) + void tpointTrajectory_returns_linestring_wkt() throws Exception { + String r = TransformUDFs.tpointTrajectory.call(TRIP); + assertNotNull(r); + // A multi-point sequence should produce a LINESTRING + assertTrue(r.toUpperCase().startsWith("LINESTRING"), + "Expected LINESTRING WKT but got: " + r); + } + + // ------------------------------------------------------------------ + // Null-input guards + // ------------------------------------------------------------------ + + @Test @Order(6) + void null_input_returns_null() throws Exception { + assertNull(TransformUDFs.temporalSimplifyMinDist.call(null, 1.0)); + assertNull(TransformUDFs.temporalTSample.call(null, "1 day", "Linear")); + assertNull(TransformUDFs.tpointTrajectory.call(null)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/temporal/TransformUDFsTest.java b/src/test/java/org/mobilitydb/spark/temporal/TransformUDFsTest.java new file mode 100644 index 00000000..3184e102 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/temporal/TransformUDFsTest.java @@ -0,0 +1,193 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.temporal; + +import org.junit.jupiter.api.*; + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for TransformUDFs β€” subtype conversion, interpolation change, + * type casting, value/time-domain shifting and scaling, SRID assignment, + * coordinate rounding, and trajectory simplification. + * + * MEOS function authority: meos/include/meos.h, meos/include/meos_geo.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class TransformUDFsTest { + + private static String TRIP; + private static String TINT_SEQ; + private static String TFLOAT_SEQ; + private static String TFLOAT_STEP; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + + TRIP = temporal_as_hexwkb( + tgeompoint_in("[POINT(0 0)@2020-01-01 00:00:00+00, POINT(1 0)@2020-01-01 01:00:00+00]"), + (byte) 0); + TINT_SEQ = temporal_as_hexwkb(tint_in("[1@2020-01-01, 3@2020-01-03]"), (byte) 0); + TFLOAT_SEQ = temporal_as_hexwkb(tfloat_in("[1.0@2020-01-01, 3.0@2020-01-03]"), (byte) 0); + TFLOAT_STEP = temporal_as_hexwkb( + tfloat_in("Interp=Step;[1.0@2020-01-01, 3.0@2020-01-03]"), (byte) 0); + } + + // ------------------------------------------------------------------ + // Subtype conversion + // ------------------------------------------------------------------ + + @Test @Order(1) + void temporalToTInstant_single_instant_sequence_becomes_instant() throws Exception { + String singleSeq = temporal_as_hexwkb( + tgeompoint_in("[POINT(0 0)@2020-01-01]"), (byte) 0); + String r = TransformUDFs.temporalToTInstant.call(singleSeq); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(2) + void temporalToTSequence_step_to_linear() throws Exception { + String r = TransformUDFs.temporalToTSequence.call(TFLOAT_STEP, "Linear"); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(3) + void temporalToTSequenceSet_converts() throws Exception { + String r = TransformUDFs.temporalToTSequenceSet.call(TFLOAT_SEQ, "Linear"); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + // ------------------------------------------------------------------ + // Interpolation change + // ------------------------------------------------------------------ + + @Test @Order(4) + void temporalSetInterp_step_to_linear() throws Exception { + String r = TransformUDFs.temporalSetInterp.call(TFLOAT_STEP, "Linear"); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + // ------------------------------------------------------------------ + // Type casting + // ------------------------------------------------------------------ + + @Test @Order(5) + void tfloatToTint_returns_tint_hexwkb() throws Exception { + // tfloat_to_tint only works on step-interpolated sequences + String r = TransformUDFs.tfloatToTint.call(TFLOAT_STEP); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + // ------------------------------------------------------------------ + // Value-domain shifting and scaling + // ------------------------------------------------------------------ + + @Test @Order(6) + void tfloatShiftValue_positive() throws Exception { + String r = TransformUDFs.tfloatShiftValue.call(TFLOAT_SEQ, 10.0); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(7) + void tfloatScaleValue_doubles() throws Exception { + String r = TransformUDFs.tfloatScaleValue.call(TFLOAT_SEQ, 2.0); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(8) + void tfloatShiftScaleValue() throws Exception { + String r = TransformUDFs.tfloatShiftScaleValue.call(TFLOAT_SEQ, 1.0, 2.0); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + // ------------------------------------------------------------------ + // Time-domain shifting and scaling + // ------------------------------------------------------------------ + + @Test @Order(9) + void temporalShiftScaleTime() throws Exception { + String r = TransformUDFs.temporalShiftScaleTime.call(TRIP, "01:00:00", "02:00:00"); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + // ------------------------------------------------------------------ + // Spatial transformations + // ------------------------------------------------------------------ + + @Test @Order(10) + void tpointSetSrid_to_4326() throws Exception { + String r = TransformUDFs.tpointSetSrid.call(TRIP, 4326); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + @Test @Order(11) + void tpointRound_2_digits() throws Exception { + String r = TransformUDFs.tpointRound.call(TRIP, 2); + assertNotNull(r); + assertFalse(r.isBlank()); + } + + // ------------------------------------------------------------------ + // Trajectory simplification + // ------------------------------------------------------------------ + + @Test @Order(12) + void temporalSimplifyDp_returns_nonnull_or_null() throws Exception { + // A 2-point sequence may be returned as-is or simplified to null; + // accept either outcome but never a blank string. + String r = TransformUDFs.temporalSimplifyDp.call(TRIP, 0.001); + assertTrue(r == null || !r.isBlank()); + } + + @Test @Order(13) + void temporalSimplifyMaxDist_returns_nonnull_or_null() throws Exception { + String r = TransformUDFs.temporalSimplifyMaxDist.call(TRIP, 0.001); + assertTrue(r == null || !r.isBlank()); + } + + // ------------------------------------------------------------------ + // Null-input guard + // ------------------------------------------------------------------ + + @Test @Order(14) + void null_input_returns_null() throws Exception { + assertNull(TransformUDFs.tfloatToTint.call(null)); + assertNull(TransformUDFs.tpointSetSrid.call(null, 4326)); + } +} diff --git a/src/test/java/org/mobilitydb/spark/udfs/TemporalUDFsTest.java b/src/test/java/org/mobilitydb/spark/udfs/TemporalUDFsTest.java new file mode 100644 index 00000000..57a99e02 --- /dev/null +++ b/src/test/java/org/mobilitydb/spark/udfs/TemporalUDFsTest.java @@ -0,0 +1,117 @@ +/***************************************************************************** + * + * This MobilityDB code is provided under The PostgreSQL License. + * Copyright (c) 2020-2026, UniversitΓ© libre de Bruxelles and MobilityDB + * contributors + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written + * agreement is hereby granted, provided that the above copyright notice and + * this paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, + * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON + * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + *****************************************************************************/ + +package org.mobilitydb.spark.udfs; + +import org.junit.jupiter.api.*; +import org.mobilitydb.spark.temporal.TemporalUDFs; + + +import static functions.GeneratedFunctions.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for temporal (time-axis) UDFs β€” runs without a Spark session. + * + * Geo-specific UDFs (eIntersects, nearestApproachDistance, eDwithin) are + * covered in {@link org.mobilitydb.spark.geo.GeoUDFsTest}. + * + * MEOS function authority: meos/include/meos.h + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class TemporalUDFsTest { + + private static String TRIP_HEX; + + @BeforeAll + static void initMeos() { + meos_initialize(); + meos_initialize_timezone("UTC"); + TRIP_HEX = temporal_as_hexwkb( + tgeompoint_in("[POINT(0.0 0.0)@2020-01-01 00:00:00+00, POINT(0.1 0.0)@2020-01-01 01:00:00+00]"), + (byte) 0); + } + + @AfterAll + static void finalizeMeos() { + meos_finalize(); + } + + @Test @Order(1) + void atTime_instant_inside_interval_returns_nonnull() throws Exception { + String result = TemporalUDFs.atTime.call(TRIP_HEX, "2020-01-01 00:30:00+00"); + assertNotNull(result, "atTime should return a value inside the trip interval"); + assertFalse(result.isBlank()); + } + + @Test @Order(2) + void atTime_instant_outside_interval_returns_null() throws Exception { + assertNull(TemporalUDFs.atTime.call(TRIP_HEX, "2020-06-01 00:00:00+00"), + "atTime should return null outside the trip interval"); + } + + @Test @Order(3) + void atTime_null_trip_returns_null() throws Exception { + assertNull(TemporalUDFs.atTime.call(null, "2020-01-01 00:30:00+00")); + } + + @Test @Order(4) + void atTime_period_inside_interval_returns_nonnull() throws Exception { + String result = TemporalUDFs.atTime.call(TRIP_HEX, "[2020-01-01 00:00:00+00,2020-01-01 00:30:00+00]"); + assertNotNull(result, "atTime with period should return a value when trip overlaps the period"); + assertFalse(result.isBlank()); + } + + @Test @Order(5) + void atTime_period_outside_interval_returns_null() throws Exception { + assertNull(TemporalUDFs.atTime.call(TRIP_HEX, "[2020-06-01 00:00:00+00,2020-06-01 01:00:00+00]"), + "atTime with period should return null when trip does not overlap the period"); + } + + @Test @Order(6) + void asHexWKB_is_identity_on_hexwkb() throws Exception { + String result = TemporalUDFs.asHexWKB.call(TRIP_HEX); + assertEquals(TRIP_HEX, result, + "asHexWKB(hexwkb) must be a lossless identity: parse then re-serialize"); + } + + @Test @Order(7) + void asHexWKB_null_returns_null() throws Exception { + assertNull(TemporalUDFs.asHexWKB.call(null)); + } + + @Test @Order(8) + void asHexWKB_matches_mbdb_expected() throws Exception { + // Known hex-WKB for [POINT(0 0)@2020-01-01 00:00:00+00, POINT(100 0)@2020-01-01 00:10:00+00] + // Generated from MobilityDB: SELECT asHexWKB(trip) FROM Trips WHERE tripId = 1; + String wkt = "[POINT(0 0)@2020-01-01 00:00:00+00, POINT(100 0)@2020-01-01 00:10:00+00]"; + String hexwkb = temporal_as_hexwkb(tgeompoint_in(wkt), (byte) 0); + String expected = "012E000E02000000030101000000000000000000000000000000000000000060C286073E020001010000000000000000005940000000000000000000A685AA073E0200"; + assertEquals(expected, hexwkb, + "MEOS hex-WKB must match MobilityDB asHexWKB() byte-for-byte"); + // asHexWKB UDF returns the same value + assertEquals(expected, TemporalUDFs.asHexWKB.call(hexwkb)); + } +} diff --git a/tools/scripts/check_license.sh b/tools/scripts/check_license.sh new file mode 100755 index 00000000..43be56dd --- /dev/null +++ b/tools/scripts/check_license.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# Check that all Java source files have the PostgreSQL License header. + +DIR=$(git rev-parse --show-toplevel) +error=0 + +while IFS= read -r -d '' f; do + if ! grep -q "PostgreSQL License" "$f"; then + echo "Missing license header: $f" + error=1 + fi +done < <(find "$DIR/src/main" "$DIR/src/test" -name "*.java" -print0 2>/dev/null) + +if [ $error -eq 0 ]; then + echo "License check passed." +fi +exit $error