Skip to content
This repository was archived by the owner on Jan 24, 2024. It is now read-only.

Commit 0cfba1b

Browse files
authoredSep 8, 2021
Add readonly connections (#80)
1 parent 167eb3b commit 0cfba1b

15 files changed

+447
-19
lines changed
 

‎.github/workflows/main.yml

-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ jobs:
6666
file: ./coverage.xml
6767

6868
sqlite:
69-
needs: lint
7069
name: SQLite PHP ${{ matrix.php-versions }}
7170
runs-on: ubuntu-latest
7271
strategy:

‎.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ build/
99
*.db
1010
clover.xml
1111
clover.json
12-
.php_cs.cache
12+
.php_cs.cache
13+
.phpunit.result.cache

‎phpunit.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,4 @@
2222
<directory>src/</directory>
2323
</whitelist>
2424
</filter>
25-
</phpunit>
25+
</phpunit>

‎src/Driver/Driver.php

+18-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Psr\Log\LoggerAwareInterface;
2020
use Psr\Log\LoggerAwareTrait;
2121
use Spiral\Database\Exception\DriverException;
22+
use Spiral\Database\Exception\ReadonlyConnectionException;
2223
use Spiral\Database\Exception\StatementException;
2324
use Spiral\Database\Injection\ParameterInterface;
2425
use Spiral\Database\Query\BuilderInterface;
@@ -69,7 +70,10 @@ abstract class Driver implements DriverInterface, LoggerAwareInterface
6970
'queryCache' => true,
7071

7172
// disable schema modifications
72-
'readonlySchema' => false
73+
'readonlySchema' => false,
74+
75+
// disable write expressions
76+
'readonly' => false,
7377
];
7478

7579
/** @var PDO|null */
@@ -125,6 +129,14 @@ public function __construct(
125129
}
126130
}
127131

132+
/**
133+
* {@inheritDoc}
134+
*/
135+
public function isReadonly(): bool
136+
{
137+
return (bool)($this->options['readonly'] ?? false);
138+
}
139+
128140
/**
129141
* Disconnect and destruct.
130142
*/
@@ -316,9 +328,14 @@ public function query(string $statement, array $parameters = []): StatementInter
316328
* @return int
317329
*
318330
* @throws StatementException
331+
* @throws ReadonlyConnectionException
319332
*/
320333
public function execute(string $query, array $parameters = []): int
321334
{
335+
if ($this->isReadonly()) {
336+
throw ReadonlyConnectionException::onWriteStatementExecution();
337+
}
338+
322339
return $this->statement($query, $parameters)->rowCount();
323340
}
324341

‎src/Driver/DriverInterface.php

+10
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use DateTimeZone;
1515
use PDO;
1616
use Spiral\Database\Exception\DriverException;
17+
use Spiral\Database\Exception\ReadonlyConnectionException;
1718
use Spiral\Database\Exception\StatementException;
1819
use Spiral\Database\Query\BuilderInterface;
1920
use Spiral\Database\StatementInterface;
@@ -86,6 +87,14 @@ interface DriverInterface
8687
*/
8788
public const ISOLATION_READ_UNCOMMITTED = 'READ UNCOMMITTED';
8889

90+
/**
91+
* Returns {@see true} in the case that the connection is available only
92+
* for reading or {@see false} instead.
93+
*
94+
* @return bool
95+
*/
96+
public function isReadonly(): bool;
97+
8998
/**
9099
* Driver type (name).
91100
*
@@ -169,6 +178,7 @@ public function query(string $statement, array $parameters = []): StatementInter
169178
* @return int
170179
*
171180
* @throws StatementException
181+
* @throws ReadonlyConnectionException
172182
*/
173183
public function execute(string $query, array $parameters = []): int;
174184

‎src/Driver/Postgres/Query/PostgresInsertQuery.php

+5
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Spiral\Database\Driver\DriverInterface;
1515
use Spiral\Database\Driver\Postgres\PostgresDriver;
1616
use Spiral\Database\Exception\BuilderException;
17+
use Spiral\Database\Exception\ReadonlyConnectionException;
1718
use Spiral\Database\Query\InsertQuery;
1819
use Spiral\Database\Query\QueryInterface;
1920
use Spiral\Database\Query\QueryParameters;
@@ -67,6 +68,10 @@ public function run()
6768
$params = new QueryParameters();
6869
$queryString = $this->sqlStatement($params);
6970

