Skip to content

Commit 5955f88

Browse files
author
Gaetano Giunta
committed
run sql commands in a temp schema by default
1 parent 673542a commit 5955f88

File tree

7 files changed

+218
-21
lines changed

7 files changed

+218
-21
lines changed

TODO.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
- worker: improve sql execution cmd:
22
+ allow it to pick a set of desired servers
3+
+ allow it to pick an existing db schema & user
34
+ disallow execution of commands that are part of the db client instead of being sent to the server, such as eg. 'use db'
4-
- ok for mysql? (to be tested), missing for psql
5-
+ examine in detail the differences between running a command vs a file (eg. transaction usage)
5+
- ok for mysql? (to be tested)
6+
- missing for psql? (according to slack discussion: this is impossible using psql and can only be done using a different
7+
driver... it might be in fact already happening when NOT using input via files...)
8+
+ examine in detail and document the differences between running a command vs a file (eg. transaction usage)
9+
10+
- improve cli scripts:
11+
+ add scripts to create and drop a schema on all servers
612

713
- worker: improve profile of 'user' account (esp: add APP_ENV and APP_DEBUG env vars)
814

@@ -15,9 +21,9 @@
1521

1622
- web: allow to insert sql snippet, pick the desired servers, run it and show results
1723

18-
- web/worker: allow auto db-schema create+teardown (w. user-defined charset)
24+
- web/worker: allow user-defined charset for auto db-schema create
1925

20-
- web/worker: allow custom db init scripts (load data and set session vars)
26+
- web/worker: allow custom db init scripts (to load data and set session vars)
2127

2228
- pick up a library which allows to load db-agnostic schema defs and data
2329

@@ -35,7 +41,7 @@
3541
https://www.percona.com/blog/2011/02/01/sample-datasets-for-benchmarking-and-testing/
3642
https://docs.microsoft.com/en-us/azure/sql-database/sql-database-public-data-sets
3743

38-
- web gui: store previous snippets in a dedicated db, list them
44+
- web gui: store previous snippets in a dedicated db, list them (private to each user session)
3945

4046
- mariadb/mysql: allow to define in docker parameters the size of the ramdisk used for /tmpfs;
4147
also in default configs, do use /tmpfs for temp tables? At least add it commented out
@@ -56,7 +62,7 @@
5662
- rate-limit http requests
5763
- size-limit http requests
5864
- add caching in nginx of static assets
59-
- add firewall rules to the web containers to block access to outside world at bootstrap
65+
- add firewall rules to the all containers to block access to outside world at bootstrap
6066
- make php code non-writeable by www-data user
6167
- harden php configuration
6268
- move execution of sql snippets to a queue, to avoid dos/overload

WHATSNEW.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ Version 0.3 (unreleased)
77
- measure time and memory taken for each db
88
- allow to print output in json/yaml/php format
99
- allow to execute sql commands stored in a file besides specifying them as cli option
10+
- all sql commands are now executed in a temporary db schema, by a corresponding temp. db user, instead of being
11+
executed with the database 'root' account
1012

1113

1214
Version 0.2

app/src/Command/SqlExecute.php

