Skip to content

Extension: Added connection timeout #260

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions .github/funding.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github: dg
custom: "https://nette.org/donate"
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
},
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
"dev-master": "3.1-dev"
}
}
}
5 changes: 5 additions & 0 deletions src/Bridges/DatabaseDI/DatabaseExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public function getConfigSchema(): Nette\Schema\Schema
'options' => Expect::array(),
'debugger' => Expect::bool(true),
'explain' => Expect::bool(true),
'connectionTimeout' => Expect::int()->min(0),
'reflection' => Expect::string(), // BC
'conventions' => Expect::string('discovered'), // Nette\Database\Conventions\DiscoveredConventions
'autowired' => Expect::bool(),
Expand Down Expand Up @@ -75,6 +76,10 @@ private function setupDatabase(\stdClass $config, string $name): void
}
}

if ($config->connectionTimeout !== null) {
$config->options[\PDO::ATTR_TIMEOUT] = $config->connectionTimeout;
}

$connection = $builder->addDefinition($this->prefix("$name.connection"))
->setFactory(Nette\Database\Connection::class, [$config->dsn, $config->user, $config->password, $config->options])
->setAutowired($config->autowired);
Expand Down
8 changes: 6 additions & 2 deletions src/Bridges/DatabaseTracy/ConnectionPanel.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ class ConnectionPanel implements Tracy\IBarPanel
/** @var bool */
public $disabled = false;

/** @var float */
public $performanceScale = 0.25;

/** @var float logged time */
private $totalTime = 0;

Expand All @@ -46,11 +49,11 @@ class ConnectionPanel implements Tracy\IBarPanel

public function __construct(Connection $connection)
{
$connection->onQuery[] = [$this, 'logQuery'];
$connection->onQuery[] = \Closure::fromCallable([$this, 'logQuery']);
}


public function logQuery(Connection $connection, $result): void
private function logQuery(Connection $connection, $result): void
{
if ($this->disabled) {
return;
Expand Down Expand Up @@ -138,6 +141,7 @@ public function getPanel(): ?string
$name = $this->name;
$count = $this->count;
$totalTime = $this->totalTime;
$performanceScale = $this->performanceScale;
require __DIR__ . '/templates/ConnectionPanel.panel.phtml';
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ use Tracy\Helpers;
[$connection, $sql, $params, $source, $time, $rows, $error, $command, $explain] = $query;
?>
<tr>
<td>
<td style="background:rgba(255, 95, 23, <?= sprintf('%0.3f', log($time * 1000 + 1, 10) * $performanceScale) ?>)">
<?php if ($error): ?>
<span title="<?= Helpers::escapeHtml($error) ?>">ERROR</span>
<?php elseif ($time !== null): echo sprintf('%0.3f', $time * 1000); endif ?>
Expand Down
17 changes: 17 additions & 0 deletions src/Database/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,23 @@ public function rollBack(): void
}


/**
* @return mixed
*/
public function transaction(callable $callback)
{
$this->beginTransaction();
try {
$res = $callback();
} catch (\Throwable $e) {
$this->rollBack();
throw $e;
}
$this->commit();
return $res;
}


/**
* Generates and executes SQL query.
*/
Expand Down
9 changes: 9 additions & 0 deletions src/Database/Context.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,15 @@ public function rollBack(): void
}


/**
* @return mixed
*/
public function transaction(callable $callback)
{
return $this->connection->transaction($callback);
}


