Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 57bc920

Browse files
committedMar 25, 2025·
Wip
1 parent c516752 commit 57bc920

23 files changed

+1849
-548
lines changed
 

‎.docker/php/Dockerfile

+2-2
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,11 @@ SHELL ["/bin/sh", "-e", "-c"]
5353

5454
RUN <<EOF
5555
apt update
56-
apt install --yes git libzip-dev libpq-dev libsqlite3-mod-spatialite
56+
apt install --yes git sqlite3 libzip-dev libpq-dev libsqlite3-mod-spatialite
5757
rm -rf /var/lib/apt/lists/*
5858
EOF
5959

60-
RUN docker-php-ext-install opcache zip pdo pdo_pgsql pdo_mysql
60+
RUN docker-php-ext-install opcache zip pdo pdo_pgsql pdo_mysql pgsql
6161

6262
# SQLite3 configuration
6363
RUN echo "[sqlite3]\nsqlite3.extension_dir = /usr/lib/x86_64-linux-gnu"> /usr/local/etc/php/conf.d/sqlite3.ini

‎README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -188,11 +188,11 @@ all you need to do is create an additional in-memory SQLite3 database just to po
188188
- Use this bootstrap code in your project:
189189

190190
```php
191-
use Brick\Geo\Engine\Sqlite3Engine;
191+
use Brick\Geo\Engine\SpatialiteEngine;
192192

193193
$sqlite3 = new SQLite3(':memory:');
194194
$sqlite3->loadExtension('mod_spatialite.so');
195-
$geometryEngine = new Sqlite3Engine($sqlite3);
195+
$geometryEngine = new SpatialiteEngine($sqlite3);
196196
```
197197

198198
- Depending on the functions you use, you will probably need to initialize the spatial metadata by running this query:

‎composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
},
1616
"require-dev": {
1717
"ext-pdo": "*",
18-
"ext-json": "*",
18+
"ext-pgsql": "*",
1919
"ext-sqlite3": "*",
2020
"brick/reflection": "~0.5.0",
2121
"phpunit/phpunit": "^11.0",

‎phpunit-bootstrap.php

+85-20
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
<?php
22

3+
use Brick\Geo\Engine\Database\Driver\PdoMysqlDriver;
4+
use Brick\Geo\Engine\Database\Driver\PdoPgsqlDriver;
5+
use Brick\Geo\Engine\Database\Driver\PgsqlDriver;
6+
use Brick\Geo\Engine\Database\Driver\Sqlite3Driver;
37
use Brick\Geo\Engine\GeosOpEngine;
4-
use Brick\Geo\Engine\PdoEngine;
5-
use Brick\Geo\Engine\Sqlite3Engine;
8+
use Brick\Geo\Engine\MariadbEngine;
9+
use Brick\Geo\Engine\MysqlEngine;
10+
use Brick\Geo\Engine\PostgisEngine;
11+
use Brick\Geo\Engine\SpatialiteEngine;
612
use Brick\Geo\Engine\GeosEngine;
713

814
function getOptionalEnv(string $name): ?string
@@ -43,10 +49,11 @@ function getRequiredEnv(string $name): string
4349
echo 'Available engines: pdo_mysql, pdo_pgsql, sqlite3, geos, geosop', PHP_EOL;
4450
} else {
4551
switch ($engine) {
46-
case 'pdo_mysql':
52+
// TODO reintroduce MYSQL_HOST in .env, add MARIADB_HOST
53+
case 'mysql_pdo':
4754
$emulatePrepares = getOptionalEnv('EMULATE_PREPARES') === 'ON';
4855

49-
echo 'Using PdoEngine for MySQL', PHP_EOL;
56+
echo 'Using MysqlEngine', PHP_EOL;
5057
echo 'with emulated prepares ', ($emulatePrepares ? 'ON' : 'OFF'), PHP_EOL;
5158

5259
$host = getRequiredEnv('MYSQL_HOST');
@@ -55,7 +62,7 @@ function getRequiredEnv(string $name): string
5562
$password = getRequiredEnv('MYSQL_PASSWORD');
5663

5764
$pdo = new PDO(
58-
sprintf('mysql:host=%s;port=%d', $host, $port),
65+
sprintf('mysql:host=%s;port=%d;dbname=usertest', $host, $port), // TODO remove dbname
5966
$username,
6067
$password,
6168
[
@@ -64,16 +71,46 @@ function getRequiredEnv(string $name): string
6471
],
6572
);
6673

67-
$statement = $pdo->query('SELECT VERSION()');
68-
$version = $statement->fetchColumn();
74+
$driver = new PdoMysqlDriver($pdo);
75+
$engine = new MysqlEngine($driver);
6976

77+
$version = $driver->executeQuery('SELECT VERSION()')->get(0)->asString();
7078
echo 'MySQL version: ', $version, PHP_EOL;
7179

72-
$engine = new PdoEngine($pdo);
7380
break;
7481

75-
case 'pdo_pgsql':
76-
echo 'Using PdoEngine for PostgreSQL', PHP_EOL;
82+
// TODO reintroduce MYSQL_HOST in .env, add MARIADB_HOST
83+
case 'mariadb_pdo':
84+
$emulatePrepares = getOptionalEnv('EMULATE_PREPARES') === 'ON';
85+
86+
echo 'Using MariadbEngine', PHP_EOL;
87+
echo 'with emulated prepares ', ($emulatePrepares ? 'ON' : 'OFF'), PHP_EOL;
88+
89+
$host = getRequiredEnv('MYSQL_HOST');
90+
$port = getOptionalEnvOrDefault('MYSQL_PORT', '3306');
91+
$username = getRequiredEnv('MYSQL_USER');
92+
$password = getRequiredEnv('MYSQL_PASSWORD');
93+
94+
$pdo = new PDO(
95+
sprintf('mysql:host=%s;port=%d;dbname=usertest', $host, $port), // TODO remove dbname
96+
$username,
97+
$password,
98+
[
99+
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
100+
PDO::ATTR_EMULATE_PREPARES => $emulatePrepares,
101+
],
102+
);
103+
104+
$driver = new PdoMysqlDriver($pdo);
105+
$engine = new MariadbEngine($driver);
106+
107+
$version = $driver->executeQuery('SELECT VERSION()')->get(0)->asString();
108+
echo 'MySQL version: ', $version, PHP_EOL;
109+
110+
break;
111+
112+
case 'postgis_pdo':
113+
echo 'Using PostgisEngine with PdoMysqlDriver', PHP_EOL;
77114

78115
$host = getRequiredEnv('POSTGRES_HOST');
79116
$port = getOptionalEnvOrDefault('POSTGRES_PORT', '5432');
@@ -89,38 +126,66 @@ function getRequiredEnv(string $name): string
89126
],
90127
);
91128

129+
// TODO move to docker compose
92130
$pdo->exec('CREATE EXTENSION IF NOT EXISTS postgis;');
93131

94-
$statement = $pdo->query('SELECT version()');
95-
$version = $statement->fetchColumn();
132+
$driver = new PdoPgsqlDriver($pdo);
133+
$engine = new PostgisEngine($driver);
96134

135+
$version = $driver->executeQuery('SELECT version()')->get(0)->asString();
97136
echo 'PostgreSQL version: ', $version, PHP_EOL;
98137

99-
$statement = $pdo->query('SELECT PostGIS_Full_Version()');
100-
$version = $statement->fetchColumn();
138+
$version = $driver->executeQuery('SELECT PostGIS_Full_Version()')->get(0)->asString();
139+
echo 'PostGIS version: ', $version, PHP_EOL;
140+
141+
break;
142+
143+
case 'postgis_pgsql':
144+
echo 'Using PostgisEngine with PgsqlDriver', PHP_EOL;
101145

146+
$host = getRequiredEnv('POSTGRES_HOST');
147+
$port = getOptionalEnvOrDefault('POSTGRES_PORT', '5432');
148+
$username = getRequiredEnv('POSTGRES_USER');
149+
$password = getRequiredEnv('POSTGRES_PASSWORD');
150+
151+
$connection = pg_connect(sprintf(
152+
'host=%s port=%d user=%s password=%s',
153+
$host,
154+
$port,
155+
$username,
156+
$password,
157+
));
158+
159+
$driver = new PgsqlDriver($connection);
160+
$engine = new PostgisEngine($driver);
161+
162+
$version = $driver->executeQuery('SELECT version()')->get(0)->asString();
163+
echo 'PostgreSQL version: ', $version, PHP_EOL;
164+
165+
$version = $driver->executeQuery('SELECT PostGIS_Full_Version()')->get(0)->asString();
102166
echo 'PostGIS version: ', $version, PHP_EOL;
103167

104-
$engine = new PdoEngine($pdo);
105168
break;
106169

107-
case 'sqlite3':
108-
echo 'Using Sqlite3Engine', PHP_EOL;
170+
case 'spatialite_sqlite3':
171+
echo 'Using SpatialiteEngine', PHP_EOL;
109172

110173
$sqlite3 = new SQLite3(':memory:');
111174
$sqlite3->enableExceptions(true);
112175

113-
$sqliteVersion = $sqlite3->querySingle('SELECT sqlite_version()');
176+
$driver = new Sqlite3Driver($sqlite3);
177+
178+
$sqliteVersion = $driver->executeQuery('SELECT sqlite_version()')->get(0)->asString();
114179
echo 'SQLite version: ', $sqliteVersion, PHP_EOL;
115180

116181
$sqlite3->loadExtension('mod_spatialite.so');
117182

118-
$spatialiteVersion = $sqlite3->querySingle('SELECT spatialite_version()');
183+
$spatialiteVersion = $driver->executeQuery('SELECT spatialite_version()')->get(0)->asString();
119184
echo 'SpatiaLite version: ', $spatialiteVersion, PHP_EOL;
120185

121186
$sqlite3->exec('SELECT InitSpatialMetaData()');
122187

123-
$engine = new Sqlite3Engine($sqlite3);
188+
$engine = new SpatialiteEngine($driver);
124189
break;
125190

126191
case 'geos':
+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Brick\Geo\Engine\Database;
6+
7+
use Brick\Geo\Engine\Database\Query\BinaryValue;
8+
use Brick\Geo\Engine\Database\Query\ScalarValue;
9+
use Brick\Geo\Engine\Database\Result\Row;
10+
use Brick\Geo\Exception\GeometryEngineException;
11+
12+
interface DatabaseDriver
13+
{
14+
/**
15+
* Executes a SQL query and returns exactly one row.
16+
*
17+
* Accepts one or more query components where each element can be:
18+
* - a string (inserted directly into the query),
19+
* - a BinaryValue or ScalarValue instance (typically replaced with a placeholder
20+
* and bound as a prepared statement parameter).
21+
*
22+
* Example: executeQuery('SELECT ST_Length(ST_GeomFromEWKB(', new BinaryValue($ewkb), '))')
23+
*
24+
* @throws GeometryEngineException If the query fails, or if the result is not exactly one row.
25+
*/
26+
public function executeQuery(string|BinaryValue|ScalarValue ...$query) : Row;
27+
28+
/**
29+
* @throws GeometryEngineException
30+
*/
31+
public function convertBinaryResult(mixed $value) : string;
32+
33+
/**
34+
* @throws GeometryEngineException
35+
*/
36+
public function convertStringResult(mixed $value) : string;
37+
38+
/**
39+
* @throws GeometryEngineException
40+
*/
41+
public function convertIntResult(mixed $value) : int;
42+
43+
/**
44+
* @throws GeometryEngineException
45+
*/
46+
public function convertFloatResult(mixed $value) : float;
47+
48+
/**
49+
* @throws GeometryEngineException
50+
*/
51+
public function convertBoolResult(mixed $value) : bool;
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Brick\Geo\Engine\Database\Driver;
6+
7+
use Brick\Geo\Engine\Database\DatabaseDriver;
8+
use Brick\Geo\Engine\Database\Query\BinaryValue;
9+
use Brick\Geo\Engine\Database\Query\ScalarValue;
10+
use Brick\Geo\Engine\Database\Result\Row;
11+
use Brick\Geo\Exception\GeometryEngineException;
12+
use Override;
13+
use PDO;
14+
use PDOException;
15+
use PDOStatement;
16+
17+
/**
18+
* Database driver using a pdo_mysql connection.
19+
*/
20+
final class PdoMysqlDriver implements DatabaseDriver
21+
{
22+
public function __construct(
23+
private readonly PDO $pdo,
24+
) {
25+
}
26+
27+
#[Override]
28+
public function executeQuery(string|BinaryValue|ScalarValue ...$query) : Row
29+
{
30+
$queryString = '';
31+
$queryParams = [];
32+
33+
foreach ($query as $queryPart) {
34+
if ($queryPart instanceof BinaryValue) {
35+
$queryString .= 'BINARY ?';
36+
$queryParams[] = [$queryPart->value, PDO::PARAM_LOB];
37+
} elseif ($queryPart instanceof ScalarValue) {
38+
$queryString .= '?';
39+
40+
if (is_int($queryPart->value)) {
41+
$queryParams[] = [$queryPart->value, PDO::PARAM_INT];
42+
} elseif (is_bool($queryPart->value)) {
43+
$queryParams[] = [$queryPart->value, PDO::PARAM_BOOL];
44+
} else {
45+
$queryParams[] = [$queryPart->value, PDO::PARAM_STR];
46+
}
47+
} else {
48+
$queryString .= $queryPart;
49+
}
50+
}
51+
52+
/** @var int $errMode */
53+
$errMode = $this->pdo->getAttribute(PDO::ATTR_ERRMODE);
54+
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
55+
56+
try {
57+
$statement = $this->pdo->prepare($queryString);
58+
59+
$position = 1;
60+
61+
foreach ($queryParams as [$value, $type]) {
62+
$statement->bindValue($position++, $value, $type);
63+
}
64+
65+
$statement->execute();
66+
67+
/** @var list<list<mixed>> $result */
68+
$result = $statement->fetchAll(PDO::FETCH_NUM);
69+
} catch (PDOException $e) {
70+
throw GeometryEngineException::wrap($e);
71+
} finally {
72+
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, $errMode);
73+
}
74+
75+
if (count($result) !== 1) {
76+
throw new GeometryEngineException(sprintf('Expected exactly one row, got %d.', count($result)));
77+
}
78+
79+
return new Row($this, $result[0]);
80+
}
81+
82+
public function convertBinaryResult(mixed $value) : string
83+
{
84+
if (is_string($value)) {
85+
return $value;
86+
}
87+
88+
throw GeometryEngineException::unexpectedDatabaseReturnType('string', $value);
89+
}
90+
91+
public function convertStringResult(mixed $value) : string
92+
{
93+
if (is_string($value)) {
94+
return $value;
95+
}
96+
97+
throw GeometryEngineException::unexpectedDatabaseReturnType('string', $value);
98+
}
99+
100+
public function convertIntResult(mixed $value) : int
101+
{
102+
// TODO check that actually returned as int;
103+
// maybe checks for all types sent & received for each driver?
104+
105+
if (is_int($value)) {
106+
return $value;
107+
}
108+
109+
throw GeometryEngineException::unexpectedDatabaseReturnType('int', $value);
110+
}
111+
112+
public function convertFloatResult(mixed $value) : float
113+
{
114+
if (is_numeric($value)) {
115+
return (float) $value;
116+
}
117+
118+
throw GeometryEngineException::unexpectedDatabaseReturnType('number or numeric string', $value);
119+
}
120+
121+
public function convertBoolResult(mixed $value) : bool
122+
{
123+
return match ($value) {
124+
0 => false,
125+
1 => true,
126+
default => throw GeometryEngineException::unexpectedDatabaseReturnType('0 or 1', $value),
127+
};
128+
}
129+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Brick\Geo\Engine\Database\Driver;
6+
7+
use Brick\Geo\Engine\Database\DatabaseDriver;
8+
use Brick\Geo\Engine\Database\Query\BinaryValue;
9+
use Brick\Geo\Engine\Database\Query\ScalarValue;
10+
use Brick\Geo\Engine\Database\Result\Row;
11+
use Brick\Geo\Exception\GeometryEngineException;
12+
use Override;
13+
use PDO;
14+
use PDOException;
15+
16+
/**
17+
* Database driver using a pdo_pgsql connection.
18+
*/
19+
final class PdoPgsqlDriver implements DatabaseDriver
20+
{
21+
public function __construct(
22+
private readonly PDO $pdo,
23+
) {
24+
}
25+
26+
#[Override]
27+
public function executeQuery(string|BinaryValue|ScalarValue ...$query) : Row
28+
{
29+
$queryString = '';
30+
$queryParams = [];
31+
32+
foreach ($query as $queryPart) {
33+
if ($queryPart instanceof BinaryValue) {
34+
$queryString .= '?';
35+
$queryParams[] = [$queryPart->value, PDO::PARAM_LOB];
36+
} elseif ($queryPart instanceof ScalarValue) {
37+
$queryString .= '?';
38+
39+
if (is_int($queryPart->value)) {
40+
$queryString .= '::int'; // PARAM_INT seems to have no effect on pdo_pgsql
41+
$queryParams[] = [$queryPart->value, PDO::PARAM_INT];
42+
} elseif (is_float($queryPart->value)) {
43+
$queryString .= '::float';
44+
$queryParams[] = [$queryPart->value, PDO::PARAM_STR];
45+
} elseif (is_bool($queryPart->value)) {
46+
$queryParams[] = [$queryPart->value, PDO::PARAM_BOOL];
47+
} else {
48+
$queryParams[] = [$queryPart->value, PDO::PARAM_STR];
49+
}
50+
} else {
51+
$queryString .= $queryPart;
52+
}
53+
}
54+
55+
/** @var int $errMode */
56+
$errMode = $this->pdo->getAttribute(PDO::ATTR_ERRMODE);
57+
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
58+
59+
try {
60+
$statement = $this->pdo->prepare($queryString);
61+
62+
$position = 1;
63+
64+
foreach ($queryParams as [$value, $type]) {
65+
$statement->bindValue($position++, $value, $type);
66+
}
67+
68+
$statement->execute();
69+
70+
/** @var list<list<mixed>> $result */
71+
$result = $statement->fetchAll(PDO::FETCH_NUM);
72+
} catch (PDOException $e) {
73+
throw GeometryEngineException::wrap($e);
74+
} finally {
75+
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, $errMode);
76+
}
77+
78+
if (count($result) !== 1) {
79+
throw new GeometryEngineException(sprintf('Expected exactly one row, got %d.', count($result)));
80+
}
81+
82+
return new Row($this, $result[0]);
83+
}
84+
85+
public function convertBinaryResult(mixed $value) : string
86+
{
87+
if (is_resource($value)) {
88+
return stream_get_contents($value);
89+
}
90+
91+
throw GeometryEngineException::unexpectedDatabaseReturnType('resource', $value);
92+
}
93+
94+
public function convertStringResult(mixed $value) : string
95+
{
96+
if (is_string($value)) {
97+
return $value;
98+
}
99+
100+
throw GeometryEngineException::unexpectedDatabaseReturnType('string', $value);
101+
}
102+
103+
public function convertIntResult(mixed $value) : int
104+
{
105+
// TODO check that actually returned as int;
106+
// maybe checks for all types sent & received for each driver?
107+
108+
if (is_int($value)) {
109+
return $value;
110+
}
111+
112+
throw GeometryEngineException::unexpectedDatabaseReturnType('int', $value);
113+
}
114+
115+
public function convertFloatResult(mixed $value) : float
116+
{
117+
if (is_numeric($value)) {
118+
return (float) $value;
119+
}
120+
121+
throw GeometryEngineException::unexpectedDatabaseReturnType('number or numeric string', $value);
122+
}
123+
124+
public function convertBoolResult(mixed $value) : bool
125+
{
126+
if (is_bool($value)) {
127+
return $value;
128+
}
129+
130+
throw GeometryEngineException::unexpectedDatabaseReturnType('bool', $value);
131+
}
132+
}
+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Brick\Geo\Engine\Database\Driver;
6+
7+
use Brick\Geo\Engine\Database\DatabaseDriver;
8+
use Brick\Geo\Engine\Database\Query\BinaryValue;
9+
use Brick\Geo\Engine\Database\Query\ScalarValue;
10+
use Brick\Geo\Engine\Database\Result\Row;
11+
use Brick\Geo\Exception\GeometryEngineException;
12+
use Override;
13+
use PgSql\Connection;
14+
15+
/**
16+
* Database driver using the pgsql extension for PostgreSQL.
17+
*
18+
* TODO error handling!
19+
*/
20+
final class PgsqlDriver implements DatabaseDriver
21+
{
22+
public function __construct(
23+
private readonly Connection $connection,
24+
) {
25+
}
26+
27+
#[Override]
28+
public function executeQuery(string|BinaryValue|ScalarValue ...$query) : Row
29+
{
30+
$position = 1;
31+
32+
$queryString = '';
33+
$queryParams = [];
34+
35+
foreach ($query as $queryPart) {
36+
if ($queryPart instanceof BinaryValue) {
37+
$queryString .= '$' . $position++ . '::bytea';
38+
$queryParams[] = pg_escape_bytea($this->connection, $queryPart->value);
39+
} elseif ($queryPart instanceof ScalarValue) {
40+
$queryString .= '$' . $position++;
41+
42+
if (is_int($queryPart->value)) {
43+
$queryString .= '::int';
44+
$queryParams[] = $queryPart->value;
45+
} elseif (is_float($queryPart->value)) {
46+
$queryString .= '::float';
47+
$queryParams[] = $queryPart->value;
48+
} elseif (is_bool($queryPart->value)) {
49+
$queryString .= '::bool';
50+
$queryParams[] = $queryPart->value ? 't' : 'f';
51+
} else {
52+
$queryParams[] = $queryPart->value;
53+
}
54+
} else {
55+
$queryString .= $queryPart;
56+
}
57+
}
58+
59+
$value = pg_prepare($this->connection, '', $queryString);
60+
61+
if ($value === false) {
62+
die(pg_last_error());
63+
}
64+
65+
$result = pg_execute($this->connection, '', $queryParams);
66+
67+
if ($result === false) {
68+
var_dump($queryParams);
69+
var_dump(pg_last_error($this->connection));
70+
die;
71+
}
72+
73+
/** @var list<list<mixed>> $rows */
74+
$rows = pg_fetch_all($result, PGSQL_NUM);
75+
76+
if ($rows === false) {
77+
die(pg_last_error());
78+
}
79+
80+
if (count($rows) !== 1) {
81+
throw new GeometryEngineException(sprintf('Expected exactly one row, got %d.', count($rows)));
82+
}
83+
84+
return new Row($this, $rows[0]);
85+
}
86+
87+
public function convertBinaryResult(mixed $value) : string
88+
{
89+
if (is_string($value)) {
90+
return pg_unescape_bytea($value);
91+
}
92+
93+
throw GeometryEngineException::unexpectedDatabaseReturnType('string', $value);
94+
}
95+
96+
public function convertStringResult(mixed $value) : string
97+
{
98+
if (is_string($value)) {
99+
return $value;
100+
}
101+
102+
throw GeometryEngineException::unexpectedDatabaseReturnType('string', $value);
103+
}
104+
105+
public function convertIntResult(mixed $value) : int
106+
{
107+
// TODO check that actually returned as int;
108+
// maybe checks for all types sent & received for each driver?
109+
110+
if (is_int($value)) {
111+
return $value;
112+
}
113+
114+
throw GeometryEngineException::unexpectedDatabaseReturnType('int', $value);
115+
}
116+
117+
public function convertFloatResult(mixed $value) : float
118+
{
119+
if (is_numeric($value)) {
120+
return (float) $value;
121+
}
122+
123+
throw GeometryEngineException::unexpectedDatabaseReturnType('number or numeric string', $value);
124+
}
125+
126+
public function convertBoolResult(mixed $value) : bool
127+
{
128+
return match ($value) {
129+
't' => true,
130+
'f' => false,
131+
default => throw GeometryEngineException::unexpectedDatabaseReturnType('t or f', $value),
132+
};
133+
}
134+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Brick\Geo\Engine\Database\Driver;
6+
7+
use Brick\Geo\Engine\Database\DatabaseDriver;
8+
use Brick\Geo\Engine\Database\Query\BinaryValue;
9+
use Brick\Geo\Engine\Database\Query\ScalarValue;
10+
use Brick\Geo\Engine\Database\Result\Row;
11+
use Brick\Geo\Exception\GeometryEngineException;
12+
use Override;
13+
use SQLite3;
14+
15+
/**
16+
* Database driver using an SQLite3 connection.
17+
*/
18+
class Sqlite3Driver implements DatabaseDriver
19+
{
20+
public function __construct(
21+
private SQLite3 $sqlite3,
22+
) {
23+
}
24+
25+
#[Override]
26+
public function executeQuery(string|BinaryValue|ScalarValue ...$query) : Row
27+
{
28+
$queryString = '';
29+
$queryParams = [];
30+
31+
foreach ($query as $queryPart) {
32+
if ($queryPart instanceof BinaryValue) {
33+
$queryString .= '?';
34+
$queryParams[] = [$queryPart->value, SQLITE3_BLOB];
35+
} elseif ($queryPart instanceof ScalarValue) {
36+
$queryString .= '?';
37+
38+
if (is_float($queryPart->value)) {
39+
$queryParams[] = [$queryPart->value, SQLITE3_FLOAT];
40+
} elseif (is_int($queryPart->value) || is_bool($queryPart->value)) {
41+
$queryParams[] = [$queryPart->value, SQLITE3_INTEGER];
42+
} else {
43+
$queryParams[] = [$queryPart->value, SQLITE3_TEXT];
44+
}
45+
} else {
46+
$queryString .= $queryPart;
47+
}
48+
}
49+
50+
$enableExceptions = $this->sqlite3->enableExceptions(true);
51+
52+
try {
53+
$statement = $this->sqlite3->prepare($queryString);
54+
55+
$position = 1;
56+
57+
foreach ($queryParams as [$value, $type]) {
58+
$statement->bindValue($position++, $value, $type);
59+
}
60+
61+
$sqlite3Result = $statement->execute();
62+
63+
$result = [];
64+
65+
while (false !== $row = $sqlite3Result->fetchArray(SQLITE3_NUM)) {
66+
$result[] = $row;
67+
}
68+
69+
} catch (\Exception $e) {
70+
throw GeometryEngineException::wrap($e);
71+
} finally {
72+
$this->sqlite3->enableExceptions($enableExceptions);
73+
}
74+
75+
if (count($result) !== 1) {
76+
throw new GeometryEngineException(sprintf('Expected exactly one row, got %d.', count($result)));
77+
}
78+
79+
return new Row($this, $result[0]);
80+
}
81+
82+
public function convertBinaryResult(mixed $value) : string
83+
{
84+
if (is_string($value)) {
85+
return $value;
86+
}
87+
88+
throw GeometryEngineException::unexpectedDatabaseReturnType('string', $value);
89+
}
90+
91+
public function convertStringResult(mixed $value) : string
92+
{
93+
if (is_string($value)) {
94+
return $value;
95+
}
96+
97+
throw GeometryEngineException::unexpectedDatabaseReturnType('string', $value);
98+
}
99+
100+
public function convertIntResult(mixed $value) : int
101+
{
102+
// TODO check that actually returned as int;
103+
// maybe checks for all types sent & received for each driver?
104+
105+
if (is_int($value)) {
106+
return $value;
107+
}
108+
109+
throw GeometryEngineException::unexpectedDatabaseReturnType('int', $value);
110+
}
111+
112+
public function convertFloatResult(mixed $value) : float
113+
{
114+
if (is_numeric($value)) {
115+
return (float) $value;
116+
}
117+
118+
throw GeometryEngineException::unexpectedDatabaseReturnType('number or numeric string', $value);
119+
}
120+
121+
public function convertBoolResult(mixed $value) : bool
122+
{
123+
return match ($value) {
124+
0 => false,
125+
1 => true,
126+
default => throw GeometryEngineException::unexpectedDatabaseReturnType('t or f', $value),
127+
};
128+
}
129+
}
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Brick\Geo\Engine\Database\Query;
6+
7+
/**
8+
* Marker class for passing binary data to the database.
9+
*/
10+
final readonly class BinaryValue
11+
{
12+
public function __construct(
13+
public string $value,
14+
) {
15+
}
16+
}
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Brick\Geo\Engine\Database\Query;
6+
7+
/**
8+
* Marker class for passing scalar values to the database.
9+
*/
10+
final readonly class ScalarValue
11+
{
12+
/**
13+
* @param scalar $value
14+
*/
15+
public function __construct(
16+
public mixed $value,
17+
) {
18+
}
19+
}

‎src/Engine/Database/Result/Row.php

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Brick\Geo\Engine\Database\Result;
6+
7+
use Brick\Geo\Engine\Database\DatabaseDriver;
8+
use Brick\Geo\Exception\GeometryEngineException;
9+
10+
final readonly class Row
11+
{
12+
/**
13+
* @param list<mixed> $values
14+
*/
15+
public function __construct(
16+
private DatabaseDriver $driver,
17+
private array $values,
18+
) {
19+
}
20+
21+
public function get(int $index) : Value
22+
{
23+
if (! array_key_exists($index, $this->values)) {
24+
throw new GeometryEngineException(sprintf('Column %d not found in Result.', $index));
25+
}
26+
27+
return new Value($this->driver, $this->values[$index]);
28+
}
29+
}

‎src/Engine/Database/Result/Value.php

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Brick\Geo\Engine\Database\Result;
6+
7+
use Brick\Geo\Engine\Database\DatabaseDriver;
8+
9+
final readonly class Value
10+
{
11+
public function __construct(
12+
private DatabaseDriver $driver,
13+
private mixed $value,
14+
) {
15+
}
16+
17+
public function asBinary() : string
18+
{
19+
return $this->driver->convertBinaryResult($this->value);
20+
}
21+
22+
public function asString() : string
23+
{
24+
return $this->driver->convertStringResult($this->value);
25+
}
26+
27+
public function asInt() : int
28+
{
29+
return $this->driver->convertIntResult($this->value);
30+
}
31+
32+
public function asFloat() : float
33+
{
34+
return $this->driver->convertFloatResult($this->value);
35+
}
36+
37+
public function asBool() : bool
38+
{
39+
return $this->driver->convertBoolResult($this->value);
40+
}
41+
}

‎src/Engine/DatabaseEngine.php

+20-231
Original file line numberDiff line numberDiff line change
@@ -5,128 +5,24 @@
55
namespace Brick\Geo\Engine;
66

77
use Brick\Geo\Curve;
8+
use Brick\Geo\Engine\Database\DatabasePlatform;
9+
use Brick\Geo\Engine\Internal\GeometryParameter;
810
use Brick\Geo\Engine\Internal\TypeChecker;
911
use Brick\Geo\Exception\GeometryEngineException;
1012
use Brick\Geo\Geometry;
1113
use Brick\Geo\LineString;
1214
use Brick\Geo\MultiCurve;
1315
use Brick\Geo\MultiPoint;
1416
use Brick\Geo\MultiSurface;
15-
use Brick\Geo\MultiPolygon;
1617
use Brick\Geo\Point;
17-
use Brick\Geo\Polygon;
18-
use Brick\Geo\Proxy;
19-
use Brick\Geo\Proxy\ProxyInterface;
2018
use Brick\Geo\Surface;
2119
use Override;
2220

2321
/**
24-
* Database implementation of the GeometryEngine.
25-
*
26-
* The target database must have support for GIS functions.
22+
* Base class for database engines.
2723
*/
28-
abstract class DatabaseEngine implements GeometryEngine
24+
abstract readonly class DatabaseEngine implements GeometryEngine
2925
{
30-
private readonly bool $useProxy;
31-
32-
public function __construct(bool $useProxy)
33-
{
34-
$this->useProxy = $useProxy;
35-
}
36-
37-
/**
38-
* Executes a SQL query.
39-
*
40-
* @param string $query The SQL query to execute.
41-
* @param list<GeometryParameter|scalar> $parameters The geometry data or scalar values to pass as parameters.
42-
*
43-
* @return list<mixed> A numeric result array.
44-
*
45-
* @throws GeometryEngineException
46-
*/
47-
abstract protected function executeQuery(string $query, array $parameters) : array;
48-
49-
/**
50-
* Returns the syntax required to perform a ST_GeomFromText(), together with placeholders.
51-
*
52-
* This method may be overridden if necessary.
53-
*/
54-
protected function getGeomFromTextSyntax(): string
55-
{
56-
return 'ST_GeomFromText(?, ?)';
57-
}
58-
59-
/**
60-
* Returns the syntax required to perform a ST_GeomFromWKB(), together with placeholders.
61-
*
62-
* This method may be overridden if necessary.
63-
*/
64-
protected function getGeomFromWkbSyntax(): string
65-
{
66-
return 'ST_GeomFromWKB(?, ?)';
67-
}
68-
69-
/**
70-
* Returns the placeholder syntax for the given parameter.
71-
*
72-
* This method may be overridden to perform explicit type casts if necessary.
73-
*/
74-
protected function getParameterPlaceholder(string|float|int|bool $parameter): string
75-
{
76-
return '?';
77-
}
78-
79-
/**
80-
* Builds and executes a SQL query for a GIS function.
81-
*
82-
* @param string $function The SQL GIS function to execute.
83-
* @param array<Geometry|scalar> $parameters The Geometry objects or scalar values to pass as parameters.
84-
* @param bool $returnsGeometry Whether the GIS function returns a Geometry.
85-
*
86-
* @return list<mixed> A numeric result array.
87-
*
88-
* @throws GeometryEngineException
89-
*/
90-
private function query(string $function, array $parameters, bool $returnsGeometry) : array
91-
{
92-
$queryParameters = [];
93-
$queryValues = [];
94-
95-
foreach ($parameters as $parameter) {
96-
if ($parameter instanceof Geometry) {
97-
if ($parameter instanceof Proxy\ProxyInterface) {
98-
$sendAsBinary = $parameter->isProxyBinary();
99-
} else {
100-
$sendAsBinary = ! $parameter->isEmpty();
101-
}
102-
103-
$queryParameters[] = $sendAsBinary
104-
? $this->getGeomFromWkbSyntax()
105-
: $this->getGeomFromTextSyntax();
106-
107-
$queryValues[] = new GeometryParameter($parameter, $sendAsBinary);
108-
} else {
109-
$queryParameters[] = $this->getParameterPlaceholder($parameter);
110-
$queryValues[] = $parameter;
111-
}
112-
}
113-
114-
$query = sprintf('SELECT %s(%s)', $function, implode(', ', $queryParameters));
115-
116-
if ($returnsGeometry) {
117-
$query = sprintf('
118-
SELECT
119-
CASE WHEN ST_IsEmpty(g) THEN ST_AsText(g) ELSE NULL END,
120-
CASE WHEN ST_IsEmpty(g) THEN NULL ELSE ST_AsBinary(g) END,
121-
ST_GeometryType(g),
122-
ST_SRID(g)
123-
FROM (%s AS g) AS q
124-
', $query);
125-
}
126-
127-
return $this->executeQuery($query, $queryValues);
128-
}
129-
13026
/**
13127
* Queries a GIS function returning a boolean value.
13228
*
@@ -135,21 +31,7 @@ private function query(string $function, array $parameters, bool $returnsGeometr
13531
*
13632
* @throws GeometryEngineException
13733
*/
138-
private function queryBoolean(string $function, Geometry|string|float|int|bool ...$parameters) : bool
139-
{
140-
/** @var array{scalar|null} $result */
141-
$result = $this->query($function, $parameters, false);
142-
143-
$value = $result[0];
144-
145-
// SQLite3 returns -1 when calling a boolean GIS function on a NULL result,
146-
// MariaDB returns -1 when an unsupported operation is performed on a Z/M geometry.
147-
if ($value === null || $value === -1 || $value === '-1') {
148-
throw GeometryEngineException::operationYieldedNoResult();
149-
}
150-
151-
return (bool) $value;
152-
}
34+
abstract protected function queryBool(string $function, Geometry|string|float|int|bool ...$parameters) : bool;
15335

15436
/**
15537
* Queries a GIS function returning a floating point value.
@@ -159,19 +41,7 @@ private function queryBoolean(string $function, Geometry|string|float|int|bool .
15941
*
16042
* @throws GeometryEngineException
16143
*/
162-
private function queryFloat(string $function, Geometry|string|float|int|bool ...$parameters) : float
163-
{
164-
/** @var array{scalar|null} $result */
165-
$result = $this->query($function, $parameters, false);
166-
167-
$value = $result[0];
168-
169-
if ($value === null) {
170-
throw GeometryEngineException::operationYieldedNoResult();
171-
}
172-
173-
return (float) $value;
174-
}
44+
abstract protected function queryFloat(string $function, Geometry|string|float|int|bool ...$parameters) : float;
17545

17646
/**
17747
* Queries a GIS function returning a Geometry object.
@@ -181,94 +51,18 @@ private function queryFloat(string $function, Geometry|string|float|int|bool ...
18151
*
18252
* @throws GeometryEngineException
18353
*/
184-
final protected function queryGeometry(string $function, Geometry|string|float|int|bool ...$parameters) : Geometry
185-
{
186-
/** @var array{string|null, string|resource|null, string, int|numeric-string} $result */
187-
$result = $this->query($function, $parameters, true);
188-
189-
[$wkt, $wkb, $geometryType, $srid] = $result;
190-
191-
$srid = (int) $srid;
192-
193-
if ($wkt !== null) {
194-
if ($this->useProxy) {
195-
$proxyClassName = $this->getProxyClassName($geometryType);
196-
197-
return new $proxyClassName($wkt, false, $srid);
198-
}
199-
200-
return Geometry::fromText($wkt, $srid);
201-
}
202-
203-
if ($wkb !== null) {
204-
if (is_resource($wkb)) {
205-
$wkb = stream_get_contents($wkb);
206-
}
207-
208-
if ($this->useProxy) {
209-
$proxyClassName = $this->getProxyClassName($geometryType);
210-
211-
return new $proxyClassName($wkb, true, $srid);
212-
}
213-
214-
return Geometry::fromBinary($wkb, $srid);
215-
}
216-
217-
throw GeometryEngineException::operationYieldedNoResult();
218-
}
219-
220-
/**
221-
* @return class-string<Proxy\ProxyInterface&Geometry>
222-
*
223-
* @throws GeometryEngineException
224-
*/
225-
private function getProxyClassName(string $geometryType) : string
226-
{
227-
$proxyClasses = [
228-
'CIRCULARSTRING' => Proxy\CircularStringProxy::class,
229-
'COMPOUNDCURVE' => Proxy\CompoundCurveProxy::class,
230-
'CURVE' => Proxy\CurveProxy::class,
231-
'CURVEPOLYGON' => Proxy\CurvePolygonProxy::class,
232-
'GEOMCOLLECTION' => Proxy\GeometryCollectionProxy::class, /* MySQL 8 - https://github.com/brick/geo/pull/33 */
233-
'GEOMETRY' => Proxy\GeometryProxy::class,
234-
'GEOMETRYCOLLECTION' => Proxy\GeometryCollectionProxy::class,
235-
'LINESTRING' => Proxy\LineStringProxy::class,
236-
'MULTICURVE' => Proxy\MultiCurveProxy::class,
237-
'MULTILINESTRING' => Proxy\MultiLineStringProxy::class,
238-
'MULTIPOINT' => Proxy\MultiPointProxy::class,
239-
'MULTIPOLYGON' => Proxy\MultiPolygonProxy::class,
240-
'MULTISURFACE' => Proxy\MultiSurfaceProxy::class,
241-
'POINT' => Proxy\PointProxy::class,
242-
'POLYGON' => Proxy\PolygonProxy::class,
243-
'POLYHEDRALSURFACE' => Proxy\PolyhedralSurfaceProxy::class,
244-
'SURFACE' => Proxy\SurfaceProxy::class,
245-
'TIN' => Proxy\TinProxy::class,
246-
'TRIANGLE' => Proxy\TriangleProxy::class
247-
];
248-
249-
$geometryType = strtoupper($geometryType);
250-
$geometryType = preg_replace('/^ST_/', '', $geometryType);
251-
assert($geometryType !== null);
252-
$geometryType = preg_replace('/ .*/', '', $geometryType);
253-
assert($geometryType !== null);
254-
255-
if (! isset($proxyClasses[$geometryType])) {
256-
throw new GeometryEngineException('Unknown geometry type: ' . $geometryType);
257-
}
258-
259-
return $proxyClasses[$geometryType];
260-
}
54+
abstract protected function queryGeometry(string $function, Geometry|string|float|int|bool ...$parameters) : Geometry;
26155

26256
#[Override]
26357
public function contains(Geometry $a, Geometry $b) : bool
26458
{
265-
return $this->queryBoolean('ST_Contains', $a, $b);
59+
return $this->queryBool('ST_Contains', $a, $b);
26660
}
26761

26862
#[Override]
26963
public function intersects(Geometry $a, Geometry $b) : bool
27064
{
271-
return $this->queryBoolean('ST_Intersects', $a, $b);
65+
return $this->queryBool('ST_Intersects', $a, $b);
27266
}
27367

27468
#[Override]
@@ -340,30 +134,25 @@ public function boundary(Geometry $g) : Geometry
340134
#[Override]
341135
public function isValid(Geometry $g) : bool
342136
{
343-
return $this->queryBoolean('ST_IsValid', $g);
137+
return $this->queryBool('ST_IsValid', $g);
344138
}
345139

346140
#[Override]
347141
public function isClosed(Geometry $g) : bool
348142
{
349-
return $this->queryBoolean('ST_IsClosed', $g);
143+
return $this->queryBool('ST_IsClosed', $g);
350144
}
351145

352146
#[Override]
353147
public function isSimple(Geometry $g) : bool
354148
{
355-
return $this->queryBoolean('ST_IsSimple', $g);
149+
return $this->queryBool('ST_IsSimple', $g);
356150
}
357151

358152
#[Override]
359153
public function isRing(Curve $curve) : bool
360154
{
361-
try {
362-
return $this->queryBoolean('ST_IsRing', $curve);
363-
} catch (GeometryEngineException) {
364-
// Not all RDBMS (hello, MySQL) support ST_IsRing(), but we have an easy fallback
365-
return $this->isClosed($curve) && $this->isSimple($curve);
366-
}
155+
return $this->queryBool('ST_IsRing', $curve);
367156
}
368157

369158
#[Override]
@@ -375,43 +164,43 @@ public function makeValid(Geometry $g) : Geometry
375164
#[Override]
376165
public function equals(Geometry $a, Geometry $b) : bool
377166
{
378-
return $this->queryBoolean('ST_Equals', $a, $b);
167+
return $this->queryBool('ST_Equals', $a, $b);
379168
}
380169

381170
#[Override]
382171
public function disjoint(Geometry $a, Geometry $b) : bool
383172
{
384-
return $this->queryBoolean('ST_Disjoint', $a, $b);
173+
return $this->queryBool('ST_Disjoint', $a, $b);
385174
}
386175

387176
#[Override]
388177
public function touches(Geometry $a, Geometry $b) : bool
389178
{
390-
return $this->queryBoolean('ST_Touches', $a, $b);
179+
return $this->queryBool('ST_Touches', $a, $b);
391180
}
392181

393182
#[Override]
394183
public function crosses(Geometry $a, Geometry $b) : bool
395184
{
396-
return $this->queryBoolean('ST_Crosses', $a, $b);
185+
return $this->queryBool('ST_Crosses', $a, $b);
397186
}
398187

399188
#[Override]
400189
public function within(Geometry $a, Geometry $b) : bool
401190
{
402-
return $this->queryBoolean('ST_Within', $a, $b);
191+
return $this->queryBool('ST_Within', $a, $b);
403192
}
404193

405194
#[Override]
406195
public function overlaps(Geometry $a, Geometry $b) : bool
407196
{
408-
return $this->queryBoolean('ST_Overlaps', $a, $b);
197+
return $this->queryBool('ST_Overlaps', $a, $b);
409198
}
410199

411200
#[Override]
412201
public function relate(Geometry $a, Geometry $b, string $matrix) : bool
413202
{
414-
return $this->queryBoolean('ST_Relate', $a, $b, $matrix);
203+
return $this->queryBool('ST_Relate', $a, $b, $matrix);
415204
}
416205

417206
#[Override]

‎src/Engine/GeometryParameter.php

-37
This file was deleted.

‎src/Engine/MariadbEngine.php

+170
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Brick\Geo\Engine;
6+
7+
use Brick\Geo\Engine\Database\DatabaseDriver;
8+
use Brick\Geo\Engine\Database\Query\BinaryValue;
9+
use Brick\Geo\Engine\Database\Query\ScalarValue;
10+
use Brick\Geo\Engine\Database\Result\Row;
11+
use Brick\Geo\Exception\GeometryEngineException;
12+
use Brick\Geo\Geometry;
13+
use Brick\Geo\Io\WkbReader;
14+
use Brick\Geo\Io\WkbWriter;
15+
use Override;
16+
17+
/**
18+
* Database engine based on MariaDB.
19+
*/
20+
final readonly class MariadbEngine extends DatabaseEngine
21+
{
22+
private WkbReader $wkbReader;
23+
private WkbWriter $wkbWriter;
24+
25+
public function __construct(
26+
// TODO private
27+
public DatabaseDriver $driver,
28+
// TODO
29+
private bool $useProxy = true,
30+
) {
31+
$this->wkbReader = new WkbReader();
32+
$this->wkbWriter = new WkbWriter();
33+
}
34+
35+
/**
36+
* Builds and executes a SQL query for a GIS function.
37+
*
38+
* @param string $function The SQL GIS function to execute.
39+
* @param list<Geometry|scalar> $parameters The Geometry objects or scalar values to pass as parameters.
40+
* @param bool $returnsGeometry Whether the GIS function returns a Geometry.
41+
*
42+
* @throws GeometryEngineException
43+
*/
44+
private function query(string $function, array $parameters, bool $returnsGeometry) : Row
45+
{
46+
$query = ['SELECT '];
47+
48+
if ($returnsGeometry) {
49+
$query[] = 'ST_AsWKB(g), ST_SRID(g) FROM (SELECT ';
50+
}
51+
52+
$query[] = $function . '(';
53+
54+
foreach ($parameters as $key => $parameter) {
55+
if ($key !== 0) {
56+
$query[] = ',';
57+
}
58+
59+
if ($parameter instanceof Geometry) {
60+
if ($parameter->isEmpty() && $parameter->geometryType() !== 'GeometryCollection') {
61+
throw new GeometryEngineException('TODO'); // TODO Error message
62+
}
63+
$query[] = 'ST_GeomFromWKB(';
64+
$query[] = new BinaryValue($this->wkbWriter->write($parameter));
65+
$query[] = ',';
66+
$query[] = new ScalarValue($parameter->srid());
67+
$query[] = ')';
68+
} else {
69+
$query[] = new ScalarValue($parameter);
70+
}
71+
}
72+
73+
$query[] = ')';
74+
75+
if ($returnsGeometry) {
76+
$query[] = ' AS g) AS q';
77+
}
78+
79+
return $this->driver->executeQuery(...$query);
80+
}
81+
82+
/**
83+
* Queries a GIS function returning a boolean value.
84+
*
85+
* @param string $function The SQL GIS function to execute.
86+
* @param Geometry|scalar ...$parameters The Geometry objects or scalar values to pass as parameters.
87+
*
88+
* @throws GeometryEngineException
89+
*/
90+
#[Override]
91+
protected function queryBool(string $function, Geometry|string|float|int|bool ...$parameters) : bool
92+
{
93+
return $this->query($function, $parameters, false)->get(0)->asBool();
94+
}
95+
96+
/**
97+
* Queries a GIS function returning a floating point value.
98+
*
99+
* @param string $function The SQL GIS function to execute.
100+
* @param Geometry|scalar ...$parameters The Geometry objects or scalar values to pass as parameters.
101+
*
102+
* @throws GeometryEngineException
103+
*/
104+
#[Override]
105+
protected function queryFloat(string $function, Geometry|string|float|int|bool ...$parameters) : float
106+
{
107+
return $this->query($function, $parameters, false)->get(0)->asFloat();
108+
}
109+
110+
/**
111+
* Queries a GIS function returning a Geometry object.
112+
*
113+
* @param string $function The SQL GIS function to execute.
114+
* @param Geometry|scalar ...$parameters The Geometry objects or scalar values to pass as parameters.
115+
*
116+
* @throws GeometryEngineException
117+
*/
118+
#[Override]
119+
protected function queryGeometry(string $function, Geometry|string|float|int|bool ...$parameters) : Geometry
120+
{
121+
$row = $this->query($function, $parameters, true);
122+
123+
$wkb = $row->get(0)->asBinary();
124+
$srid = $row->get(1)->asInt();
125+
126+
return $this->wkbReader->read($wkb, $srid);
127+
}
128+
129+
// /**
130+
// * @return class-string<Proxy\ProxyInterface&Geometry>
131+
// *
132+
// * @throws GeometryEngineException
133+
// */
134+
// private function getProxyClassName(string $geometryType) : string
135+
// {
136+
// $proxyClasses = [
137+
// 'CIRCULARSTRING' => Proxy\CircularStringProxy::class,
138+
// 'COMPOUNDCURVE' => Proxy\CompoundCurveProxy::class,
139+
// 'CURVE' => Proxy\CurveProxy::class,
140+
// 'CURVEPOLYGON' => Proxy\CurvePolygonProxy::class,
141+
// 'GEOMCOLLECTION' => Proxy\GeometryCollectionProxy::class, /* MySQL 8 - https://github.com/brick/geo/pull/33 */
142+
// 'GEOMETRY' => Proxy\GeometryProxy::class,
143+
// 'GEOMETRYCOLLECTION' => Proxy\GeometryCollectionProxy::class,
144+
// 'LINESTRING' => Proxy\LineStringProxy::class,
145+
// 'MULTICURVE' => Proxy\MultiCurveProxy::class,
146+
// 'MULTILINESTRING' => Proxy\MultiLineStringProxy::class,
147+
// 'MULTIPOINT' => Proxy\MultiPointProxy::class,
148+
// 'MULTIPOLYGON' => Proxy\MultiPolygonProxy::class,
149+
// 'MULTISURFACE' => Proxy\MultiSurfaceProxy::class,
150+
// 'POINT' => Proxy\PointProxy::class,
151+
// 'POLYGON' => Proxy\PolygonProxy::class,
152+
// 'POLYHEDRALSURFACE' => Proxy\PolyhedralSurfaceProxy::class,
153+
// 'SURFACE' => Proxy\SurfaceProxy::class,
154+
// 'TIN' => Proxy\TinProxy::class,
155+
// 'TRIANGLE' => Proxy\TriangleProxy::class
156+
// ];
157+
//
158+
// $geometryType = strtoupper($geometryType);
159+
// $geometryType = preg_replace('/^ST_/', '', $geometryType);
160+
// assert($geometryType !== null);
161+
// $geometryType = preg_replace('/ .*/', '', $geometryType);
162+
// assert($geometryType !== null);
163+
//
164+
// if (! isset($proxyClasses[$geometryType])) {
165+
// throw new GeometryEngineException('Unknown geometry type: ' . $geometryType);
166+
// }
167+
//
168+
// return $proxyClasses[$geometryType];
169+
// }
170+
}

‎src/Engine/MysqlEngine.php

+180
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Brick\Geo\Engine;
6+
7+
use Brick\Geo\Curve;
8+
use Brick\Geo\Engine\Database\DatabaseDriver;
9+
use Brick\Geo\Engine\Database\Query\BinaryValue;
10+
use Brick\Geo\Engine\Database\Query\ScalarValue;
11+
use Brick\Geo\Engine\Database\Result\Row;
12+
use Brick\Geo\Exception\GeometryEngineException;
13+
use Brick\Geo\Geometry;
14+
use Brick\Geo\Io\WkbReader;
15+
use Brick\Geo\Io\WkbWriter;
16+
use Brick\Geo\Point;
17+
use Override;
18+
19+
/**
20+
* Database engine based on MySQL.
21+
*/
22+
final readonly class MysqlEngine extends DatabaseEngine
23+
{
24+
private WkbReader $wkbReader;
25+
private WkbWriter $wkbWriter;
26+
27+
public function __construct(
28+
// TODO private
29+
public DatabaseDriver $driver,
30+
// TODO
31+
private bool $useProxy = true,
32+
) {
33+
$this->wkbReader = new WkbReader();
34+
$this->wkbWriter = new WkbWriter();
35+
}
36+
37+
/**
38+
* Builds and executes a SQL query for a GIS function.
39+
*
40+
* @param string $function The SQL GIS function to execute.
41+
* @param list<Geometry|scalar> $parameters The Geometry objects or scalar values to pass as parameters.
42+
* @param bool $returnsGeometry Whether the GIS function returns a Geometry.
43+
*
44+
* @throws GeometryEngineException
45+
*/
46+
private function query(string $function, array $parameters, bool $returnsGeometry) : Row
47+
{
48+
$query = ['SELECT '];
49+
50+
if ($returnsGeometry) {
51+
$query[] = 'ST_AsWKB(g), ST_SRID(g) FROM (SELECT ';
52+
}
53+
54+
$query[] = $function . '(';
55+
56+
foreach ($parameters as $key => $parameter) {
57+
if ($key !== 0) {
58+
$query[] = ',';
59+
}
60+
61+
if ($parameter instanceof Geometry) {
62+
if ($parameter instanceof Point && $parameter->isEmpty()) {
63+
// WKB does not support empty points, and MySQL does not support them either.
64+
throw new GeometryEngineException('MySQL does not support empty points');
65+
}
66+
$query[] = 'ST_GeomFromWKB(';
67+
$query[] = new BinaryValue($this->wkbWriter->write($parameter));
68+
$query[] = ',';
69+
$query[] = new ScalarValue($parameter->srid());
70+
$query[] = ')';
71+
} else {
72+
$query[] = new ScalarValue($parameter);
73+
}
74+
}
75+
76+
$query[] = ')';
77+
78+
if ($returnsGeometry) {
79+
$query[] = ' AS g) AS q';
80+
}
81+
82+
return $this->driver->executeQuery(...$query);
83+
}
84+
85+
/**
86+
* Queries a GIS function returning a boolean value.
87+
*
88+
* @param string $function The SQL GIS function to execute.
89+
* @param Geometry|scalar ...$parameters The Geometry objects or scalar values to pass as parameters.
90+
*
91+
* @throws GeometryEngineException
92+
*/
93+
#[Override]
94+
protected function queryBool(string $function, Geometry|string|float|int|bool ...$parameters) : bool
95+
{
96+
return $this->query($function, $parameters, false)->get(0)->asBool();
97+
}
98+
99+
/**
100+
* Queries a GIS function returning a floating point value.
101+
*
102+
* @param string $function The SQL GIS function to execute.
103+
* @param Geometry|scalar ...$parameters The Geometry objects or scalar values to pass as parameters.
104+
*
105+
* @throws GeometryEngineException
106+
*/
107+
#[Override]
108+
protected function queryFloat(string $function, Geometry|string|float|int|bool ...$parameters) : float
109+
{
110+
return $this->query($function, $parameters, false)->get(0)->asFloat();
111+
}
112+
113+
/**
114+
* Queries a GIS function returning a Geometry object.
115+
*
116+
* @param string $function The SQL GIS function to execute.
117+
* @param Geometry|scalar ...$parameters The Geometry objects or scalar values to pass as parameters.
118+
*
119+
* @throws GeometryEngineException
120+
*/
121+
#[Override]
122+
protected function queryGeometry(string $function, Geometry|string|float|int|bool ...$parameters) : Geometry
123+
{
124+
$row = $this->query($function, $parameters, true);
125+
126+
$wkb = $row->get(0)->asBinary();
127+
$srid = $row->get(1)->asInt();
128+
129+
return $this->wkbReader->read($wkb, $srid);
130+
}
131+
132+
// /**
133+
// * @return class-string<Proxy\ProxyInterface&Geometry>
134+
// *
135+
// * @throws GeometryEngineException
136+
// */
137+
// private function getProxyClassName(string $geometryType) : string
138+
// {
139+
// $proxyClasses = [
140+
// 'CIRCULARSTRING' => Proxy\CircularStringProxy::class,
141+
// 'COMPOUNDCURVE' => Proxy\CompoundCurveProxy::class,
142+
// 'CURVE' => Proxy\CurveProxy::class,
143+
// 'CURVEPOLYGON' => Proxy\CurvePolygonProxy::class,
144+
// 'GEOMCOLLECTION' => Proxy\GeometryCollectionProxy::class, /* MySQL 8 - https://github.com/brick/geo/pull/33 */
145+
// 'GEOMETRY' => Proxy\GeometryProxy::class,
146+
// 'GEOMETRYCOLLECTION' => Proxy\GeometryCollectionProxy::class,
147+
// 'LINESTRING' => Proxy\LineStringProxy::class,
148+
// 'MULTICURVE' => Proxy\MultiCurveProxy::class,
149+
// 'MULTILINESTRING' => Proxy\MultiLineStringProxy::class,
150+
// 'MULTIPOINT' => Proxy\MultiPointProxy::class,
151+
// 'MULTIPOLYGON' => Proxy\MultiPolygonProxy::class,
152+
// 'MULTISURFACE' => Proxy\MultiSurfaceProxy::class,
153+
// 'POINT' => Proxy\PointProxy::class,
154+
// 'POLYGON' => Proxy\PolygonProxy::class,
155+
// 'POLYHEDRALSURFACE' => Proxy\PolyhedralSurfaceProxy::class,
156+
// 'SURFACE' => Proxy\SurfaceProxy::class,
157+
// 'TIN' => Proxy\TinProxy::class,
158+
// 'TRIANGLE' => Proxy\TriangleProxy::class
159+
// ];
160+
//
161+
// $geometryType = strtoupper($geometryType);
162+
// $geometryType = preg_replace('/^ST_/', '', $geometryType);
163+
// assert($geometryType !== null);
164+
// $geometryType = preg_replace('/ .*/', '', $geometryType);
165+
// assert($geometryType !== null);
166+
//
167+
// if (! isset($proxyClasses[$geometryType])) {
168+
// throw new GeometryEngineException('Unknown geometry type: ' . $geometryType);
169+
// }
170+
//
171+
// return $proxyClasses[$geometryType];
172+
// }
173+
174+
#[Override]
175+
public function isRing(Curve $curve) : bool
176+
{
177+
// MySQL does not support ST_IsRing(), but we have an easy fallback.
178+
return $this->isClosed($curve) && $this->isSimple($curve);
179+
}
180+
}

‎src/Engine/PdoEngine.php

+87-111
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,88 @@
11
<?php
2-
3-
declare(strict_types=1);
4-
5-
namespace Brick\Geo\Engine;
6-
7-
use Brick\Geo\Exception\GeometryEngineException;
8-
use Brick\Geo\Geometry;
9-
use Override;
10-
use PDO;
11-
use PDOException;
12-
use PDOStatement;
13-
14-
/**
15-
* Database engine based on a PDO driver.
16-
*/
17-
final class PdoEngine extends DatabaseEngine
18-
{
19-
/**
20-
* The database connection.
21-
*/
22-
private readonly PDO $pdo;
23-
24-
/**
25-
* A cache of the prepared statements, indexed by query.
26-
*
27-
* @var array<string, PDOStatement>
28-
*/
29-
private array $statements = [];
30-
31-
public function __construct(PDO $pdo, bool $useProxy = true)
32-
{
33-
parent::__construct($useProxy);
34-
35-
$this->pdo = $pdo;
36-
}
37-
38-
public function getPDO() : PDO
39-
{
40-
return $this->pdo;
41-
}
42-
43-
#[Override]
44-
protected function executeQuery(string $query, array $parameters) : array
45-
{
46-
/** @var int $errMode */
47-
$errMode = $this->pdo->getAttribute(PDO::ATTR_ERRMODE);
48-
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
49-
50-
try {
51-
if (! isset($this->statements[$query])) {
52-
$this->statements[$query] = $this->pdo->prepare($query);
53-
}
54-
55-
$statement = $this->statements[$query];
56-
57-
$index = 1;
58-
59-
foreach ($parameters as $parameter) {
60-
if ($parameter instanceof GeometryParameter) {
61-
$statement->bindValue($index++, $parameter->data, $parameter->isBinary ? PDO::PARAM_LOB : PDO::PARAM_STR);
62-
$statement->bindValue($index++, $parameter->srid, PDO::PARAM_INT);
63-
} else {
64-
$type = match (true) {
65-
is_int($parameter) => PDO::PARAM_INT,
66-
is_bool($parameter) => PDO::PARAM_BOOL,
67-
default => PDO::PARAM_STR,
68-
};
69-
70-
$statement->bindValue($index++, $parameter, $type);
71-
}
72-
}
73-
74-
$statement->execute();
75-
76-
/** @var list<mixed>|false $result */
77-
$result = $statement->fetch(PDO::FETCH_NUM);
78-
} catch (PDOException $e) {
79-
throw GeometryEngineException::wrap($e);
80-
} finally {
81-
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, $errMode);
82-
}
83-
84-
assert($result !== false);
85-
86-
return $result;
87-
}
88-
89-
#[Override]
90-
protected function getGeomFromWkbSyntax(): string
91-
{
92-
if ($this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'mysql') {
93-
return 'ST_GeomFromWKB(BINARY ?, ?)';
94-
}
95-
96-
return parent::getGeomFromWkbSyntax();
97-
}
98-
99-
#[Override]
100-
protected function getParameterPlaceholder(string|float|int|bool $parameter): string
101-
{
102-
if ($this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'pgsql') {
103-
if (is_int($parameter)) {
104-
// https://stackoverflow.com/q/66625661/759866
105-
// https://externals.io/message/113521
106-
return 'CAST (? AS INTEGER)';
107-
}
108-
}
109-
110-
return parent::getParameterPlaceholder($parameter);
111-
}
112-
}
2+
//
3+
//declare(strict_types=1);
4+
//
5+
//namespace Brick\Geo\Engine;
6+
//
7+
//use Brick\Geo\Exception\GeometryEngineException;
8+
//use Brick\Geo\Geometry;
9+
//use Override;
10+
//use PDO;
11+
//use PDOException;
12+
//use PDOStatement;
13+
//
14+
///**
15+
// * Database engine based on a PDO driver.
16+
// */
17+
//final class PdoEngine extends DatabaseEngine
18+
//{
19+
// /**
20+
// * The database connection.
21+
// */
22+
// private readonly PDO $pdo;
23+
//
24+
// /**
25+
// * A cache of the prepared statements, indexed by query.
26+
// *
27+
// * @var array<string, PDOStatement>
28+
// */
29+
// private array $statements = [];
30+
//
31+
// public function __construct(PDO $pdo, bool $useProxy = true)
32+
// {
33+
// parent::__construct($useProxy);
34+
//
35+
// $this->pdo = $pdo;
36+
// }
37+
//
38+
// public function getPDO() : PDO
39+
// {
40+
// return $this->pdo;
41+
// }
42+
//
43+
// #[Override]
44+
// protected function executeQuery(string $query, array $parameters) : array
45+
// {
46+
// /** @var int $errMode */
47+
// $errMode = $this->pdo->getAttribute(PDO::ATTR_ERRMODE);
48+
// $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
49+
//
50+
// try {
51+
// if (! isset($this->statements[$query])) {
52+
// $this->statements[$query] = $this->pdo->prepare($query);
53+
// }
54+
//
55+
// $statement = $this->statements[$query];
56+
//
57+
// $index = 1;
58+
//
59+
// foreach ($parameters as $parameter) {
60+
// if ($parameter instanceof GeometryParameter) {
61+
// $statement->bindValue($index++, $parameter->data, $parameter->isBinary ? PDO::PARAM_LOB : PDO::PARAM_STR);
62+
// $statement->bindValue($index++, $parameter->srid, PDO::PARAM_INT);
63+
// } else {
64+
// $type = match (true) {
65+
// is_int($parameter) => PDO::PARAM_INT,
66+
// is_bool($parameter) => PDO::PARAM_BOOL,
67+
// default => PDO::PARAM_STR,
68+
// };
69+
//
70+
// $statement->bindValue($index++, $parameter, $type);
71+
// }
72+
// }
73+
//
74+
// $statement->execute();
75+
//
76+
// /** @var list<mixed>|false $result */
77+
// $result = $statement->fetch(PDO::FETCH_NUM);
78+
// } catch (PDOException $e) {
79+
// throw GeometryEngineException::wrap($e);
80+
// } finally {
81+
// $this->pdo->setAttribute(PDO::ATTR_ERRMODE, $errMode);
82+
// }
83+
//
84+
// assert($result !== false);
85+
//
86+
// return $result;
87+
// }
88+
//}

‎src/Engine/PostgisEngine.php

+418
Large diffs are not rendered by default.

‎src/Engine/SpatialiteEngine.php

+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Brick\Geo\Engine;
6+
7+
use Brick\Geo\Engine\Database\DatabaseDriver;
8+
use Brick\Geo\Engine\Database\Query\BinaryValue;
9+
use Brick\Geo\Engine\Database\Query\ScalarValue;
10+
use Brick\Geo\Engine\Database\Result\Row;
11+
use Brick\Geo\Engine\Internal\TypeChecker;
12+
use Brick\Geo\Exception\GeometryEngineException;
13+
use Brick\Geo\Geometry;
14+
use Brick\Geo\Io\WkbReader;
15+
use Brick\Geo\Io\WkbWriter;
16+
use Brick\Geo\LineString;
17+
use Brick\Geo\Point;
18+
use Override;
19+
20+
/**
21+
* Database engine based on a SQLite3 driver.
22+
*
23+
* The spatialite extension must be loaded in this driver.
24+
*/
25+
final readonly class SpatialiteEngine extends DatabaseEngine
26+
{
27+
private WkbReader $wkbReader;
28+
private WkbWriter $wkbWriter;
29+
30+
public function __construct(
31+
// TODO private
32+
public DatabaseDriver $driver,
33+
// TODO
34+
private bool $useProxy = true,
35+
) {
36+
$this->wkbReader = new WkbReader();
37+
$this->wkbWriter = new WkbWriter();
38+
}
39+
40+
/**
41+
* Builds and executes a SQL query for a GIS function.
42+
*
43+
* @param string $function The SQL GIS function to execute.
44+
* @param list<Geometry|scalar> $parameters The Geometry objects or scalar values to pass as parameters.
45+
* @param bool $returnsGeometry Whether the GIS function returns a Geometry.
46+
*
47+
* @throws GeometryEngineException
48+
*/
49+
private function query(string $function, array $parameters, bool $returnsGeometry) : Row
50+
{
51+
$query = ['SELECT '];
52+
53+
if ($returnsGeometry) {
54+
$query[] = 'ST_AsBinary(g), ST_SRID(g) FROM (SELECT ';
55+
}
56+
57+
$query[] = $function . '(';
58+
59+
foreach ($parameters as $key => $parameter) {
60+
if ($key !== 0) {
61+
$query[] = ',';
62+
}
63+
64+
if ($parameter instanceof Geometry) {
65+
if ($parameter instanceof Point && $parameter->isEmpty()) {
66+
// WKB does not support empty points, and SpatiaLite does not support them either.
67+
throw new GeometryEngineException('MySQL does not support empty points');
68+
}
69+
$query[] = 'ST_GeomFromWKB(';
70+
$query[] = new BinaryValue($this->wkbWriter->write($parameter));
71+
$query[] = ',';
72+
$query[] = new ScalarValue($parameter->srid());
73+
$query[] = ')';
74+
} else {
75+
$query[] = new ScalarValue($parameter);
76+
}
77+
}
78+
79+
$query[] = ')';
80+
81+
if ($returnsGeometry) {
82+
$query[] = ' AS g) AS q';
83+
}
84+
85+
return $this->driver->executeQuery(...$query);
86+
}
87+
88+
/**
89+
* Queries a GIS function returning a boolean value.
90+
*
91+
* @param string $function The SQL GIS function to execute.
92+
* @param Geometry|scalar ...$parameters The Geometry objects or scalar values to pass as parameters.
93+
*
94+
* @throws GeometryEngineException
95+
*/
96+
#[Override]
97+
protected function queryBool(string $function, Geometry|string|float|int|bool ...$parameters) : bool
98+
{
99+
return $this->query($function, $parameters, false)->get(0)->asBool();
100+
}
101+
102+
/**
103+
* Queries a GIS function returning a floating point value.
104+
*
105+
* @param string $function The SQL GIS function to execute.
106+
* @param Geometry|scalar ...$parameters The Geometry objects or scalar values to pass as parameters.
107+
*
108+
* @throws GeometryEngineException
109+
*/
110+
#[Override]
111+
protected function queryFloat(string $function, Geometry|string|float|int|bool ...$parameters) : float
112+
{
113+
return $this->query($function, $parameters, false)->get(0)->asFloat();
114+
}
115+
116+
/**
117+
* Queries a GIS function returning a Geometry object.
118+
*
119+
* @param string $function The SQL GIS function to execute.
120+
* @param Geometry|scalar ...$parameters The Geometry objects or scalar values to pass as parameters.
121+
*
122+
* @throws GeometryEngineException
123+
*/
124+
#[Override]
125+
protected function queryGeometry(string $function, Geometry|string|float|int|bool ...$parameters) : Geometry
126+
{
127+
$row = $this->query($function, $parameters, true);
128+
129+
$wkb = $row->get(0)->asBinary();
130+
$srid = $row->get(1)->asInt();
131+
132+
return $this->wkbReader->read($wkb, $srid);
133+
}
134+
135+
#[Override]
136+
public function lineInterpolatePoint(LineString $lineString, float $fraction) : Point
137+
{
138+
$result = $this->queryGeometry('ST_Line_Interpolate_Point', $lineString, $fraction);
139+
TypeChecker::check($result, Point::class);
140+
141+
return $result;
142+
}
143+
}

‎src/Engine/Sqlite3Engine.php

-102
This file was deleted.

‎src/Exception/GeometryEngineException.php

+9
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@ public static function operationYieldedNoResult() : GeometryEngineException
3030
return new self('This operation yielded no result on the target database.');
3131
}
3232

33+
public static function unexpectedDatabaseReturnType(string $expectedType, mixed $actualValue) : GeometryEngineException
34+
{
35+
return new self(sprintf(
36+
'The database returned an unexpected type: expected %s, got %s.',
37+
$expectedType,
38+
get_debug_type($actualValue),
39+
));
40+
}
41+
3342
/**
3443
* @param class-string<Geometry> $expectedClassName
3544
* @param class-string<Geometry> $actualClassName

‎tests/GeometryEngineTest.php

+51-42
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@
1111
use Brick\Geo\Engine\GeometryEngine;
1212
use Brick\Geo\Engine\GeosEngine;
1313
use Brick\Geo\Engine\GeosOpEngine;
14+
use Brick\Geo\Engine\MariadbEngine;
15+
use Brick\Geo\Engine\MysqlEngine;
1416
use Brick\Geo\Engine\PdoEngine;
15-
use Brick\Geo\Engine\Sqlite3Engine;
17+
use Brick\Geo\Engine\PostgisEngine;
18+
use Brick\Geo\Engine\SpatialiteEngine;
1619
use Brick\Geo\Exception\GeometryEngineException;
1720
use Brick\Geo\Geometry;
1821
use Brick\Geo\GeometryCollection;
@@ -1416,6 +1419,7 @@ private function failsOnMysql(?string $operatorAndVersion = null) : void
14161419
{
14171420
if ($this->isMysql($operatorAndVersion)) {
14181421
$this->expectException(GeometryEngineException::class);
1422+
// $this->expectExceptionMessage('is currently not implemented');
14191423
}
14201424
}
14211425

@@ -1449,29 +1453,69 @@ private function failsOnSpatialite(?string $operatorAndVersion = null) : void
14491453

14501454
private function isMysql(?string $operatorAndVersion = null) : bool
14511455
{
1452-
return $this->isMysqlOrMariadb(false, $operatorAndVersion);
1456+
$engine = $this->getGeometryEngine();
1457+
1458+
if ($engine instanceof MysqlEngine) {
1459+
// TODO remove...
1460+
$version = $engine->driver->executeQuery('SELECT VERSION()')->get(0)->asString();
1461+
1462+
$pos = strpos($version, '-MariaDB');
1463+
$isMariadb = ($pos !== false);
1464+
1465+
if ($isMariadb) {
1466+
return false;
1467+
}
1468+
// TODO up to here
1469+
1470+
if ($operatorAndVersion === null) {
1471+
return true;
1472+
}
1473+
1474+
$version = $engine->driver->executeQuery('SELECT VERSION()')->get(0)->asString();
1475+
1476+
return $this->isVersion($version, $operatorAndVersion);
1477+
}
1478+
1479+
return false;
14531480
}
14541481

14551482
private function isMariadb(?string $operatorAndVersion = null) : bool
14561483
{
1457-
return $this->isMysqlOrMariadb(true, $operatorAndVersion);
1484+
$engine = $this->getGeometryEngine();
1485+
1486+
if ($engine instanceof MariadbEngine) {
1487+
if ($operatorAndVersion === null) {
1488+
return true;
1489+
}
1490+
1491+
$version = $engine->driver->executeQuery('SELECT VERSION()')->get(0)->asString();
1492+
$pos = strpos($version, '-MariaDB');
1493+
1494+
if ($pos !== false) {
1495+
$version = substr($version, 0, $pos);
1496+
}
1497+
1498+
return $this->isVersion($version, $operatorAndVersion);
1499+
}
1500+
1501+
return false;
14581502
}
14591503

14601504
private function isPostgis() : bool
14611505
{
1462-
return $this->isPdoDriver('pgsql');
1506+
return $this->getGeometryEngine() instanceof PostgisEngine;
14631507
}
14641508

14651509
private function isSpatialite(?string $operatorAndVersion = null) : bool
14661510
{
14671511
$engine = $this->getGeometryEngine();
14681512

1469-
if ($engine instanceof Sqlite3Engine) {
1513+
if ($engine instanceof SpatialiteEngine) {
14701514
if ($operatorAndVersion === null) {
14711515
return true;
14721516
}
14731517

1474-
$version = $engine->getSQLite3()->querySingle('SELECT spatialite_version()');
1518+
$version = $engine->driver->executeQuery('SELECT spatialite_version()')->get(0)->asString();
14751519

14761520
return $this->isVersion($version, $operatorAndVersion);
14771521
}
@@ -1580,6 +1624,7 @@ private function skipIfUnsupportedGeometry(Geometry $geometry) : void
15801624
$this->failsOnMariadb();
15811625

15821626
if ($this->isSpatialite()) {
1627+
// TODO check if throws exceptions
15831628
self::markTestSkipped('SpatiaLite does not correctly handle empty geometries.');
15841629
}
15851630
}
@@ -1670,42 +1715,6 @@ private function isPdoDriver(string $name) : bool
16701715
}
16711716
}
16721717

1673-
return false;
1674-
}
1675-
/**
1676-
* @param bool $testMariadb False to check for MYSQL, true to check for MariaDB.
1677-
* @param string|null $operatorAndVersion An optional comparison operator and version number to test against.
1678-
*/
1679-
private function isMysqlOrMariadb(bool $testMariadb, ?string $operatorAndVersion = null) : bool
1680-
{
1681-
$engine = $this->getGeometryEngine();
1682-
1683-
if ($engine instanceof PdoEngine) {
1684-
$pdo = $engine->getPDO();
1685-
1686-
if ($pdo->getAttribute(\PDO::ATTR_DRIVER_NAME) === 'mysql') {
1687-
$statement = $pdo->query('SELECT VERSION()');
1688-
$version = $statement->fetchColumn();
1689-
1690-
$pos = strpos($version, '-MariaDB');
1691-
$isMariadb = ($pos !== false);
1692-
1693-
if ($isMariadb) {
1694-
$version = substr($version, 0, $pos);
1695-
}
1696-
1697-
if ($testMariadb !== $isMariadb) {
1698-
return false;
1699-
}
1700-
1701-
if ($operatorAndVersion === null) {
1702-
return true;
1703-
}
1704-
1705-
return $this->isVersion($version, $operatorAndVersion);
1706-
}
1707-
}
1708-
17091718
return false;
17101719
}
17111720
}

0 commit comments

Comments
 (0)
Please sign in to comment.