Lines changed: 107 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Symfony\Component\Yaml\Yaml;
99
use Db3v4l\API\Interfaces\TimedExecutor;
1010
use Db3v4l\Service\DatabaseConfigurationManager;
11+
use Db3v4l\Service\DatabaseSchemaManager;
1112
use Db3v4l\Service\SqlExecutorFactory;
1213
use Db3v4l\Service\ProcessManager;
1314
use Db3v4l\Util\Process;
@@ -37,7 +38,7 @@ public function __construct(
3738
protected function configure()
3839
{
3940
$this
40-
->setDescription('Executes an SQL command in parallel on all configured database servers')
41+
->setDescription('Executes an SQL command in parallel on all configured database servers, creating a dedicated database schema (and user)')
4142
->addOption('sql', null, InputOption::VALUE_REQUIRED, 'The sql command(s) string to execute')
4243
->addOption('file', null, InputOption::VALUE_REQUIRED, 'A file with sql commands to execute')
4344
->addOption('output-type', null, InputOption::VALUE_REQUIRED, 'The format for the output: json, php, text or yml', 'text')
@@ -83,19 +84,23 @@ protected function execute(InputInterface $input, OutputInterface $output)
8384
// We thus force it, but give end users an option to disable this
8485
// For more details, see comment 12 at https://bugs.launchpad.net/ubuntu/+source/php5/+bug/516061
8586
if (!$dontForceSigchildEnabled) {
86-
8787
Process::forceSigchildEnabled(true);
8888
}
8989

90+
if ($format === 'text') {
91+
$this->writeln('<info>Creating temporary schemas...</info>', OutputInterface::VERBOSITY_VERBOSE);
92+
}
93+
94+
$dbConnectionSpecs = $this->createSchemas($dbList, $maxParallel);
95+
9096
if ($format === 'text') {
9197
$this->writeln('<info>Preparing commands...</info>', OutputInterface::VERBOSITY_VERBOSE);
9298
}
9399

94100
/** @var Process[] $processes */
95101
$processes = [];
96102
$executors = [];
97-
foreach ($dbList as $dbName) {
98-
$dbConnectionSpec = $this->dbManager->getDatabaseConnectionSpecification($dbName);
103+
foreach ($dbConnectionSpecs as $dbName => $dbConnectionSpec) {
99104

100105
$executor = $this->executorFactory->createForkedExecutor($dbConnectionSpec);
101106

@@ -144,11 +149,109 @@ protected function execute(InputInterface $input, OutputInterface $output)
144149
}
145150
}
146151

152+
if ($format === 'text') {
153+
$this->writeln('<info>Dropping temporary schemas...</info>', OutputInterface::VERBOSITY_VERBOSE);
154+
}
155+
$this->dropSchemas($dbConnectionSpecs, $maxParallel);
156+
147157
$time = microtime(true) - $start;
148158

149159
$this->writeResults($results, $succeeded, $failed, $time, $format);
150160
}
151161

162+
/**
163+
* @param string[] $dbList
164+
* @param int $maxParallel
165+
* @return array same format as dbManager::getDatabaseConnectionSpecification
166+
*/
167+
protected function createSchemas($dbList, $maxParallel)
168+
{
169+
$processes = [];
170+
$connectionSpecs = [];
171+
$tempSQLFileNames = [];
172+
173+
/// @todo inject more randomness in the username, by allowing more chars than bin2hex produces
174+
$userName = bin2hex(random_bytes(8)); // old mysql versions have a limitation of 16 chars for usernames
175+
$password = bin2hex(random_bytes(16));
176+
//$schemaName = bin2hex(random_bytes(31));
177+
$schemaName = null; // $userName will be used as schema name
178+
179+
foreach ($dbList as $dbName) {
180+
$dbConnectionSpec = $this->dbManager->getDatabaseConnectionSpecification($dbName);
181+
182+
$schemaManager = new DatabaseSchemaManager($dbConnectionSpec);
183+
$sql = $schemaManager->getCreateSchemaSQL($userName, $password, $schemaName);
184+
// sadly, psql does not allow to create a db and a user using a multiple-sql-commands string,
185+
// and we have to resort to using temp files
186+
/// @todo can we make this safer? Ideally the new userv name and pwd should neither hit disk nor the process list...
187+
$tempSQLFileName = tempnam(sys_get_temp_dir(), 'db3val_');
188+
file_put_contents($tempSQLFileName, $sql);
189+
$tempSQLFileNames[] = $tempSQLFileName;
190+
191+
$executor = $this->executorFactory->createForkedExecutor($dbConnectionSpec, 'NativeClient', false);
192+
$process = $executor->getExecuteFileProcess($tempSQLFileName);
193+
$processes[$dbName] = $process;
194+
$connectionSpecs[$dbName] = $dbConnectionSpec;
195+
}
196+
197+
$this->processManager->runParallel($processes, $maxParallel, 100);
198+
199+
$results = array();
200+
foreach ($processes as $dbName => $process) {
201+
if ($process->isSuccessful()) {
202+
$results[$dbName] = array_merge($connectionSpecs[$dbName], array(
203+
'user' => $userName,
204+
'password' => $password,
205+
'dbname' => $userName
206+
));
207+
} else {
208+
$this->writeErrorln("\n<error>Creation of new schema & user on database '$dbName' failed! Reason: " . $process->getErrorOutput() . "</error>\n", OutputInterface::VERBOSITY_NORMAL);
209+
}
210+
}
211+
212+
foreach($tempSQLFileNames as $tempSQLFileName) {
213+
unlink($tempSQLFileName);
214+
}
215+
216+
return $results;
217+
}
218+
219+
/**
220+
* @param array $dbSpecList
221+
* @param int $maxParallel
222+
*/
223+
protected function dropSchemas($dbSpecList, $maxParallel)
224+
{
225+
$processes = [];
226+
$tempSQLFileNames = [];
227+
228+
foreach ($dbSpecList as $dbName => $dbConnectionSpec) {
229+
$dbConnectionSpec = $this->dbManager->getDatabaseConnectionSpecification($dbName);
230+
231+
$schemaManager = new DatabaseSchemaManager($dbConnectionSpec);
232+
$sql= $schemaManager->getDropSchemaSQL($dbConnectionSpec['user'], isset($dbConnectionSpec['dbname']) ? $dbConnectionSpec['dbname'] : null );
233+
$tempSQLFileName = tempnam(sys_get_temp_dir(), 'db3val_');
234+
file_put_contents($tempSQLFileName, $sql);
235+
$tempSQLFileNames[] = $tempSQLFileName;
236+
237+
$executor = $this->executorFactory->createForkedExecutor($dbConnectionSpec, 'NativeClient', false);
238+
$process = $executor->getExecuteFileProcess($tempSQLFileName);
239+
$processes[$dbName] = $process;
240+
}
241+
242+
$this->processManager->runParallel($processes, $maxParallel, 100);
243+
244+
foreach ($processes as $dbName => $process) {
245+
if (!$process->isSuccessful()) {
246+
$this->writeErrorln("\n<error>Drop of new schema & user on database '$dbName' failed! Reason: " . $process->getErrorOutput() . "</error>\n", OutputInterface::VERBOSITY_NORMAL);
247+
}
248+
}
249+
250+
foreach($tempSQLFileNames as $tempSQLFileName) {
251+
unlink($tempSQLFileName);
252+
}
253+
}
254+
152255
/**
153256
* @param array $results
154257
* @param int $succeeded
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
namespace Db3v4l\Service;
4+
5+
class DatabaseSchemaManager
6+
{
7+
protected $databaseConfiguration;
8+
9+
public function __construct(array $databaseConfiguration)
10+
{
11+
$this->databaseConfiguration = $databaseConfiguration;
12+
}
13+
14+
/**
15+
* Returns the sql commands used to create a new db schema and accompanying user
16+
* @param string $userName used both for user and schema if passed schemaName is null. Max 16 chars for MySQL 5.5
17+
* @param string $password
18+
* @param string $schemaName Max 63 chars for Postgres
19+
* @param string $charset
20+
* @return string
21+
*/
22+
public function getCreateSchemaSQL($userName, $password, $schemaName = null, $charset = null)
23+
{
24+
if ($schemaName === null) {
25+
$schemaName = $userName;
26+
}
27+
28+
$dbType = $this->getDbTypeFromDriver($this->databaseConfiguration['driver']);
29+
30+
switch ($dbType) {
31+
case 'mysql':
32+
return
33+
"CREATE DATABASE `$schemaName`" .
34+
($charset !== null ? " CHARACTER SET $charset" : '') . /// @todo transform charset name into a supported one
35+
"; CREATE USER '$userName'@'%' IDENTIFIED BY '$password'" .
36+
"; GRANT ALL PRIVILEGES ON `$schemaName`.* TO '$userName'@'%';";
37+
case 'pgsql':
38+
return
39+
"CREATE DATABASE \"$schemaName\"" . // q: do we need to add 'TEMPLATE template0' ?
40+
($charset !== null ? " ENCODING $charset" : '') . /// @todo transform charset name into a supported one
41+
"; COMMIT; CREATE USER \"$userName\" WITH PASSWORD '$password'" .
42+
"; GRANT ALL ON DATABASE \"$schemaName\" TO \"$userName\""; // q: should we avoid granting CREATE?
43+
default:
44+
throw new \OutOfBoundsException("Unsupported database type '$dbType'");
45+
}
46+
}
47+
48+
/**
49+
* Returns the sql commands used to create a new db schema and accompanying user
50+
* @param string $userName
51+
* @param string $schemaName
52+
* @return string
53+
*/
54+
public function getDropSchemaSQL($userName, $schemaName = null)
55+
{
56+
if ($schemaName === null) {
57+
$schemaName = $userName;
58+
}
59+
60+
$dbType = $this->getDbTypeFromDriver($this->databaseConfiguration['driver']);
61+
62+
switch ($dbType) {
63+
case 'mysql':
64+
return
65+
"DROP DATABASE IF EXISTS `$schemaName`; DROP USER '$userName'@'%';";
66+
case 'pgsql':
67+
return
68+
"DROP DATABASE IF EXISTS \"$schemaName\"; DROP USER IF EXISTS \"$userName\";";
69+
default:
70+
throw new \OutOfBoundsException("Unsupported database type '$dbType'");
71+
}
72+
}
73+
74+
/**
75+
* @param string $driver
76+
* @return string
77+
*/
78+
protected function getDbTypeFromDriver($driver)
79+
{
80+
return str_replace('pdo_', '', $driver);
81+
}
82+
}

app/src/Service/SqlExecutor/Forked/Doctrine.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
class Doctrine extends ForkedExecutor implements ForkedCommandExecutor
99
{
1010
/**
11-
* @param string $sql
11+
* @param string|string[] $sql
1212
* @return Process
1313
*/
1414
public function getExecuteCommandProcess($sql)

app/src/Service/SqlExecutor/Forked/NativeClient.php

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public function getExecuteFileProcess($filename)
3434
*/
3535
public function getProcess($sqlOrFilename, $isFile = false)
3636
{
37-
$clientType = $this->getDbClientFromDriver($this->databaseConfiguration['driver']);
37+
$clientType = $this->getDbClientTypeFromDriver($this->databaseConfiguration['driver']);
3838

3939
switch ($clientType) {
4040
case 'mysql':
@@ -45,8 +45,10 @@ public function getProcess($sqlOrFilename, $isFile = false)
4545
'--user=' . $this->databaseConfiguration['user'],
4646
'-p' . $this->databaseConfiguration['password'],
4747
'--binary-mode', // 'It also disables all mysql commands except charset and delimiter in non-interactive mode (for input piped to mysql or loaded using the source command)'
48-
// $dbname
4948
];
49+
if (isset($this->databaseConfiguration['dbname'])) {
50+
$options[] = $this->databaseConfiguration['dbname'];
51+
}
5052
if (!$isFile) {
5153
$options[] = '--execute=' . $sqlOrFilename;
5254
}
@@ -57,14 +59,16 @@ public function getProcess($sqlOrFilename, $isFile = false)
5759
break;
5860
case 'pgsql':
5961
$command = 'psql';
62+
$connectString = "postgresql://".$this->databaseConfiguration['user'].":".$this->databaseConfiguration['password'].
63+
"@{$this->databaseConfiguration['host']}:".($this->databaseConfiguration['port'] ?? '5432').'/';
64+
if (isset($this->databaseConfiguration['dbname'])) {
65+
$connectString .= $this->databaseConfiguration['dbname'];
66+
}
6067
$options = [
61-
"postgresql://".$this->databaseConfiguration['user'].":".$this->databaseConfiguration['password'].
62-
"@{$this->databaseConfiguration['host']}:".($this->databaseConfiguration['port'] ?? '5432').'/',
63-
//'--host=' . $this->databaseConfiguration['host'],
64-
//'--port=' . $this->databaseConfiguration['port'] ?? '5432',
65-
//'--username=' . $this->databaseConfiguration['user'],
66-
//'--dbname=' . $dbname
68+
$connectString
6769
];
70+
// NB: this triggers a different behaviour that piping multiple commands to stdin, namely
71+
// it wraps all of the commands in a transaction and allows either sql commands or a single meta-command
6872
if (!$isFile) {
6973
$options[] = '--command=' . $sqlOrFilename;
7074
}
@@ -91,7 +95,7 @@ public function getProcess($sqlOrFilename, $isFile = false)
9195
* @param string $driver
9296
* @return string
9397
*/
94-
protected function getDbClientFromDriver($driver)
98+
protected function getDbClientTypeFromDriver($driver)
9599
{
96100
return str_replace('pdo_', '', $driver);
97101
}

app/src/Service/SqlExecutorFactory.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class SqlExecutorFactory
1717
* @return ForkedCommandExecutor|ForkedFileExecutor
1818
* @throws \OutOfBoundsException
1919
*/
20-
public function createForkedExecutor($databaseConnectionConfiguration, $executionStrategy = 'NativeClient', $timed = true)
20+
public function createForkedExecutor(array $databaseConnectionConfiguration, $executionStrategy = 'NativeClient', $timed = true)
2121
{
2222
switch ($executionStrategy) {
2323
case 'Doctrine':

0 commit comments

Comments
 (0)