public function getInsertId(string $sequence = null): string
{
return $this->connection->getInsertId($sequence);
Expand Down
82 changes: 50 additions & 32 deletions src/Database/SqlPreprocessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,26 @@ class SqlPreprocessor
{
use Nette\SmartObject;

/** @var array */
private const MODE_LIST = ['and', 'or', 'set', 'values', 'order'];
private const
MODE_AND = 'and', // (key [operator] value) AND ...
MODE_OR = 'or', // (key [operator] value) OR ...
MODE_SET = 'set', // key=value, key=value, ...
MODE_VALUES = 'values', // (key, key, ...) VALUES (value, value, ...)
MODE_ORDER = 'order', // key, key DESC, ...
MODE_LIST = 'list', // value, value, ... | (tuple), (tuple), ...
MODE_AUTO = 'auto'; // arrayMode for arrays

private const MODES = [self::MODE_AND, self::MODE_OR, self::MODE_SET, self::MODE_VALUES, self::MODE_ORDER, self::MODE_LIST];

private const ARRAY_MODES = [
'INSERT' => 'values',
'REPLACE' => 'values',
'KEY UPDATE' => 'set',
'SET' => 'set',
'WHERE' => 'and',
'HAVING' => 'and',
'ORDER BY' => 'order',
'GROUP BY' => 'order',
'INSERT' => self::MODE_VALUES,
'REPLACE' => self::MODE_VALUES,
'KEY UPDATE' => self::MODE_SET,
'SET' => self::MODE_SET,
'WHERE' => self::MODE_AND,
'HAVING' => self::MODE_AND,
'ORDER BY' => self::MODE_ORDER,
'GROUP BY' => self::MODE_ORDER,
];

private const PARAMETRIC_COMMANDS = [
Expand Down Expand Up @@ -60,7 +68,7 @@ class SqlPreprocessor
/** @var bool */
private $useParams;

/** @var string|null values|set|and|order */
/** @var string|null values|set|and|order|items */
private $arrayMode;


Expand Down Expand Up @@ -88,16 +96,15 @@ public function process(array $params, bool $useParams = false): array
$param = $params[$this->counter++];

if (($this->counter === 2 && count($params) === 2) || !is_scalar($param)) {
$res[] = $this->formatValue($param, 'auto');
$this->arrayMode = null;
$res[] = $this->formatValue($param, self::MODE_AUTO);

} elseif (is_string($param) && $this->counter > $prev + 1) {
$prev = $this->counter;
$this->arrayMode = null;
$res[] = Nette\Utils\Strings::replace(
$param,
'~\'[^\']*+\'|"[^"]*+"|\?[a-z]*|^\s*+(?:\(?\s*SELECT|INSERT|UPDATE|DELETE|REPLACE|EXPLAIN)\b|\b(?:SET|WHERE|HAVING|ORDER BY|GROUP BY|KEY UPDATE)(?=\s*$|\s*\?)|/\*.*?\*/|--[^\n]*~Dsi',
[$this, 'callback']
'~\'[^\']*+\'|"[^"]*+"|\?[a-z]*|^\s*+(?:\(?\s*SELECT|INSERT|UPDATE|DELETE|REPLACE|EXPLAIN)\b|\b(?:SET|WHERE|HAVING|ORDER BY|GROUP BY|KEY UPDATE)(?=\s*$|\s*\?)|\bIN\s+\(\?\)|/\*.*?\*/|--[^\n]*~Dsi',
\Closure::fromCallable([$this, 'callback'])
);
} else {
throw new Nette\InvalidArgumentException('There are more parameters than placeholders.');
Expand All @@ -108,19 +115,24 @@ public function process(array $params, bool $useParams = false): array
}


/** @internal */
public function callback(array $m): string
private function callback(array $m): string
{
$m = $m[0];
if ($m[0] === '?') { // placeholder
if ($this->counter >= count($this->params)) {
throw new Nette\InvalidArgumentException('There are more placeholders than passed parameters.');
}
return $this->formatValue($this->params[$this->counter++], substr($m, 1) ?: 'auto');
return $this->formatValue($this->params[$this->counter++], substr($m, 1) ?: self::MODE_AUTO);

} elseif ($m[0] === "'" || $m[0] === '"' || $m[0] === '/' || $m[0] === '-') { // string or comment
return $m;

} elseif (substr($m, -3) === '(?)') { // IN (?)
if ($this->counter >= count($this->params)) {
throw new Nette\InvalidArgumentException('There are more placeholders than passed parameters.');
}
return 'IN (' . $this->formatValue($this->params[$this->counter++], self::MODE_LIST) . ')';

} else { // command
$cmd = ltrim(strtoupper($m), "\t\n\r (");
$this->arrayMode = self::ARRAY_MODES[$cmd] ?? null;
Expand All @@ -132,7 +144,7 @@ public function callback(array $m): string

private function formatValue($value, string $mode = null): string
{
if (!$mode || $mode === 'auto') {
if (!$mode || $mode === self::MODE_AUTO) {
if (is_scalar($value) || is_resource($value)) {
if ($this->useParams) {
$this->remaining[] = $value;
Expand Down Expand Up @@ -187,16 +199,16 @@ private function formatValue($value, string $mode = null): string
$value = iterator_to_array($value);
}

if (is_array($value)) {
if ($mode && is_array($value)) {
$vx = $kx = [];
if ($mode === 'auto') {
$mode = $this->arrayMode;
if ($mode === self::MODE_AUTO) {
$mode = $this->arrayMode ?? self::MODE_LIST;
}

if ($mode === 'values') { // (key, key, ...) VALUES (value, value, ...)
if ($mode === self::MODE_VALUES) { // (key, key, ...) VALUES (value, value, ...)
if (array_key_exists(0, $value)) { // multi-insert
if (!is_array($value[0]) && !$value[0] instanceof Row) {
throw new Nette\InvalidArgumentException('Automaticaly detected multi-insert, but values aren\'t array. If you need try to change mode like "?[' . implode('|', self::MODE_LIST) . ']". Mode "' . $mode . '" was used.');
throw new Nette\InvalidArgumentException('Automaticaly detected multi-insert, but values aren\'t array. If you need try to change mode like "?[' . implode('|', self::MODES) . ']". Mode "' . $mode . '" was used.');
}
foreach ($value[0] as $k => $v) {
$kx[] = $this->delimite($k);
Expand All @@ -219,10 +231,10 @@ private function formatValue($value, string $mode = null): string
}
return '(' . implode(', ', $kx) . ') VALUES (' . implode(', ', $vx) . ')';

} elseif (!$mode || $mode === 'set') {
} elseif ($mode === self::MODE_SET) {
foreach ($value as $k => $v) {
if (is_int($k)) { // value, value, ... OR (1, 2), (3, 4)
$vx[] = is_array($v) ? '(' . $this->formatValue($v) . ')' : $this->formatValue($v);
if (is_int($k)) { // value, value, ...
$vx[] = $this->formatValue($v);
} elseif (substr($k, -1) === '=') { // key+=value, key-=value, ...
$k2 = $this->delimite(substr($k, 0, -2));
$vx[] = $k2 . '=' . $k2 . ' ' . substr($k, -2, 1) . ' ' . $this->formatValue($v);
Expand All @@ -232,7 +244,13 @@ private function formatValue($value, string $mode = null): string
}
return implode(', ', $vx);

} elseif ($mode === 'and' || $mode === 'or') { // (key [operator] value) AND ...
} elseif ($mode === self::MODE_LIST) { // value, value, ... | (tuple), (tuple), ...
foreach ($value as $k => $v) {
$vx[] = is_array($v) ? '(' . $this->formatValue($v, self::MODE_LIST) . ')' : $this->formatValue($v);
}
return implode(', ', $vx);

} elseif ($mode === self::MODE_AND || $mode === self::MODE_OR) { // (key [operator] value) AND ...
foreach ($value as $k => $v) {
if (is_int($k)) {
$vx[] = $this->formatValue($v);
Expand All @@ -242,7 +260,7 @@ private function formatValue($value, string $mode = null): string
$k = $this->delimite($k);
if (is_array($v)) {
if ($v) {
$vx[] = $k . ' ' . ($operator ? $operator . ' ' : '') . 'IN (' . $this->formatValue(array_values($v)) . ')';
$vx[] = $k . ' ' . ($operator ? $operator . ' ' : '') . 'IN (' . $this->formatValue(array_values($v), self::MODE_LIST) . ')';
} elseif ($operator === 'NOT') {
} else {
$vx[] = '1=0';
Expand All @@ -257,7 +275,7 @@ private function formatValue($value, string $mode = null): string
}
return $value ? '(' . implode(') ' . strtoupper($mode) . ' (', $vx) . ')' : '1=1';

} elseif ($mode === 'order') { // key, key DESC, ...
} elseif ($mode === self::MODE_ORDER) { // key, key DESC, ...
foreach ($value as $k => $v) {
$vx[] = $this->delimite($k) . ($v > 0 ? '' : ' DESC');
}
Expand All @@ -267,11 +285,11 @@ private function formatValue($value, string $mode = null): string
throw new Nette\InvalidArgumentException("Unknown placeholder ?$mode.");
}

} elseif (in_array($mode, self::MODE_LIST, true)) {
} elseif (in_array($mode, self::MODES, true)) {
$type = gettype($value);
throw new Nette\InvalidArgumentException("Placeholder ?$mode expects array or Traversable object, $type given.");

} elseif ($mode && $mode !== 'auto') {
} elseif ($mode && $mode !== self::MODE_AUTO) {
throw new Nette\InvalidArgumentException("Unknown placeholder ?$mode.");

} else {
Expand Down
7 changes: 2 additions & 5 deletions src/Database/Structure.php
Original file line number Diff line number Diff line change
Expand Up @@ -177,14 +177,11 @@ protected function needStructure(): void
return;
}

$this->structure = $this->cache->load('structure', [$this, 'loadStructure']);
$this->structure = $this->cache->load('structure', \Closure::fromCallable([$this, 'loadStructure']));
}


/**
* @internal
*/
public function loadStructure(): array
protected function loadStructure(): array
{
$driver = $this->connection->getSupplementalDriver();

Expand Down
12 changes: 12 additions & 0 deletions tests/Database/Context.transaction.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ test(function () use ($context) {
});


test(function () use ($context) {
Assert::exception(function () use ($context) {
$context->transaction(function () use ($context) {
$context->query('DELETE FROM book');
throw new Exception('my exception');
});
}, Exception::class, 'my exception');

Assert::same(3, $context->fetchField('SELECT id FROM book WHERE id = ', 3));
});


test(function () use ($context) {
$context->beginTransaction();
$context->query('DELETE FROM book');
Expand Down
17 changes: 11 additions & 6 deletions tests/Database/SqlPreprocessor.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ test(function () use ($preprocessor) { // IN

Assert::same(reformat('SELECT id FROM author WHERE ([a] IN (NULL, ?, ?, ?)) AND (1=0) AND ([c] NOT IN (NULL, ?, ?, ?))'), $sql);
Assert::same([1, 2, 3, 1, 2, 3], $params);


[$sql, $params] = $preprocessor->process(['SELECT * FROM table WHERE ? AND id IN (?) AND ?', ['a' => 111], [3, 4], ['b' => 222]]);
Assert::same(reformat('SELECT * FROM table WHERE ([a] = ?) AND id IN (?, ?) AND ([b] = ?)'), $sql);
Assert::same([111, 3, 4, 222], $params);
});


Expand Down Expand Up @@ -332,7 +337,7 @@ test(function () use ($preprocessor) { // insert
[$sql, $params] = $preprocessor->process(['/* comment */ INSERT INTO author',
['name' => 'Catelyn Stark'],
]);
Assert::same(reformat("/* comment */ INSERT INTO author [name]='Catelyn Stark'"), $sql); // autodetection not used
Assert::same(reformat("/* comment */ INSERT INTO author 'Catelyn Stark'"), $sql); // autodetection not used
Assert::same([], $params);
});

Expand All @@ -347,10 +352,10 @@ test(function () use ($preprocessor) { // ?values
});


test(function () use ($preprocessor) { // automatic detection faild
test(function () use ($preprocessor) { // automatic detection failed
Assert::exception(function () use ($preprocessor) {
$preprocessor->process(['INSERT INTO author (name) SELECT name FROM user WHERE id IN (?)', [11, 12]]);
}, Nette\InvalidArgumentException::class, 'Automaticaly detected multi-insert, but values aren\'t array. If you need try to change mode like "?[and|or|set|values|order]". Mode "values" was used.');
dump($preprocessor->process(['INSERT INTO author (name) SELECT name FROM user WHERE id ?', [11, 12]])); // invalid sql
}, Nette\InvalidArgumentException::class, 'Automaticaly detected multi-insert, but values aren\'t array. If you need try to change mode like "?[and|or|set|values|order|list]". Mode "values" was used.');
});


Expand Down Expand Up @@ -440,10 +445,10 @@ test(function () use ($preprocessor) { // update
Assert::same([12, 'John Doe'], $params);


[$sql, $params] = $preprocessor->process(['UPDATE author SET a=1,',
[$sql, $params] = $preprocessor->process(['UPDATE author SET a=1,', // autodetection not used
['id' => 12, 'name' => 'John Doe'],
]);
Assert::same(reformat('UPDATE author SET a=1, [id]=?, [name]=?'), $sql);
Assert::same(reformat('UPDATE author SET a=1, ?, ?'), $sql);
Assert::same([12, 'John Doe'], $params);
});

Expand Down