71+
if ($this->driver->isReadonly()) {
72+
throw ReadonlyConnectionException::onWriteStatementExecution();
73+
}
74+
7075
$result = $this->driver->query($queryString, $params->getParameters());
7176

7277
try {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
/**
4+
* This file is part of database package.
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
declare(strict_types=1);
11+
12+
namespace Spiral\Database\Exception;
13+
14+
class ReadonlyConnectionException extends DBALException
15+
{
16+
private const WRITE_STMT_MESSAGE = 'Can not execute non-query statement on readonly connection.';
17+
18+
/**
19+
* @param int $code
20+
* @param \Throwable|null $prev
21+
* @return static
22+
*/
23+
public static function onWriteStatementExecution(int $code = 0, \Throwable $prev = null): self
24+
{
25+
return new self(self::WRITE_STMT_MESSAGE, $code, $prev);
26+
}
27+
}

‎src/Query/InsertQuery.php

-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
namespace Spiral\Database\Query;
1313

1414
use Spiral\Database\Driver\CompilerInterface;
15-
use Spiral\Database\Exception\BuilderException;
1615
use Spiral\Database\Injection\Parameter;
1716

1817
/**

‎tests/Database/BaseTest.php

+13-13
Original file line numberDiff line numberDiff line change
@@ -51,23 +51,23 @@ public function setUp(): void
5151
}
5252

5353
/**
54+
* @param array $options
5455
* @return Driver
5556
*/
56-
public function getDriver(): Driver
57+
public function getDriver(array $options = []): Driver
5758
{
5859
$config = self::$config[static::DRIVER];
60+
5961
if (!isset($this->driver)) {
6062
$class = $config['driver'];
6163

62-
$this->driver = new $class(
63-
[
64-
'connection' => $config['conn'],
65-
'username' => $config['user'],
66-
'password' => $config['pass'],
67-
'options' => [],
68-
'queryCache' => true
69-
]
70-
);
64+
$this->driver = new $class(\array_merge($options, [
65+
'connection' => $config['conn'],
66+
'username' => $config['user'],
67+
'password' => $config['pass'],
68+
'options' => [],
69+
'queryCache' => true
70+
]));
7171
}
7272

7373
static::$logger = static::$logger ?? new TestLogger();
@@ -83,15 +83,15 @@ public function getDriver(): Driver
8383
/**
8484
* @param string $name
8585
* @param string $prefix
86-
*
86+
* @param array $config
8787
* @return Database|null When non empty null will be given, for safety, for science.
8888
*/
89-
protected function db(string $name = 'default', string $prefix = '')
89+
protected function db(string $name = 'default', string $prefix = '', array $config = []): ?Database
9090
{
9191
if (isset(static::$driverCache[static::DRIVER])) {
9292
$driver = static::$driverCache[static::DRIVER];
9393
} else {
94-
static::$driverCache[static::DRIVER] = $driver = $this->getDriver();
94+
static::$driverCache[static::DRIVER] = $driver = $this->getDriver($config);
9595
}
9696

9797
return new Database($name, $prefix, $driver);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/**
4+
* Spiral Framework.
5+
*
6+
* @license MIT
7+
* @author Anton Titov (Wolfy-J)
8+
*/
9+
10+
declare(strict_types=1);
11+
12+
namespace Spiral\Database\Tests\Driver\MySQL;
13+
14+
/**
15+
* @group driver
16+
* @group driver-mysql
17+
*/
18+
class ReadonlyTest extends \Spiral\Database\Tests\ReadonlyTest
19+
{
20+
public const DRIVER = 'mysql';
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/**
4+
* Spiral Framework.
5+
*
6+
* @license MIT
7+
* @author Anton Titov (Wolfy-J)
8+
*/
9+
10+
declare(strict_types=1);
11+
12+
namespace Spiral\Database\Tests\Driver\Postgres;
13+
14+
/**
15+
* @group driver
16+
* @group driver-postgres
17+
*/
18+
class ReadonlyTest extends \Spiral\Database\Tests\ReadonlyTest
19+
{
20+
public const DRIVER = 'postgres';
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/**
4+
* Spiral Framework.
5+
*
6+
* @license MIT
7+
* @author Anton Titov (Wolfy-J)
8+
*/
9+
10+
declare(strict_types=1);
11+
12+
namespace Spiral\Database\Tests\Driver\SQLServer;
13+
14+
/**
15+
* @group driver
16+
* @group driver-sqlserver
17+
*/
18+
class ReadonlyTest extends \Spiral\Database\Tests\ReadonlyTest
19+
{
20+
public const DRIVER = 'sqlserver';
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/**
4+
* Spiral Framework.
5+
*
6+
* @license MIT
7+
* @author Anton Titov (Wolfy-J)
8+
*/
9+
10+
declare(strict_types=1);
11+
12+
namespace Spiral\Database\Tests\Driver\SQLite;
13+
14+
/**
15+
* @group driver
16+
* @group driver-sqlite
17+
*/
18+
class ReadonlyTest extends \Spiral\Database\Tests\ReadonlyTest
19+
{
20+
public const DRIVER = 'sqlite';
21+
}

‎tests/Database/ReadonlyTest.php

+286
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Spiral\Database\Tests;
6+
7+
use Spiral\Database\Database;
8+
use Spiral\Database\Driver\Driver;
9+
use Spiral\Database\Exception\ReadonlyConnectionException;
10+
use Spiral\Database\Table;
11+
12+
abstract class ReadonlyTest extends BaseTest
13+
{
14+
/**
15+
* @var string
16+
*/
17+
protected $table = 'readonly_tests';
18+
19+
public function setUp(): void
20+
{
21+
$this->database = new Database('default', '', $this->getDriver(['readonly' => true]));
22+
23+
$this->allowWrite(function () {
24+
$table = $this->database->table($this->table);
25+
$schema = $table->getSchema();
26+
$schema->primary('id');
27+
$schema->string('value')->nullable();
28+
$schema->save();
29+
});
30+
}
31+
32+
private function allowWrite(\Closure $then): void
33+
{
34+
/** @var Driver $driver */
35+
$driver = $this->database->getDriver();
36+
37+
(function (\Closure $then): void {
38+
$this->options['readonly'] = false;
39+
try {
40+
$then();
41+
} finally {
42+
$this->options['readonly'] = true;
43+
}
44+
})->call($driver, $then);
45+
}
46+
47+
public function tearDown(): void
48+
{
49+
$this->allowWrite(function () {
50+
$schema = $this->database->table($this->table)
51+
->getSchema();
52+
53+
$schema->declareDropped();
54+
$schema->save();
55+
});
56+
}
57+
58+
protected function table(): Table
59+
{
60+
return $this->database->table($this->table);
61+
}
62+
63+
public function testTableAllowSelection(): void
64+
{
65+
$this->expectNotToPerformAssertions();
66+
67+
$this->table()
68+
->select()
69+
->run()
70+
;
71+
}
72+
73+
public function testTableAllowCount(): void
74+
{
75+
$this->expectNotToPerformAssertions();
76+
77+
$this->table()
78+
->count()
79+
;
80+
}
81+
82+
public function testTableAllowExists(): void
83+
{
84+
$this->expectNotToPerformAssertions();
85+
86+
$this->table()
87+
->exists()
88+
;
89+
}
90+
91+
public function testTableAllowGetPrimaryKeys(): void
92+
{
93+
$this->expectNotToPerformAssertions();
94+
95+
$this->table()
96+
->getPrimaryKeys()
97+
;
98+
}
99+
100+
public function testTableAllowHasColumn(): void
101+
{
102+
$this->expectNotToPerformAssertions();
103+
104+
$this->table()
105+
->hasColumn('column')
106+
;
107+
}
108+
109+
public function testTableAllowGetColumns(): void
110+
{
111+
$this->expectNotToPerformAssertions();
112+
113+
$this->table()
114+
->getColumns()
115+
;
116+
}
117+
118+
public function testTableAllowHasIndex(): void
119+
{
120+
$this->expectNotToPerformAssertions();
121+
122+
$this->table()
123+
->hasIndex(['column'])
124+
;
125+
}
126+
127+
public function testTableAllowGetIndexes(): void
128+
{
129+
$this->expectNotToPerformAssertions();
130+
131+
$this->table()
132+
->getIndexes()
133+
;
134+
}
135+
136+
public function testTableAllowHasForeignKey(): void
137+
{
138+
$this->expectNotToPerformAssertions();
139+
140+
$this->table()
141+
->hasForeignKey(['column'])
142+
;
143+
}
144+
145+
public function testTableAllowGetForeignKeys(): void
146+
{
147+
$this->expectNotToPerformAssertions();
148+
149+
$this->table()
150+
->getForeignKeys()
151+
;
152+
}
153+
154+
public function testTableAllowGetDependencies(): void
155+
{
156+
$this->expectNotToPerformAssertions();
157+
158+
$this->table()
159+
->getDependencies()
160+
;
161+
}
162+
163+
public function testTableRejectInsertOne(): void
164+
{
165+
$this->expectException(ReadonlyConnectionException::class);
166+
167+
$this->table()
168+
->insertOne(['value' => 'example'])
169+
;
170+
}
171+
172+
public function testTableRejectInsertMultiple(): void
173+
{
174+
$this->expectException(ReadonlyConnectionException::class);
175+
176+
$this->table()
177+
->insertMultiple(['value'], ['example'])
178+
;
179+
}
180+
181+
public function testTableRejectInsert(): void
182+
{
183+
$this->expectException(ReadonlyConnectionException::class);
184+
185+
$this->table()
186+
->insert()
187+
->columns('value')
188+
->values('example')
189+
->run();
190+
}
191+
192+
public function testTableRejectUpdate(): void
193+
{
194+
$this->expectException(ReadonlyConnectionException::class);
195+
196+
$this->table()
197+
->update(['value' => 'updated'])
198+
->run()
199+
;
200+
}
201+
202+
public function testTableRejectDelete(): void
203+
{
204+
$this->expectException(ReadonlyConnectionException::class);
205+
206+
$this->table()
207+
->delete()
208+
->run()
209+
;
210+
}
211+
212+
public function testTableRejectEraseData(): void
213+
{
214+
$this->expectException(ReadonlyConnectionException::class);
215+
216+
$this->table()
217+
->eraseData()
218+
;
219+
}
220+
221+
public function testSchemaRejectSaving(): void
222+
{
223+
$this->expectException(ReadonlyConnectionException::class);
224+
225+
$table = $this->database
226+
->table('not_allowed_to_creation');
227+
228+
$schema = $table->getSchema();
229+
$schema->primary('id');
230+
$schema->string('value')->nullable();
231+
$schema->save();
232+
}
233+
234+
public function testDatabaseAllowSelection(): void
235+
{
236+
$this->expectNotToPerformAssertions();
237+
238+
$this->database->select()
239+
->from($this->table)
240+
->run()
241+
;
242+
}
243+
244+
public function testDatabaseRejectUpdate(): void
245+
{
246+
$this->expectException(ReadonlyConnectionException::class);
247+
248+
$this->database->update($this->table, ['value' => 'example'])
249+
->run()
250+
;
251+
}
252+
253+
public function testDatabaseRejectInsert(): void
254+
{
255+
$this->expectException(ReadonlyConnectionException::class);
256+
257+
$this->database->insert($this->table)
258+
->columns('value')
259+
->values('example')
260+
->run()
261+
;
262+
}
263+
264+
public function testDatabaseRejectDelete(): void
265+
{
266+
$this->expectException(ReadonlyConnectionException::class);
267+
268+
$this->database->delete($this->table)
269+
->run()
270+
;
271+
}
272+
273+
public function testDatabaseAllowRawQuery(): void
274+
{
275+
$this->expectNotToPerformAssertions();
276+
277+
$this->database->query('SELECT 1');
278+
}
279+
280+
public function testDatabaseRejectRawExecution(): void
281+
{
282+
$this->expectException(ReadonlyConnectionException::class);
283+
284+
$this->database->execute("DROP TABLE {$this->table}");
285+
}
286+
}

‎tests/bootstrap.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353

5454
$db = getenv('DB') ?: null;
5555
Database\Tests\BaseTest::$config = [
56-
'debug' => false,
56+
'debug' => getenv('DB_DEBUG') ?: false,
5757
] + ($db === null
5858
? $drivers
5959
: array_intersect_key($drivers, array_flip((array)$db))

0 commit comments

Comments
 (0)
This repository has been archived.