Skip to content

Conversation

@fogelito
Copy link
Contributor

@fogelito fogelito commented Sep 11, 2025

Summary by CodeRabbit

  • New Features
    • Adapters now provide unified decoding for POINT, LINESTRING, and POLYGON from WKT/WKB (SRID-aware).
  • Bug Fixes
    • Tighter null handling and validation for spatial attributes to avoid errors on empty or malformed data.
  • Refactor
    • Spatial decoding moved out of core into adapters and selection/projection logic simplified.
  • Tests
    • E2E tests enhanced with spatial fixtures and round-trip assertions for spatial fields.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 11, 2025

Warning

Rate limit exceeded

@fogelito has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 5 minutes and 55 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 0275563 and 09be1c0.

📒 Files selected for processing (2)
  • src/Database/Adapter/Postgres.php (1 hunks)
  • src/Database/Adapter/SQL.php (5 hunks)

Walkthrough

Spatial decoding was moved from Database to adapters: Database delegates spatial decode operations to adapter methods. Adapter API gained decodePoint/decodeLinestring/decodePolygon and tightened getAttributeProjection to return string. SQL/Postgres/Pool implement or delegate new decoders; tests add spatial fixtures and assertions.

Changes

Cohort / File(s) Summary of changes
Database spatial delegation
src/Database/Database.php
Delegates spatial decode closures to adapter decode methods; adds explicit null handling for spatial decoders; removes public decodeSpatialData(string $wkt): array; encoding left in place.
Adapter interface updates
src/Database/Adapter.php
Narrows getAttributeProjection(...): string; adds abstract decodePoint(string): array, decodeLinestring(string): array, decodePolygon(string): array.
Adapter pooling delegation
src/Database/Adapter/Pool.php
Updates getAttributeProjection signature; adds decodePoint/Linestring/Polygon methods that delegate to underlying adapter.
SQL adapter projection & decoding
src/Database/Adapter/SQL.php
Simplifies projection logic (no spatial-specific branches); updates getAttributeProjection signature; adds decodePoint/Linestring/Polygon implementations (WKT/WKB decoding, validation, errors).
Postgres adapter decoding
src/Database/Adapter/Postgres.php
Adds decodePoint/Linestring/Polygon implementations handling WKT, hex WKB, SRID and endianness, with validation and errors.
E2E spatial tests
tests/e2e/Adapter/Scopes/SpatialTests.php
Introduces point, linestring, polygon fixtures; replaces inline literals; adds assertions after create and fetch to verify spatial attribute equality.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client
  participant Database
  participant Pool as AdapterPool
  participant Impl as AdapterImpl (SQL/Postgres)

  Client->>Database: getDocument/createDocument(...)
  Database->>Pool: fetch rows (spatial columns as WKB/WKT)
  alt spatial columns present
    Database->>Pool: decodePoint/Linestring/Polygon(wkb|wkt)
    Pool->>Impl: delegate decode*(...)
    Impl-->>Pool: geometry arrays
    Pool-->>Database: geometry arrays
    Database-->>Client: document with decoded spatial fields
  else no spatial
    Database-->>Client: document (no decode calls)
  end
Loading
sequenceDiagram
  autonumber
  participant Database
  participant SQL as SQL Adapter

  Database->>SQL: getDocument(selections, alias)
  note over SQL: Projection generation
  SQL->>SQL: getAttributeProjection(selections, alias)
  SQL-->>Database: rows (spatial columns still WKB/WKT)
  Database->>SQL: decode*(...) per spatial field
  SQL-->>Database: decoded coordinate arrays
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Default alias getDocument #606 — Modifies getAttributeProjection signatures and likely overlaps with SQL/Adapter projection changes.
  • Spatial attribute support #661 — Refactors spatial decoding into adapter-level methods and touches Database/Adapter/SQL/Postgres/Pool.
  • Sync 1.x #695 — Changes spatial handling and adapter implementations; directly related to adding adapter decode methods.

Suggested reviewers

  • abnegate

Poem

A hop, a nibble, decode begun,
From WKT streams to points of sun.
Adapters hum and rings align,
Fields return as arrays fine.
I twitch my nose — the shapes are done. 🥕🐇

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "Spatial decoding" is a short, focused summary that accurately reflects the main change in the diff—moving spatial decoding into adapter-specific methods (decodePoint, decodeLinestring, decodePolygon) and related test updates. It is concise, avoids noise, and gives a teammate scanning history a clear sense of the primary change.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/Database/Database.php (3)

2179-2184: Prevent removal of required filters in updateAttributeFilters.

This path can strip mandatory filters from spatial/datetime attributes, breaking encoding/decoding. Validate here too.

Apply:

 public function updateAttributeFilters(string $collection, string $id, array $filters): Document
 {
-    return $this->updateAttributeMeta($collection, $id, function ($attribute) use ($filters) {
-        $attribute->setAttribute('filters', $filters);
-    });
+    return $this->updateAttributeMeta($collection, $id, function ($attribute) use ($filters) {
+        $type = $attribute->getAttribute('type');
+        $required = $this->getRequiredFilters($type);
+        if (!empty(array_diff($required, $filters))) {
+            throw new DatabaseException("Attribute of type: {$type} requires the following filters: " . implode(",", $required));
+        }
+        $attribute->setAttribute('filters', $filters);
+    });
 }

2023-2033: Bug: spatial default validation always throws “Unknown attribute type”.

Spatial cases fall through to default without a break, so even valid arrays raise an exception.

Fix:

-            case self::VAR_POINT:
-            case self::VAR_LINESTRING:
-            case self::VAR_POLYGON:
-                // Spatial types expect arrays as default values
-                if ($defaultType !== 'array') {
-                    throw new DatabaseException('Default value for spatial type ' . $type . ' must be an array');
-                }
-                // no break
-            default:
+            case self::VAR_POINT:
+            case self::VAR_LINESTRING:
+            case self::VAR_POLYGON:
+                // Spatial types expect arrays as default values
+                if ($defaultType !== 'array') {
+                    throw new DatabaseException('Default value for spatial type ' . $type . ' must be an array');
+                }
+                break;
+            default:
                 throw new DatabaseException('Unknown attribute type: ' . $type . '. Must be one of ' . self::VAR_STRING . ', ' . self::VAR_INTEGER . ', ' . self::VAR_FLOAT . ', ' . self::VAR_BOOLEAN . ', ' . self::VAR_DATETIME . ', ' . self::VAR_RELATIONSHIP . ', ' . self::VAR_POINT . ', ' . self::VAR_LINESTRING . ', ' . self::VAR_POLYGON);

1970-1979: updateAttributeFilters bypasses required-filter validation — fix in Database.php

