diff --git a/.github/funding.yml b/.github/funding.yml
new file mode 100644
index 000000000..25adc9520
--- /dev/null
+++ b/.github/funding.yml
@@ -0,0 +1,2 @@
+github: dg
+custom: "https://nette.org/donate"
diff --git a/composer.json b/composer.json
index 46ebba32d..4380a83d9 100644
--- a/composer.json
+++ b/composer.json
@@ -40,7 +40,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "3.0-dev"
+ "dev-master": "3.1-dev"
}
}
}
diff --git a/src/Bridges/DatabaseDI/DatabaseExtension.php b/src/Bridges/DatabaseDI/DatabaseExtension.php
index 1272a1259..5b0737b6e 100644
--- a/src/Bridges/DatabaseDI/DatabaseExtension.php
+++ b/src/Bridges/DatabaseDI/DatabaseExtension.php
@@ -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(),
@@ -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);
diff --git a/src/Bridges/DatabaseTracy/ConnectionPanel.php b/src/Bridges/DatabaseTracy/ConnectionPanel.php
index 5bd58bab8..6d6392966 100644
--- a/src/Bridges/DatabaseTracy/ConnectionPanel.php
+++ b/src/Bridges/DatabaseTracy/ConnectionPanel.php
@@ -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;
@@ -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;
@@ -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';
});
}
diff --git a/src/Bridges/DatabaseTracy/templates/ConnectionPanel.panel.phtml b/src/Bridges/DatabaseTracy/templates/ConnectionPanel.panel.phtml
index ef83eddf9..a31f7b9cb 100644
--- a/src/Bridges/DatabaseTracy/templates/ConnectionPanel.panel.phtml
+++ b/src/Bridges/DatabaseTracy/templates/ConnectionPanel.panel.phtml
@@ -29,7 +29,7 @@ use Tracy\Helpers;
[$connection, $sql, $params, $source, $time, $rows, $error, $command, $explain] = $query;
?>
-
+ |
ERROR
diff --git a/src/Database/Connection.php b/src/Database/Connection.php
index 18ffe26ac..7b589983e 100644
--- a/src/Database/Connection.php
+++ b/src/Database/Connection.php
@@ -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.
*/
diff --git a/src/Database/Context.php b/src/Database/Context.php
index 280bd16f5..9cf68c1b4 100644
--- a/src/Database/Context.php
+++ b/src/Database/Context.php
@@ -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);
diff --git a/src/Database/SqlPreprocessor.php b/src/Database/SqlPreprocessor.php
index 0517f8dcc..1fbdbd422 100644
--- a/src/Database/SqlPreprocessor.php
+++ b/src/Database/SqlPreprocessor.php
@@ -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 = [
@@ -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;
@@ -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.');
@@ -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;
@@ -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;
@@ -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);
@@ -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);
@@ -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);
@@ -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';
@@ -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');
}
@@ -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 {
diff --git a/src/Database/Structure.php b/src/Database/Structure.php
index b2bc08ca4..5ec06790b 100644
--- a/src/Database/Structure.php
+++ b/src/Database/Structure.php
@@ -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();
diff --git a/tests/Database/Context.transaction.phpt b/tests/Database/Context.transaction.phpt
index c5d293845..b5f9f4a3e 100644
--- a/tests/Database/Context.transaction.phpt
+++ b/tests/Database/Context.transaction.phpt
@@ -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');
diff --git a/tests/Database/SqlPreprocessor.phpt b/tests/Database/SqlPreprocessor.phpt
index d6cf7314d..75392dae4 100644
--- a/tests/Database/SqlPreprocessor.phpt
+++ b/tests/Database/SqlPreprocessor.phpt
@@ -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);
});
@@ -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);
});
@@ -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.');
});
@@ -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);
});
|