-
-
Notifications
You must be signed in to change notification settings - Fork 56
feat(#305): add support for PostGIS's types of GEOGRAPHY, GEOMETRY and their array variations
#421
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
a9aabf3
3801e55
9fa3471
1b9d623
d0fb182
90f6509
2b6a808
b1eeaf4
8787b19
94b9b46
1679b8b
76d0b6d
8f6d4a8
2f8b760
3717176
caab1d8
9ee1f11
63e1350
e69e412
10fcc61
c1d7a3d
f8e91a9
ef344b1
3c87171
a442120
d1b5e36
d873c4b
382c20a
687cb6a
002cbd3
20e8abe
94dc439
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -60,14 +60,26 @@ in | |||||||||||||||||||||||||||||||||||||||||||||||
| services.postgres = { | ||||||||||||||||||||||||||||||||||||||||||||||||
| enable = true; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # Use PostgreSQL 17 to match Docker Compose and CI | ||||||||||||||||||||||||||||||||||||||||||||||||
| package = pkgs.postgresql_17; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| listen_addresses = "127.0.0.1"; | ||||||||||||||||||||||||||||||||||||||||||||||||
| port = config.env.POSTGRES_PORT; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| initialDatabases = [ { name = config.env.POSTGRES_DB; } ]; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # Enable PostGIS extension | ||||||||||||||||||||||||||||||||||||||||||||||||
| extensions = extensions: [ | ||||||||||||||||||||||||||||||||||||||||||||||||
| extensions.postgis | ||||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| initialScript = '' | ||||||||||||||||||||||||||||||||||||||||||||||||
| CREATE ROLE "${config.env.POSTGRES_USER}" | ||||||||||||||||||||||||||||||||||||||||||||||||
| WITH SUPERUSER LOGIN PASSWORD '${config.env.POSTGRES_PASSWORD}'; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| -- Enable PostGIS extension in the database | ||||||||||||||||||||||||||||||||||||||||||||||||
| \c ${config.env.POSTGRES_DB} | ||||||||||||||||||||||||||||||||||||||||||||||||
| CREATE EXTENSION IF NOT EXISTS postgis; | ||||||||||||||||||||||||||||||||||||||||||||||||
| ''; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Idempotency issue: creating the default 'postgres' role will fail
- initialScript = ''
- CREATE ROLE "${config.env.POSTGRES_USER}"
- WITH SUPERUSER LOGIN PASSWORD '${config.env.POSTGRES_PASSWORD}';
-
- -- Enable PostGIS extension in the database
- \c ${config.env.POSTGRES_DB}
- CREATE EXTENSION IF NOT EXISTS postgis;
- '';
+ initialScript = ''
+ DO $$
+ BEGIN
+ IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${config.env.POSTGRES_USER}') THEN
+ CREATE ROLE "${config.env.POSTGRES_USER}" WITH SUPERUSER LOGIN PASSWORD '${config.env.POSTGRES_PASSWORD}';
+ ELSE
+ ALTER ROLE "${config.env.POSTGRES_USER}" WITH LOGIN PASSWORD '${config.env.POSTGRES_PASSWORD}';
+ END IF;
+ END
+ $$;
+
+ -- Enable PostGIS extension in the target database
+ \c ${config.env.POSTGRES_DB}
+ CREATE EXTENSION IF NOT EXISTS postgis;
+ '';Optionally, if you want the custom user to own the database:
📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace MartinGeorgiev\Doctrine\DBAL\Types\ValueObject\Exceptions; | ||
|
|
||
| use MartinGeorgiev\Doctrine\DBAL\Types\ValueObject\WktGeometryType; | ||
|
|
||
| /** | ||
| * Exception thrown when creating or manipulating Geometry value objects with invalid data. | ||
| * | ||
| * This exception is specifically for validation errors within the Geometry value object itself, | ||
| * separate from DBAL conversion exceptions. | ||
| * | ||
| * @since 3.5 | ||
| * | ||
| * @author Martin Georgiev <martin.georgiev@gmail.com> | ||
| */ | ||
| final class InvalidGeometryException extends \InvalidArgumentException | ||
| { | ||
| public static function forEmptyWkt(): self | ||
| { | ||
| return new self('Empty Wkt string provided'); | ||
| } | ||
|
|
||
| public static function forMissingSemicolonInEwkt(): self | ||
| { | ||
| return new self('Invalid Ewkt: missing semicolon after Srid prefix'); | ||
| } | ||
|
|
||
| public static function forInvalidSridValue(mixed $sridValue): self | ||
| { | ||
| return new self(\sprintf('Invalid Srid value in Ewkt: %s', \var_export($sridValue, true))); | ||
| } | ||
|
|
||
| public static function forInvalidWktFormat(string $wkt): self | ||
| { | ||
| return new self(\sprintf('Invalid Wkt format: %s', \var_export($wkt, true))); | ||
| } | ||
|
|
||
| public static function forEmptyCoordinateSection(): self | ||
| { | ||
| return new self('Invalid Wkt: empty coordinate/body section'); | ||
| } | ||
|
|
||
| public static function forUnsupportedGeometryType(string $type): self | ||
| { | ||
| $supportedTypes = \array_map(fn(WktGeometryType $case) => $case->value, WktGeometryType::cases()); | ||
|
|
||
| return new self(\sprintf( | ||
| 'Unsupported Wkt geometry type: %s. Supported types: %s', | ||
| \var_export($type, true), | ||
| \implode(', ', $supportedTypes) | ||
| )); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace MartinGeorgiev\Doctrine\DBAL\Types\ValueObject\Exceptions; | ||
|
|
||
| /** | ||
| * Exception thrown when creating or manipulating Point value objects with invalid data. | ||
| * | ||
| * This exception is specifically for validation errors within the Point value object itself, | ||
| * separate from DBAL conversion exceptions. | ||
| * | ||
| * @since 3.5 | ||
| * | ||
| * @author Martin Georgiev <martin.georgiev@gmail.com> | ||
| */ | ||
| final class InvalidPointException extends \InvalidArgumentException | ||
| { | ||
| public static function forInvalidPointFormat(string $pointString, string $expectedPattern): self | ||
| { | ||
| return new self(\sprintf( | ||
| 'Invalid point format. Expected format matching %s, got: %s', | ||
| \var_export($expectedPattern, true), | ||
| \var_export($pointString, true) | ||
| )); | ||
| } | ||
|
|
||
| public static function forInvalidCoordinate(string $coordinateName, string $value): self | ||
| { | ||
| return new self(\sprintf( | ||
| 'Invalid %s coordinate format: %s', | ||
| \var_export($coordinateName, true), | ||
| \var_export($value, true) | ||
| )); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace MartinGeorgiev\Doctrine\DBAL\Types\ValueObject; | ||
|
|
||
| use MartinGeorgiev\Doctrine\DBAL\Types\ValueObject\Exceptions\InvalidGeometryException; | ||
|
|
||
| /** | ||
| * Lightweight Geometry value object supporting Ewkt (with optional Srid prefix) and Wkt. | ||
| * | ||
| * Examples: | ||
| * - POINT(1 2) | ||
| * - SRID=4326;POINT(-122.4194 37.7749) | ||
| * - LINESTRING(0 0, 1 1) | ||
| * - POLYGON((0 0, 0 1, 1 1, 1 0, 0 0)) | ||
| * | ||
| * @since 3.5 | ||
| * | ||
| * @author Martin Georgiev <martin.georgiev@gmail.com> | ||
| */ | ||
| final class Geometry implements \Stringable | ||
| { | ||
| private ?int $srid; | ||
|
|
||
| private WktGeometryType $wktType; | ||
|
|
||
| private string $wktBody; | ||
|
|
||
| private function __construct(?int $srid, WktGeometryType $wktType, string $wktBody) | ||
| { | ||
| $this->srid = $srid; | ||
| $this->wktType = $wktType; | ||
| $this->wktBody = $wktBody; | ||
| } | ||
martin-georgiev marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| public static function fromWkt(string $wkt): self | ||
| { | ||
| $wkt = trim($wkt); | ||
| if ($wkt === '') { | ||
| throw InvalidGeometryException::forEmptyWkt(); | ||
| } | ||
|
|
||
| $srid = null; | ||
| $expectSrid = str_starts_with($wkt, 'SRID='); | ||
| if ($expectSrid) { | ||
| $sridSeparatorPosition = strpos($wkt, ';'); | ||
| if ($sridSeparatorPosition === false) { | ||
| throw InvalidGeometryException::forMissingSemicolonInEwkt(); | ||
| } | ||
| $sridRawValue = substr($wkt, 5, $sridSeparatorPosition - 5); | ||
| if ($sridRawValue === '' || !ctype_digit($sridRawValue)) { | ||
| throw InvalidGeometryException::forInvalidSridValue($sridRawValue); | ||
| } | ||
| $srid = (int) $sridRawValue; | ||
| $wkt = substr($wkt, $sridSeparatorPosition + 1); | ||
| } | ||
|
|
||
| $wktTypeWithOptionalModifiersPattern = '/^([A-Z][A-Z0-9_]*)(?:\s+(?:ZM|Z|M))?\s*\((.*)\)$/s'; | ||
| if (!preg_match($wktTypeWithOptionalModifiersPattern, $wkt, $matches)) { | ||
| throw InvalidGeometryException::forInvalidWktFormat($wkt); | ||
| } | ||
|
|
||
martin-georgiev marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| $typeString = $matches[1]; | ||
| $body = $matches[2]; | ||
| if ($body === '') { | ||
| throw InvalidGeometryException::forEmptyCoordinateSection(); | ||
| } | ||
martin-georgiev marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| $geometryType = WktGeometryType::tryFrom($typeString); | ||
| if ($geometryType === null) { | ||
| throw InvalidGeometryException::forUnsupportedGeometryType($typeString); | ||
| } | ||
|
|
||
| return new self($srid, $geometryType, $body); | ||
| } | ||
|
|
||
| public function __toString(): string | ||
| { | ||
| $typeAndBody = $this->wktType->value.'('.$this->wktBody.')'; | ||
| if ($this->srid === null) { | ||
| return $typeAndBody; | ||
| } | ||
|
|
||
| return 'SRID='.$this->srid.';'.$typeAndBody; | ||
| } | ||
martin-georgiev marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| public function getSrid(): ?int | ||
| { | ||
| return $this->srid; | ||
| } | ||
|
|
||
| public function getGeometryType(): WktGeometryType | ||
| { | ||
| return $this->wktType; | ||
| } | ||
|
|
||
| public function getWkt(): string | ||
| { | ||
| return (string) $this; | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Create the extension before querying PostGIS_Version()
On some images/databases, PostGIS is installed but not enabled in the target DB. Explicitly creating the extension avoids flakiness.
Apply this tweak to the verification step:
Minor: the earlier echo lines use "\n", which many shells won’t interpret without -e. Consider printf for consistent newlines.
🏁 Script executed:
Length of output: 631
Enable PostGIS extension before querying its version
The workflow currently lists available extensions but never creates/enables PostGIS in the test database, which can lead to flakiness on images where PostGIS isn’t auto-enabled.
• File: .github/workflows/integration-tests.yml
• Around lines 108–112
Apply this diff:
- name: Verify available PostGIS extensions run: | echo "Available extensions:" PGPASSWORD=postgres psql -h localhost -U postgres -d postgres_doctrine_test -c "SELECT * FROM pg_available_extensions;" + echo "Enabling PostGIS extension (idempotent):" + PGPASSWORD=postgres psql -h localhost -U postgres -d postgres_doctrine_test -c "CREATE EXTENSION IF NOT EXISTS postgis;" - name: Verify PostGIS installation run: | - echo "\nVerifying PostGIS installation:" - PGPASSWORD=postgres psql -h localhost -U postgres -d postgres_doctrine_test -c "SELECT PostGIS_Version();" + # Use printf or echo -e for consistent newlines + printf "\nVerifying PostGIS installation:\n" + PGPASSWORD=postgres psql -h localhost -U postgres -d postgres_doctrine_test -c "SELECT PostGIS_Version();" + # Optional sanity check for a PostGIS function + PGPASSWORD=postgres psql -h localhost -U postgres -d postgres_doctrine_test -c "SELECT COUNT(*) FROM pg_proc WHERE proname='st_astext';"📝 Committable suggestion
🤖 Prompt for AI Agents