src/Database/Database.php:2179 (updateAttributeFilters) sets filters directly via updateAttributeMeta without checking $this->getRequiredFilters($type); src/Database/Mirror.php:961 delegates to it. Enforce the same required-filter check used in updateAttribute/createAttribute (compute requiredFilters from the attribute's type and throw DatabaseException if any are missing) or route this call through updateAttribute to reuse existing validation.

🧹 Nitpick comments (3)
src/Database/Database.php (3)

528-534: Harden linestring decode against non-string input (parity with point).

Guard early if $value isn’t a string to avoid TypeError/mis-decoding.

-            function (?string $value) {
-                if (is_null($value)) {
-                    return $value;
-                }
-                return self::decodeSpatialData($value);
-            }
+            function (?string $value) {
+                if (!is_string($value)) {
+                    return $value;
+                }
+                return self::decodeSpatialData($value);
+            }

555-561: Harden polygon decode against non-string input (parity with point).

Same early-return guard as other decoders.

-            function (?string $value) {
-                if (is_null($value)) {
-                    return $value;
-                }
-                return self::decodeSpatialData($value);
-            }
+            function (?string $value) {
+                if (!is_string($value)) {
+                    return $value;
+                }
+                return self::decodeSpatialData($value);
+            }

498-506: Docblock return types don’t match actual decoder returns.

Decode callbacks for spatial types return arrays, but annotations say string|null. Adjust to reduce confusion for static analyzers.

-             * @param string|null $value
-             * @return string|null
+             * @param string|null $value
+             * @return array<mixed>|string|null

Also applies to: 525-533, 552-560

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 44af82d and 745d785.

📒 Files selected for processing (4)
  • phpunit.xml (1 hunks)
  • src/Database/Database.php (1 hunks)
  • tests/e2e/Adapter/Base.php (1 hunks)
  • tests/e2e/Adapter/Scopes/SpatialTests.php (16 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tests/e2e/Adapter/Scopes/SpatialTests.php (3)
src/Database/Database.php (2)
  • createAttribute (1676-1727)
  • Database (37-7364)
src/Database/Adapter/Pool.php (2)
  • createAttribute (168-171)
  • getSupportForSpatialIndexNull (433-436)
src/Database/Adapter.php (2)
  • createAttribute (558-558)
  • getSupportForSpatialIndexNull (1065-1065)
🪛 GitHub Actions: Linter
tests/e2e/Adapter/Base.php

[error] 1-1: Pint lint: PSR-12 statement_indentation violation detected in this file.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Setup & Build Docker Image
🔇 Additional comments (20)
phpunit.xml (1)

10-10: Fail-fast tests: confirm this is desired for CI.

stopOnFailure="true" speeds up feedback but hides downstream failures. If the goal is faster local runs only, consider gating via an env variable or a separate local phpunit.xml.dist.

tests/e2e/Adapter/Scopes/SpatialTests.php (18)

80-80: LGTM! The explicit filter specification aligns with the PR's approach to enforce spatial type filters.

Based on the relevant code snippets from src/Database/Database.php, the PR has updated the required filter validation to enforce spatial attributes must include their specific type filters (point, linestring, polygon). This change ensures proper spatial type validation without relying on auto-injection.


108-110: LGTM! Consistent application of required spatial filters.

The explicit filter specification across all spatial types (point, linestring, polygon) properly addresses the PR's validation changes. Each createAttribute call now correctly includes the required filters parameter.


237-237: LGTM! Consistent spatial filter specification in relationship tests.

The explicit filters: ['point'] parameter correctly follows the new requirement for spatial attributes to specify their type filters.


342-344: LGTM! Proper implementation of required spatial filters.

All spatial attribute types are correctly specified with their corresponding filters, maintaining consistency with the PR's validation changes.


393-393: LGTM! Consistent spatial attribute definition.

The point attribute correctly includes the required filters: ['point'] parameter, following the established pattern throughout the PR.


505-505: LGTM! Consistent spatial filter application.

The spatial attribute definition properly includes the required filter specification.


609-612: LGTM! Comprehensive spatial filter implementation.

Both point and polygon attributes correctly specify their respective filters, demonstrating proper adherence to the new spatial validation requirements.


711-711: LGTM! Consistent spatial attribute handling.

The spatial attribute creation properly includes the required filter specification.


772-772: LGTM! Proper spatial filter specification.

The attribute creation correctly includes the required filters: ['point'] parameter.


832-832: LGTM! Consistent application of spatial filters.

The spatial attribute creation properly specifies the required filter parameter.


908-914: LGTM! Comprehensive spatial filter implementation.

All spatial attribute types (polygon, polygon, polygon, point, polygon, linestring) are correctly defined with their respective required filters, demonstrating thorough compliance with the PR's validation changes.


1338-1341: LGTM! Complete spatial filter specification.

All three spatial attribute types are properly defined with their corresponding required filters, ensuring compliance with the new validation requirements.


1769-1770: LGTM! Proper spatial filter implementation.

Both spatial attributes correctly specify their required filters in the aggregation test context.


1858-1858: LGTM! Consistent spatial attribute handling.

The spatial attribute creation properly includes the required filter specification for validation testing.


1865-1865: LGTM! Proper error case testing with filters.

The test correctly includes the required filter even in the error case scenario, ensuring consistent validation behavior.


1872-1872: LGTM! Consistent spatial filter specification.

The spatial attribute creation properly includes the required filter parameter for the main test case.


1943-1945: LGTM! Comprehensive spatial defaults testing with filters.

All three spatial attribute types correctly specify their required filters while testing default value functionality, ensuring proper validation coverage.


2150-2150: LGTM! Proper spatial filter specification in distance testing.

The spatial attribute creation correctly includes the required filter parameter for distance measurement functionality.

tests/e2e/Adapter/Base.php (1)

21-26: Confirm intent: commented-out trait suites in tests/e2e/Adapter/Base.php

Commented trait uses (tests/e2e/Adapter/Base.php, lines 21–26) disable CollectionTests, DocumentTests, AttributeTests, IndexTests, PermissionTests, RelationshipTests; the corresponding scope files still exist at tests/e2e/Adapter/Scopes/{Attribute,Collection,Document,Index,Permission,Relationship}Tests.php — confirm whether this coverage reduction is intentional/temporary for the spatial encode/decode work or a permanent test-strategy change.

@fogelito fogelito closed this Sep 11, 2025
@fogelito fogelito deleted the spatial-encode-decode branch September 11, 2025 09:25
@fogelito fogelito reopened this Sep 15, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/Database/Adapter/MariaDB.php (1)

1-1: Remove debug var_dump calls and run the formatter (Pint).

rg found debug dumps — remove/replace these and then run Pint to fix PSR-12 "statement_indentation" failures.

  • Locations to remove/replace:
    • src/Database/Adapter/Postgres.php: lines 996, 2052, 2114
    • src/Database/Adapter/SQL.php: lines 369, 2757
    • src/Database/Adapter/MariaDB.php: line 867

vendor/bin/pint is missing; install dev deps (e.g., composer install or composer require --dev laravel/pint) and run: vendor/bin/pint -v. Re-run: rg -n "var_dump(" -S to confirm no debug dumps remain.

src/Database/Database.php (3)

514-523: Linestring encode silently passes invalid input; align with point behavior

Returning the original value on invalid input lets bad data slip through. Match point’s strictness: null passthrough, otherwise encode or throw.

Apply:

-            function (mixed $value) {
-                if (!is_array($value)) {
-                    return $value;
-                }
-                try {
-                    return  self::encodeSpatialData($value, Database::VAR_LINESTRING);
-                } catch (\Throwable) {
-                    return $value;
-                }
-            },
+            function (mixed $value) {
+                if ($value === null) {
+                    return null;
+                }
+                try {
+                    return self::encodeSpatialData($value, Database::VAR_LINESTRING);
+                } catch (\Throwable $e) {
+                    throw new StructureException('Invalid linestring');
+                }
+            },

541-550: Polygon encode also returns invalid input; enforce strictness

Same issue as linestring: invalid inputs should raise, not pass through.

Apply:

-            function (mixed $value) {
-                if (!is_array($value)) {
-                    return $value;
-                }
-                try {
-                    return  self::encodeSpatialData($value, Database::VAR_POLYGON);
-                } catch (\Throwable) {
-                    return $value;
-                }
-            },
+            function (mixed $value) {
+                if ($value === null) {
+                    return null;
+                }
+                try {
+                    return self::encodeSpatialData($value, Database::VAR_POLYGON);
+                } catch (\Throwable $e) {
+                    throw new StructureException('Invalid polygon');
+                }
+            },

2251-2260: Updating an attribute to spatial type doesn’t enforce adding the spatial filter

When changing type to point/linestring/polygon, filters may not include the corresponding spatial filter, breaking encode/decode. Ensure filters are updated automatically.

Apply:

             $formatOptions ??= $attribute->getAttribute('formatOptions');
-            $filters ??= $attribute->getAttribute('filters');
+            $filters ??= $attribute->getAttribute('filters');
+            // Normalize filters for spatial/non-spatial types
+            if (\in_array($type, self::SPATIAL_TYPES, true)) {
+                $filters = \array_values(\array_unique([...$filters, $type]));
+            } else {
+                $filters = \array_values(\array_diff($filters, self::SPATIAL_TYPES));
+            }
🧹 Nitpick comments (8)
tests/e2e/Adapter/Scopes/SpatialTests.php (1)

123-139: Round‑trip assertions look good; consider delta when asserting floats.

Spatial decoders may introduce tiny FP rounding. If you see flakes, switch these to use a small delta.

Example:

- $this->assertEquals($point, $createdDoc->getAttribute('pointAttr'));
+ $this->assertEqualsWithDelta($point[0], $createdDoc->getAttribute('pointAttr')[0], 1.0E-9);
+ $this->assertEqualsWithDelta($point[1], $createdDoc->getAttribute('pointAttr')[1], 1.0E-9);
src/Database/Adapter/Postgres.php (1)

2129-2129: Remove unused variable

The variable $isLE is assigned but never used.

Remove the unused variable declaration that was pointed out by PHPMD.

src/Database/Database.php (6)

485-493: Point encode: preserve validator details and remove dead code

Catching and replacing the validator’s message with a generic "Invalid point" discards actionable details. Also, the commented adapter call should be removed.

Apply:

-                try {
-                    return self::encodeSpatialData($value, Database::VAR_POINT);
-                    //return $this->adapter->encodePoint($value);
-                } catch (\Throwable) {
-                    throw new StructureException('Invalid point');
-                }
+                try {
+                    return self::encodeSpatialData($value, Database::VAR_POINT);
+                } catch (\Throwable $e) {
+                    throw new StructureException($e->getMessage(), previous: $e);
+                }

496-509: Remove stray TODO; decode expects string input

"todo:validate array point" is misplaced in a decode path that receives string/Blob WKT/WKB and causes noise.

Apply:

-                /**
-                 * todo:validate array point
-                 */

461-465: Fix Pint: explicit null return

The encoder for datetime returns bare "return;". Pint reports simplified_null_return; return null explicitly.

Apply:

-                if (is_null($value)) {
-                    return;
-                }
+                if (is_null($value)) {
+                    return null;
+                }

2333-2336: Typo: setattribute → setAttribute

Method name casing is inconsistent and hurts readability.

Apply:

-                ->setattribute('key', $newKey ?? $id)
+                ->setAttribute('key', $newKey ?? $id)

525-536: Error message exposure

Re-throwing adapter decode messages verbatim can leak engine-specific details. Consider wrapping with a consistent StructureException message and keep the original as previous for logs.

Also applies to: 551-562


482-510: Add adapter-level spatial encoders or standardize error handling

  • Issue: encode uses self::encodeSpatialData (WKT) while decode uses $this->adapter->decodePoint, preventing adapters from applying SRID/engine-specific transforms.
  • Action: add encodePoint/encodeLinestring/encodePolygon to Adapter and call them here; if you keep the WKT builder, make error handling consistent across point/linestring/polygon.
  • Location: src/Database/Database.php lines 482–510 (also applies to 513–565).
  • I can draft the Adapter interface + implementations in a follow-up PR.
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 745d785 and b94dada.

📒 Files selected for processing (7)
  • src/Database/Adapter.php (1 hunks)
  • src/Database/Adapter/MariaDB.php (1 hunks)
  • src/Database/Adapter/Postgres.php (2 hunks)
  • src/Database/Adapter/SQL.php (2 hunks)
  • src/Database/Database.php (3 hunks)
  • src/Database/Validator/Spatial.php (1 hunks)
  • tests/e2e/Adapter/Scopes/SpatialTests.php (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/Database/Validator/Spatial.php
🧰 Additional context used
🧬 Code graph analysis (5)
src/Database/Adapter.php (2)
src/Database/Adapter/SQL.php (3)
  • decodePoint (2714-2741)
  • decodeLinestring (2743-2775)
  • decodePolygon (2777-2852)
src/Database/Adapter/Postgres.php (3)
  • decodePoint (2012-2036)
  • decodeLinestring (2038-2094)
  • decodePolygon (2096-2165)
src/Database/Adapter/Postgres.php (2)
src/Database/Adapter.php (3)
  • decodePoint (1289-1289)
  • decodeLinestring (1290-1290)
  • decodePolygon (1291-1291)
src/Database/Adapter/SQL.php (3)
  • decodePoint (2714-2741)
  • decodeLinestring (2743-2775)
  • decodePolygon (2777-2852)
tests/e2e/Adapter/Scopes/SpatialTests.php (3)
src/Database/Adapter.php (2)
  • createDocument (702-702)
  • getDocument (692-692)
src/Database/Database.php (2)
  • createDocument (3836-3927)
  • getDocument (3444-3564)
src/Database/Adapter/SQL.php (1)
  • getDocument (351-418)
src/Database/Adapter/SQL.php (2)
src/Database/Adapter.php (6)
  • getAttributeProjection (1158-1158)
  • quote (468-468)
  • getTenantQuery (1281-1281)
  • decodePoint (1289-1289)
  • decodeLinestring (1290-1290)
  • decodePolygon (1291-1291)
src/Database/Adapter/Pool.php (3)
  • getAttributeProjection (479-482)
  • quote (118-121)
  • getTenantQuery (504-507)
src/Database/Database.php (3)
src/Database/Adapter.php (3)
  • decodePoint (1289-1289)
  • decodeLinestring (1290-1290)
  • decodePolygon (1291-1291)
src/Database/Adapter/SQL.php (3)
  • decodePoint (2714-2741)
  • decodeLinestring (2743-2775)
  • decodePolygon (2777-2852)
src/Database/Adapter/Postgres.php (3)
  • decodePoint (2012-2036)
  • decodeLinestring (2038-2094)
  • decodePolygon (2096-2165)
🪛 GitHub Actions: CodeQL
src/Database/Adapter.php

[error] 1289-1289: Method Utopia\Database\Adapter::decodePoint() return type has no value type specified in iterable type array.


[error] 1290-1290: Method Utopia\Database\Adapter::decodeLinestring() return type has no value type specified in iterable type array.


[error] 1291-1291: Method Utopia\Database\Adapter::decodePolygon() return type has no value type specified in iterable type array.

src/Database/Adapter/Postgres.php

[error] 2006-2006: Method Utopia\Database\Adapter\Postgres::encodePoint() has parameter $point with no value type specified in iterable type array.


[error] 2012-2012: Method Utopia\Database\Adapter\Postgres::decodePoint() return type has no value type specified in iterable type array.


[error] 2027-2027: Cannot access offset 0 on string|false.


[error] 2028-2028: Cannot access offset 1 on array|false.


[error] 2028-2028: Parameter #1 $string of function substr expects string, string|false given.


[error] 2032-2032: Cannot access offset 1 on array|false.


[error] 2032-2032: Parameter #1 $string of function substr expects string, string|false given.


[error] 2033-2033: Cannot access offset 1 on array|false.


[error] 2033-2033: Parameter #1 $string of function substr expects string, string|false given.


[error] 2038-2038: Method Utopia\Database\Adapter\Postgres::decodeLinestring() return type has no value type specified in iterable type array.


[error] 2070-2070: Cannot access offset 1 on array|false.


[error] 2083-2083: Cannot access offset 1 on array|false.


[error] 2088-2088: Cannot access offset 1 on array|false.


[error] 2089-2089: Cannot access offset 1 on array|false.


[error] 2096-2096: Method Utopia\Database\Adapter\Postgres::decodePolygon() return type has no value type specified in iterable type array.


[error] 2133-2133: Cannot access offset 1 on array|false.


[error] 2147-2147: Cannot access offset 1 on array|false.


[error] 2152-2152: Cannot access offset 1 on array|false.


[error] 2156-2156: Cannot access offset 1 on array|false.


[error] 2157-2157: Cannot access offset 1 on array|false.

src/Database/Adapter/SQL.php

[error] 2714-2714: Method Utopia\Database\Adapter\SQL::decodePoint() return type has no value type specified in iterable type array.


[error] 2740-2740: Cannot access offset 1 on array|false.


[error] 2740-2740: Cannot access offset 2 on array|false.


[error] 2743-2743: Method Utopia\Database\Adapter\SQL::decodeLinestring() return type has no value type specified in iterable type array.


[error] 2763-2763: Cannot access offset 1 on array|false.


[error] 2768-2768: Cannot access offset 1 on array|false.


[error] 2769-2769: Cannot access offset 1 on array|false.


[error] 2777-2777: Method Utopia\Database\Adapter\SQL::decodePolygon() return type has no value type specified in iterable type array.


[error] 2816-2816: Cannot access offset 1 on array|false.


[error] 2830-2830: Cannot access offset 1 on array|false.


[error] 2836-2836: Cannot access offset 1 on array|false.


[error] 2841-2841: Cannot access offset 1 on array|false.

🪛 GitHub Actions: Linter
src/Database/Adapter/MariaDB.php

[error] 1-1: PSR-12: statement_indentation violation reported by Pint.

src/Database/Adapter/Postgres.php

[error] 1-1: PSR-12: no_multiple_statements_per_line; statement_indentation violations reported by Pint.

src/Database/Adapter/SQL.php

[error] 1-1: PSR-12: statement_indentation; binary_operator_space violations reported by Pint.

src/Database/Database.php

[error] 1-1: PSR-12: method_argument_space; simplified_null_return violations reported by Pint.

🪛 PHPMD (2.15.0)
src/Database/Adapter/Postgres.php

2129-2129: Avoid unused local variables such as '$isLE'. (undefined)

(UnusedLocalVariable)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Adapter Tests (SharedTables/SQLite)
  • GitHub Check: Adapter Tests (Pool)
  • GitHub Check: Adapter Tests (SharedTables/Postgres)
  • GitHub Check: Adapter Tests (SharedTables/MariaDB)
  • GitHub Check: Adapter Tests (SharedTables/MySQL)
  • GitHub Check: Adapter Tests (Mirror)
  • GitHub Check: Adapter Tests (MariaDB)
  • GitHub Check: Adapter Tests (SQLite)
  • GitHub Check: Adapter Tests (Postgres)
  • GitHub Check: Adapter Tests (MySQL)
  • GitHub Check: Unit Test
🔇 Additional comments (1)
tests/e2e/Adapter/Scopes/SpatialTests.php (1)

116-119: Good: test data hoisted to locals for reuse.

This reduces duplication and clarifies expectations.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/Database/Adapter/MariaDB.php (1)

1-1: Remove stray var_dump() calls and run Pint to fix PSR-12 indentation.

Detected var_dump() locations:

  • src/Database/Adapter/SQL.php:369, 2757
  • src/Database/Adapter/Postgres.php:996, 2046, 2108
  • src/Database/Adapter/MariaDB.php:867

Remove those debug statements, then run vendor/bin/pint and re-run the grep check for var_dump.

src/Database/Adapter/SQL.php (1)

1-1: Remove debug var_dump() calls and run Pint to fix PSR-12

Remove the var_dump() debug prints below and run vendor/bin/pint to auto-fix formatting; re-run the search to confirm none remain.

  • src/Database/Adapter/Postgres.php:996 — var_dump($sql);
  • src/Database/Adapter/Postgres.php:2046 — var_dump($wkb);
  • src/Database/Adapter/Postgres.php:2108 — var_dump($wkb);
  • src/Database/Adapter/SQL.php:369 — var_dump($sql);
  • src/Database/Adapter/SQL.php:2757 — var_dump($wkb);
  • src/Database/Adapter/MariaDB.php:867 — var_dump($sql);
src/Database/Database.php (2)

461-469: Fix Pint failure: simplify the early null-return in datetime encoder

Pint reports simplified_null_return. Prefer strict compare and returning the variable itself to satisfy the fixer and keep semantics unchanged.

Apply this diff:

-                if (is_null($value)) {
-                    return null;
-                }
+                if ($value === null) {
+                    return $value;
+                }

499-507: Critical: static filter closure captures $this; breaks multi-adapter instances + wrong return phpdoc

These filters are stored in a static map (self::$filters). Capturing $this binds the first/last Database instance into a static closure, causing wrong adapter usage when multiple Database instances/adapters coexist. Also, phpdoc says “string|null” but decodePoint returns array.

Use the instance passed by decodeAttribute (third argument) instead, and fix phpdoc/ternary for Pint.

Apply this diff:

-            /**
-             * @param string|null $value
-             * @return string|null
-             */
-            function (?string $value) {
-                if ($value === null) {
-                    return null;
-                }
-                return $this->adapter->decodePoint($value);
-            }
+            /**
+             * @param string|null $value
+             * @param Document $_
+             * @param Database $db
+             * @return array|null
+             */
+            function (?string $value, Document $_, Database $db) {
+                return $value === null ? null : $db->adapter->decodePoint($value);
+            }
♻️ Duplicate comments (12)
src/Database/Adapter/SQL.php (5)

362-362: Do not override computed spatial attributes

This debug assignment disables ST_AsText projections. Remove it.

-        $spatialAttributes=[];
+        // keep computed spatial attributes

369-369: Remove debug output leaking SQL

Strip var_dump($sql); before merge.

-var_dump($sql);

2714-2741: Harden POINT decoder: type-hint, hex/binary handling, endianness-safe unpack, and error checks

Prevents “array|false offset” issues and aligns with Adapter contract.

-    public function decodePoint(mixed $wkb): array
-    {
-        if (str_starts_with(strtoupper($wkb), 'POINT(')) {
-            $start = strpos($wkb, '(') + 1;
-            $end = strrpos($wkb, ')');
-            $inside = substr($wkb, $start, $end - $start);
-            $coords = explode(' ', trim($inside));
-            return [(float)$coords[0], (float)$coords[1]];
-        }
-
-        // MySQL SRID-aware WKB layout:
-        // 1 byte  = endian (1 = little endian)
-        // 4 bytes = type + SRID flag
-        // 4 bytes = SRID
-        // 16 bytes = X,Y coordinates (double each, little endian)
-
-        $byteOrder = ord($wkb[0]);
-        $littleEndian = ($byteOrder === 1);
-
-        // Skip 1 + 4 + 4 = 9 bytes to get coordinates
-        $coordsBin = substr($wkb, 9, 16);
-
-        // Unpack doubles
-        $format = $littleEndian ? 'd2' : 'd2'; // little-endian doubles
-        $coords = unpack($format, $coordsBin);
-
-        return [$coords[1], $coords[2]];
-    }
+    /**
+     * @param string $wkb WKT "POINT(x y)" or hex/binary WKB
+     * @return array{0: float, 1: float}
+     * @throws DatabaseException
+     */
+    public function decodePoint(string $wkb): array
+    {
+        $wkb = trim($wkb);
+        if (str_starts_with(strtoupper($wkb), 'POINT(')) {
+            $start = strpos($wkb, '(');
+            $end = strrpos($wkb, ')');
+            if ($start === false || $end === false || $end <= $start + 1) {
+                throw new DatabaseException('Invalid POINT WKT');
+            }
+            $inside = substr($wkb, $start + 1, $end - $start - 1);
+            $coords = preg_split('/\s+/', trim($inside));
+            if (count($coords) !== 2 || !is_numeric($coords[0]) || !is_numeric($coords[1])) {
+                throw new DatabaseException('Invalid POINT coordinates');
+            }
+            return [(float)$coords[0], (float)$coords[1]];
+        }
+
+        if (str_starts_with($wkb, '0x') || ctype_xdigit($wkb)) {
+            $bin = hex2bin(str_starts_with($wkb, '0x') ? substr($wkb, 2) : $wkb);
+            if ($bin === false) {
+                throw new DatabaseException('Invalid hex WKB');
+            }
+            $wkb = $bin;
+        }
+        if (strlen($wkb) < 1 + 4 + 16) {
+            throw new DatabaseException('WKB too short for POINT');
+        }
+
+        $byteOrder = ord($wkb[0]);
+        $isLE = ($byteOrder === 1);
+        $typeArr = unpack($isLE ? 'V' : 'N', substr($wkb, 1, 4));
+        if ($typeArr === false || !isset($typeArr[1])) {
+            throw new DatabaseException('Invalid WKB type');
+        }
+        $typeField = $typeArr[1];
+        $hasSRID = ($typeField & 0x20000000) !== 0;
+        $geomType = $typeField & 0xFF;
+        if ($geomType !== 1) {
+            throw new DatabaseException("Not a POINT geometry type, got {$geomType}");
+        }
+        $offset = 5 + ($hasSRID ? 4 : 0);
+        if (strlen($wkb) < $offset + 16) {
+            throw new DatabaseException('WKB too short for POINT coords');
+        }
+        $fmt = $isLE ? 'e' : 'E';
+        $xArr = unpack($fmt, substr($wkb, $offset, 8));
+        $yArr = unpack($fmt, substr($wkb, $offset + 8, 8));
+        if ($xArr === false || $yArr === false || !isset($xArr[1], $yArr[1])) {
+            throw new DatabaseException('Failed to decode POINT coordinates');
+        }
+        return [(float)$xArr[1], (float)$yArr[1]];
+    }

2777-2852: Harden POLYGON decoder (hex handling, endianness-safe doubles, robust bounds)

Adds PHPDoc generics and consistent offset logic with optional SRID.

-    public function decodePolygon(string $wkb): array
-    {
-        // POLYGON((x1,y1),(x2,y2))
-        if (str_starts_with($wkb, 'POLYGON((')) {
-            $start = strpos($wkb, '((') + 2;
-            $end = strrpos($wkb, '))');
-            $inside = substr($wkb, $start, $end - $start);
-
-            $rings = explode('),(', $inside);
-            return array_map(function ($ring) {
-                $points = explode(',', $ring);
-                return array_map(function ($point) {
-                    $coords = explode(' ', trim($point));
-                    return [(float)$coords[0], (float)$coords[1]];
-                }, $points);
-            }, $rings);
-        }
-
-        // Convert HEX string to binary if needed
-        if (str_starts_with($wkb, '0x') || ctype_xdigit($wkb)) {
-            $wkb = hex2bin(str_starts_with($wkb, '0x') ? substr($wkb, 2) : $wkb);
-            if ($wkb === false) {
-                throw new DatabaseException('Invalid hex WKB');
-            }
-        }
-
-        if (strlen($wkb) < 21) {
-            throw new DatabaseException('WKB too short to be a POLYGON');
-        }
-
-        // MySQL SRID-aware WKB layout: 4 bytes SRID prefix
-        $offset = 4;
-
-        $byteOrder = ord($wkb[$offset]);
-        if ($byteOrder !== 1) {
-            throw new DatabaseException('Only little-endian WKB supported');
-        }
-        $offset += 1;
-
-        $type = unpack('V', substr($wkb, $offset, 4))[1];
-        $hasSRID = ($type & 0x20000000) === 0x20000000;
-        $geomType = $type & 0xFF;
-        $offset += 4;
-
-        if ($geomType !== 3) { // 3 = POLYGON
-            throw new DatabaseException("Not a POLYGON geometry type, got {$geomType}");
-        }
-
-        // Skip SRID in type flag if present
-        if ($hasSRID) {
-            $offset += 4;
-        }
-
-        $numRings = unpack('V', substr($wkb, $offset, 4))[1];
-        $offset += 4;
-
-        $rings = [];
-
-        for ($r = 0; $r < $numRings; $r++) {
-            $numPoints = unpack('V', substr($wkb, $offset, 4))[1];
-            $offset += 4;
-            $ring = [];
-
-            for ($p = 0; $p < $numPoints; $p++) {
-                $x = unpack('d', substr($wkb, $offset, 8))[1];
-                $y = unpack('d', substr($wkb, $offset + 8, 8))[1];
-                $ring[] = [(float)$x, (float)$y];
-                $offset += 16;
-            }
-
-            $rings[] = $ring;
-        }
-
-        return $rings;
-
-    }
+    /**
+     * @param string $wkb WKT "POLYGON((...))" or hex/binary WKB
+     * @return list<list<array{0: float, 1: float}>>
+     * @throws DatabaseException
+     */
+    public function decodePolygon(string $wkb): array
+    {
+        if (str_starts_with(strtoupper($wkb), 'POLYGON((')) {
+            $start = strpos($wkb, '((');
+            $end = strrpos($wkb, '))');
+            if ($start === false || $end === false || $end <= $start + 2) {
+                throw new DatabaseException('Invalid POLYGON WKT');
+            }
+            $inside = substr($wkb, $start + 2, $end - $start - 2);
+            $ringsStr = explode('),(', $inside);
+            $rings = [];
+            foreach ($ringsStr as $ring) {
+                $pts = array_map('trim', explode(',', $ring));
+                $r = [];
+                foreach ($pts as $pt) {
+                    $coords = preg_split('/\s+/', $pt);
+                    if (count($coords) !== 2 || !is_numeric($coords[0]) || !is_numeric($coords[1])) {
+                        throw new DatabaseException('Invalid point in POLYGON ring');
+                    }
+                    $r[] = [(float)$coords[0], (float)$coords[1]];
+                }
+                $rings[] = $r;
+            }
+            return $rings;
+        }
+
+        if (str_starts_with($wkb, '0x') || ctype_xdigit($wkb)) {
+            $bin = hex2bin(str_starts_with($wkb, '0x') ? substr($wkb, 2) : $wkb);
+            if ($bin === false) {
+                throw new DatabaseException('Invalid hex WKB');
+            }
+            $wkb = $bin;
+        }
+        if (strlen($wkb) < 1 + 4 + 4) {
+            throw new DatabaseException('WKB too short');
+        }
+
+        $byteOrder = ord($wkb[0]);
+        $isLE = ($byteOrder === 1);
+        $typeArr = unpack($isLE ? 'V' : 'N', substr($wkb, 1, 4));
+        if ($typeArr === false || !isset($typeArr[1])) {
+            throw new DatabaseException('Invalid WKB type');
+        }
+        $typeField = $typeArr[1];
+        $hasSRID = ($typeField & 0x20000000) !== 0;
+        $geomType = $typeField & 0xFF;
+        if ($geomType !== 3) {
+            throw new DatabaseException("Not a POLYGON geometry type, got {$geomType}");
+        }
+
+        $offset = 5 + ($hasSRID ? 4 : 0);
+        $numRingsArr = unpack($isLE ? 'V' : 'N', substr($wkb, $offset, 4));
+        if ($numRingsArr === false || !isset($numRingsArr[1])) {
+            throw new DatabaseException('Failed to decode POLYGON ring count');
+        }
+        $numRings = $numRingsArr[1];
+        $offset += 4;
+
+        $fmt = $isLE ? 'e' : 'E';
+        $rings = [];
+        for ($r = 0; $r < $numRings; $r++) {
+            $numPointsArr = unpack($isLE ? 'V' : 'N', substr($wkb, $offset, 4));
+            if ($numPointsArr === false || !isset($numPointsArr[1])) {
+                throw new DatabaseException('Failed to decode POLYGON point count');
+            }
+            $numPoints = $numPointsArr[1];
+            $offset += 4;
+            $ring = [];
+            for ($p = 0; $p < $numPoints; $p++) {
+                if (strlen($wkb) < $offset + 16) {
+                    throw new DatabaseException('WKB too short while reading POLYGON points');
+                }
+                $xArr = unpack($fmt, substr($wkb, $offset, 8));
+                $yArr = unpack($fmt, substr($wkb, $offset + 8, 8));
+                if ($xArr === false || $yArr === false || !isset($xArr[1], $yArr[1])) {
+                    throw new DatabaseException('Failed to decode POLYGON point');
+                }
+                $ring[] = [(float)$xArr[1], (float)$yArr[1]];
+                $offset += 16;
+            }
+            $rings[] = $ring;
+        }
+        return $rings;
+    }

2743-2775: Harden LINESTRING decoder and remove debug

Add PHPDoc generics, support hex WKB, validate unpack results, and use endian-safe doubles.

-    public function decodeLinestring(string $wkb): array
-    {
-        if (str_starts_with(strtoupper($wkb), 'LINESTRING(')) {
-            $start = strpos($wkb, '(') + 1;
-            $end = strrpos($wkb, ')');
-            $inside = substr($wkb, $start, $end - $start);
-
-            $points = explode(',', $inside);
-            return array_map(function ($point) {
-                $coords = explode(' ', trim($point));
-                return [(float)$coords[0], (float)$coords[1]];
-            }, $points);
-        }
-
-        var_dump($wkb);
-
-        // Skip 1 byte (endianness) + 4 bytes (type) + 4 bytes (SRID)
-        $offset = 9;
-
-        // Number of points (4 bytes little-endian)
-        $numPoints = unpack('V', substr($wkb, $offset, 4))[1];
-        $offset += 4;
-
-        $points = [];
-        for ($i = 0; $i < $numPoints; $i++) {
-            $x = unpack('d', substr($wkb, $offset, 8))[1];
-            $y = unpack('d', substr($wkb, $offset + 8, 8))[1];
-            $points[] = [(float)$x, (float)$y];
-            $offset += 16;
-        }
-
-        return $points;
-    }
+    /**
+     * @param string $wkb WKT "LINESTRING(...)" or hex/binary WKB
+     * @return list<array{0: float, 1: float}>
+     * @throws DatabaseException
+     */
+    public function decodeLinestring(string $wkb): array
+    {
+        $wkb = trim($wkb);
+        if (str_starts_with(strtoupper($wkb), 'LINESTRING(')) {
+            $start = strpos($wkb, '(');
+            $end = strrpos($wkb, ')');
+            if ($start === false || $end === false || $end <= $start + 1) {
+                throw new DatabaseException('Invalid LINESTRING WKT');
+            }
+            $inside = substr($wkb, $start + 1, $end - $start - 1);
+            $parts = array_map('trim', explode(',', $inside));
+            $out = [];
+            foreach ($parts as $p) {
+                $coords = preg_split('/\s+/', $p);
+                if (count($coords) !== 2 || !is_numeric($coords[0]) || !is_numeric($coords[1])) {
+                    throw new DatabaseException('Invalid point in LINESTRING');
+                }
+                $out[] = [(float)$coords[0], (float)$coords[1]];
+            }
+            return $out;
+        }
+
+        if (str_starts_with($wkb, '0x') || ctype_xdigit($wkb)) {
+            $bin = hex2bin(str_starts_with($wkb, '0x') ? substr($wkb, 2) : $wkb);
+            if ($bin === false) {
+                throw new DatabaseException('Invalid hex WKB');
+            }
+            $wkb = $bin;
+        }
+        if (strlen($wkb) < 1 + 4 + 4) {
+            throw new DatabaseException('WKB too short to be a valid geometry');
+        }
+
+        $byteOrder = ord($wkb[0]);
+        $isLE = ($byteOrder === 1);
+        $typeArr = unpack($isLE ? 'V' : 'N', substr($wkb, 1, 4));
+        if ($typeArr === false || !isset($typeArr[1])) {
+            throw new DatabaseException('Invalid WKB type');
+        }
+        $typeField = $typeArr[1];
+        $hasSRID = ($typeField & 0x20000000) !== 0;
+        $geomType = $typeField & 0xFF;
+        if ($geomType !== 2) {
+            throw new DatabaseException("Not a LINESTRING geometry type, got {$geomType}");
+        }
+
+        $offset = 5 + ($hasSRID ? 4 : 0);
+        $numPointsArr = unpack($isLE ? 'V' : 'N', substr($wkb, $offset, 4));
+        if ($numPointsArr === false || !isset($numPointsArr[1])) {
+            throw new DatabaseException('Failed to decode LINESTRING point count');
+        }
+        $numPoints = $numPointsArr[1];
+        $offset += 4;
+
+        $fmt = $isLE ? 'e' : 'E';
+        $points = [];
+        for ($i = 0; $i < $numPoints; $i++) {
+            if (strlen($wkb) < $offset + 16) {
+                throw new DatabaseException('WKB too short while reading LINESTRING points');
+            }
+            $xArr = unpack($fmt, substr($wkb, $offset, 8));
+            $yArr = unpack($fmt, substr($wkb, $offset + 8, 8));
+            if ($xArr === false || $yArr === false || !isset($xArr[1], $yArr[1])) {
+                throw new DatabaseException('Failed to decode LINESTRING point');
+            }
+            $points[] = [(float)$xArr[1], (float)$yArr[1]];
+            $offset += 16;
+        }
+        return $points;
+    }
src/Database/Adapter/MariaDB.php (1)

867-867: Remove debug output leaking SQL

Delete var_dump($sql); to avoid leaking queries in prod/CI.

-var_dump($sql);
src/Database/Adapter.php (1)

1289-1291: Fix CodeQL by documenting shaped return types for spatial decoders and align implementations

  • Add precise PHPDoc shaped return types to the three abstract methods in src/Database/Adapter.php (lines 1289–1291) — the original suggested PHPDoc is valid and should be applied to the abstract signatures.
  • Mirror the same PHPDoc in each concrete implementation and ensure parameter-type intent is consistent: implementations found at src/Database/Adapter/SQL.php (decodePoint @2714, decodeLinestring @2743, decodePolygon @2777) and src/Database/Adapter/Postgres.php (decodePoint @2006, decodeLinestring @2032, decodePolygon @2090) currently use mixed for some params — either keep the abstract param as string and tighten implementations, or intentionally relax the abstract param to mixed, then add the shaped PHPDoc everywhere.

Suggested diff for Adapter.php (apply and replicate similar PHPDoc to implementations):

-    abstract public function decodePoint(string $wkb): array;
-    abstract public function decodeLinestring(string $wkb): array;
-    abstract public function decodePolygon(string $wkb): array;
+    /**
+     * @param string $wkb WKT like "POINT(x y)" or hex/binary WKB
+     * @return array{0: float, 1: float}
+     */
+    abstract public function decodePoint(string $wkb): array;
+    /**
+     * @param string $wkb WKT like "LINESTRING(...)" or hex/binary WKB
+     * @return list<array{0: float, 1: float}>
+     */
+    abstract public function decodeLinestring(string $wkb): array;
+    /**
+     * @param string $wkb WKT like "POLYGON((...))" or hex/binary WKB
+     * @return list<list<array{0: float, 1: float}>>
+     */
+    abstract public function decodePolygon(string $wkb): array;
src/Database/Database.php (1)

503-507: Heads up: adapter decode paths still have var_dump noise in adapters

This code calls adapter->decodePoint/Linestring/Polygon(). The adapter implementations still contain var_dump() per prior review; those will leak to output/logs during decode.

Run to confirm and scrub:

#!/bin/bash
rg -n "var_dump\s*\(" -C2 src/Database/Adapter

Also applies to: 531-535, 559-563

src/Database/Adapter/Postgres.php (4)

996-996: Remove debug output (var_dump($sql))

Leftover debug will leak SQL in production and break tests/logs. Delete it.

Apply this diff:

-        var_dump($sql);

2090-2159: Fix POLYGON decoder: consistent exceptions, endian handling, float size, unsafe unpack, and debug

  • Replace RuntimeException with DatabaseException.
  • Remove var_dump.
  • Validate hex even length and lengths before substr/unpack.
  • Enforce LE or select correct double format; use 'g' (LE double).
  • Add precise PHPDoc.

Apply this diff:

-    public function decodePolygon(string $wkb): array
+    /**
+     * @param string $wkb WKT or WKB (hex/binary)
+     * @return array<int, array<int, array{0: float, 1: float}>>
+     * @throws DatabaseException
+     */
+    public function decodePolygon(string $wkb): array
     {
-        // POLYGON((x1,y1),(x2,y2))
-        if (str_starts_with($wkb, 'POLYGON((')) {
+        $wkb = trim($wkb);
+        // WKT
+        if (str_starts_with(strtoupper($wkb), 'POLYGON(')) {
             $start = strpos($wkb, '((') + 2;
             $end = strrpos($wkb, '))');
-            $inside = substr($wkb, $start, $end - $start);
+            if ($start === false || $end === false || $end <= $start) {
+                throw new DatabaseException('Invalid POLYGON WKT');
+            }
+            $inside = substr($wkb, $start, $end - $start);
 
             $rings = explode('),(', $inside);
             return array_map(function ($ring) {
                 $points = explode(',', $ring);
                 return array_map(function ($point) {
                     $coords = explode(' ', trim($point));
+                    if (count($coords) !== 2 || !is_numeric($coords[0]) || !is_numeric($coords[1])) {
+                        throw new DatabaseException('Invalid point in POLYGON ring');
+                    }
                     return [(float)$coords[0], (float)$coords[1]];
                 }, $points);
             }, $rings);
         }
 
-        var_dump($wkb);
-
         // Convert hex string to binary if needed
-        if (preg_match('/^[0-9a-fA-F]+$/', $wkb)) {
-            $wkb = hex2bin($wkb);
-            if ($wkb === false) {
-                throw new \RuntimeException("Invalid hex WKB");
+        $bin = $wkb;
+        if (preg_match('/^[0-9a-fA-F]+$/', $wkb) && (strlen($wkb) % 2 === 0)) {
+            $bin = hex2bin($wkb);
+            if ($bin === false) {
+                throw new DatabaseException("Invalid hex WKB");
             }
         }
 
-        if (strlen($wkb) < 9) {
-            throw new \RuntimeException("WKB too short");
+        if (!is_string($bin) || strlen($bin) < 9) {
+            throw new DatabaseException("WKB too short");
         }
 
-        $byteOrder = ord($wkb[0]);
-        $isLE = $byteOrder === 1; // assume little-endian
-        $uInt32 = 'V'; // little-endian 32-bit unsigned
-        $uDouble = 'd'; // little-endian double
+        $byteOrder = ord($bin[0]);
+        if ($byteOrder === 0) {
+            throw new DatabaseException("Big-endian WKB not supported");
+        } elseif ($byteOrder !== 1) {
+            throw new DatabaseException("Invalid byte order in WKB");
+        }
+        $uInt32 = 'V'; // little-endian 32-bit unsigned
+        $uDouble = 'g'; // little-endian double (64-bit)
 
-        $typeInt = unpack($uInt32, substr($wkb, 1, 4))[1];
+        $typeIntArr = unpack($uInt32, substr($bin, 1, 4));
+        if ($typeIntArr === false || !isset($typeIntArr[1])) {
+            throw new DatabaseException('Failed to decode POLYGON type');
+        }
+        $typeInt = $typeIntArr[1];
         $hasSrid = ($typeInt & 0x20000000) !== 0;
         $geomType = $typeInt & 0xFF;
 
         if ($geomType !== 3) { // 3 = POLYGON
-            throw new \RuntimeException("Not a POLYGON geometry type, got {$geomType}");
+            throw new DatabaseException("Not a POLYGON geometry type, got {$geomType}");
         }
 
-        $offset = 5;
-        if ($hasSrid) {
-            $offset += 4;
-        }
+        $offset = 5 + ($hasSrid ? 4 : 0);
 
         // Number of rings
-        $numRings = unpack($uInt32, substr($wkb, $offset, 4))[1];
+        if (strlen($bin) < $offset + 4) {
+            throw new DatabaseException('WKB too short for POLYGON ring count');
+        }
+        $numRingsArr = unpack($uInt32, substr($bin, $offset, 4));
+        if ($numRingsArr === false || !isset($numRingsArr[1])) {
+            throw new DatabaseException('Failed to decode POLYGON ring count');
+        }
+        $numRings = (int)$numRingsArr[1];
         $offset += 4;
 
         $rings = [];
         for ($r = 0; $r < $numRings; $r++) {
-            $numPoints = unpack($uInt32, substr($wkb, $offset, 4))[1];
+            if (strlen($bin) < $offset + 4) {
+                throw new DatabaseException('WKB too short for POLYGON point count');
+            }
+            $numPointsArr = unpack($uInt32, substr($bin, $offset, 4));
+            if ($numPointsArr === false || !isset($numPointsArr[1])) {
+                throw new DatabaseException('Failed to decode POLYGON point count');
+            }
+            $numPoints = (int)$numPointsArr[1];
             $offset += 4;
             $points = [];
-            for ($i = 0; $i < $numPoints; $i++) {
-                $x = unpack($uDouble, substr($wkb, $offset, 8))[1];
-                $y = unpack($uDouble, substr($wkb, $offset + 8, 8))[1];
-                $points[] = [(float)$x, (float)$y];
-                $offset += 16;
+            if (strlen($bin) < $offset + (16 * $numPoints)) {
+                throw new DatabaseException('WKB too short for POLYGON coordinates');
+            }
+            for ($i = 0; $i < $numPoints; $i++) {
+                $xArr = unpack($uDouble, substr($bin, $offset, 8));
+                $yArr = unpack($uDouble, substr($bin, $offset + 8, 8));
+                if ($xArr === false || $yArr === false || !isset($xArr[1], $yArr[1])) {
+                    throw new DatabaseException('Failed to decode POLYGON point');
+                }
+                $points[] = [(float)$xArr[1], (float)$yArr[1]];
+                $offset += 16;
             }
             $rings[] = $points;
         }
 
         return $rings; // array of rings, each ring is array of [x,y]
     }

2032-2088: Fix LINESTRING decoder: wrong float format, unsafe unpack, stray debug, and signature mismatch

  • Replace 'e' with 'g' (doubles).
  • Remove var_dump.
  • Validate hex, lengths, type = 2, and counts.
  • Split multiple statements per line to satisfy PSR‑12.
  • Signature must be string.

Apply this diff:

-    public function decodeLinestring(mixed $wkb): array
+    /**
+     * @param string $wkb WKT or WKB (hex/binary)
+     * @return array<int, array{0: float, 1: float}>
+     * @throws DatabaseException
+     */
+    public function decodeLinestring(string $wkb): array
     {
-        if (str_starts_with(strtoupper($wkb), 'LINESTRING(')) {
-            $start = strpos($wkb, '(') + 1;
-            $end = strrpos($wkb, ')');
-            $inside = substr($wkb, $start, $end - $start);
-
-            $points = explode(',', $inside);
-            return array_map(function ($point) {
-                $coords = explode(' ', trim($point));
-                return [(float)$coords[0], (float)$coords[1]];
-            }, $points);
-        }
-
-        var_dump($wkb);
-
-        if (ctype_xdigit($wkb)) {
-            $wkb = hex2bin($wkb);
-        }
-
-        if (strlen($wkb) < 9) {
+        $wkb = trim($wkb);
+        if (str_starts_with(strtoupper($wkb), 'LINESTRING(')) {
+            $start = strpos($wkb, '(');
+            $end = strrpos($wkb, ')');
+            if ($start === false || $end === false || $end <= $start + 1) {
+                throw new DatabaseException('Invalid LINESTRING WKT');
+            }
+            $inside = substr($wkb, $start + 1, $end - $start - 1);
+            $parts = array_filter(array_map('trim', explode(',', $inside)), fn($p) => $p !== '');
+            $out = [];
+            foreach ($parts as $point) {
+                $coords = preg_split('/\s+/', $point);
+                if (count($coords) !== 2 || !is_numeric($coords[0]) || !is_numeric($coords[1])) {
+                    throw new DatabaseException('Invalid point in LINESTRING WKT');
+                }
+                $out[] = [(float)$coords[0], (float)$coords[1]];
+            }
+            return $out;
+        }
+
+        // Hex or binary WKB
+        $bin = $wkb;
+        if (ctype_xdigit($wkb) && (strlen($wkb) % 2 === 0)) {
+            $bin = hex2bin($wkb);
+            if ($bin === false) {
+                throw new DatabaseException('Invalid hex WKB');
+            }
+        }
+
+        if (!is_string($bin) || strlen($bin) < 9) {
             throw new DatabaseException("WKB too short to be a valid geometry");
         }
 
-        $byteOrder = ord($wkb[0]);
+        $byteOrder = ord($bin[0]);
         if ($byteOrder === 0) {
             throw new DatabaseException("Big-endian WKB not supported");
         } elseif ($byteOrder !== 1) {
             throw new DatabaseException("Invalid byte order in WKB");
         }
 
         // Type + SRID flag
-        $typeField = unpack('V', substr($wkb, 1, 4))[1];
+        $typeFieldArr = unpack('V', substr($bin, 1, 4));
+        if ($typeFieldArr === false || !isset($typeFieldArr[1])) {
+            throw new DatabaseException('Failed to decode LINESTRING type');
+        }
+        $typeField = $typeFieldArr[1];
         $geomType = $typeField & 0xFF;
         $hasSRID = ($typeField & 0x20000000) !== 0;
 
         if ($geomType !== 2) { // 2 = LINESTRING
             throw new DatabaseException("Not a LINESTRING geometry type, got {$geomType}");
         }
 
-        $offset = 5;
-        if ($hasSRID) {
-            $offset += 4;
-        }
+        $offset = 5 + ($hasSRID ? 4 : 0);
 
-        $numPoints = unpack('V', substr($wkb, $offset, 4))[1];
+        if (strlen($bin) < $offset + 4) {
+            throw new DatabaseException('WKB too short for LINESTRING point count');
+        }
+        $numPointsArr = unpack('V', substr($bin, $offset, 4));
+        if ($numPointsArr === false || !isset($numPointsArr[1])) {
+            throw new DatabaseException('Failed to decode LINESTRING point count');
+        }
+        $numPoints = (int)$numPointsArr[1];
         $offset += 4;
+        if ($numPoints < 0) {
+            throw new DatabaseException('Invalid LINESTRING point count');
+        }
+        if (strlen($bin) < $offset + (16 * $numPoints)) {
+            throw new DatabaseException('WKB too short for LINESTRING coordinates');
+        }
 
         $points = [];
         for ($i = 0; $i < $numPoints; $i++) {
-            $x = unpack('e', substr($wkb, $offset, 8))[1]; $offset += 8;
-            $y = unpack('e', substr($wkb, $offset, 8))[1]; $offset += 8;
-            $points[] = [(float)$x, (float)$y];
+            $xArr = unpack('g', substr($bin, $offset, 8));
+            $yArr = unpack('g', substr($bin, $offset + 8, 8));
+            if ($xArr === false || $yArr === false || !isset($xArr[1], $yArr[1])) {
+                throw new DatabaseException('Failed to decode LINESTRING point');
+            }
+            $x = (float)$xArr[1];
+            $y = (float)$yArr[1];
+            $points[] = [$x, $y];
+            $offset += 16;
         }
 
         return $points;
     }

2006-2030: Fix POINT decoder: wrong float format, unsafe unpack, and signature mismatch

  • Use 64‑bit doubles ('g'/'G'), not 32‑bit floats ('e'/'E').
  • Guard hex2bin/length before ord()/substr()/unpack().
  • Enforce geometry type = 1 (POINT).
  • Match Adapter::decodePoint(string $wkb): array.
  • Add precise return PHPDoc to satisfy CodeQL.

Apply this diff:

-    public function decodePoint(mixed $wkb): array
+    /**
+     * Decode a POINT from WKT or WKB (hex or binary).
+     * @param string $wkb
+     * @return array{0: float, 1: float}
+     * @throws DatabaseException
+     */
+    public function decodePoint(string $wkb): array
     {
-        //$wkb = str_replace('SRID=4326;', '', $wkb); // Remove if was added in encode
-
-        if (str_starts_with(strtoupper($wkb), 'POINT(')) {
-            $start = strpos($wkb, '(') + 1;
-            $end = strrpos($wkb, ')');
-            $inside = substr($wkb, $start, $end - $start);
-
-            $coords = explode(' ', trim($inside));
-            return [(float)$coords[0], (float)$coords[1]];
-        }
-
-        $bin = hex2bin($wkb);
-
-        $isLE = ord($bin[0]) === 1;
-        $type  = unpack($isLE ? 'V' : 'N', substr($bin, 1, 4))[1];
-        $offset = 5 + (($type & 0x20000000) ? 4 : 0);
-
-        $fmt = $isLE ? 'e' : 'E'; // little vs big endian double
-        $x = unpack($fmt, substr($bin, $offset, 8))[1];
-        $y = unpack($fmt, substr($bin, $offset + 8, 8))[1];
-
-        return [(float)$x, (float)$y];
+        $wkb = trim($wkb);
+
+        // WKT
+        if (str_starts_with(strtoupper($wkb), 'POINT(')) {
+            $start = strpos($wkb, '(');
+            $end = strrpos($wkb, ')');
+            if ($start === false || $end === false || $end <= $start + 1) {
+                throw new DatabaseException('Invalid POINT WKT');
+            }
+            $inside = substr($wkb, $start + 1, $end - $start - 1);
+            $coords = preg_split('/\s+/', trim($inside));
+            if (count($coords) !== 2 || !is_numeric($coords[0]) || !is_numeric($coords[1])) {
+                throw new DatabaseException('Invalid POINT WKT coordinates');
+            }
+            return [(float)$coords[0], (float)$coords[1]];
+        }
+
+        // Hex or binary WKB
+        $bin = $wkb;
+        if (ctype_xdigit($wkb) && (strlen($wkb) % 2 === 0)) {
+            $bin = hex2bin($wkb);
+            if ($bin === false) {
+                throw new DatabaseException('Invalid hex WKB for POINT');
+            }
+        }
+        if (!is_string($bin) || strlen($bin) < 1 + 4 + 16) {
+            throw new DatabaseException('WKB too short for POINT');
+        }
+
+        $byteOrder = ord($bin[0]);
+        if ($byteOrder !== 0 && $byteOrder !== 1) {
+            throw new DatabaseException('Invalid byte order in WKB');
+        }
+        $isLE = ($byteOrder === 1);
+
+        $typeArr = unpack($isLE ? 'Vtype' : 'Ntype', substr($bin, 1, 4));
+        if ($typeArr === false || !isset($typeArr['type'])) {
+            throw new DatabaseException('Failed to decode POINT type');
+        }
+        $type = $typeArr['type'];
+        $hasSRID = ($type & 0x20000000) !== 0;
+        $geomType = $type & 0xFF;
+        if ($geomType !== 1) {
+            throw new DatabaseException("Not a POINT geometry type, got {$geomType}");
+        }
+
+        $offset = 5 + ($hasSRID ? 4 : 0);
+        if (strlen($bin) < $offset + 16) {
+            throw new DatabaseException('WKB too short for POINT coordinates');
+        }
+        $fmt = $isLE ? 'g' : 'G'; // little/big-endian double (64-bit)
+        $xArr = unpack($fmt, substr($bin, $offset, 8));
+        $yArr = unpack($fmt, substr($bin, $offset + 8, 8));
+        if ($xArr === false || $yArr === false || !isset($xArr[1], $yArr[1])) {
+            throw new DatabaseException('Failed to decode POINT coordinates');
+        }
+        return [(float)$xArr[1], (float)$yArr[1]];
     }
🧹 Nitpick comments (1)
src/Database/Adapter/Postgres.php (1)

2082-2083: PSR‑12: one statement per line

Two statements per line trigger the Pint error. The diff above splits them; if you keep current structure, split them manually.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 745d785 and f6bd630.

📒 Files selected for processing (6)
  • src/Database/Adapter.php (1 hunks)
  • src/Database/Adapter/MariaDB.php (1 hunks)
  • src/Database/Adapter/Postgres.php (2 hunks)
  • src/Database/Adapter/SQL.php (2 hunks)
  • src/Database/Database.php (4 hunks)
  • tests/e2e/Adapter/Scopes/SpatialTests.php (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/e2e/Adapter/Scopes/SpatialTests.php
🧰 Additional context used
🧬 Code graph analysis (3)
src/Database/Adapter.php (1)
src/Database/Adapter/Postgres.php (3)
  • decodePoint (2006-2030)
  • decodeLinestring (2032-2088)
  • decodePolygon (2090-2159)
src/Database/Adapter/Postgres.php (1)
src/Database/Adapter.php (3)
  • decodePoint (1289-1289)
  • decodeLinestring (1290-1290)
  • decodePolygon (1291-1291)
src/Database/Database.php (2)
src/Database/Adapter/Postgres.php (3)
  • decodePoint (2006-2030)
  • decodeLinestring (2032-2088)
  • decodePolygon (2090-2159)
src/Database/Adapter.php (3)
  • decodePoint (1289-1289)
  • decodeLinestring (1290-1290)
  • decodePolygon (1291-1291)
🪛 GitHub Actions: CodeQL
src/Database/Adapter.php

[error] 1289-1289: Method Utopia\Database\Adapter::decodePoint() return type has no value type specified in iterable type array.


[error] 1290-1290: Method Utopia\Database\Adapter::decodeLinestring() return type has no value type specified in iterable type array.


[error] 1291-1291: Method Utopia\Database\Adapter::decodePolygon() return type has no value type specified in iterable type array.

src/Database/Adapter/Postgres.php

[error] 2006-2006: Utopia\Database\Adapter\Postgres::decodePoint() return type has no value type specified in iterable type array.


[error] 2021-2021: Cannot access offset 0 on string|false.


[error] 2022-2022: Cannot access offset 1 on array|false.


[error] 2022-2022: Parameter #1 $string of function substr expects string, string|false given.


[error] 2026-2026: Cannot access offset 1 on array|false.


[error] 2026-2026: Parameter #1 $string of function substr expects string, string|false given.


[error] 2027-2027: Cannot access offset 1 on array|false.


[error] 2027-2027: Parameter #1 $string of function substr expects string, string|false given.


[error] 2032-2032: Method Utopia\Database\Adapter\Postgres::decodeLinestring() return type has no value type specified in iterable type array.


[error] 2064-2064: Cannot access offset 1 on array|false.


[error] 2077-2077: Cannot access offset 1 on array|false.


[error] 2082-2082: Cannot access offset 1 on array|false.


[error] 2083-2083: Cannot access offset 1 on array|false.


[error] 2090-2090: Method Utopia\Database\Adapter\Postgres::decodePolygon() return type has no value type specified in iterable type array.


[error] 2127-2127: Cannot access offset 1 on array|false.


[error] 2141-2141: Cannot access offset 1 on array|false.


[error] 2146-2146: Cannot access offset 1 on array|false.


[error] 2150-2150: Cannot access offset 1 on array|false.

src/Database/Adapter/SQL.php

[error] 2714-2714: Method Utopia\Database\Adapter\SQL::decodePoint() return type has no value type specified in iterable type array.


[error] 2740-2740: Cannot access offset 1 on array|false.


[error] 2740-2740: Cannot access offset 2 on array|false.


[error] 2743-2743: Method Utopia\Database\Adapter\SQL::decodeLinestring() return type has no value type specified in iterable type array.


[error] 2763-2763: Cannot access offset 1 on array|false.


[error] 2768-2768: Cannot access offset 1 on array|false.


[error] 2769-2769: Cannot access offset 1 on array|false.

🪛 GitHub Actions: Linter
src/Database/Adapter/MariaDB.php

[error] 1-1: Pint PSR-12 lint error: 'statement_indentation' violation.

src/Database/Adapter/Postgres.php

[error] 1-1: Pint PSR-12 lint error: 'no_multiple_statements_per_line'.

src/Database/Database.php

[error] 1-1: Pint PSR-12 lint error: 'simplified_null_return'.

src/Database/Adapter/SQL.php

[error] 1-1: Pint PSR-12 lint error: 'statement_indentation' and 'binary_operator_space'.

🪛 PHPMD (2.15.0)
src/Database/Adapter/Postgres.php

2123-2123: Avoid unused local variables such as '$isLE'. (undefined)

(UnusedLocalVariable)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: Adapter Tests (SharedTables/MariaDB)
  • GitHub Check: Adapter Tests (Pool)
  • GitHub Check: Adapter Tests (SharedTables/MySQL)
  • GitHub Check: Adapter Tests (SharedTables/Postgres)
  • GitHub Check: Adapter Tests (MariaDB)
  • GitHub Check: Adapter Tests (Postgres)
  • GitHub Check: Adapter Tests (SQLite)
  • GitHub Check: Adapter Tests (MySQL)
  • GitHub Check: Unit Test

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (5)
src/Database/Adapter/Postgres.php (5)

2006-2006: Change parameter type from mixed to string to match interface

The abstract method signature in Adapter.php expects string $wkb, but this implementation uses mixed. This violates the Liskov Substitution Principle and causes static analysis errors.

-public function decodePoint(mixed $wkb): array
+public function decodePoint(string $wkb): array

2032-2032: Change parameter type from mixed to string to match interface

Same issue as decodePoint - the interface expects string but implementation uses mixed.

-public function decodeLinestring(mixed $wkb): array
+public function decodeLinestring(string $wkb): array

2108-2157: Fix exception types and unsafe operations

  1. Using \RuntimeException instead of DatabaseException
  2. Missing error handling for unpack() operations
  3. Unused variable $isLE (static analysis warning)
 // Convert hex string to binary if needed
 if (preg_match('/^[0-9a-fA-F]+$/', $wkb)) {
     $wkb = hex2bin($wkb);
     if ($wkb === false) {
-        throw new \RuntimeException("Invalid hex WKB");
+        throw new DatabaseException("Invalid hex WKB");
     }
 }

 if (strlen($wkb) < 9) {
-    throw new \RuntimeException("WKB too short");
+    throw new DatabaseException("WKB too short");
 }

 $byteOrder = ord($wkb[0]);
-$isLE = $byteOrder === 1; // assume little-endian
+if ($byteOrder !== 1) {
+    throw new DatabaseException("Only little-endian WKB supported");
+}
 $uInt32 = 'V'; // little-endian 32-bit unsigned
 $uDouble = 'd'; // little-endian double

-$typeInt = unpack($uInt32, substr($wkb, 1, 4))[1];
+$typeIntArr = unpack($uInt32, substr($wkb, 1, 4));
+if ($typeIntArr === false || !isset($typeIntArr[1])) {
+    throw new DatabaseException('Failed to decode POLYGON type');
+}
+$typeInt = $typeIntArr[1];

 if ($geomType !== 3) { // 3 = POLYGON
-    throw new \RuntimeException("Not a POLYGON geometry type, got {$geomType}");
+    throw new DatabaseException("Not a POLYGON geometry type, got {$geomType}");
 }

 // Number of rings
-$numRings = unpack($uInt32, substr($wkb, $offset, 4))[1];
+$numRingsArr = unpack($uInt32, substr($wkb, $offset, 4));
+if ($numRingsArr === false || !isset($numRingsArr[1])) {
+    throw new DatabaseException('Failed to decode ring count');
+}
+$numRings = $numRingsArr[1];

-    $numPoints = unpack($uInt32, substr($wkb, $offset, 4))[1];
+    $numPointsArr = unpack($uInt32, substr($wkb, $offset, 4));
+    if ($numPointsArr === false || !isset($numPointsArr[1])) {
+        throw new DatabaseException('Failed to decode point count');
+    }
+    $numPoints = $numPointsArr[1];

-        $x = unpack($uDouble, substr($wkb, $offset, 8))[1];
-        $y = unpack($uDouble, substr($wkb, $offset + 8, 8))[1];
+        $xArr = unpack($uDouble, substr($wkb, $offset, 8));
+        $yArr = unpack($uDouble, substr($wkb, $offset + 8, 8));
+        if ($xArr === false || $yArr === false || !isset($xArr[1], $yArr[1])) {
+            throw new DatabaseException('Failed to decode point coordinates');
+        }
+        $x = $xArr[1];
+        $y = $yArr[1];

2019-2030: Fix multiple issues in WKB decoding logic

  1. Missing error handling for hex2bin() failure
  2. Unguarded unpack() operations that can return false
  3. Incorrect float unpacking format - using 32-bit format ('e') while consuming 8 bytes
-$bin = hex2bin($wkb);
+$bin = hex2bin($wkb);
+if ($bin === false || strlen($bin) < 21) {
+    throw new DatabaseException('Invalid WKB data');
+}

 $isLE = ord($bin[0]) === 1;
-$type  = unpack($isLE ? 'V' : 'N', substr($bin, 1, 4))[1];
+$typeArr = unpack($isLE ? 'V' : 'N', substr($bin, 1, 4));
+if ($typeArr === false || !isset($typeArr[1])) {
+    throw new DatabaseException('Failed to decode geometry type');
+}
+$type = $typeArr[1];
 $offset = 5 + (($type & 0x20000000) ? 4 : 0);

-$fmt = $isLE ? 'e' : 'E'; // little vs big endian double
-$x = unpack($fmt, substr($bin, $offset, 8))[1];
-$y = unpack($fmt, substr($bin, $offset + 8, 8))[1];
+$fmt = $isLE ? 'd' : 'D'; // 64-bit double format
+$xArr = unpack($fmt, substr($bin, $offset, 8));
+$yArr = unpack($fmt, substr($bin, $offset + 8, 8));
+if ($xArr === false || $yArr === false || !isset($xArr[1], $yArr[1])) {
+    throw new DatabaseException('Failed to decode coordinates');
+}
+$x = $xArr[1];
+$y = $yArr[1];

2046-2088: Fix unsafe operations and incorrect format specifier

Multiple issues similar to decodePoint:

  1. No error handling for hex2bin() failure
  2. Unguarded unpack() operations
  3. Incorrect float format specifier
 if (ctype_xdigit($wkb)) {
     $wkb = hex2bin($wkb);
+    if ($wkb === false) {
+        throw new DatabaseException('Invalid hex WKB');
+    }
 }

 // Type + SRID flag
-$typeField = unpack('V', substr($wkb, 1, 4))[1];
+$typeFieldArr = unpack('V', substr($wkb, 1, 4));
+if ($typeFieldArr === false || !isset($typeFieldArr[1])) {
+    throw new DatabaseException('Failed to decode LINESTRING type');
+}
+$typeField = $typeFieldArr[1];

-$numPoints = unpack('V', substr($wkb, $offset, 4))[1];
+$numPointsArr = unpack('V', substr($wkb, $offset, 4));
+if ($numPointsArr === false || !isset($numPointsArr[1])) {
+    throw new DatabaseException('Failed to decode point count');
+}
+$numPoints = $numPointsArr[1];

 $points = [];
 for ($i = 0; $i < $numPoints; $i++) {
-    $x = unpack('e', substr($wkb, $offset, 8))[1];
-    $offset += 8;
-    $y = unpack('e', substr($wkb, $offset, 8))[1];
-    $offset += 8;
+    $xArr = unpack('d', substr($wkb, $offset, 8));
+    $yArr = unpack('d', substr($wkb, $offset + 8, 8));
+    if ($xArr === false || $yArr === false || !isset($xArr[1], $yArr[1])) {
+        throw new DatabaseException('Failed to decode point coordinates');
+    }
+    $x = $xArr[1];
+    $y = $yArr[1];
     $points[] = [(float)$x, (float)$y];
+    $offset += 16;
 }
🧹 Nitpick comments (2)
src/Database/Adapter/Postgres.php (2)

2006-2157: Add missing PHPDoc blocks with proper type hints

All three decode methods need PHPDoc blocks to satisfy static analysis requirements and improve developer experience.

+/**
+ * Decode a POINT geometry from WKT or WKB format
+ * @param string $wkb WKT string or WKB hex string
+ * @return array{0: float, 1: float} Point coordinates [x, y]
+ * @throws DatabaseException
+ */
 public function decodePoint(string $wkb): array

+/**
+ * Decode a LINESTRING geometry from WKT or WKB format  
+ * @param string $wkb WKT string or WKB hex string
+ * @return array<int, array{0: float, 1: float}> Array of point coordinates
+ * @throws DatabaseException
+ */
 public function decodeLinestring(string $wkb): array

+/**
+ * Decode a POLYGON geometry from WKT or WKB format
+ * @param string $wkb WKT string or WKB hex string  
+ * @return array<int, array<int, array{0: float, 1: float}>> Array of rings, each containing points
+ * @throws DatabaseException
+ */
 public function decodePolygon(string $wkb): array

2010-2017: Add input validation for WKT parsing

The WKT parsing sections lack validation for coordinate format, which could cause errors with malformed input.

 if (str_starts_with(strtoupper($wkb), 'POINT(')) {
     $start = strpos($wkb, '(') + 1;
     $end = strrpos($wkb, ')');
     $inside = substr($wkb, $start, $end - $start);

     $coords = explode(' ', trim($inside));
+    if (count($coords) !== 2 || !is_numeric($coords[0]) || !is_numeric($coords[1])) {
+        throw new DatabaseException('Invalid POINT format in WKT');
+    }
     return [(float)$coords[0], (float)$coords[1]];
 }

Apply similar validation to the LINESTRING and POLYGON WKT parsing sections.

Also applies to: 2034-2044, 2093-2106

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f6bd630 and 1b9b095.

📒 Files selected for processing (4)
  • src/Database/Adapter/Postgres.php (1 hunks)
  • src/Database/Adapter/SQL.php (2 hunks)
  • src/Database/Database.php (3 hunks)
  • tests/e2e/Adapter/Scopes/SpatialTests.php (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/Database/Adapter/SQL.php
  • tests/e2e/Adapter/Scopes/SpatialTests.php
  • src/Database/Database.php
🧰 Additional context used
🧬 Code graph analysis (1)
src/Database/Adapter/Postgres.php (2)
src/Database/Adapter/SQL.php (3)
  • decodePoint (2714-2741)
  • decodeLinestring (2743-2773)
  • decodePolygon (2775-2850)
src/Database/Adapter.php (3)
  • decodePoint (1289-1289)
  • decodeLinestring (1290-1290)
  • decodePolygon (1291-1291)
🪛 PHPMD (2.15.0)
src/Database/Adapter/Postgres.php

2121-2121: Avoid unused local variables such as '$isLE'. (undefined)

(UnusedLocalVariable)

🪛 GitHub Actions: CodeQL
src/Database/Adapter/Postgres.php

[error] 2006-2006: Method Utopia\Database\Adapter\Postgres::decodePoint() return type has no value type specified in iterable type array.


[error] 2021-2021: Cannot access offset 0 on string|false.


[error] 2022-2022: Cannot access offset 1 on array|false.


[error] 2022-2022: Parameter #1 $string of function substr expects string, string|false given.


[error] 2026-2026: Cannot access offset 1 on array|false.


[error] 2026-2026: Parameter #1 $string of function substr expects string, string|false given.


[error] 2027-2027: Cannot access offset 1 on array|false.


[error] 2027-2027: Parameter #1 $string of function substr expects string, string|false given.


[error] 2032-2032: Method Utopia\Database\Adapter\Postgres::decodeLinestring() return type has no value type specified in iterable type array.


[error] 2062-2062: Cannot access offset 1 on array|false.


[error] 2075-2075: Cannot access offset 1 on array|false.


[error] 2080-2080: Cannot access offset 1 on array|false.


[error] 2082-2082: Cannot access offset 1 on array|false.


[error] 2090-2090: Method Utopia\Database\Adapter\Postgres::decodePolygon() return type has no value type specified in iterable type array.


[error] 2125-2125: Cannot access offset 1 on array|false.


[error] 2139-2139: Cannot access offset 1 on array|false.


[error] 2144-2144: Cannot access offset 1 on array|false.


[error] 2149-2149: Cannot access offset 1 on array|false.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Adapter Tests (SharedTables/MySQL)
  • GitHub Check: Adapter Tests (Mirror)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Database/Adapter/SQL.php (1)

519-527: Avoid misclassifying spatial attrs during update (json-encoding bug).

isset($spatialAttributes[$attributeName]) is wrong for a list; this will JSON‑encode spatial arrays.

-        foreach ($attributes as $attributeName => $value) {
-            if (!isset($spatialAttributes[$attributeName]) && is_array($value)) {
+        foreach ($attributes as $attributeName => $value) {
+            if (!in_array($attributeName, $spatialAttributes, true) && is_array($value)) {
                 $value = json_encode($value);
             }
♻️ Duplicate comments (4)
src/Database/Adapter/SQL.php (1)

362-362: Don’t wipe spatial projection.

Overriding $spatialAttributes disables ST_AsText in getDocument(). Remove the reset.

-        $spatialAttributes = [];
src/Database/Adapter/Postgres.php (3)

2030-2086: Align signature with Adapter and harden hex handling in decodeLinestring.

Change mixed→string and guard hex2bin(); fixes LSP and static analysis.

-    public function decodeLinestring(mixed $wkb): array
+    public function decodeLinestring(string $wkb): array
     {
         if (str_starts_with(strtoupper($wkb), 'LINESTRING(')) {
@@
-        if (ctype_xdigit($wkb)) {
-            $wkb = hex2bin($wkb);
-        }
+        if (ctype_xdigit($wkb)) {
+            $wkb = hex2bin($wkb);
+            if ($wkb === false) {
+                throw new DatabaseException('Invalid hex WKB');
+            }
+        }

2006-2028: Guard hex2bin and array offsets in decodePoint.

Fix CodeQL errors by validating hex2bin() and lengths before indexing/unpack.

     public function decodePoint(string $wkb): array
     {
         if (str_starts_with(strtoupper($wkb), 'POINT(')) {
@@
         }

-        $bin = hex2bin($wkb);
+        $bin = hex2bin($wkb);
+        if ($bin === false || strlen($bin) < 21) {
+            throw new DatabaseException('Invalid hex WKB for POINT');
+        }
 
-        $isLE = ord($bin[0]) === 1;
-        $type  = unpack($isLE ? 'V' : 'N', substr($bin, 1, 4))[1];
+        $isLE = ord($bin[0]) === 1;
+        $typeArr  = unpack($isLE ? 'V' : 'N', substr($bin, 1, 4));
+        if ($typeArr === false || !isset($typeArr[1])) {
+            throw new DatabaseException('Failed to decode POINT type');
+        }
+        $type = $typeArr[1];
         $offset = 5 + (($type & 0x20000000) ? 4 : 0);
 
-        $fmt = $isLE ? 'e' : 'E'; // little vs big endian double
-        $x = unpack($fmt, substr($bin, $offset, 8))[1];
-        $y = unpack($fmt, substr($bin, $offset + 8, 8))[1];
+        $fmt = $isLE ? 'e' : 'E'; // little vs big endian double
+        $xArr = unpack($fmt, substr($bin, $offset, 8));
+        $yArr = unpack($fmt, substr($bin, $offset + 8, 8));
+        if ($xArr === false || $yArr === false || !isset($xArr[1], $yArr[1])) {
+            throw new DatabaseException('Failed to decode POINT coordinates');
+        }
-        return [(float)$x, (float)$y];
+        return [(float)$xArr[1], (float)$yArr[1]];
     }

2106-2154: Use DatabaseException and enforce endianness in decodePolygon; drop unused var.

Replace RuntimeException, validate byte order, and remove $isLE PHPMD warning.

-        if (preg_match('/^[0-9a-fA-F]+$/', $wkb)) {
+        if (preg_match('/^[0-9a-fA-F]+$/', $wkb)) {
             $wkb = hex2bin($wkb);
             if ($wkb === false) {
-                throw new \RuntimeException("Invalid hex WKB");
+                throw new DatabaseException("Invalid hex WKB");
             }
         }
 
         if (strlen($wkb) < 9) {
-            throw new \RuntimeException("WKB too short");
+            throw new DatabaseException("WKB too short");
         }
 
         $byteOrder = ord($wkb[0]);
-        $isLE = $byteOrder === 1; // assume little-endian
+        if ($byteOrder !== 1) {
+            throw new DatabaseException("Only little-endian WKB supported");
+        }
         $uInt32 = 'V'; // little-endian 32-bit unsigned
-        $uDouble = 'd'; // little-endian double
+        $uDouble = 'e'; // little-endian double
 
-        $typeInt = unpack($uInt32, substr($wkb, 1, 4))[1];
+        $typeIntArr = unpack($uInt32, substr($wkb, 1, 4));
+        if ($typeIntArr === false || !isset($typeIntArr[1])) {
+            throw new DatabaseException('Failed to decode POLYGON type');
+        }
+        $typeInt = $typeIntArr[1];
         $hasSrid = ($typeInt & 0x20000000) !== 0;
         $geomType = $typeInt & 0xFF;
 
         if ($geomType !== 3) { // 3 = POLYGON
-            throw new \RuntimeException("Not a POLYGON geometry type, got {$geomType}");
+            throw new DatabaseException("Not a POLYGON geometry type, got {$geomType}");
         }
@@
-        $numRings = unpack($uInt32, substr($wkb, $offset, 4))[1];
+        $numRingsArr = unpack($uInt32, substr($wkb, $offset, 4));
+        if ($numRingsArr === false || !isset($numRingsArr[1])) {
+            throw new DatabaseException('Failed to decode POLYGON ring count');
+        }
+        $numRings = $numRingsArr[1];
         $offset += 4;
@@
-            $numPoints = unpack($uInt32, substr($wkb, $offset, 4))[1];
+            $numPointsArr = unpack($uInt32, substr($wkb, $offset, 4));
+            if ($numPointsArr === false || !isset($numPointsArr[1])) {
+                throw new DatabaseException('Failed to decode POLYGON point count');
+            }
+            $numPoints = $numPointsArr[1];
🧹 Nitpick comments (1)
tests/e2e/Adapter/Base.php (1)

21-26: Fix PSR-12 indentation for commented trait lines (or remove them).

They’re starting at column 0 inside the class, tripping Pint. Indent them to 4 spaces.

-//    use CollectionTests;
-//    use DocumentTests;
-//    use AttributeTests;
-//    use IndexTests;
-//    use PermissionTests;
-//    use RelationshipTests;
+    // use CollectionTests;
+    // use DocumentTests;
+    // use AttributeTests;
+    // use IndexTests;
+    // use PermissionTests;
+    // use RelationshipTests;
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1b9b095 and 44cc3f3.

📒 Files selected for processing (5)
  • src/Database/Adapter.php (1 hunks)
  • src/Database/Adapter/Pool.php (1 hunks)
  • src/Database/Adapter/Postgres.php (1 hunks)
  • src/Database/Adapter/SQL.php (2 hunks)
  • tests/e2e/Adapter/Base.php (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/Database/Adapter.php
🧰 Additional context used
🧬 Code graph analysis (2)
src/Database/Adapter/SQL.php (2)
src/Database/Adapter.php (3)
  • decodePoint (1295-1295)
  • decodeLinestring (1303-1303)
  • decodePolygon (1311-1311)
src/Database/Adapter/Postgres.php (3)
  • decodePoint (2006-2028)
  • decodeLinestring (2030-2086)
  • decodePolygon (2088-2155)
src/Database/Adapter/Postgres.php (2)
src/Database/Adapter.php (3)
  • decodePoint (1295-1295)
  • decodeLinestring (1303-1303)
  • decodePolygon (1311-1311)
src/Database/Adapter/SQL.php (3)
  • decodePoint (2714-2745)
  • decodeLinestring (2747-2787)
  • decodePolygon (2789-2885)
🪛 GitHub Actions: Linter
tests/e2e/Adapter/Base.php

[error] 1-1: Pint: PSR-12 statement_indentation violation detected in tests/e2e/Adapter/Base.php.

🪛 PHPMD (2.15.0)
src/Database/Adapter/Postgres.php

2119-2119: Avoid unused local variables such as '$isLE'. (undefined)

(UnusedLocalVariable)

🪛 GitHub Actions: CodeQL
src/Database/Adapter/Postgres.php

[error] 2019-2019: Cannot access offset 0 on string|false.


[error] 2020-2020: Cannot access offset 1 on array|false.


[error] 2020-2020: Parameter #1 $string of function substr expects string, string|false given.


[error] 2024-2024: Cannot access offset 1 on array|false.


[error] 2024-2024: Parameter #1 $string of function substr expects string, string|false given.


[error] 2025-2025: Cannot access offset 1 on array|false.


[error] 2025-2025: Parameter #1 $string of function substr expects string, string|false given.


[error] 2048-2048: Parameter #1 $string of function strlen expects string, string|false given.


[error] 2052-2052: Cannot access offset 0 on string|false.


[error] 2060-2060: Cannot access offset 1 on array|false.


[error] 2060-2060: Parameter #1 $string of function substr expects string, string|false given.


[error] 2073-2073: Cannot access offset 1 on array|false.


[error] 2073-2073: Parameter #1 $string of function substr expects string, string|false given.


[error] 2078-2078: Cannot access offset 1 on array|false.


[error] 2078-2078: Parameter #1 $string of function substr expects string, string|false given.


[error] 2080-2080: Cannot access offset 1 on array|false.


[error] 2080-2080: Parameter #1 $string of function substr expects string, string|false given.


[error] 2123-2123: Cannot access offset 1 on array|false.


[error] 2137-2137: Cannot access offset 1 on array|false.


[error] 2142-2142: Cannot access offset 1 on array|false.


[error] 2146-2146: Cannot access offset 1 on array|false.


[error] 2147-2147: Cannot access offset 1 on array|false.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Adapter Tests (Mirror)
  • GitHub Check: Adapter Tests (SharedTables/SQLite)
  • GitHub Check: Adapter Tests (SharedTables/Postgres)
  • GitHub Check: Adapter Tests (SharedTables/MySQL)
  • GitHub Check: Adapter Tests (SharedTables/MariaDB)
  • GitHub Check: Adapter Tests (Postgres)
  • GitHub Check: Adapter Tests (MySQL)
  • GitHub Check: Adapter Tests (SQLite)
  • GitHub Check: Adapter Tests (MariaDB)
  • GitHub Check: Adapter Tests (Pool)
  • GitHub Check: Unit Test
🔇 Additional comments (1)
src/Database/Adapter/Pool.php (1)

553-566: Delegation for spatial decoders looks good.

Signatures match Adapter contract and delegate correctly.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/Database/Adapter/SQL.php (3)

2458-2461: Fix broken call: getAttributeProjection now has 2 params

This is the source of the pipeline failure. Update the call to match the new signature.

Apply this diff:

-            SELECT {$this->getAttributeProjection($selections, $alias, $spatialAttributes)}
+            SELECT {$this->getAttributeProjection($selections, $alias)}

518-527: Spatial attribute check uses isset on a list — switch to in_array

isset($spatialAttributes[$attributeName]) is wrong because $spatialAttributes is a list of names. This will JSON‑encode spatial values by mistake.

-            if (!isset($spatialAttributes[$attributeName]) && is_array($value)) {
+            if (!in_array($attributeName, $spatialAttributes, true) && is_array($value)) {
                 $value = json_encode($value);
             }

2458-2465: Fix mismatched getAttributeProjection call (3rd arg $spatialAttributes)

3‑arg invocation found at src/Database/Adapter/SQL.php:2459; the abstract signature (src/Database/Adapter.php:1158) and current implementation (src/Database/Adapter/SQL.php:1882) accept two parameters. Remove the third argument from the call site or update the abstract and implementations to accept and handle $spatialAttributes.

♻️ Duplicate comments (2)
src/Database/Adapter/SQL.php (2)

2716-2756: LINESTRING WKB: honor byte order, handle hex, parse type/SRID

Assumes little‑endian, fixed SRID layout, and machine‑endian doubles; can misdecode or throw on valid inputs.

     public function decodeLinestring(string $wkb): array
     {
         if (str_starts_with(strtoupper($wkb), 'LINESTRING(')) {
             $start = strpos($wkb, '(') + 1;
             $end = strrpos($wkb, ')');
             $inside = substr($wkb, $start, $end - $start);
 
             $points = explode(',', $inside);
             return array_map(function ($point) {
                 $coords = explode(' ', trim($point));
                 return [(float)$coords[0], (float)$coords[1]];
             }, $points);
         }
 
-        // Skip 1 byte (endianness) + 4 bytes (type) + 4 bytes (SRID)
-        $offset = 9;
-
-        // Number of points (4 bytes little-endian)
-        $numPointsArr = unpack('V', substr($wkb, $offset, 4));
+        if (str_starts_with($wkb, '0x') || ctype_xdigit($wkb)) {
+            $wkb = hex2bin(str_starts_with($wkb, '0x') ? substr($wkb, 2) : $wkb);
+            if ($wkb === false) {
+                throw new DatabaseException('Invalid hex WKB for LINESTRING');
+            }
+        }
+        if (!is_string($wkb) || strlen($wkb) < 1 + 4 + 4) {
+            throw new DatabaseException('WKB too short to be a LINESTRING');
+        }
+        $byteOrder = ord($wkb[0]);
+        if ($byteOrder !== 0 && $byteOrder !== 1) {
+            throw new DatabaseException('Invalid byte order in WKB');
+        }
+        $le = ($byteOrder === 1);
+        $u32 = $le ? 'V' : 'N';
+        $f64 = $le ? 'e' : 'E';
+
+        // Type + SRID flag
+        $typeArr = unpack($u32, substr($wkb, 1, 4));
+        if ($typeArr === false || !isset($typeArr[1])) {
+            throw new DatabaseException('Invalid WKB: cannot unpack geometry type');
+        }
+        $type = (int)$typeArr[1];
+        $geomType = $type & 0xFF;
+        if ($geomType !== 2) {
+            throw new DatabaseException("Not a LINESTRING geometry type, got {$geomType}");
+        }
+        $hasSRID = ($type & 0x20000000) !== 0;
+
+        $offset = 1 + 4 + ($hasSRID ? 4 : 0);
+
+        $numPointsArr = unpack($u32, substr($wkb, $offset, 4));
         if ($numPointsArr === false || !isset($numPointsArr[1])) {
             throw new DatabaseException('Invalid WKB: cannot unpack number of points');
         }
 
         $numPoints = $numPointsArr[1];
         $offset += 4;
 
         $points = [];
         for ($i = 0; $i < $numPoints; $i++) {
-            $xArr = unpack('d', substr($wkb, $offset, 8));
-            $yArr = unpack('d', substr($wkb, $offset + 8, 8));
+            if (strlen($wkb) < $offset + 16) {
+                throw new DatabaseException('WKB too short for LINESTRING point');
+            }
+            $xArr = unpack($f64, substr($wkb, $offset, 8));
+            $yArr = unpack($f64, substr($wkb, $offset + 8, 8));
 
             if ($xArr === false || !isset($xArr[1]) || $yArr === false || !isset($yArr[1])) {
                 throw new DatabaseException('Invalid WKB: cannot unpack point coordinates');
             }
 
             $points[] = [(float)$xArr[1], (float)$yArr[1]];
             $offset += 16;
         }
 
         return $points;
     }

2683-2714: POINT WKB: add hex handling, endianness, SRID offset, and input validation

Current code assumes little‑endian machine doubles ('d'), doesn’t parse the type/SRID flag, and can’t accept hex input. This will decode incorrectly for big‑endian WKB or when SRID is absent/present.

     public function decodePoint(string $wkb): array
     {
         if (str_starts_with(strtoupper($wkb), 'POINT(')) {
             $start = strpos($wkb, '(') + 1;
             $end = strrpos($wkb, ')');
             $inside = substr($wkb, $start, $end - $start);
             $coords = explode(' ', trim($inside));
             return [(float)$coords[0], (float)$coords[1]];
         }
 
-        // MySQL SRID-aware WKB layout:
-        // 1 byte  = endian (1 = little endian)
-        // 4 bytes = type + SRID flag
-        // 4 bytes = SRID
-        // 16 bytes = X,Y coordinates (double each, little endian)
-
-        $byteOrder = ord($wkb[0]);
-        $littleEndian = ($byteOrder === 1);
-
-        // Skip 1 + 4 + 4 = 9 bytes to get coordinates
-        $coordsBin = substr($wkb, 9, 16);
-
-        // Unpack doubles
-        $format = $littleEndian ? 'd2' : 'd2'; // little-endian doubles
-        $coords = unpack($format, $coordsBin);
-
-        if ($coords === false || !isset($coords[1], $coords[2])) {
-            throw new DatabaseException('Invalid WKB for POINT: cannot unpack coordinates');
-        }
-
-        return [(float)$coords[1], (float)$coords[2]];
+        // Accept hex WKB as well as binary
+        if (str_starts_with($wkb, '0x') || ctype_xdigit($wkb)) {
+            $wkb = hex2bin(str_starts_with($wkb, '0x') ? substr($wkb, 2) : $wkb);
+            if ($wkb === false) {
+                throw new DatabaseException('Invalid hex WKB for POINT');
+            }
+        }
+        if (!is_string($wkb) || strlen($wkb) < 1 + 4 + 16) {
+            throw new DatabaseException('WKB too short for POINT');
+        }
+
+        $byteOrder = ord($wkb[0]);
+        if ($byteOrder !== 0 && $byteOrder !== 1) {
+            throw new DatabaseException('Invalid byte order in WKB');
+        }
+        $le = ($byteOrder === 1);
+        $u32 = $le ? 'V' : 'N';
+        $f64 = $le ? 'e' : 'E';
+
+        // Read type field and optional SRID
+        $typeArr = unpack($u32, substr($wkb, 1, 4));
+        if ($typeArr === false || !isset($typeArr[1])) {
+            throw new DatabaseException('Invalid WKB: cannot unpack geometry type');
+        }
+        $type = (int)$typeArr[1];
+        $hasSRID = ($type & 0x20000000) !== 0;
+
+        $offset = 1 + 4 + ($hasSRID ? 4 : 0);
+        if (strlen($wkb) < $offset + 16) {
+            throw new DatabaseException('WKB too short for POINT coordinates');
+        }
+
+        $xArr = unpack($f64, substr($wkb, $offset, 8));
+        $yArr = unpack($f64, substr($wkb, $offset + 8, 8));
+        if ($xArr === false || $yArr === false || !isset($xArr[1], $yArr[1])) {
+            throw new DatabaseException('Invalid WKB for POINT: cannot unpack coordinates');
+        }
+        return [(float)$xArr[1], (float)$yArr[1]];
     }
🧹 Nitpick comments (2)
src/Database/Adapter/SQL.php (2)

39-42: Make float formatting locale-independent

sprintf('%.…F', …) can be locale-sensitive; databases expect a dot decimal separator. Use a locale‑independent formatter.

-        return sprintf('%.'. $this->floatPrecision . 'F', $value);
+        return number_format($value, $this->floatPrecision, '.', '');

541-606: Nit: unused variable $removeBindKeys

$removeBindKeys is appended to but never read. Clean up to avoid confusion.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7849c6c and 2aed9dc.

📒 Files selected for processing (3)
  • src/Database/Adapter.php (2 hunks)
  • src/Database/Adapter/Pool.php (2 hunks)
  • src/Database/Adapter/SQL.php (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/Database/Adapter.php
🧰 Additional context used
🧬 Code graph analysis (2)
src/Database/Adapter/Pool.php (4)
src/Database/Adapter.php (4)
  • getAttributeProjection (1158-1158)
  • decodePoint (1295-1295)
  • decodeLinestring (1303-1303)
  • decodePolygon (1311-1311)
src/Database/Adapter/SQL.php (4)
  • getAttributeProjection (1882-1911)
  • decodePoint (2683-2714)
  • decodeLinestring (2716-2756)
  • decodePolygon (2758-2854)
src/Database/Adapter/Postgres.php (3)
  • decodePoint (2006-2053)
  • decodeLinestring (2055-2144)
  • decodePolygon (2146-2248)
src/Database/Mirror.php (1)
  • delegate (88-103)
src/Database/Adapter/SQL.php (4)
src/Database/Adapter.php (5)
  • getAttributeProjection (1158-1158)
  • quote (468-468)
  • decodePoint (1295-1295)
  • decodeLinestring (1303-1303)
  • decodePolygon (1311-1311)
src/Database/Adapter/Pool.php (5)
  • getAttributeProjection (473-476)
  • quote (118-121)
  • decodePoint (547-550)
  • decodeLinestring (552-555)
  • decodePolygon (557-560)
src/Database/Adapter/Postgres.php (4)
  • quote (1941-1944)
  • decodePoint (2006-2053)
  • decodeLinestring (2055-2144)
  • decodePolygon (2146-2248)
src/Database/Adapter/MariaDB.php (1)
  • quote (1825-1828)
🪛 GitHub Actions: CodeQL
src/Database/Adapter/SQL.php

[error] 2459-2459: PHPStan: Method Utopia\Database\Adapter\SQL::getAttributeProjection() invoked with 3 parameters, 2 required.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Adapter Tests (SQLite)
  • GitHub Check: Adapter Tests (SharedTables/SQLite)
  • GitHub Check: Adapter Tests (Pool)
  • GitHub Check: Adapter Tests (MariaDB)
  • GitHub Check: Adapter Tests (Mirror)
  • GitHub Check: Adapter Tests (SharedTables/MariaDB)
  • GitHub Check: Adapter Tests (MySQL)
  • GitHub Check: Adapter Tests (SharedTables/MySQL)
  • GitHub Check: Adapter Tests (Postgres)
  • GitHub Check: Unit Test
  • GitHub Check: Adapter Tests (SharedTables/Postgres)
🔇 Additional comments (4)
src/Database/Adapter/SQL.php (2)

363-371: Projection behavior change: confirm downstream spatial decoding

ST_AsText-based spatial projections were removed. Ensure the code that consumes rows from getDocument() decodes spatial columns via the new adapter decoders before returning to callers.

If needed, I can help wire decoding into the fetch->Document transformation path.


2466-2482: Bind doubles as strings: confirm precision vs server settings

Switching to stringified doubles is good; verify MySQL/MariaDB SQL modes won’t reject very long decimals and that precision aligns with index usage if any sorted by floats.

src/Database/Adapter/Pool.php (2)

473-476: Signature sync with Adapter/SQL: LGTM

Delegation matches the updated 2‑arg getAttributeProjection signature.


547-560: Decoder delegation added: LGTM

Forwarders for decodePoint/Linestring/Polygon are correct.

@fogelito fogelito changed the title Spatial encode decode Spatial decoding Sep 15, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/Database/Adapter/SQL.php (1)

2850-2859: Inconsistent exception types in Polygon decoder.

Lines 2851 and 2858 throw \RuntimeException while the rest of the spatial decoders consistently throw DatabaseException. This creates inconsistent error handling.

Apply this fix for consistency:

-                if ($xArr === false) {
-                    throw new \RuntimeException('Failed to unpack X coordinate from WKB.');
-                }
+                if ($xArr === false) {
+                    throw new DatabaseException('Failed to unpack X coordinate from WKB.');
+                }

-                if ($yArr === false) {
-                    throw new \RuntimeException('Failed to unpack Y coordinate from WKB.');
-                }
+                if ($yArr === false) {
+                    throw new DatabaseException('Failed to unpack Y coordinate from WKB.');
+                }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2aed9dc and a1a52f6.

📒 Files selected for processing (2)
  • src/Database/Adapter/Postgres.php (1 hunks)
  • src/Database/Adapter/SQL.php (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/Database/Adapter/Postgres.php
🧰 Additional context used
🧬 Code graph analysis (1)
src/Database/Adapter/SQL.php (4)
src/Database/Adapter.php (5)
  • getAttributeProjection (1158-1158)
  • quote (468-468)
  • decodePoint (1295-1295)
  • decodeLinestring (1303-1303)
  • decodePolygon (1311-1311)
src/Database/Adapter/Pool.php (5)
  • getAttributeProjection (473-476)
  • quote (118-121)
  • decodePoint (547-550)
  • decodeLinestring (552-555)
  • decodePolygon (557-560)
src/Database/Adapter/Postgres.php (4)
  • quote (1941-1944)
  • decodePoint (2006-2048)
  • decodeLinestring (2050-2132)
  • decodePolygon (2134-2226)
src/Database/Adapter/MariaDB.php (1)
  • quote (1825-1828)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: Adapter Tests (SharedTables/MySQL)
  • GitHub Check: Adapter Tests (SharedTables/SQLite)
  • GitHub Check: Adapter Tests (SharedTables/Postgres)
  • GitHub Check: Adapter Tests (Postgres)
  • GitHub Check: Adapter Tests (SharedTables/MariaDB)
  • GitHub Check: Adapter Tests (MySQL)
  • GitHub Check: Adapter Tests (Pool)
  • GitHub Check: Adapter Tests (SQLite)
  • GitHub Check: Adapter Tests (Mirror)
  • GitHub Check: Adapter Tests (MariaDB)
🔇 Additional comments (1)
src/Database/Adapter/SQL.php (1)

2682-2871: Align WKB decoding and error handling between SQL and Postgres adapters

  • Observed differences: SQL (src/Database/Adapter/SQL.php) accepts WKT text (e.g. "POINT("), assumes MySQL-style SRID prefix, only little-endian, and throws DatabaseException; Postgres (src/Database/Adapter/Postgres.php) performs hex→binary conversion, has more granular byte-order/type/unpack checks and throws \RuntimeException.
  • Action: unify supported input formats (binary WKB, hex WKB, WKT), decide and document SRID semantics (4‑byte prefix vs type-flag), standardize endianness handling (accept/explicitly reject big-endian) and exception types/messages across both adapters. Add cross-adapter unit tests covering: little-endian/ big-endian, SRID/no-SRID, hex vs binary, and WKT forms.

@utopia-php utopia-php deleted a comment from coderabbitai bot Sep 16, 2025
@utopia-php utopia-php deleted a comment from abnegate Sep 16, 2025
@abnegate abnegate merged commit f499a46 into main Sep 16, 2025
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants