no message
This commit is contained in:
Vendored
+41
@@ -0,0 +1,41 @@
|
||||
build: off
|
||||
cache:
|
||||
- c:\php -> appveyor.yml
|
||||
- '%LOCALAPPDATA%\Composer\files -> appveyor.yml'
|
||||
|
||||
clone_folder: c:\projects\database
|
||||
|
||||
services:
|
||||
- mssql2008r2sp2
|
||||
# - mssql2012sp1
|
||||
# - mssql2014
|
||||
- mysql
|
||||
|
||||
init:
|
||||
- SET PATH=c:\php;%PATH%
|
||||
- SET PHP=1
|
||||
- SET ANSICON=121x90 (121x90)
|
||||
|
||||
install:
|
||||
# Install PHP
|
||||
- IF EXIST c:\php (SET PHP=0) ELSE (mkdir c:\php)
|
||||
- IF %PHP%==1 cd c:\php
|
||||
- IF %PHP%==1 curl https://windows.php.net/downloads/releases/archives/php-5.6.14-Win32-VC11-x86.zip --output php.zip
|
||||
- IF %PHP%==1 7z x php.zip >nul
|
||||
- IF %PHP%==1 echo extension_dir=ext >> php.ini
|
||||
- IF %PHP%==1 echo extension=php_openssl.dll >> php.ini
|
||||
- IF %PHP%==1 appveyor DownloadFile https://files.nette.org/misc/php-sqlsrv.zip
|
||||
- IF %PHP%==1 7z x php-sqlsrv.zip >nul
|
||||
- IF %PHP%==1 copy SQLSRV\php_pdo_sqlsrv_56_ts.dll ext\php_pdo_sqlsrv_ts.dll
|
||||
- IF %PHP%==1 del /Q *.zip
|
||||
- cd c:\projects\database
|
||||
|
||||
# Install Nette Tester
|
||||
- appveyor DownloadFile https://getcomposer.org/composer.phar
|
||||
- php composer.phar install --prefer-dist --no-interaction --no-progress
|
||||
|
||||
# Create databases.ini
|
||||
- copy tests\databases.appveyor.ini tests\Database\databases.ini
|
||||
|
||||
test_script:
|
||||
- vendor\bin\tester tests -s -c tests\php-win.ini
|
||||
Vendored
+41
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "nette/database",
|
||||
"description": "💾 Nette Database: layer with a familiar PDO-like API but much more powerful. Building queries, advanced joins, drivers for MySQL, PostgreSQL, SQLite, MS SQL Server and Oracle.",
|
||||
"keywords": ["nette", "database", "notorm", "queries", "pdo", "mysql", "postgresql", "sqlite", "mssql", "oracle"],
|
||||
"homepage": "https://nette.org",
|
||||
"license": ["BSD-3-Clause", "GPL-2.0", "GPL-3.0"],
|
||||
"authors": [
|
||||
{
|
||||
"name": "David Grudl",
|
||||
"homepage": "https://davidgrudl.com"
|
||||
},
|
||||
{
|
||||
"name": "Nette Community",
|
||||
"homepage": "https://nette.org/contributors"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.6.0",
|
||||
"ext-pdo": "*",
|
||||
"nette/caching": "^2.2",
|
||||
"nette/utils": "^2.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"nette/tester": "^2.0",
|
||||
"nette/di": "^2.4 || ~3.0.0",
|
||||
"mockery/mockery": "^1.0.0",
|
||||
"tracy/tracy": "^2.4"
|
||||
},
|
||||
"conflict": {
|
||||
"nette/nette": "<2.2"
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": ["src/"]
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.4-dev"
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
How to contribute & use the issue tracker
|
||||
=========================================
|
||||
|
||||
Nette welcomes your contributions. There are several ways to help out:
|
||||
|
||||
* Create an issue on GitHub, if you have found a bug
|
||||
* Write test cases for open bug issues
|
||||
* Write fixes for open bug/feature issues, preferably with test cases included
|
||||
* Contribute to the [documentation](https://nette.org/en/writing)
|
||||
|
||||
Issues
|
||||
------
|
||||
|
||||
Please **do not use the issue tracker to ask questions**. We will be happy to help you
|
||||
on [Nette forum](https://forum.nette.org) or chat with us on [Gitter](https://gitter.im/nette/nette).
|
||||
|
||||
A good bug report shouldn't leave others needing to chase you up for more
|
||||
information. Please try to be as detailed as possible in your report.
|
||||
|
||||
**Feature requests** are welcome. But take a moment to find out whether your idea
|
||||
fits with the scope and aims of the project. It's up to *you* to make a strong
|
||||
case to convince the project's developers of the merits of this feature.
|
||||
|
||||
Contributing
|
||||
------------
|
||||
|
||||
If you'd like to contribute, please take a moment to read [the contributing guide](https://nette.org/en/contributing).
|
||||
|
||||
The best way to propose a feature is to discuss your ideas on [Nette forum](https://forum.nette.org) before implementing them.
|
||||
|
||||
Please do not fix whitespace, format code, or make a purely cosmetic patch.
|
||||
|
||||
Thanks! :heart:
|
||||
Vendored
+60
@@ -0,0 +1,60 @@
|
||||
Licenses
|
||||
========
|
||||
|
||||
Good news! You may use Nette Framework under the terms of either
|
||||
the New BSD License or the GNU General Public License (GPL) version 2 or 3.
|
||||
|
||||
The BSD License is recommended for most projects. It is easy to understand and it
|
||||
places almost no restrictions on what you can do with the framework. If the GPL
|
||||
fits better to your project, you can use the framework under this license.
|
||||
|
||||
You don't have to notify anyone which license you are using. You can freely
|
||||
use Nette Framework in commercial projects as long as the copyright header
|
||||
remains intact.
|
||||
|
||||
Please be advised that the name "Nette Framework" is a protected trademark and its
|
||||
usage has some limitations. So please do not use word "Nette" in the name of your
|
||||
project or top-level domain, and choose a name that stands on its own merits.
|
||||
If your stuff is good, it will not take long to establish a reputation for yourselves.
|
||||
|
||||
|
||||
New BSD License
|
||||
---------------
|
||||
|
||||
Copyright (c) 2004, 2014 David Grudl (https://davidgrudl.com)
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of "Nette Framework" nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
This software is provided by the copyright holders and contributors "as is" and
|
||||
any express or implied warranties, including, but not limited to, the implied
|
||||
warranties of merchantability and fitness for a particular purpose are
|
||||
disclaimed. In no event shall the copyright owner or contributors be liable for
|
||||
any direct, indirect, incidental, special, exemplary, or consequential damages
|
||||
(including, but not limited to, procurement of substitute goods or services;
|
||||
loss of use, data, or profits; or business interruption) however caused and on
|
||||
any theory of liability, whether in contract, strict liability, or tort
|
||||
(including negligence or otherwise) arising in any way out of the use of this
|
||||
software, even if advised of the possibility of such damage.
|
||||
|
||||
|
||||
GNU General Public License
|
||||
--------------------------
|
||||
|
||||
GPL licenses are very very long, so instead of including them here we offer
|
||||
you URLs with full text:
|
||||
|
||||
- [GPL version 2](http://www.gnu.org/licenses/gpl-2.0.html)
|
||||
- [GPL version 3](http://www.gnu.org/licenses/gpl-3.0.html)
|
||||
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
Nette Database
|
||||
==============
|
||||
|
||||
[](https://packagist.org/packages/nette/database)
|
||||
[](https://travis-ci.org/nette/database)
|
||||
[](https://ci.appveyor.com/project/dg/database/branch/v2.4)
|
||||
|
||||
Nette provides a powerful layer for accessing your database easily.
|
||||
|
||||
- composes SQL queries with ease
|
||||
- easily fetches data
|
||||
- uses efficient queries and does not transmit unnecessary data
|
||||
|
||||
The [Nette Database Core](https://doc.nette.org/database-core) is a wrapper around the PDO and provides core functionality.
|
||||
|
||||
The [Nette Database Explorer](https://doc.nette.org/database-explorer) layer helps you to fetch database data more easily and in a more optimized way.
|
||||
|
||||
[Please see documentation on our website](https://doc.nette.org/database).
|
||||
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
namespace Nette\Bridges\DatabaseDI;
|
||||
|
||||
use Nette;
|
||||
|
||||
|
||||
/**
|
||||
* Nette Framework Database services.
|
||||
*/
|
||||
class DatabaseExtension extends Nette\DI\CompilerExtension
|
||||
{
|
||||
public $databaseDefaults = [
|
||||
'dsn' => null,
|
||||
'user' => null,
|
||||
'password' => null,
|
||||
'options' => null,
|
||||
'debugger' => true,
|
||||
'explain' => true,
|
||||
'reflection' => null, // BC
|
||||
'conventions' => 'discovered', // Nette\Database\Conventions\DiscoveredConventions
|
||||
'autowired' => null,
|
||||
];
|
||||
|
||||
/** @var bool */
|
||||
private $debugMode;
|
||||
|
||||
|
||||
public function __construct($debugMode = false)
|
||||
{
|
||||
$this->debugMode = $debugMode;
|
||||
}
|
||||
|
||||
|
||||
public function loadConfiguration()
|
||||
{
|
||||
$configs = $this->getConfig();
|
||||
foreach ($configs as $k => $v) {
|
||||
if (is_scalar($v)) {
|
||||
$configs = ['default' => $configs];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$defaults = $this->databaseDefaults;
|
||||
$defaults['autowired'] = true;
|
||||
foreach ((array) $configs as $name => $config) {
|
||||
if (!is_array($config)) {
|
||||
continue;
|
||||
}
|
||||
$config = $this->validateConfig($defaults, $config, $this->prefix($name));
|
||||
$defaults['autowired'] = false;
|
||||
$this->setupDatabase($config, $name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function setupDatabase($config, $name)
|
||||
{
|
||||
$builder = $this->getContainerBuilder();
|
||||
|
||||
foreach ((array) $config['options'] as $key => $value) {
|
||||
if (is_string($value) && preg_match('#^PDO::\w+\z#', $value)) {
|
||||
$config['options'][$key] = $value = constant($value);
|
||||
}
|
||||
if (preg_match('#^PDO::\w+\z#', $key)) {
|
||||
unset($config['options'][$key]);
|
||||
$config['options'][constant($key)] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$connection = $builder->addDefinition($this->prefix("$name.connection"))
|
||||
->setFactory(Nette\Database\Connection::class, [$config['dsn'], $config['user'], $config['password'], $config['options']])
|
||||
->setAutowired($config['autowired']);
|
||||
|
||||
$structure = $builder->addDefinition($this->prefix("$name.structure"))
|
||||
->setFactory(Nette\Database\Structure::class)
|
||||
->setArguments([$connection])
|
||||
->setAutowired($config['autowired']);
|
||||
|
||||
if (!empty($config['reflection'])) {
|
||||
$conventionsServiceName = 'reflection';
|
||||
$config['conventions'] = $config['reflection'];
|
||||
if (is_string($config['conventions']) && strtolower($config['conventions']) === 'conventional') {
|
||||
$config['conventions'] = 'Static';
|
||||
}
|
||||
} else {
|
||||
$conventionsServiceName = 'conventions';
|
||||
}
|
||||
|
||||
if (!$config['conventions']) {
|
||||
$conventions = null;
|
||||
|
||||
} elseif (is_string($config['conventions'])) {
|
||||
$conventions = $builder->addDefinition($this->prefix("$name.$conventionsServiceName"))
|
||||
->setFactory(preg_match('#^[a-z]+\z#i', $config['conventions'])
|
||||
? 'Nette\Database\Conventions\\' . ucfirst($config['conventions']) . 'Conventions'
|
||||
: $config['conventions'])
|
||||
->setArguments(strtolower($config['conventions']) === 'discovered' ? [$structure] : [])
|
||||
->setAutowired($config['autowired']);
|
||||
|
||||
} else {
|
||||
$class = method_exists(Nette\DI\Helpers::class, 'filterArguments') ? Nette\DI\Helpers::class : Nette\DI\Compiler::class;
|
||||
$conventions = $class::filterArguments([$config['conventions']])[0];
|
||||
}
|
||||
|
||||
$builder->addDefinition($this->prefix("$name.context"))
|
||||
->setFactory(Nette\Database\Context::class, [$connection, $structure, $conventions])
|
||||
->setAutowired($config['autowired']);
|
||||
|
||||
if ($config['debugger']) {
|
||||
$connection->addSetup('@Tracy\BlueScreen::addPanel', [
|
||||
'Nette\Bridges\DatabaseTracy\ConnectionPanel::renderException',
|
||||
]);
|
||||
if ($this->debugMode) {
|
||||
$connection->addSetup('Nette\Database\Helpers::createDebugPanel', [$connection, !empty($config['explain']), $name]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->name === 'database') {
|
||||
$builder->addAlias($this->prefix($name), $this->prefix("$name.connection"));
|
||||
$builder->addAlias("nette.database.$name", $this->prefix($name));
|
||||
$builder->addAlias("nette.database.$name.context", $this->prefix("$name.context"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
namespace Nette\Bridges\DatabaseTracy;
|
||||
|
||||
use Nette;
|
||||
use Nette\Database\Helpers;
|
||||
use Tracy;
|
||||
|
||||
|
||||
/**
|
||||
* Debug panel for Nette\Database.
|
||||
*/
|
||||
class ConnectionPanel implements Tracy\IBarPanel
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
/** @var int */
|
||||
public $maxQueries = 100;
|
||||
|
||||
/** @var string */
|
||||
public $name;
|
||||
|
||||
/** @var bool|string explain queries? */
|
||||
public $explain = true;
|
||||
|
||||
/** @var bool */
|
||||
public $disabled = false;
|
||||
|
||||
/** @var int logged time */
|
||||
private $totalTime = 0;
|
||||
|
||||
/** @var int */
|
||||
private $count = 0;
|
||||
|
||||
/** @var array */
|
||||
private $queries = [];
|
||||
|
||||
|
||||
public function __construct(Nette\Database\Connection $connection)
|
||||
{
|
||||
$connection->onQuery[] = [$this, 'logQuery'];
|
||||
}
|
||||
|
||||
|
||||
public function logQuery(Nette\Database\Connection $connection, $result)
|
||||
{
|
||||
if ($this->disabled) {
|
||||
return;
|
||||
}
|
||||
$this->count++;
|
||||
|
||||
$source = null;
|
||||
$trace = $result instanceof \PDOException ? $result->getTrace() : debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
|
||||
foreach ($trace as $row) {
|
||||
if (isset($row['file']) && is_file($row['file']) && !Tracy\Debugger::getBluescreen()->isCollapsed($row['file'])) {
|
||||
if ((isset($row['function']) && strpos($row['function'], 'call_user_func') === 0)
|
||||
|| (isset($row['class']) && is_subclass_of($row['class'], '\\Nette\\Database\\Connection'))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
$source = [$row['file'], (int) $row['line']];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($result instanceof Nette\Database\ResultSet) {
|
||||
$this->totalTime += $result->getTime();
|
||||
if ($this->count < $this->maxQueries) {
|
||||
$this->queries[] = [$connection, $result->getQueryString(), $result->getParameters(), $source, $result->getTime(), $result->getRowCount(), null];
|
||||
}
|
||||
|
||||
} elseif ($result instanceof \PDOException && $this->count < $this->maxQueries) {
|
||||
$this->queries[] = [$connection, $result->queryString, null, $source, null, null, $result->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static function renderException($e)
|
||||
{
|
||||
if (!$e instanceof \PDOException) {
|
||||
return;
|
||||
}
|
||||
if (isset($e->queryString)) {
|
||||
$sql = $e->queryString;
|
||||
|
||||
} elseif ($item = Tracy\Helpers::findTrace($e->getTrace(), 'PDO::prepare')) {
|
||||
$sql = $item['args'][0];
|
||||
}
|
||||
return isset($sql) ? [
|
||||
'tab' => 'SQL',
|
||||
'panel' => Helpers::dumpSql($sql),
|
||||
] : null;
|
||||
}
|
||||
|
||||
|
||||
public function getTab()
|
||||
{
|
||||
$name = $this->name;
|
||||
$count = $this->count;
|
||||
$totalTime = $this->totalTime;
|
||||
ob_start(function () {});
|
||||
require __DIR__ . '/templates/ConnectionPanel.tab.phtml';
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
|
||||
public function getPanel()
|
||||
{
|
||||
$this->disabled = true;
|
||||
if (!$this->count) {
|
||||
return;
|
||||
}
|
||||
|
||||
$name = $this->name;
|
||||
$count = $this->count;
|
||||
$totalTime = $this->totalTime;
|
||||
$queries = [];
|
||||
foreach ($this->queries as $query) {
|
||||
list($connection, $sql, $params, $source, $time, $rows, $error) = $query;
|
||||
$explain = null;
|
||||
if (!$error && $this->explain && preg_match('#\s*\(?\s*SELECT\s#iA', $sql)) {
|
||||
try {
|
||||
$cmd = is_string($this->explain) ? $this->explain : 'EXPLAIN';
|
||||
$explain = $connection->queryArgs("$cmd $sql", $params)->fetchAll();
|
||||
} catch (\PDOException $e) {
|
||||
}
|
||||
}
|
||||
$query[] = $explain;
|
||||
$queries[] = $query;
|
||||
}
|
||||
|
||||
ob_start(function () {});
|
||||
require __DIR__ . '/templates/ConnectionPanel.panel.phtml';
|
||||
return ob_get_clean();
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace Nette\Bridges\DatabaseTracy;
|
||||
|
||||
use Nette\Database\Helpers;
|
||||
use Tracy;
|
||||
|
||||
?>
|
||||
<style class="tracy-debug">
|
||||
#tracy-debug td.nette-DbConnectionPanel-sql { background: white !important }
|
||||
#tracy-debug .nette-DbConnectionPanel-source { color: #BBB !important }
|
||||
#tracy-debug .nette-DbConnectionPanel-explain td { white-space: pre }
|
||||
</style>
|
||||
|
||||
<h1 title="<?= htmlspecialchars($connection->getDsn(), ENT_QUOTES, 'UTF-8') ?>">Queries: <?php
|
||||
echo $count, ($totalTime ? sprintf(', time: %0.3f ms', $totalTime * 1000) : ''), ', ', htmlspecialchars($name, ENT_NOQUOTES, 'UTF-8') ?></h1>
|
||||
|
||||
<div class="tracy-inner">
|
||||
<div class="tracy-inner-container">
|
||||
<table>
|
||||
<tr><th>Time ms</th><th>SQL Query</th><th>Rows</th></tr>
|
||||
<?php
|
||||
foreach ($queries as $query):
|
||||
list($connection, $sql, $params, $source, $time, $rows, $error, $explain) = $query;
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<?php if ($error): ?>
|
||||
<span title="<?= htmlspecialchars($error, ENT_IGNORE | ENT_QUOTES, 'UTF-8') ?>">ERROR</span>
|
||||
<?php elseif ($time !== null): echo sprintf('%0.3f', $time * 1000); endif ?>
|
||||
<?php if ($explain): ?>
|
||||
<br /><a class="tracy-toggle tracy-collapsed" data-tracy-ref="^tr .nette-DbConnectionPanel-explain">explain</a>
|
||||
<?php endif ?>
|
||||
</td>
|
||||
<td class="nette-DbConnectionPanel-sql"><?= Helpers::dumpSql($sql, $params, $connection) ?>
|
||||
<?php if ($explain): ?>
|
||||
<table class="tracy-collapsed nette-DbConnectionPanel-explain">
|
||||
<tr>
|
||||
<?php foreach ($explain[0] as $col => $foo): ?>
|
||||
<th><?= htmlspecialchars($col, ENT_NOQUOTES, 'UTF-8') ?></th>
|
||||
<?php endforeach ?>
|
||||
</tr>
|
||||
<?php foreach ($explain as $row): ?>
|
||||
<tr>
|
||||
<?php foreach ($row as $col): ?>
|
||||
<td><?= htmlspecialchars($col, ENT_NOQUOTES, 'UTF-8') ?></td>
|
||||
<?php endforeach ?>
|
||||
</tr>
|
||||
<?php endforeach ?>
|
||||
</table>
|
||||
<?php endif ?>
|
||||
<?php if ($source) echo substr_replace(Tracy\Helpers::editorLink($source[0], $source[1]), ' class="nette-DbConnectionPanel-source"', 2, 0); ?>
|
||||
</td>
|
||||
<td><?= $rows ?></td>
|
||||
</tr>
|
||||
<?php endforeach ?>
|
||||
</table>
|
||||
<?php if (count($queries) < $count): ?><p>...and more</p><?php endif ?>
|
||||
</div>
|
||||
</div>
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Nette\Bridges\DatabaseTracy;
|
||||
|
||||
?>
|
||||
<span title="Nette\Database <?= htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?>">
|
||||
<svg viewBox="0 0 2048 2048"><path fill="<?= $count ? '#b079d6' : '#aaa' ?>" d="M1024 896q237 0 443-43t325-127v170q0 69-103 128t-280 93.5-385 34.5-385-34.5-280-93.5-103-128v-170q119 84 325 127t443 43zm0 768q237 0 443-43t325-127v170q0 69-103 128t-280 93.5-385 34.5-385-34.5-280-93.5-103-128v-170q119 84 325 127t443 43zm0-384q237 0 443-43t325-127v170q0 69-103 128t-280 93.5-385 34.5-385-34.5-280-93.5-103-128v-170q119 84 325 127t443 43zm0-1152q208 0 385 34.5t280 93.5 103 128v128q0 69-103 128t-280 93.5-385 34.5-385-34.5-280-93.5-103-128v-128q0-69 103-128t280-93.5 385-34.5z"/>
|
||||
</svg><span class="tracy-label"><?= ($totalTime ? sprintf('%0.1f ms / ', $totalTime * 1000) : '') . $count ?></span>
|
||||
</span>
|
||||
+293
@@ -0,0 +1,293 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
namespace Nette\Database;
|
||||
|
||||
use Nette;
|
||||
use PDO;
|
||||
use PDOException;
|
||||
|
||||
|
||||
/**
|
||||
* Represents a connection between PHP and a database server.
|
||||
*/
|
||||
class Connection
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
/** @var callable[] function (Connection $connection): void; Occurs after connection is established */
|
||||
public $onConnect;
|
||||
|
||||
/** @var callable[] function (Connection $connection, ResultSet|DriverException $result): void; Occurs after query is executed */
|
||||
public $onQuery;
|
||||
|
||||
/** @var array */
|
||||
private $params;
|
||||
|
||||
/** @var array */
|
||||
private $options;
|
||||
|
||||
/** @var ISupplementalDriver */
|
||||
private $driver;
|
||||
|
||||
/** @var SqlPreprocessor */
|
||||
private $preprocessor;
|
||||
|
||||
/** @var PDO */
|
||||
private $pdo;
|
||||
|
||||
/** @var string|null */
|
||||
private $sql;
|
||||
|
||||
|
||||
public function __construct($dsn, $user = null, $password = null, array $options = null)
|
||||
{
|
||||
if (func_num_args() > 4) { // compatibility
|
||||
trigger_error(__METHOD__ . " fifth argument is deprecated, use \$options['driverClass'].", E_USER_DEPRECATED);
|
||||
$options['driverClass'] = func_get_arg(4);
|
||||
}
|
||||
$this->params = [$dsn, $user, $password];
|
||||
$this->options = (array) $options;
|
||||
|
||||
if (empty($options['lazy'])) {
|
||||
$this->connect();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** @return void */
|
||||
public function connect()
|
||||
{
|
||||
if ($this->pdo) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->pdo = new PDO($this->params[0], $this->params[1], $this->params[2], $this->options);
|
||||
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
} catch (PDOException $e) {
|
||||
throw ConnectionException::from($e);
|
||||
}
|
||||
|
||||
$class = empty($this->options['driverClass'])
|
||||
? 'Nette\Database\Drivers\\' . ucfirst(str_replace('sql', 'Sql', $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME))) . 'Driver'
|
||||
: $this->options['driverClass'];
|
||||
$this->driver = new $class($this, $this->options);
|
||||
$this->preprocessor = new SqlPreprocessor($this);
|
||||
$this->onConnect($this);
|
||||
}
|
||||
|
||||
|
||||
/** @return void */
|
||||
public function reconnect()
|
||||
{
|
||||
$this->disconnect();
|
||||
$this->connect();
|
||||
}
|
||||
|
||||
|
||||
/** @return void */
|
||||
public function disconnect()
|
||||
{
|
||||
$this->pdo = null;
|
||||
}
|
||||
|
||||
|
||||
/** @return string */
|
||||
public function getDsn()
|
||||
{
|
||||
return $this->params[0];
|
||||
}
|
||||
|
||||
|
||||
/** @return PDO */
|
||||
public function getPdo()
|
||||
{
|
||||
$this->connect();
|
||||
return $this->pdo;
|
||||
}
|
||||
|
||||
|
||||
/** @return ISupplementalDriver */
|
||||
public function getSupplementalDriver()
|
||||
{
|
||||
$this->connect();
|
||||
return $this->driver;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string sequence object
|
||||
* @return string
|
||||
*/
|
||||
public function getInsertId($name = null)
|
||||
{
|
||||
try {
|
||||
$res = $this->getPdo()->lastInsertId($name);
|
||||
return $res === false ? '0' : $res;
|
||||
} catch (PDOException $e) {
|
||||
throw $this->driver->convertException($e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string string to be quoted
|
||||
* @param int data type hint
|
||||
* @return string
|
||||
*/
|
||||
public function quote($string, $type = PDO::PARAM_STR)
|
||||
{
|
||||
try {
|
||||
$res = $this->getPdo()->quote($string, $type);
|
||||
} catch (PDOException $e) {
|
||||
throw DriverException::from($e);
|
||||
}
|
||||
if (!is_string($res)) {
|
||||
throw new DriverException('PDO driver is unable to quote string.');
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
||||
/** @return void */
|
||||
public function beginTransaction()
|
||||
{
|
||||
$this->query('::beginTransaction');
|
||||
}
|
||||
|
||||
|
||||
/** @return void */
|
||||
public function commit()
|
||||
{
|
||||
$this->query('::commit');
|
||||
}
|
||||
|
||||
|
||||
/** @return void */
|
||||
public function rollBack()
|
||||
{
|
||||
$this->query('::rollBack');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generates and executes SQL query.
|
||||
* @param string
|
||||
* @return ResultSet
|
||||
*/
|
||||
public function query($sql, ...$params)
|
||||
{
|
||||
list($this->sql, $params) = $this->preprocess($sql, ...$params);
|
||||
try {
|
||||
$result = new ResultSet($this, $this->sql, $params);
|
||||
} catch (PDOException $e) {
|
||||
$this->onQuery($this, $e);
|
||||
throw $e;
|
||||
}
|
||||
$this->onQuery($this, $result);
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string
|
||||
* @return ResultSet
|
||||
*/
|
||||
public function queryArgs($sql, array $params)
|
||||
{
|
||||
return $this->query($sql, ...$params);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return [string, array]
|
||||
*/
|
||||
public function preprocess($sql, ...$params)
|
||||
{
|
||||
$this->connect();
|
||||
return $params
|
||||
? $this->preprocessor->process(func_get_args())
|
||||
: [$sql, []];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getLastQueryString()
|
||||
{
|
||||
return $this->sql;
|
||||
}
|
||||
|
||||
|
||||
/********************* shortcuts ****************d*g**/
|
||||
|
||||
|
||||
/**
|
||||
* Shortcut for query()->fetch()
|
||||
* @param string
|
||||
* @return Row
|
||||
*/
|
||||
public function fetch($sql, ...$params)
|
||||
{
|
||||
return $this->query($sql, ...$params)->fetch();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Shortcut for query()->fetchField()
|
||||
* @param string
|
||||
* @return mixed
|
||||
*/
|
||||
public function fetchField($sql, ...$params)
|
||||
{
|
||||
return $this->query($sql, ...$params)->fetchField();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Shortcut for query()->fetchFields()
|
||||
* @param string
|
||||
* @return array|null
|
||||
*/
|
||||
public function fetchFields($sql, ...$params)
|
||||
{
|
||||
return $this->query($sql, ...$params)->fetchFields();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Shortcut for query()->fetchPairs()
|
||||
* @param string
|
||||
* @return array
|
||||
*/
|
||||
public function fetchPairs($sql, ...$params)
|
||||
{
|
||||
return $this->query($sql, ...$params)->fetchPairs();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Shortcut for query()->fetchAll()
|
||||
* @param string
|
||||
* @return array
|
||||
*/
|
||||
public function fetchAll($sql, ...$params)
|
||||
{
|
||||
return $this->query($sql, ...$params)->fetchAll();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return SqlLiteral
|
||||
*/
|
||||
public static function literal($value, ...$params)
|
||||
{
|
||||
return new SqlLiteral($value, $params);
|
||||
}
|
||||
}
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
namespace Nette\Database;
|
||||
|
||||
use Nette;
|
||||
use Nette\Database\Conventions\StaticConventions;
|
||||
|
||||
|
||||
/**
|
||||
* Database context.
|
||||
*/
|
||||
class Context
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
/** @var Connection */
|
||||
private $connection;
|
||||
|
||||
/** @var IStructure */
|
||||
private $structure;
|
||||
|
||||
/** @var IConventions */
|
||||
private $conventions;
|
||||
|
||||
/** @var Nette\Caching\IStorage */
|
||||
private $cacheStorage;
|
||||
|
||||
|
||||
public function __construct(Connection $connection, IStructure $structure, IConventions $conventions = null, Nette\Caching\IStorage $cacheStorage = null)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
$this->structure = $structure;
|
||||
$this->conventions = $conventions ?: new StaticConventions;
|
||||
$this->cacheStorage = $cacheStorage;
|
||||
}
|
||||
|
||||
|
||||
/** @return void */
|
||||
public function beginTransaction()
|
||||
{
|
||||
$this->connection->beginTransaction();
|
||||
}
|
||||
|
||||
|
||||
/** @return void */
|
||||
public function commit()
|
||||
{
|
||||
$this->connection->commit();
|
||||
}
|
||||
|
||||
|
||||
/** @return void */
|
||||
public function rollBack()
|
||||
{
|
||||
$this->connection->rollBack();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string sequence object
|
||||
* @return string
|
||||
*/
|
||||
public function getInsertId($name = null)
|
||||
{
|
||||
return $this->connection->getInsertId($name);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generates and executes SQL query.
|
||||
* @param string
|
||||
* @return ResultSet
|
||||
*/
|
||||
public function query($sql, ...$params)
|
||||
{
|
||||
return $this->connection->query($sql, ...$params);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string
|
||||
* @return ResultSet
|
||||
*/
|
||||
public function queryArgs($sql, array $params)
|
||||
{
|
||||
return $this->connection->query($sql, ...$params);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string
|
||||
* @return Table\Selection
|
||||
*/
|
||||
public function table($table)
|
||||
{
|
||||
return new Table\Selection($this, $this->conventions, $table, $this->cacheStorage);
|
||||
}
|
||||
|
||||
|
||||
/** @return Connection */
|
||||
public function getConnection()
|
||||
{
|
||||
return $this->connection;
|
||||
}
|
||||
|
||||
|
||||
/** @return IStructure */
|
||||
public function getStructure()
|
||||
{
|
||||
return $this->structure;
|
||||
}
|
||||
|
||||
|
||||
/** @return IConventions */
|
||||
public function getConventions()
|
||||
{
|
||||
return $this->conventions;
|
||||
}
|
||||
|
||||
|
||||
/********************* shortcuts ****************d*g**/
|
||||
|
||||
|
||||
/**
|
||||
* Shortcut for query()->fetch()
|
||||
* @param string
|
||||
* @return Row
|
||||
*/
|
||||
public function fetch($sql, ...$params)
|
||||
{
|
||||
return $this->connection->query($sql, ...$params)->fetch();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Shortcut for query()->fetchField()
|
||||
* @param string
|
||||
* @return mixed
|
||||
*/
|
||||
public function fetchField($sql, ...$params)
|
||||
{
|
||||
return $this->connection->query($sql, ...$params)->fetchField();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Shortcut for query()->fetchFields()
|
||||
* @param string
|
||||
* @return array|null
|
||||
*/
|
||||
public function fetchFields($sql, ...$params)
|
||||
{
|
||||
return $this->connection->query($sql, ...$params)->fetchFields();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Shortcut for query()->fetchPairs()
|
||||
* @param string
|
||||
* @return array
|
||||
*/
|
||||
public function fetchPairs($sql, ...$params)
|
||||
{
|
||||
return $this->connection->query($sql, ...$params)->fetchPairs();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Shortcut for query()->fetchAll()
|
||||
* @param string
|
||||
* @return array
|
||||
*/
|
||||
public function fetchAll($sql, ...$params)
|
||||
{
|
||||
return $this->connection->query($sql, ...$params)->fetchAll();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return SqlLiteral
|
||||
*/
|
||||
public static function literal($value, ...$params)
|
||||
{
|
||||
return new SqlLiteral($value, $params);
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
namespace Nette\Database\Conventions;
|
||||
|
||||
|
||||
/**
|
||||
* Ambiguous reference key exception.
|
||||
*/
|
||||
class AmbiguousReferenceKeyException extends \RuntimeException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
namespace Nette\Database\Conventions;
|
||||
|
||||
use Nette\Database\IConventions;
|
||||
use Nette\Database\IStructure;
|
||||
|
||||
|
||||
/**
|
||||
* Conventions based on database structure.
|
||||
*/
|
||||
class DiscoveredConventions implements IConventions
|
||||
{
|
||||
/** @var IStructure */
|
||||
protected $structure;
|
||||
|
||||
|
||||
public function __construct(IStructure $structure)
|
||||
{
|
||||
$this->structure = $structure;
|
||||
}
|
||||
|
||||
|
||||
public function getPrimary($table)
|
||||
{
|
||||
return $this->structure->getPrimaryKey($table);
|
||||
}
|
||||
|
||||
|
||||
public function getHasManyReference($nsTable, $key)
|
||||
{
|
||||
$candidates = $columnCandidates = [];
|
||||
$targets = $this->structure->getHasManyReference($nsTable);
|
||||
$table = preg_replace('#^(.*\.)?(.*)$#', '$2', $nsTable);
|
||||
|
||||
foreach ($targets as $targetNsTable => $targetColumns) {
|
||||
$targetTable = preg_replace('#^(.*\.)?(.*)$#', '$2', $targetNsTable);
|
||||
if (stripos($targetNsTable, $key) === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($targetColumns as $targetColumn) {
|
||||
if (stripos($targetColumn, $table) !== false) {
|
||||
$columnCandidates[] = $candidate = [$targetNsTable, $targetColumn];
|
||||
if (strcmp($targetTable, $key) === 0 || strcmp($targetNsTable, $key) === 0) {
|
||||
return $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
$candidates[] = [$targetTable, [$targetNsTable, $targetColumn]];
|
||||
}
|
||||
}
|
||||
|
||||
if (count($columnCandidates) === 1) {
|
||||
return $columnCandidates[0];
|
||||
} elseif (count($candidates) === 1) {
|
||||
return $candidates[0][1];
|
||||
}
|
||||
|
||||
foreach ($candidates as $candidate) {
|
||||
if (strtolower($candidate[0]) === strtolower($key)) {
|
||||
return $candidate[1];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($candidates)) {
|
||||
throw new AmbiguousReferenceKeyException('Ambiguous joining column in related call.');
|
||||
}
|
||||
|
||||
if ($this->structure->isRebuilt()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->structure->rebuild();
|
||||
return $this->getHasManyReference($nsTable, $key);
|
||||
}
|
||||
|
||||
|
||||
public function getBelongsToReference($table, $key)
|
||||
{
|
||||
$tableColumns = $this->structure->getBelongsToReference($table);
|
||||
|
||||
foreach ($tableColumns as $column => $targetTable) {
|
||||
if (stripos($column, $key) !== false) {
|
||||
return [$targetTable, $column];
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->structure->isRebuilt()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->structure->rebuild();
|
||||
return $this->getBelongsToReference($table, $key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
namespace Nette\Database\Conventions;
|
||||
|
||||
use Nette;
|
||||
use Nette\Database\IConventions;
|
||||
|
||||
|
||||
/**
|
||||
* Conventions based on static definition.
|
||||
*/
|
||||
class StaticConventions implements IConventions
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
/** @var string */
|
||||
protected $primary;
|
||||
|
||||
/** @var string */
|
||||
protected $foreign;
|
||||
|
||||
/** @var string */
|
||||
protected $table;
|
||||
|
||||
|
||||
/**
|
||||
* Create static conventional structure.
|
||||
* @param string %s stands for table name
|
||||
* @param string %1$s stands for key used after ->, %2$s for table name
|
||||
* @param string %1$s stands for key used after ->, %2$s for table name
|
||||
*/
|
||||
public function __construct($primary = 'id', $foreign = '%s_id', $table = '%s')
|
||||
{
|
||||
$this->primary = $primary;
|
||||
$this->foreign = $foreign;
|
||||
$this->table = $table;
|
||||
}
|
||||
|
||||
|
||||
public function getPrimary($table)
|
||||
{
|
||||
return sprintf($this->primary, $this->getColumnFromTable($table));
|
||||
}
|
||||
|
||||
|
||||
public function getHasManyReference($table, $key)
|
||||
{
|
||||
$table = $this->getColumnFromTable($table);
|
||||
return [
|
||||
sprintf($this->table, $key, $table),
|
||||
sprintf($this->foreign, $table, $key),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
public function getBelongsToReference($table, $key)
|
||||
{
|
||||
$table = $this->getColumnFromTable($table);
|
||||
return [
|
||||
sprintf($this->table, $key, $table),
|
||||
sprintf($this->foreign, $key, $table),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
protected function getColumnFromTable($name)
|
||||
{
|
||||
if ($this->table !== '%s' && preg_match('(^' . str_replace('%s', '(.*)', preg_quote($this->table)) . '\z)', $name, $match)) {
|
||||
return $match[1];
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
namespace Nette\Database;
|
||||
|
||||
|
||||
/**
|
||||
* Base class for all errors in the driver or SQL server.
|
||||
*/
|
||||
class DriverException extends \PDOException
|
||||
{
|
||||
/** @var string */
|
||||
public $queryString;
|
||||
|
||||
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
public static function from(\PDOException $src)
|
||||
{
|
||||
$e = new static($src->message, null, $src);
|
||||
if (!$src->errorInfo && preg_match('#SQLSTATE\[(.*?)\] \[(.*?)\] (.*)#A', $src->message, $m)) {
|
||||
$m[2] = (int) $m[2];
|
||||
$e->errorInfo = array_slice($m, 1);
|
||||
$e->code = $m[1];
|
||||
} else {
|
||||
$e->errorInfo = $src->errorInfo;
|
||||
$e->code = $src->code;
|
||||
}
|
||||
return $e;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return int|string|null Driver-specific error code
|
||||
*/
|
||||
public function getDriverCode()
|
||||
{
|
||||
return isset($this->errorInfo[1]) ? $this->errorInfo[1] : null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return string|null SQLSTATE error code
|
||||
*/
|
||||
public function getSqlState()
|
||||
{
|
||||
return isset($this->errorInfo[0]) ? $this->errorInfo[0] : null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return string|null SQL command
|
||||
*/
|
||||
public function getQueryString()
|
||||
{
|
||||
return $this->queryString;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
namespace Nette\Database\Drivers;
|
||||
|
||||
use Nette;
|
||||
|
||||
|
||||
/**
|
||||
* Supplemental MS SQL database driver.
|
||||
*/
|
||||
class MsSqlDriver implements Nette\Database\ISupplementalDriver
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
public function convertException(\PDOException $e)
|
||||
{
|
||||
return Nette\Database\DriverException::from($e);
|
||||
}
|
||||
|
||||
|
||||
/********************* SQL ****************d*g**/
|
||||
|
||||
|
||||
public function delimite($name)
|
||||
{
|
||||
// @see https://msdn.microsoft.com/en-us/library/ms176027.aspx
|
||||
return '[' . str_replace(['[', ']'], ['[[', ']]'], $name) . ']';
|
||||
}
|
||||
|
||||
|
||||
public function formatBool($value)
|
||||
{
|
||||
return $value ? '1' : '0';
|
||||
}
|
||||
|
||||
|
||||
public function formatDateTime(/*\DateTimeInterface*/ $value)
|
||||
{
|
||||
return $value->format("'Y-m-d H:i:s'");
|
||||
}
|
||||
|
||||
|
||||
public function formatDateInterval(\DateInterval $value)
|
||||
{
|
||||
throw new Nette\NotSupportedException;
|
||||
}
|
||||
|
||||
|
||||
public function formatLike($value, $pos)
|
||||
{
|
||||
$value = strtr($value, ["'" => "''", '%' => '[%]', '_' => '[_]', '[' => '[[]']);
|
||||
return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'");
|
||||
}
|
||||
|
||||
|
||||
public function applyLimit(&$sql, $limit, $offset)
|
||||
{
|
||||
if ($offset) {
|
||||
throw new Nette\NotSupportedException('Offset is not supported by this database.');
|
||||
|
||||
} elseif ($limit < 0) {
|
||||
throw new Nette\InvalidArgumentException('Negative offset or limit.');
|
||||
|
||||
} elseif ($limit !== null) {
|
||||
$sql = preg_replace('#^\s*(SELECT(\s+DISTINCT|\s+ALL)?|UPDATE|DELETE)#i', '$0 TOP ' . (int) $limit, $sql, 1, $count);
|
||||
if (!$count) {
|
||||
throw new Nette\InvalidArgumentException('SQL query must begin with SELECT, UPDATE or DELETE command.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function normalizeRow($row)
|
||||
{
|
||||
return $row;
|
||||
}
|
||||
|
||||
|
||||
/********************* reflection ****************d*g**/
|
||||
|
||||
|
||||
public function getTables()
|
||||
{
|
||||
throw new Nette\NotImplementedException;
|
||||
}
|
||||
|
||||
|
||||
public function getColumns($table)
|
||||
{
|
||||
throw new Nette\NotImplementedException;
|
||||
}
|
||||
|
||||
|
||||
public function getIndexes($table)
|
||||
{
|
||||
throw new Nette\NotImplementedException;
|
||||
}
|
||||
|
||||
|
||||
public function getForeignKeys($table)
|
||||
{
|
||||
throw new Nette\NotImplementedException;
|
||||
}
|
||||
|
||||
|
||||
public function getColumnTypes(\PDOStatement $statement)
|
||||
{
|
||||
return Nette\Database\Helpers::detectTypes($statement);
|
||||
}
|
||||
|
||||
|
||||
public function isSupported($item)
|
||||
{
|
||||
return $item === self::SUPPORT_SUBSELECT;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
namespace Nette\Database\Drivers;
|
||||
|
||||
use Nette;
|
||||
|
||||
|
||||
/**
|
||||
* Supplemental MySQL database driver.
|
||||
*/
|
||||
class MySqlDriver implements Nette\Database\ISupplementalDriver
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
const ERROR_ACCESS_DENIED = 1045;
|
||||
|
||||
const ERROR_DUPLICATE_ENTRY = 1062;
|
||||
|
||||
const ERROR_DATA_TRUNCATED = 1265;
|
||||
|
||||
/** @var Nette\Database\Connection */
|
||||
private $connection;
|
||||
|
||||
|
||||
/**
|
||||
* Driver options:
|
||||
* - charset => character encoding to set (default is utf8 or utf8mb4 since MySQL 5.5.3)
|
||||
* - sqlmode => see http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html
|
||||
*/
|
||||
public function __construct(Nette\Database\Connection $connection, array $options)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
$charset = isset($options['charset'])
|
||||
? $options['charset']
|
||||
: (version_compare($connection->getPdo()->getAttribute(\PDO::ATTR_SERVER_VERSION), '5.5.3', '>=') ? 'utf8mb4' : 'utf8');
|
||||
if ($charset) {
|
||||
$connection->query("SET NAMES '$charset'");
|
||||
}
|
||||
if (isset($options['sqlmode'])) {
|
||||
$connection->query("SET sql_mode='$options[sqlmode]'");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function convertException(\PDOException $e)
|
||||
{
|
||||
$code = isset($e->errorInfo[1]) ? $e->errorInfo[1] : null;
|
||||
if (in_array($code, [1216, 1217, 1451, 1452, 1701], true)) {
|
||||
return Nette\Database\ForeignKeyConstraintViolationException::from($e);
|
||||
|
||||
} elseif (in_array($code, [1062, 1557, 1569, 1586], true)) {
|
||||
return Nette\Database\UniqueConstraintViolationException::from($e);
|
||||
|
||||
} elseif ($code >= 2001 && $code <= 2028) {
|
||||
return Nette\Database\ConnectionException::from($e);
|
||||
|
||||
} elseif (in_array($code, [1048, 1121, 1138, 1171, 1252, 1263, 1566], true)) {
|
||||
return Nette\Database\NotNullConstraintViolationException::from($e);
|
||||
|
||||
} else {
|
||||
return Nette\Database\DriverException::from($e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/********************* SQL ****************d*g**/
|
||||
|
||||
|
||||
public function delimite($name)
|
||||
{
|
||||
// @see http://dev.mysql.com/doc/refman/5.0/en/identifiers.html
|
||||
return '`' . str_replace('`', '``', $name) . '`';
|
||||
}
|
||||
|
||||
|
||||
public function formatBool($value)
|
||||
{
|
||||
return $value ? '1' : '0';
|
||||
}
|
||||
|
||||
|
||||
public function formatDateTime(/*\DateTimeInterface*/ $value)
|
||||
{
|
||||
return $value->format("'Y-m-d H:i:s'");
|
||||
}
|
||||
|
||||
|
||||
public function formatDateInterval(\DateInterval $value)
|
||||
{
|
||||
return $value->format("'%r%h:%I:%S'");
|
||||
}
|
||||
|
||||
|
||||
public function formatLike($value, $pos)
|
||||
{
|
||||
$value = str_replace('\\', '\\\\', $value);
|
||||
$value = addcslashes(substr($this->connection->quote($value), 1, -1), '%_');
|
||||
return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'");
|
||||
}
|
||||
|
||||
|
||||
public function applyLimit(&$sql, $limit, $offset)
|
||||
{
|
||||
if ($limit < 0 || $offset < 0) {
|
||||
throw new Nette\InvalidArgumentException('Negative offset or limit.');
|
||||
|
||||
} elseif ($limit !== null || $offset) {
|
||||
// see http://dev.mysql.com/doc/refman/5.0/en/select.html
|
||||
$sql .= ' LIMIT ' . ($limit === null ? '18446744073709551615' : (int) $limit)
|
||||
. ($offset ? ' OFFSET ' . (int) $offset : '');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function normalizeRow($row)
|
||||
{
|
||||
return $row;
|
||||
}
|
||||
|
||||
|
||||
/********************* reflection ****************d*g**/
|
||||
|
||||
|
||||
public function getTables()
|
||||
{
|
||||
$tables = [];
|
||||
foreach ($this->connection->query('SHOW FULL TABLES') as $row) {
|
||||
$tables[] = [
|
||||
'name' => $row[0],
|
||||
'view' => isset($row[1]) && $row[1] === 'VIEW',
|
||||
];
|
||||
}
|
||||
return $tables;
|
||||
}
|
||||
|
||||
|
||||
public function getColumns($table)
|
||||
{
|
||||
$columns = [];
|
||||
foreach ($this->connection->query('SHOW FULL COLUMNS FROM ' . $this->delimite($table)) as $row) {
|
||||
$row = array_change_key_case((array) $row, CASE_LOWER);
|
||||
$type = explode('(', $row['type']);
|
||||
$columns[] = [
|
||||
'name' => $row['field'],
|
||||
'table' => $table,
|
||||
'nativetype' => strtoupper($type[0]),
|
||||
'size' => isset($type[1]) ? (int) $type[1] : null,
|
||||
'unsigned' => (bool) strstr($row['type'], 'unsigned'),
|
||||
'nullable' => $row['null'] === 'YES',
|
||||
'default' => $row['default'],
|
||||
'autoincrement' => $row['extra'] === 'auto_increment',
|
||||
'primary' => $row['key'] === 'PRI',
|
||||
'vendor' => (array) $row,
|
||||
];
|
||||
}
|
||||
return $columns;
|
||||
}
|
||||
|
||||
|
||||
public function getIndexes($table)
|
||||
{
|
||||
$indexes = [];
|
||||
foreach ($this->connection->query('SHOW INDEX FROM ' . $this->delimite($table)) as $row) {
|
||||
$row = array_change_key_case((array) $row, CASE_LOWER);
|
||||
$indexes[$row['key_name']]['name'] = $row['key_name'];
|
||||
$indexes[$row['key_name']]['unique'] = !$row['non_unique'];
|
||||
$indexes[$row['key_name']]['primary'] = $row['key_name'] === 'PRIMARY';
|
||||
$indexes[$row['key_name']]['columns'][$row['seq_in_index'] - 1] = $row['column_name'];
|
||||
}
|
||||
return array_values($indexes);
|
||||
}
|
||||
|
||||
|
||||
public function getForeignKeys($table)
|
||||
{
|
||||
$keys = [];
|
||||
$query = 'SELECT CONSTRAINT_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME FROM information_schema.KEY_COLUMN_USAGE '
|
||||
. 'WHERE TABLE_SCHEMA = DATABASE() AND REFERENCED_TABLE_NAME IS NOT NULL AND TABLE_NAME = ' . $this->connection->quote($table);
|
||||
|
||||
foreach ($this->connection->query($query) as $id => $row) {
|
||||
$row = array_change_key_case((array) $row, CASE_LOWER);
|
||||
$keys[$id]['name'] = $row['constraint_name']; // foreign key name
|
||||
$keys[$id]['local'] = $row['column_name']; // local columns
|
||||
$keys[$id]['table'] = $row['referenced_table_name']; // referenced table
|
||||
$keys[$id]['foreign'] = $row['referenced_column_name']; // referenced columns
|
||||
}
|
||||
|
||||
return array_values($keys);
|
||||
}
|
||||
|
||||
|
||||
public function getColumnTypes(\PDOStatement $statement)
|
||||
{
|
||||
$types = [];
|
||||
$count = $statement->columnCount();
|
||||
for ($col = 0; $col < $count; $col++) {
|
||||
$meta = $statement->getColumnMeta($col);
|
||||
if (isset($meta['native_type'])) {
|
||||
$types[$meta['name']] = $type = Nette\Database\Helpers::detectType($meta['native_type']);
|
||||
if ($type === Nette\Database\IStructure::FIELD_TIME) {
|
||||
$types[$meta['name']] = Nette\Database\IStructure::FIELD_TIME_INTERVAL;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $types;
|
||||
}
|
||||
|
||||
|
||||
public function isSupported($item)
|
||||
{
|
||||
// MULTI_COLUMN_AS_OR_COND due to mysql bugs:
|
||||
// - http://bugs.mysql.com/bug.php?id=31188
|
||||
// - http://bugs.mysql.com/bug.php?id=35819
|
||||
// and more.
|
||||
return $item === self::SUPPORT_SELECT_UNGROUPED_COLUMNS || $item === self::SUPPORT_MULTI_COLUMN_AS_OR_COND;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
namespace Nette\Database\Drivers;
|
||||
|
||||
use Nette;
|
||||
|
||||
|
||||
/**
|
||||
* Supplemental Oracle database driver.
|
||||
*/
|
||||
class OciDriver implements Nette\Database\ISupplementalDriver
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
/** @var Nette\Database\Connection */
|
||||
private $connection;
|
||||
|
||||
/** @var string Datetime format */
|
||||
private $fmtDateTime;
|
||||
|
||||
|
||||
public function __construct(Nette\Database\Connection $connection, array $options)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
$this->fmtDateTime = isset($options['formatDateTime']) ? $options['formatDateTime'] : 'U';
|
||||
}
|
||||
|
||||
|
||||
public function convertException(\PDOException $e)
|
||||
{
|
||||
$code = isset($e->errorInfo[1]) ? $e->errorInfo[1] : null;
|
||||
if (in_array($code, [1, 2299, 38911], true)) {
|
||||
return Nette\Database\UniqueConstraintViolationException::from($e);
|
||||
|
||||
} elseif (in_array($code, [1400], true)) {
|
||||
return Nette\Database\NotNullConstraintViolationException::from($e);
|
||||
|
||||
} elseif (in_array($code, [2266, 2291, 2292], true)) {
|
||||
return Nette\Database\ForeignKeyConstraintViolationException::from($e);
|
||||
|
||||
} else {
|
||||
return Nette\Database\DriverException::from($e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/********************* SQL ****************d*g**/
|
||||
|
||||
|
||||
public function delimite($name)
|
||||
{
|
||||
// @see http://download.oracle.com/docs/cd/B10500_01/server.920/a96540/sql_elements9a.htm
|
||||
return '"' . str_replace('"', '""', $name) . '"';
|
||||
}
|
||||
|
||||
|
||||
public function formatBool($value)
|
||||
{
|
||||
return $value ? '1' : '0';
|
||||
}
|
||||
|
||||
|
||||
public function formatDateTime(/*\DateTimeInterface*/ $value)
|
||||
{
|
||||
return $value->format($this->fmtDateTime);
|
||||
}
|
||||
|
||||
|
||||
public function formatDateInterval(\DateInterval $value)
|
||||
{
|
||||
throw new Nette\NotSupportedException;
|
||||
}
|
||||
|
||||
|
||||
public function formatLike($value, $pos)
|
||||
{
|
||||
throw new Nette\NotImplementedException;
|
||||
}
|
||||
|
||||
|
||||
public function applyLimit(&$sql, $limit, $offset)
|
||||
{
|
||||
if ($limit < 0 || $offset < 0) {
|
||||
throw new Nette\InvalidArgumentException('Negative offset or limit.');
|
||||
|
||||
} elseif ($offset) {
|
||||
// see http://www.oracle.com/technology/oramag/oracle/06-sep/o56asktom.html
|
||||
$sql = 'SELECT * FROM (SELECT t.*, ROWNUM AS "__rnum" FROM (' . $sql . ') t '
|
||||
. ($limit !== null ? 'WHERE ROWNUM <= ' . ((int) $offset + (int) $limit) : '')
|
||||
. ') WHERE "__rnum" > ' . (int) $offset;
|
||||
|
||||
} elseif ($limit !== null) {
|
||||
$sql = 'SELECT * FROM (' . $sql . ') WHERE ROWNUM <= ' . (int) $limit;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function normalizeRow($row)
|
||||
{
|
||||
return $row;
|
||||
}
|
||||
|
||||
|
||||
/********************* reflection ****************d*g**/
|
||||
|
||||
|
||||
public function getTables()
|
||||
{
|
||||
$tables = [];
|
||||
foreach ($this->connection->query('SELECT * FROM cat') as $row) {
|
||||
if ($row[1] === 'TABLE' || $row[1] === 'VIEW') {
|
||||
$tables[] = [
|
||||
'name' => $row[0],
|
||||
'view' => $row[1] === 'VIEW',
|
||||
];
|
||||
}
|
||||
}
|
||||
return $tables;
|
||||
}
|
||||
|
||||
|
||||
public function getColumns($table)
|
||||
{
|
||||
throw new Nette\NotImplementedException;
|
||||
}
|
||||
|
||||
|
||||
public function getIndexes($table)
|
||||
{
|
||||
throw new Nette\NotImplementedException;
|
||||
}
|
||||
|
||||
|
||||
public function getForeignKeys($table)
|
||||
{
|
||||
throw new Nette\NotImplementedException;
|
||||
}
|
||||
|
||||
|
||||
public function getColumnTypes(\PDOStatement $statement)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
public function isSupported($item)
|
||||
{
|
||||
return $item === self::SUPPORT_SEQUENCE || $item === self::SUPPORT_SUBSELECT;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
namespace Nette\Database\Drivers;
|
||||
|
||||
use Nette;
|
||||
|
||||
|
||||
/**
|
||||
* Supplemental ODBC database driver.
|
||||
*/
|
||||
class OdbcDriver implements Nette\Database\ISupplementalDriver
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
public function convertException(\PDOException $e)
|
||||
{
|
||||
return Nette\Database\DriverException::from($e);
|
||||
}
|
||||
|
||||
|
||||
/********************* SQL ****************d*g**/
|
||||
|
||||
|
||||
public function delimite($name)
|
||||
{
|
||||
return '[' . str_replace(['[', ']'], ['[[', ']]'], $name) . ']';
|
||||
}
|
||||
|
||||
|
||||
public function formatBool($value)
|
||||
{
|
||||
return $value ? '1' : '0';
|
||||
}
|
||||
|
||||
|
||||
public function formatDateTime(/*\DateTimeInterface*/ $value)
|
||||
{
|
||||
return $value->format('#m/d/Y H:i:s#');
|
||||
}
|
||||
|
||||
|
||||
public function formatDateInterval(\DateInterval $value)
|
||||
{
|
||||
throw new Nette\NotSupportedException;
|
||||
}
|
||||
|
||||
|
||||
public function formatLike($value, $pos)
|
||||
{
|
||||
$value = strtr($value, ["'" => "''", '%' => '[%]', '_' => '[_]', '[' => '[[]']);
|
||||
return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'");
|
||||
}
|
||||
|
||||
|
||||
public function applyLimit(&$sql, $limit, $offset)
|
||||
{
|
||||
if ($offset) {
|
||||
throw new Nette\NotSupportedException('Offset is not supported by this database.');
|
||||
|
||||
} elseif ($limit < 0) {
|
||||
throw new Nette\InvalidArgumentException('Negative offset or limit.');
|
||||
|
||||
} elseif ($limit !== null) {
|
||||
$sql = preg_replace('#^\s*(SELECT(\s+DISTINCT|\s+ALL)?|UPDATE|DELETE)#i', '$0 TOP ' . (int) $limit, $sql, 1, $count);
|
||||
if (!$count) {
|
||||
throw new Nette\InvalidArgumentException('SQL query must begin with SELECT, UPDATE or DELETE command.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function normalizeRow($row)
|
||||
{
|
||||
return $row;
|
||||
}
|
||||
|
||||
|
||||
/********************* reflection ****************d*g**/
|
||||
|
||||
|
||||
public function getTables()
|
||||
{
|
||||
throw new Nette\NotImplementedException;
|
||||
}
|
||||
|
||||
|
||||
public function getColumns($table)
|
||||
{
|
||||
throw new Nette\NotImplementedException;
|
||||
}
|
||||
|
||||
|
||||
public function getIndexes($table)
|
||||
{
|
||||
throw new Nette\NotImplementedException;
|
||||
}
|
||||
|
||||
|
||||
public function getForeignKeys($table)
|
||||
{
|
||||
throw new Nette\NotImplementedException;
|
||||
}
|
||||
|
||||
|
||||
public function getColumnTypes(\PDOStatement $statement)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
public function isSupported($item)
|
||||
{
|
||||
return $item === self::SUPPORT_SUBSELECT;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
namespace Nette\Database\Drivers;
|
||||
|
||||
use Nette;
|
||||
|
||||
|
||||
/**
|
||||
* Supplemental PostgreSQL database driver.
|
||||
*/
|
||||
class PgSqlDriver implements Nette\Database\ISupplementalDriver
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
/** @var Nette\Database\Connection */
|
||||
private $connection;
|
||||
|
||||
|
||||
public function __construct(Nette\Database\Connection $connection, array $options)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
}
|
||||
|
||||
|
||||
public function convertException(\PDOException $e)
|
||||
{
|
||||
$code = isset($e->errorInfo[0]) ? $e->errorInfo[0] : null;
|
||||
if ($code === '0A000' && strpos($e->getMessage(), 'truncate') !== false) {
|
||||
return Nette\Database\ForeignKeyConstraintViolationException::from($e);
|
||||
|
||||
} elseif ($code === '23502') {
|
||||
return Nette\Database\NotNullConstraintViolationException::from($e);
|
||||
|
||||
} elseif ($code === '23503') {
|
||||
return Nette\Database\ForeignKeyConstraintViolationException::from($e);
|
||||
|
||||
} elseif ($code === '23505') {
|
||||
return Nette\Database\UniqueConstraintViolationException::from($e);
|
||||
|
||||
} elseif ($code === '08006') {
|
||||
return Nette\Database\ConnectionException::from($e);
|
||||
|
||||
} else {
|
||||
return Nette\Database\DriverException::from($e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/********************* SQL ****************d*g**/
|
||||
|
||||
|
||||
public function delimite($name)
|
||||
{
|
||||
// @see http://www.postgresql.org/docs/8.2/static/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS
|
||||
return '"' . str_replace('"', '""', $name) . '"';
|
||||
}
|
||||
|
||||
|
||||
public function formatBool($value)
|
||||
{
|
||||
return $value ? 'TRUE' : 'FALSE';
|
||||
}
|
||||
|
||||
|
||||
public function formatDateTime(/*\DateTimeInterface*/ $value)
|
||||
{
|
||||
return $value->format("'Y-m-d H:i:s'");
|
||||
}
|
||||
|
||||
|
||||
public function formatDateInterval(\DateInterval $value)
|
||||
{
|
||||
throw new Nette\NotSupportedException;
|
||||
}
|
||||
|
||||
|
||||
public function formatLike($value, $pos)
|
||||
{
|
||||
$bs = substr($this->connection->quote('\\'), 1, -1); // standard_conforming_strings = on/off
|
||||
$value = substr($this->connection->quote($value), 1, -1);
|
||||
$value = strtr($value, ['%' => $bs . '%', '_' => $bs . '_', '\\' => '\\\\']);
|
||||
return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'");
|
||||
}
|
||||
|
||||
|
||||
public function applyLimit(&$sql, $limit, $offset)
|
||||
{
|
||||
if ($limit < 0 || $offset < 0) {
|
||||
throw new Nette\InvalidArgumentException('Negative offset or limit.');
|
||||
}
|
||||
if ($limit !== null) {
|
||||
$sql .= ' LIMIT ' . (int) $limit;
|
||||
}
|
||||
if ($offset) {
|
||||
$sql .= ' OFFSET ' . (int) $offset;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function normalizeRow($row)
|
||||
{
|
||||
return $row;
|
||||
}
|
||||
|
||||
|
||||
/********************* reflection ****************d*g**/
|
||||
|
||||
|
||||
public function getTables()
|
||||
{
|
||||
$tables = [];
|
||||
foreach ($this->connection->query("
|
||||
SELECT DISTINCT ON (c.relname)
|
||||
c.relname::varchar AS name,
|
||||
c.relkind IN ('v', 'm') AS view,
|
||||
n.nspname::varchar || '.' || c.relname::varchar AS \"fullName\"
|
||||
FROM
|
||||
pg_catalog.pg_class AS c
|
||||
JOIN pg_catalog.pg_namespace AS n ON n.oid = c.relnamespace
|
||||
WHERE
|
||||
c.relkind IN ('r', 'v', 'm')
|
||||
AND n.nspname = ANY (pg_catalog.current_schemas(FALSE))
|
||||
ORDER BY
|
||||
c.relname
|
||||
") as $row) {
|
||||
$tables[] = (array) $row;
|
||||
}
|
||||
|
||||
return $tables;
|
||||
}
|
||||
|
||||
|
||||
public function getColumns($table)
|
||||
{
|
||||
$columns = [];
|
||||
foreach ($this->connection->query("
|
||||
SELECT
|
||||
a.attname::varchar AS name,
|
||||
c.relname::varchar AS table,
|
||||
upper(t.typname) AS nativetype,
|
||||
CASE WHEN a.atttypmod = -1 THEN NULL ELSE a.atttypmod -4 END AS size,
|
||||
FALSE AS unsigned,
|
||||
NOT (a.attnotnull OR t.typtype = 'd' AND t.typnotnull) AS nullable,
|
||||
pg_catalog.pg_get_expr(ad.adbin, 'pg_catalog.pg_attrdef'::regclass)::varchar AS default,
|
||||
coalesce(co.contype = 'p' AND strpos(ad.adsrc, 'nextval') = 1, FALSE) AS autoincrement,
|
||||
coalesce(co.contype = 'p', FALSE) AS primary,
|
||||
substring(pg_catalog.pg_get_expr(ad.adbin, 'pg_catalog.pg_attrdef'::regclass) from 'nextval[(]''\"?([^''\"]+)') AS sequence
|
||||
FROM
|
||||
pg_catalog.pg_attribute AS a
|
||||
JOIN pg_catalog.pg_class AS c ON a.attrelid = c.oid
|
||||
JOIN pg_catalog.pg_type AS t ON a.atttypid = t.oid
|
||||
LEFT JOIN pg_catalog.pg_attrdef AS ad ON ad.adrelid = c.oid AND ad.adnum = a.attnum
|
||||
LEFT JOIN pg_catalog.pg_constraint AS co ON co.connamespace = c.relnamespace AND contype = 'p' AND co.conrelid = c.oid AND a.attnum = ANY(co.conkey)
|
||||
WHERE
|
||||
c.relkind IN ('r', 'v')
|
||||
AND c.oid = {$this->connection->quote($this->delimiteFQN($table))}::regclass
|
||||
AND a.attnum > 0
|
||||
AND NOT a.attisdropped
|
||||
ORDER BY
|
||||
a.attnum
|
||||
") as $row) {
|
||||
$column = (array) $row;
|
||||
$column['vendor'] = $column;
|
||||
unset($column['sequence']);
|
||||
|
||||
$columns[] = $column;
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
|
||||
public function getIndexes($table)
|
||||
{
|
||||
$indexes = [];
|
||||
foreach ($this->connection->query("
|
||||
SELECT
|
||||
c2.relname::varchar AS name,
|
||||
i.indisunique AS unique,
|
||||
i.indisprimary AS primary,
|
||||
a.attname::varchar AS column
|
||||
FROM
|
||||
pg_catalog.pg_class AS c1
|
||||
JOIN pg_catalog.pg_index AS i ON c1.oid = i.indrelid
|
||||
JOIN pg_catalog.pg_class AS c2 ON i.indexrelid = c2.oid
|
||||
LEFT JOIN pg_catalog.pg_attribute AS a ON c1.oid = a.attrelid AND a.attnum = ANY(i.indkey)
|
||||
WHERE
|
||||
c1.relkind = 'r'
|
||||
AND c1.oid = {$this->connection->quote($this->delimiteFQN($table))}::regclass
|
||||
") as $row) {
|
||||
$indexes[$row['name']]['name'] = $row['name'];
|
||||
$indexes[$row['name']]['unique'] = $row['unique'];
|
||||
$indexes[$row['name']]['primary'] = $row['primary'];
|
||||
$indexes[$row['name']]['columns'][] = $row['column'];
|
||||
}
|
||||
|
||||
return array_values($indexes);
|
||||
}
|
||||
|
||||
|
||||
public function getForeignKeys($table)
|
||||
{
|
||||
/* Does't work with multicolumn foreign keys */
|
||||
return $this->connection->query("
|
||||
SELECT
|
||||
co.conname::varchar AS name,
|
||||
al.attname::varchar AS local,
|
||||
nf.nspname || '.' || cf.relname::varchar AS table,
|
||||
af.attname::varchar AS foreign
|
||||
FROM
|
||||
pg_catalog.pg_constraint AS co
|
||||
JOIN pg_catalog.pg_class AS cl ON co.conrelid = cl.oid
|
||||
JOIN pg_catalog.pg_class AS cf ON co.confrelid = cf.oid
|
||||
JOIN pg_catalog.pg_namespace AS nf ON nf.oid = cf.relnamespace
|
||||
JOIN pg_catalog.pg_attribute AS al ON al.attrelid = cl.oid AND al.attnum = co.conkey[1]
|
||||
JOIN pg_catalog.pg_attribute AS af ON af.attrelid = cf.oid AND af.attnum = co.confkey[1]
|
||||
WHERE
|
||||
co.contype = 'f'
|
||||
AND cl.oid = {$this->connection->quote($this->delimiteFQN($table))}::regclass
|
||||
AND nf.nspname = ANY (pg_catalog.current_schemas(FALSE))
|
||||
")->fetchAll();
|
||||
}
|
||||
|
||||
|
||||
public function getColumnTypes(\PDOStatement $statement)
|
||||
{
|
||||
static $cache;
|
||||
$item = &$cache[$statement->queryString];
|
||||
if ($item === null) {
|
||||
$item = Nette\Database\Helpers::detectTypes($statement);
|
||||
}
|
||||
return $item;
|
||||
}
|
||||
|
||||
|
||||
public function isSupported($item)
|
||||
{
|
||||
return $item === self::SUPPORT_SEQUENCE || $item === self::SUPPORT_SUBSELECT || $item === self::SUPPORT_SCHEMA;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts: schema.name => "schema"."name"
|
||||
* @param string
|
||||
* @return string
|
||||
*/
|
||||
private function delimiteFQN($name)
|
||||
{
|
||||
return implode('.', array_map([$this, 'delimite'], explode('.', $name)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
namespace Nette\Database\Drivers;
|
||||
|
||||
use Nette;
|
||||
|
||||
|
||||
/**
|
||||
* Supplemental SQLite2 database driver.
|
||||
*/
|
||||
class Sqlite2Driver extends SqliteDriver
|
||||
{
|
||||
public function formatLike($value, $pos)
|
||||
{
|
||||
throw new Nette\NotSupportedException;
|
||||
}
|
||||
|
||||
|
||||
public function getForeignKeys($table)
|
||||
{
|
||||
throw new Nette\NotSupportedException; // @see http://www.sqlite.org/foreignkeys.html
|
||||
}
|
||||
|
||||
|
||||
public function getColumnTypes(\PDOStatement $statement)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
namespace Nette\Database\Drivers;
|
||||
|
||||
use Nette;
|
||||
|
||||
|
||||
/**
|
||||
* Supplemental SQLite3 database driver.
|
||||
*/
|
||||
class SqliteDriver implements Nette\Database\ISupplementalDriver
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
/** @var Nette\Database\Connection */
|
||||
private $connection;
|
||||
|
||||
/** @var string Datetime format */
|
||||
private $fmtDateTime;
|
||||
|
||||
|
||||
public function __construct(Nette\Database\Connection $connection, array $options)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
$this->fmtDateTime = isset($options['formatDateTime']) ? $options['formatDateTime'] : 'U';
|
||||
}
|
||||
|
||||
|
||||
public function convertException(\PDOException $e)
|
||||
{
|
||||
$code = isset($e->errorInfo[1]) ? $e->errorInfo[1] : null;
|
||||
$msg = $e->getMessage();
|
||||
if ($code !== 19) {
|
||||
return Nette\Database\DriverException::from($e);
|
||||
|
||||
} elseif (strpos($msg, 'must be unique') !== false
|
||||
|| strpos($msg, 'is not unique') !== false
|
||||
|| strpos($msg, 'UNIQUE constraint failed') !== false
|
||||
) {
|
||||
return Nette\Database\UniqueConstraintViolationException::from($e);
|
||||
|
||||
} elseif (strpos($msg, 'may not be null') !== false
|
||||
|| strpos($msg, 'NOT NULL constraint failed') !== false
|
||||
) {
|
||||
return Nette\Database\NotNullConstraintViolationException::from($e);
|
||||
|
||||
} elseif (strpos($msg, 'foreign key constraint failed') !== false
|
||||
|| strpos($msg, 'FOREIGN KEY constraint failed') !== false
|
||||
) {
|
||||
return Nette\Database\ForeignKeyConstraintViolationException::from($e);
|
||||
|
||||
} else {
|
||||
return Nette\Database\ConstraintViolationException::from($e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/********************* SQL ****************d*g**/
|
||||
|
||||
|
||||
public function delimite($name)
|
||||
{
|
||||
return '[' . strtr($name, '[]', ' ') . ']';
|
||||
}
|
||||
|
||||
|
||||
public function formatBool($value)
|
||||
{
|
||||
return $value ? '1' : '0';
|
||||
}
|
||||
|
||||
|
||||
public function formatDateTime(/*\DateTimeInterface*/ $value)
|
||||
{
|
||||
return $value->format($this->fmtDateTime);
|
||||
}
|
||||
|
||||
|
||||
public function formatDateInterval(\DateInterval $value)
|
||||
{
|
||||
throw new Nette\NotSupportedException;
|
||||
}
|
||||
|
||||
|
||||
public function formatLike($value, $pos)
|
||||
{
|
||||
$value = addcslashes(substr($this->connection->quote($value), 1, -1), '%_\\');
|
||||
return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'") . " ESCAPE '\\'";
|
||||
}
|
||||
|
||||
|
||||
public function applyLimit(&$sql, $limit, $offset)
|
||||
{
|
||||
if ($limit < 0 || $offset < 0) {
|
||||
throw new Nette\InvalidArgumentException('Negative offset or limit.');
|
||||
|
||||
} elseif ($limit !== null || $offset) {
|
||||
$sql .= ' LIMIT ' . ($limit === null ? '-1' : (int) $limit)
|
||||
. ($offset ? ' OFFSET ' . (int) $offset : '');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function normalizeRow($row)
|
||||
{
|
||||
foreach ($row as $key => $value) {
|
||||
unset($row[$key]);
|
||||
if ($key[0] === '[' || $key[0] === '"') {
|
||||
$key = substr($key, 1, -1);
|
||||
}
|
||||
$row[$key] = $value;
|
||||
}
|
||||
return $row;
|
||||
}
|
||||
|
||||
|
||||
/********************* reflection ****************d*g**/
|
||||
|
||||
|
||||
public function getTables()
|
||||
{
|
||||
$tables = [];
|
||||
foreach ($this->connection->query("
|
||||
SELECT name, type = 'view' as view FROM sqlite_master WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%'
|
||||
UNION ALL
|
||||
SELECT name, type = 'view' as view FROM sqlite_temp_master WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%'
|
||||
ORDER BY name
|
||||
") as $row) {
|
||||
$tables[] = [
|
||||
'name' => $row->name,
|
||||
'view' => (bool) $row->view,
|
||||
];
|
||||
}
|
||||
|
||||
return $tables;
|
||||
}
|
||||
|
||||
|
||||
public function getColumns($table)
|
||||
{
|
||||
$meta = $this->connection->query("
|
||||
SELECT sql FROM sqlite_master WHERE type = 'table' AND name = {$this->connection->quote($table)}
|
||||
UNION ALL
|
||||
SELECT sql FROM sqlite_temp_master WHERE type = 'table' AND name = {$this->connection->quote($table)}
|
||||
")->fetch();
|
||||
|
||||
$columns = [];
|
||||
foreach ($this->connection->query("PRAGMA table_info({$this->delimite($table)})") as $row) {
|
||||
$column = $row['name'];
|
||||
$pattern = "/(\"$column\"|`$column`|\[$column\]|$column)\\s+[^,]+\\s+PRIMARY\\s+KEY\\s+AUTOINCREMENT/Ui";
|
||||
$type = explode('(', $row['type']);
|
||||
$columns[] = [
|
||||
'name' => $column,
|
||||
'table' => $table,
|
||||
'nativetype' => strtoupper($type[0]),
|
||||
'size' => isset($type[1]) ? (int) $type[1] : null,
|
||||
'unsigned' => false,
|
||||
'nullable' => $row['notnull'] == '0',
|
||||
'default' => $row['dflt_value'],
|
||||
'autoincrement' => (bool) preg_match($pattern, $meta['sql']),
|
||||
'primary' => $row['pk'] > 0,
|
||||
'vendor' => (array) $row,
|
||||
];
|
||||
}
|
||||
return $columns;
|
||||
}
|
||||
|
||||
|
||||
public function getIndexes($table)
|
||||
{
|
||||
$indexes = [];
|
||||
foreach ($this->connection->query("PRAGMA index_list({$this->delimite($table)})") as $row) {
|
||||
$indexes[$row['name']]['name'] = $row['name'];
|
||||
$indexes[$row['name']]['unique'] = (bool) $row['unique'];
|
||||
$indexes[$row['name']]['primary'] = false;
|
||||
}
|
||||
|
||||
foreach ($indexes as $index => $values) {
|
||||
$res = $this->connection->query("PRAGMA index_info({$this->delimite($index)})");
|
||||
while ($row = $res->fetch()) {
|
||||
$indexes[$index]['columns'][$row['seqno']] = $row['name'];
|
||||
}
|
||||
}
|
||||
|
||||
$columns = $this->getColumns($table);
|
||||
foreach ($indexes as $index => $values) {
|
||||
$column = $indexes[$index]['columns'][0];
|
||||
foreach ($columns as $info) {
|
||||
if ($column == $info['name']) {
|
||||
$indexes[$index]['primary'] = (bool) $info['primary'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$indexes) { // @see http://www.sqlite.org/lang_createtable.html#rowid
|
||||
foreach ($columns as $column) {
|
||||
if ($column['vendor']['pk']) {
|
||||
$indexes[] = [
|
||||
'name' => 'ROWID',
|
||||
'unique' => true,
|
||||
'primary' => true,
|
||||
'columns' => [$column['name']],
|
||||
];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($indexes);
|
||||
}
|
||||
|
||||
|
||||
public function getForeignKeys($table)
|
||||
{
|
||||
$keys = [];
|
||||
foreach ($this->connection->query("PRAGMA foreign_key_list({$this->delimite($table)})") as $row) {
|
||||
$keys[$row['id']]['name'] = $row['id']; // foreign key name
|
||||
$keys[$row['id']]['local'] = $row['from']; // local columns
|
||||
$keys[$row['id']]['table'] = $row['table']; // referenced table
|
||||
$keys[$row['id']]['foreign'] = $row['to']; // referenced columns
|
||||
$keys[$row['id']]['onDelete'] = $row['on_delete'];
|
||||
$keys[$row['id']]['onUpdate'] = $row['on_update'];
|
||||
|
||||
if ($keys[$row['id']]['foreign'][0] == null) {
|
||||
$keys[$row['id']]['foreign'] = null;
|
||||
}
|
||||
}
|
||||
return array_values($keys);
|
||||
}
|
||||
|
||||
|
||||
public function getColumnTypes(\PDOStatement $statement)
|
||||
{
|
||||
$types = [];
|
||||
$count = $statement->columnCount();
|
||||
for ($col = 0; $col < $count; $col++) {
|
||||
$meta = $statement->getColumnMeta($col);
|
||||
if (isset($meta['sqlite:decl_type'])) {
|
||||
if (in_array($meta['sqlite:decl_type'], ['DATE', 'DATETIME'], true)) {
|
||||
$types[$meta['name']] = Nette\Database\IStructure::FIELD_UNIX_TIMESTAMP;
|
||||
} else {
|
||||
$types[$meta['name']] = Nette\Database\Helpers::detectType($meta['sqlite:decl_type']);
|
||||
}
|
||||
} elseif (isset($meta['native_type'])) {
|
||||
$types[$meta['name']] = Nette\Database\Helpers::detectType($meta['native_type']);
|
||||
}
|
||||
}
|
||||
return $types;
|
||||
}
|
||||
|
||||
|
||||
public function isSupported($item)
|
||||
{
|
||||
return $item === self::SUPPORT_MULTI_INSERT_AS_SELECT || $item === self::SUPPORT_SUBSELECT || $item === self::SUPPORT_MULTI_COLUMN_AS_OR_COND;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
namespace Nette\Database\Drivers;
|
||||
|
||||
use Nette;
|
||||
|
||||
|
||||
/**
|
||||
* Supplemental SQL Server 2005 and later database driver.
|
||||
*/
|
||||
class SqlsrvDriver implements Nette\Database\ISupplementalDriver
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
/** @var Nette\Database\Connection */
|
||||
private $connection;
|
||||
|
||||
/** @var string */
|
||||
private $version;
|
||||
|
||||
|
||||
public function __construct(Nette\Database\Connection $connection, array $options)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
$this->version = $connection->getPdo()->getAttribute(\PDO::ATTR_SERVER_VERSION);
|
||||
}
|
||||
|
||||
|
||||
public function convertException(\PDOException $e)
|
||||
{
|
||||
return Nette\Database\DriverException::from($e);
|
||||
}
|
||||
|
||||
|
||||
/********************* SQL ****************d*g**/
|
||||
|
||||
|
||||
public function delimite($name)
|
||||
{
|
||||
/** @see https://msdn.microsoft.com/en-us/library/ms176027.aspx */
|
||||
return '[' . str_replace(']', ']]', $name) . ']';
|
||||
}
|
||||
|
||||
|
||||
public function formatBool($value)
|
||||
{
|
||||
return $value ? '1' : '0';
|
||||
}
|
||||
|
||||
|
||||
public function formatDateTime(/*\DateTimeInterface*/ $value)
|
||||
{
|
||||
/** @see https://msdn.microsoft.com/en-us/library/ms187819.aspx */
|
||||
return $value->format("'Y-m-d\\TH:i:s'");
|
||||
}
|
||||
|
||||
|
||||
public function formatDateInterval(\DateInterval $value)
|
||||
{
|
||||
throw new Nette\NotSupportedException;
|
||||
}
|
||||
|
||||
|
||||
public function formatLike($value, $pos)
|
||||
{
|
||||
/** @see https://msdn.microsoft.com/en-us/library/ms179859.aspx */
|
||||
$value = strtr($value, ["'" => "''", '%' => '[%]', '_' => '[_]', '[' => '[[]']);
|
||||
return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'");
|
||||
}
|
||||
|
||||
|
||||
public function applyLimit(&$sql, $limit, $offset)
|
||||
{
|
||||
if ($limit < 0 || $offset < 0) {
|
||||
throw new Nette\InvalidArgumentException('Negative offset or limit.');
|
||||
|
||||
} elseif (version_compare($this->version, 11, '<')) { // 11 == SQL Server 2012
|
||||
if ($offset) {
|
||||
throw new Nette\NotSupportedException('Offset is not supported by this database.');
|
||||
|
||||
} elseif ($limit !== null) {
|
||||
$sql = preg_replace('#^\s*(SELECT(\s+DISTINCT|\s+ALL)?|UPDATE|DELETE)#i', '$0 TOP ' . (int) $limit, $sql, 1, $count);
|
||||
if (!$count) {
|
||||
throw new Nette\InvalidArgumentException('SQL query must begin with SELECT, UPDATE or DELETE command.');
|
||||
}
|
||||
}
|
||||
|
||||
} elseif ($limit !== null || $offset) {
|
||||
// requires ORDER BY, see https://technet.microsoft.com/en-us/library/gg699618(v=sql.110).aspx
|
||||
$sql .= ' OFFSET ' . (int) $offset . ' ROWS '
|
||||
. 'FETCH NEXT ' . (int) $limit . ' ROWS ONLY';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function normalizeRow($row)
|
||||
{
|
||||
return $row;
|
||||
}
|
||||
|
||||
|
||||
/********************* reflection ****************d*g**/
|
||||
|
||||
|
||||
public function getTables()
|
||||
{
|
||||
$tables = [];
|
||||
foreach ($this->connection->query("
|
||||
SELECT
|
||||
name,
|
||||
CASE type
|
||||
WHEN 'U' THEN 0
|
||||
WHEN 'V' THEN 1
|
||||
END AS [view]
|
||||
FROM
|
||||
sys.objects
|
||||
WHERE
|
||||
type IN ('U', 'V')
|
||||
") as $row) {
|
||||
$tables[] = [
|
||||
'name' => $row->name,
|
||||
'view' => (bool) $row->view,
|
||||
];
|
||||
}
|
||||
|
||||
return $tables;
|
||||
}
|
||||
|
||||
|
||||
public function getColumns($table)
|
||||
{
|
||||
$columns = [];
|
||||
foreach ($this->connection->query("
|
||||
SELECT
|
||||
c.name AS name,
|
||||
o.name AS [table],
|
||||
UPPER(t.name) AS nativetype,
|
||||
NULL AS size,
|
||||
0 AS unsigned,
|
||||
c.is_nullable AS nullable,
|
||||
OBJECT_DEFINITION(c.default_object_id) AS [default],
|
||||
c.is_identity AS autoincrement,
|
||||
CASE WHEN i.index_id IS NULL
|
||||
THEN 0
|
||||
ELSE 1
|
||||
END AS [primary]
|
||||
FROM
|
||||
sys.columns c
|
||||
JOIN sys.objects o ON c.object_id = o.object_id
|
||||
LEFT JOIN sys.types t ON c.user_type_id = t.user_type_id
|
||||
LEFT JOIN sys.key_constraints k ON o.object_id = k.parent_object_id AND k.type = 'PK'
|
||||
LEFT JOIN sys.index_columns i ON k.parent_object_id = i.object_id AND i.index_id = k.unique_index_id AND i.column_id = c.column_id
|
||||
WHERE
|
||||
o.type IN ('U', 'V')
|
||||
AND o.name = {$this->connection->quote($table)}
|
||||
") as $row) {
|
||||
$row = (array) $row;
|
||||
$row['vendor'] = $row;
|
||||
$row['unsigned'] = (bool) $row['unsigned'];
|
||||
$row['nullable'] = (bool) $row['nullable'];
|
||||
$row['autoincrement'] = (bool) $row['autoincrement'];
|
||||
$row['primary'] = (bool) $row['primary'];
|
||||
|
||||
$columns[] = $row;
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
|
||||
public function getIndexes($table)
|
||||
{
|
||||
$indexes = [];
|
||||
foreach ($this->connection->query("
|
||||
SELECT
|
||||
i.name AS name,
|
||||
CASE WHEN i.is_unique = 1 OR i.is_unique_constraint = 1
|
||||
THEN 1
|
||||
ELSE 0
|
||||
END AS [unique],
|
||||
i.is_primary_key AS [primary],
|
||||
c.name AS [column]
|
||||
FROM
|
||||
sys.indexes i
|
||||
JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
|
||||
JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
|
||||
JOIN sys.tables t ON i.object_id = t.object_id
|
||||
WHERE
|
||||
t.name = {$this->connection->quote($table)}
|
||||
ORDER BY
|
||||
i.index_id,
|
||||
ic.index_column_id
|
||||
") as $row) {
|
||||
$indexes[$row->name]['name'] = $row->name;
|
||||
$indexes[$row->name]['unique'] = (bool) $row->unique;
|
||||
$indexes[$row->name]['primary'] = (bool) $row->primary;
|
||||
$indexes[$row->name]['columns'][] = $row->column;
|
||||
}
|
||||
|
||||
return array_values($indexes);
|
||||
}
|
||||
|
||||
|
||||
public function getForeignKeys($table)
|
||||
{
|
||||
// Does't work with multicolumn foreign keys
|
||||
$keys = [];
|
||||
foreach ($this->connection->query("
|
||||
SELECT
|
||||
fk.name AS name,
|
||||
cl.name AS local,
|
||||
tf.name AS [table],
|
||||
cf.name AS [column]
|
||||
FROM
|
||||
sys.foreign_keys fk
|
||||
JOIN sys.foreign_key_columns fkc ON fk.object_id = fkc.constraint_object_id
|
||||
JOIN sys.tables tl ON fkc.parent_object_id = tl.object_id
|
||||
JOIN sys.columns cl ON fkc.parent_object_id = cl.object_id AND fkc.parent_column_id = cl.column_id
|
||||
JOIN sys.tables tf ON fkc.referenced_object_id = tf.object_id
|
||||
JOIN sys.columns cf ON fkc.referenced_object_id = cf.object_id AND fkc.referenced_column_id = cf.column_id
|
||||
WHERE
|
||||
tl.name = {$this->connection->quote($table)}
|
||||
") as $row) {
|
||||
$keys[$row->name] = (array) $row;
|
||||
}
|
||||
|
||||
return array_values($keys);
|
||||
}
|
||||
|
||||
|
||||
public function getColumnTypes(\PDOStatement $statement)
|
||||
{
|
||||
$types = [];
|
||||
$count = $statement->columnCount();
|
||||
for ($col = 0; $col < $count; $col++) {
|
||||
$meta = $statement->getColumnMeta($col);
|
||||
if (isset($meta['sqlsrv:decl_type']) && $meta['sqlsrv:decl_type'] !== 'timestamp') { // timestamp does not mean time in sqlsrv
|
||||
$types[$meta['name']] = Nette\Database\Helpers::detectType($meta['sqlsrv:decl_type']);
|
||||
} elseif (isset($meta['native_type'])) {
|
||||
$types[$meta['name']] = Nette\Database\Helpers::detectType($meta['native_type']);
|
||||
}
|
||||
}
|
||||
return $types;
|
||||
}
|
||||
|
||||
|
||||
public function isSupported($item)
|
||||
{
|
||||
return $item === self::SUPPORT_SUBSELECT;
|
||||
}
|
||||
}
|
||||
+298
@@ -0,0 +1,298 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
namespace Nette\Database;
|
||||
|
||||
use Nette;
|
||||
use Tracy;
|
||||
|
||||
|
||||
/**
|
||||
* Database helpers.
|
||||
*/
|
||||
class Helpers
|
||||
{
|
||||
use Nette\StaticClass;
|
||||
|
||||
/** @var int maximum SQL length */
|
||||
public static $maxLength = 100;
|
||||
|
||||
/** @var array */
|
||||
public static $typePatterns = [
|
||||
'^_' => IStructure::FIELD_TEXT, // PostgreSQL arrays
|
||||
'(TINY|SMALL|SHORT|MEDIUM|BIG|LONG)(INT)?|INT(EGER|\d+| IDENTITY)?|(SMALL|BIG|)SERIAL\d*|COUNTER|YEAR|BYTE|LONGLONG|UNSIGNED BIG INT' => IStructure::FIELD_INTEGER,
|
||||
'(NEW)?DEC(IMAL)?(\(.*)?|NUMERIC|REAL|DOUBLE( PRECISION)?|FLOAT\d*|(SMALL)?MONEY|CURRENCY|NUMBER' => IStructure::FIELD_FLOAT,
|
||||
'BOOL(EAN)?' => IStructure::FIELD_BOOL,
|
||||
'TIME' => IStructure::FIELD_TIME,
|
||||
'DATE' => IStructure::FIELD_DATE,
|
||||
'(SMALL)?DATETIME(OFFSET)?\d*|TIME(STAMP.*)?' => IStructure::FIELD_DATETIME,
|
||||
'BYTEA|(TINY|MEDIUM|LONG|)BLOB|(LONG )?(VAR)?BINARY|IMAGE' => IStructure::FIELD_BINARY,
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* Displays complete result set as HTML table for debug purposes.
|
||||
* @return void
|
||||
*/
|
||||
public static function dumpResult(ResultSet $result)
|
||||
{
|
||||
echo "\n<table class=\"dump\">\n<caption>" . htmlspecialchars($result->getQueryString(), ENT_IGNORE, 'UTF-8') . "</caption>\n";
|
||||
if (!$result->getColumnCount()) {
|
||||
echo "\t<tr>\n\t\t<th>Affected rows:</th>\n\t\t<td>", $result->getRowCount(), "</td>\n\t</tr>\n</table>\n";
|
||||
return;
|
||||
}
|
||||
$i = 0;
|
||||
foreach ($result as $row) {
|
||||
if ($i === 0) {
|
||||
echo "<thead>\n\t<tr>\n\t\t<th>#row</th>\n";
|
||||
foreach ($row as $col => $foo) {
|
||||
echo "\t\t<th>" . htmlspecialchars($col, ENT_NOQUOTES, 'UTF-8') . "</th>\n";
|
||||
}
|
||||
echo "\t</tr>\n</thead>\n<tbody>\n";
|
||||
}
|
||||
echo "\t<tr>\n\t\t<th>", $i, "</th>\n";
|
||||
foreach ($row as $col) {
|
||||
if (is_bool($col)) {
|
||||
$s = $col ? 'TRUE' : 'FALSE';
|
||||
} elseif ($col === null) {
|
||||
$s = 'NULL';
|
||||
} else {
|
||||
$s = (string) $col;
|
||||
}
|
||||
echo "\t\t<td>", htmlspecialchars($s, ENT_NOQUOTES, 'UTF-8'), "</td>\n";
|
||||
}
|
||||
echo "\t</tr>\n";
|
||||
$i++;
|
||||
}
|
||||
|
||||
if ($i === 0) {
|
||||
echo "\t<tr>\n\t\t<td><em>empty result set</em></td>\n\t</tr>\n</table>\n";
|
||||
} else {
|
||||
echo "</tbody>\n</table>\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns syntax highlighted SQL command.
|
||||
* @param string
|
||||
* @return string
|
||||
*/
|
||||
public static function dumpSql($sql, array $params = null, Connection $connection = null)
|
||||
{
|
||||
static $keywords1 = 'SELECT|(?:ON\s+DUPLICATE\s+KEY)?UPDATE|INSERT(?:\s+INTO)?|REPLACE(?:\s+INTO)?|DELETE|CALL|UNION|FROM|WHERE|HAVING|GROUP\s+BY|ORDER\s+BY|LIMIT|OFFSET|SET|VALUES|LEFT\s+JOIN|INNER\s+JOIN|TRUNCATE';
|
||||
static $keywords2 = 'ALL|DISTINCT|DISTINCTROW|IGNORE|AS|USING|ON|AND|OR|IN|IS|NOT|NULL|[RI]?LIKE|REGEXP|TRUE|FALSE';
|
||||
|
||||
// insert new lines
|
||||
$sql = " $sql ";
|
||||
$sql = preg_replace("#(?<=[\\s,(])($keywords1)(?=[\\s,)])#i", "\n\$1", $sql);
|
||||
|
||||
// reduce spaces
|
||||
$sql = preg_replace('#[ \t]{2,}#', ' ', $sql);
|
||||
|
||||
$sql = wordwrap($sql, 100);
|
||||
$sql = preg_replace('#([ \t]*\r?\n){2,}#', "\n", $sql);
|
||||
|
||||
// syntax highlight
|
||||
$sql = htmlspecialchars($sql, ENT_IGNORE, 'UTF-8');
|
||||
$sql = preg_replace_callback("#(/\\*.+?\\*/)|(\\*\\*.+?\\*\\*)|(?<=[\\s,(])($keywords1)(?=[\\s,)])|(?<=[\\s,(=])($keywords2)(?=[\\s,)=])#is", function (array $matches) {
|
||||
if (!empty($matches[1])) { // comment
|
||||
return '<em style="color:gray">' . $matches[1] . '</em>';
|
||||
|
||||
} elseif (!empty($matches[2])) { // error
|
||||
return '<strong style="color:red">' . $matches[2] . '</strong>';
|
||||
|
||||
} elseif (!empty($matches[3])) { // most important keywords
|
||||
return '<strong style="color:blue">' . $matches[3] . '</strong>';
|
||||
|
||||
} elseif (!empty($matches[4])) { // other keywords
|
||||
return '<strong style="color:green">' . $matches[4] . '</strong>';
|
||||
}
|
||||
}, $sql);
|
||||
|
||||
// parameters
|
||||
$sql = preg_replace_callback('#\?#', function () use ($params, $connection) {
|
||||
static $i = 0;
|
||||
if (!isset($params[$i])) {
|
||||
return '?';
|
||||
}
|
||||
$param = $params[$i++];
|
||||
if (is_string($param) && (preg_match('#[^\x09\x0A\x0D\x20-\x7E\xA0-\x{10FFFF}]#u', $param) || preg_last_error())) {
|
||||
return '<i title="Length ' . strlen($param) . ' bytes"><binary></i>';
|
||||
|
||||
} elseif (is_string($param)) {
|
||||
$length = Nette\Utils\Strings::length($param);
|
||||
$truncated = Nette\Utils\Strings::truncate($param, self::$maxLength);
|
||||
$text = htmlspecialchars($connection ? $connection->quote($truncated) : '\'' . $truncated . '\'', ENT_NOQUOTES, 'UTF-8');
|
||||
return '<span title="Length ' . $length . ' characters">' . $text . '</span>';
|
||||
|
||||
} elseif (is_resource($param)) {
|
||||
$type = get_resource_type($param);
|
||||
if ($type === 'stream') {
|
||||
$info = stream_get_meta_data($param);
|
||||
}
|
||||
return '<i' . (isset($info['uri']) ? ' title="' . htmlspecialchars($info['uri'], ENT_NOQUOTES, 'UTF-8') . '"' : null)
|
||||
. '><' . htmlspecialchars($type, ENT_NOQUOTES, 'UTF-8') . ' resource></i> ';
|
||||
|
||||
} else {
|
||||
return htmlspecialchars($param, ENT_NOQUOTES, 'UTF-8');
|
||||
}
|
||||
}, $sql);
|
||||
|
||||
return '<pre class="dump">' . trim($sql) . "</pre>\n";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Common column type detection.
|
||||
* @return array
|
||||
*/
|
||||
public static function detectTypes(\PDOStatement $statement)
|
||||
{
|
||||
$types = [];
|
||||
$count = $statement->columnCount(); // driver must be meta-aware, see PHP bugs #53782, #54695
|
||||
for ($col = 0; $col < $count; $col++) {
|
||||
$meta = $statement->getColumnMeta($col);
|
||||
if (isset($meta['native_type'])) {
|
||||
$types[$meta['name']] = self::detectType($meta['native_type']);
|
||||
}
|
||||
}
|
||||
return $types;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Heuristic column type detection.
|
||||
* @param string
|
||||
* @return string
|
||||
* @internal
|
||||
*/
|
||||
public static function detectType($type)
|
||||
{
|
||||
static $cache;
|
||||
if (!isset($cache[$type])) {
|
||||
$cache[$type] = 'string';
|
||||
foreach (self::$typePatterns as $s => $val) {
|
||||
if (preg_match("#^($s)$#i", $type)) {
|
||||
return $cache[$type] = $val;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $cache[$type];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Import SQL dump from file - extremely fast.
|
||||
* @return int count of commands
|
||||
*/
|
||||
public static function loadFromFile(Connection $connection, $file)
|
||||
{
|
||||
@set_time_limit(0); // @ function may be disabled
|
||||
|
||||
$handle = @fopen($file, 'r'); // @ is escalated to exception
|
||||
if (!$handle) {
|
||||
throw new Nette\FileNotFoundException("Cannot open file '$file'.");
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
$delimiter = ';';
|
||||
$sql = '';
|
||||
$pdo = $connection->getPdo(); // native query without logging
|
||||
while (!feof($handle)) {
|
||||
$s = rtrim(fgets($handle));
|
||||
if (!strncasecmp($s, 'DELIMITER ', 10)) {
|
||||
$delimiter = substr($s, 10);
|
||||
|
||||
} elseif (substr($s, -strlen($delimiter)) === $delimiter) {
|
||||
$sql .= substr($s, 0, -strlen($delimiter));
|
||||
$pdo->exec($sql);
|
||||
$sql = '';
|
||||
$count++;
|
||||
|
||||
} else {
|
||||
$sql .= $s . "\n";
|
||||
}
|
||||
}
|
||||
if (trim($sql) !== '') {
|
||||
$pdo->exec($sql);
|
||||
$count++;
|
||||
}
|
||||
fclose($handle);
|
||||
return $count;
|
||||
}
|
||||
|
||||
|
||||
public static function createDebugPanel($connection, $explain = true, $name = null)
|
||||
{
|
||||
$panel = new Nette\Bridges\DatabaseTracy\ConnectionPanel($connection);
|
||||
$panel->explain = $explain;
|
||||
$panel->name = $name;
|
||||
Tracy\Debugger::getBar()->addPanel($panel);
|
||||
return $panel;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reformat source to key -> value pairs.
|
||||
* @return array
|
||||
*/
|
||||
public static function toPairs(array $rows, $key = null, $value = null)
|
||||
{
|
||||
if (!$rows) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$keys = array_keys((array) reset($rows));
|
||||
if (!count($keys)) {
|
||||
throw new \LogicException('Result set does not contain any column.');
|
||||
|
||||
} elseif ($key === null && $value === null) {
|
||||
if (count($keys) === 1) {
|
||||
list($value) = $keys;
|
||||
} else {
|
||||
list($key, $value) = $keys;
|
||||
}
|
||||
}
|
||||
|
||||
$return = [];
|
||||
if ($key === null) {
|
||||
foreach ($rows as $row) {
|
||||
$return[] = ($value === null ? $row : $row[$value]);
|
||||
}
|
||||
} else {
|
||||
foreach ($rows as $row) {
|
||||
$return[(string) $row[$key]] = ($value === null ? $row : $row[$value]);
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Finds duplicate columns in select statement
|
||||
* @param \PDOStatement
|
||||
* @return string
|
||||
*/
|
||||
public static function findDuplicates(\PDOStatement $statement)
|
||||
{
|
||||
$cols = [];
|
||||
for ($i = 0; $i < $statement->columnCount(); $i++) {
|
||||
$meta = $statement->getColumnMeta($i);
|
||||
$cols[$meta['name']][] = isset($meta['table']) ? $meta['table'] : '';
|
||||
}
|
||||
$duplicates = [];
|
||||
foreach ($cols as $name => $tables) {
|
||||
if (count($tables) > 1) {
|
||||
$tables = array_filter(array_unique($tables));
|
||||
$duplicates[] = "'$name'" . ($tables ? ' (from ' . implode(', ', $tables) . ')' : '');
|
||||
}
|
||||
}
|
||||
return implode(', ', $duplicates);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
namespace Nette\Database;
|
||||
|
||||
use Nette\Database\Conventions\AmbiguousReferenceKeyException;
|
||||
|
||||
|
||||
interface IConventions
|
||||
{
|
||||
|
||||
/**
|
||||
* Returns primary key for table.
|
||||
* @param string
|
||||
* @return string|array|null
|
||||
*/
|
||||
function getPrimary($table);
|
||||
|
||||
/**
|
||||
* Returns referenced table & referenced column.
|
||||
* Example:
|
||||
* (author, book) returns array(book, author_id)
|
||||
*
|
||||
* @param string source table
|
||||
* @param string referencing key
|
||||
* @return array|null array(referenced table, referenced column)
|
||||
* @throws AmbiguousReferenceKeyException
|
||||
*/
|
||||
function getHasManyReference($table, $key);
|
||||
|
||||
/**
|
||||
* Returns referenced table & referencing column.
|
||||
* Example
|
||||
* (book, author) returns array(author, author_id)
|
||||
* (book, translator) returns array(author, translator_id)
|
||||
*
|
||||
* @param string source table
|
||||
* @param string referencing key
|
||||
* @return array|null array(referenced table, referencing column)
|
||||
*/
|
||||
function getBelongsToReference($table, $key);
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
namespace Nette\Database;
|
||||
|
||||
|
||||
/**
|
||||
* Row interface.
|
||||
*/
|
||||
interface IRow extends \Traversable, \ArrayAccess
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
namespace Nette\Database;
|
||||
|
||||
|
||||
/**
|
||||
* Container of database result fetched into IRow.
|
||||
*/
|
||||
interface IRowContainer extends \Traversable
|
||||
{
|
||||
|
||||
/**
|
||||
* Fetches single row object.
|
||||
* @return IRow|bool if there is no row
|
||||
*/
|
||||
function fetch();
|
||||
|
||||
/**
|
||||
* Fetches all rows as associative array.
|
||||
* @param string|int column name used for an array key or null for numeric index
|
||||
* @param string|int column name used for an array value or null for the whole row
|
||||
* @return array
|
||||
*/
|
||||
function fetchPairs($key = null, $value = null);
|
||||
|
||||
/**
|
||||
* Fetches all rows.
|
||||
* @return IRow[]
|
||||
*/
|
||||
function fetchAll();
|
||||
|
||||
/**
|
||||
* Fetches all rows and returns associative tree.
|
||||
* @param string associative descriptor
|
||||
* @return array
|
||||
*/
|
||||
function fetchAssoc($path);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
namespace Nette\Database;
|
||||
|
||||
|
||||
/**
|
||||
* Provides cached reflection for database structure.
|
||||
*/
|
||||
interface IStructure
|
||||
{
|
||||
const
|
||||
FIELD_TEXT = 'string',
|
||||
FIELD_BINARY = 'bin',
|
||||
FIELD_BOOL = 'bool',
|
||||
FIELD_INTEGER = 'int',
|
||||
FIELD_FLOAT = 'float',
|
||||
FIELD_DATE = 'date',
|
||||
FIELD_TIME = 'time',
|
||||
FIELD_DATETIME = 'datetime',
|
||||
FIELD_UNIX_TIMESTAMP = 'timestamp',
|
||||
FIELD_TIME_INTERVAL = 'timeint';
|
||||
|
||||
/**
|
||||
* Returns tables list.
|
||||
* @return array
|
||||
*/
|
||||
function getTables();
|
||||
|
||||
/**
|
||||
* Returns table columns list.
|
||||
* @param string
|
||||
* @return array
|
||||
*/
|
||||
function getColumns($table);
|
||||
|
||||
/**
|
||||
* Returns table primary key.
|
||||
* @param string
|
||||
* @return string|array|null
|
||||
*/
|
||||
function getPrimaryKey($table);
|
||||
|
||||
/**
|
||||
* Returns autoincrement primary key name.
|
||||
* @param string
|
||||
* @return string|null
|
||||
*/
|
||||
//function getPrimaryAutoincrementKey($table);
|
||||
|
||||
/**
|
||||
* Returns table primary key sequence.
|
||||
* @param string
|
||||
* @return string|null
|
||||
*/
|
||||
function getPrimaryKeySequence($table);
|
||||
|
||||
/**
|
||||
* Returns hasMany reference.
|
||||
* If a targetTable is not provided, returns references for all tables.
|
||||
* @param string
|
||||
* @param string|null
|
||||
* @return array|null
|
||||
*/
|
||||
function getHasManyReference($table, $targetTable = null);
|
||||
|
||||
/**
|
||||
* Returns belongsTo reference.
|
||||
* If a column is not provided, returns references for all columns.
|
||||
* @param string
|
||||
* @param string|null
|
||||
* @return array|null
|
||||
*/
|
||||
function getBelongsToReference($table, $column = null);
|
||||
|
||||
/**
|
||||
* Rebuilds database structure cache.
|
||||
* @return void
|
||||
*/
|
||||
function rebuild();
|
||||
|
||||
/**
|
||||
* Returns true if database cached structure has been rebuilt.
|
||||
* @return bool
|
||||
*/
|
||||
function isRebuilt();
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
namespace Nette\Database;
|
||||
|
||||
|
||||
/**
|
||||
* Supplemental PDO database driver.
|
||||
*/
|
||||
interface ISupplementalDriver
|
||||
{
|
||||
const SUPPORT_SEQUENCE = 'sequence',
|
||||
SUPPORT_SELECT_UNGROUPED_COLUMNS = 'ungrouped_cols',
|
||||
SUPPORT_MULTI_INSERT_AS_SELECT = 'insert_as_select',
|
||||
SUPPORT_MULTI_COLUMN_AS_OR_COND = 'multi_column_as_or',
|
||||
SUPPORT_SUBSELECT = 'subselect',
|
||||
SUPPORT_SCHEMA = 'schema';
|
||||
|
||||
/**
|
||||
* Converts PDOException to DriverException or its descendant.
|
||||
* @return DriverException
|
||||
*/
|
||||
function convertException(\PDOException $e);
|
||||
|
||||
/**
|
||||
* Delimites identifier for use in a SQL statement.
|
||||
* @param string
|
||||
* @return string
|
||||
*/
|
||||
function delimite($name);
|
||||
|
||||
/**
|
||||
* Formats boolean for use in a SQL statement.
|
||||
* @param bool
|
||||
* @return string
|
||||
*/
|
||||
function formatBool($value);
|
||||
|
||||
/**
|
||||
* Formats date-time for use in a SQL statement.
|
||||
* @return string
|
||||
*/
|
||||
function formatDateTime(/*\DateTimeInterface*/ $value);
|
||||
|
||||
/**
|
||||
* Formats date-time interval for use in a SQL statement.
|
||||
* @return string
|
||||
*/
|
||||
//function formatDateInterval(\DateInterval $value);
|
||||
|
||||
/**
|
||||
* Encodes string for use in a LIKE statement.
|
||||
* @param string
|
||||
* @param int
|
||||
* @return string
|
||||
*/
|
||||
function formatLike($value, $pos);
|
||||
|
||||
/**
|
||||
* Injects LIMIT/OFFSET to the SQL query.
|
||||
* @param string SQL query that will be modified.
|
||||
* @param int|null
|
||||
* @param int|null
|
||||
* @return void
|
||||
*/
|
||||
function applyLimit(&$sql, $limit, $offset);
|
||||
|
||||
/**
|
||||
* Normalizes result row.
|
||||
* @param array
|
||||
* @return array
|
||||
*/
|
||||
function normalizeRow($row);
|
||||
|
||||
/********************* reflection ****************d*g**/
|
||||
|
||||
/**
|
||||
* Returns list of tables.
|
||||
* @return array of tuples [(string) name, (bool) view, [(string) fullName]]
|
||||
*/
|
||||
function getTables();
|
||||
|
||||
/**
|
||||
* Returns metadata for all columns in a table.
|
||||
* @param string
|
||||
* @return array of tuples [(string) name, (string) table, (string) nativetype, (int) size, (bool) nullable, (mixed) default, (bool) autoincrement, (bool) primary, (array) vendor]]
|
||||
*/
|
||||
function getColumns($table);
|
||||
|
||||
/**
|
||||
* Returns metadata for all indexes in a table.
|
||||
* @param string
|
||||
* @return array of tuples [(string) name, (string[]) columns, (bool) unique, (bool) primary]
|
||||
*/
|
||||
function getIndexes($table);
|
||||
|
||||
/**
|
||||
* Returns metadata for all foreign keys in a table.
|
||||
* @param string
|
||||
* @return array of tuples [(string) name, (string) local, (string) table, (string) foreign]
|
||||
*/
|
||||
function getForeignKeys($table);
|
||||
|
||||
/**
|
||||
* Returns associative array of detected types (IStructure::FIELD_*) in result set.
|
||||
* @param \PDOStatement
|
||||
* @return array
|
||||
*/
|
||||
function getColumnTypes(\PDOStatement $statement);
|
||||
|
||||
/**
|
||||
* Cheks if driver supports specific property
|
||||
* @param string self::SUPPORT_* property
|
||||
* @return bool
|
||||
*/
|
||||
function isSupported($item);
|
||||
}
|
||||
+328
@@ -0,0 +1,328 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
namespace Nette\Database;
|
||||
|
||||
use Nette;
|
||||
use PDO;
|
||||
|
||||
|
||||
/**
|
||||
* Represents a result set.
|
||||
*/
|
||||
class ResultSet implements \Iterator, IRowContainer
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
/** @var Connection */
|
||||
private $connection;
|
||||
|
||||
/** @var ISupplementalDriver */
|
||||
private $supplementalDriver;
|
||||
|
||||
/** @var \PDOStatement|null */
|
||||
private $pdoStatement;
|
||||
|
||||
/** @var IRow */
|
||||
private $result;
|
||||
|
||||
/** @var int */
|
||||
private $resultKey = -1;
|
||||
|
||||
/** @var IRow[] */
|
||||
private $results;
|
||||
|
||||
/** @var float */
|
||||
private $time;
|
||||
|
||||
/** @var string */
|
||||
private $queryString;
|
||||
|
||||
/** @var array */
|
||||
private $params;
|
||||
|
||||
/** @var array */
|
||||
private $types;
|
||||
|
||||
|
||||
public function __construct(Connection $connection, $queryString, array $params)
|
||||
{
|
||||
$time = microtime(true);
|
||||
$this->connection = $connection;
|
||||
$this->supplementalDriver = $connection->getSupplementalDriver();
|
||||
$this->queryString = $queryString;
|
||||
$this->params = $params;
|
||||
|
||||
try {
|
||||
if (substr($queryString, 0, 2) === '::') {
|
||||
$connection->getPdo()->{substr($queryString, 2)}();
|
||||
} elseif ($queryString !== null) {
|
||||
static $types = ['boolean' => PDO::PARAM_BOOL, 'integer' => PDO::PARAM_INT,
|
||||
'resource' => PDO::PARAM_LOB, 'NULL' => PDO::PARAM_NULL, ];
|
||||
$this->pdoStatement = $connection->getPdo()->prepare($queryString);
|
||||
foreach ($params as $key => $value) {
|
||||
$type = gettype($value);
|
||||
$this->pdoStatement->bindValue(is_int($key) ? $key + 1 : $key, $value, isset($types[$type]) ? $types[$type] : PDO::PARAM_STR);
|
||||
}
|
||||
$this->pdoStatement->setFetchMode(PDO::FETCH_ASSOC);
|
||||
$this->pdoStatement->execute();
|
||||
}
|
||||
} catch (\PDOException $e) {
|
||||
$e = $this->supplementalDriver->convertException($e);
|
||||
$e->queryString = $queryString;
|
||||
throw $e;
|
||||
}
|
||||
$this->time = microtime(true) - $time;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return Connection
|
||||
*/
|
||||
public function getConnection()
|
||||
{
|
||||
return $this->connection;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @return \PDOStatement
|
||||
*/
|
||||
public function getPdoStatement()
|
||||
{
|
||||
return $this->pdoStatement;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getQueryString()
|
||||
{
|
||||
return $this->queryString;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getParameters()
|
||||
{
|
||||
return $this->params;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getColumnCount()
|
||||
{
|
||||
return $this->pdoStatement ? $this->pdoStatement->columnCount() : null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getRowCount()
|
||||
{
|
||||
return $this->pdoStatement ? $this->pdoStatement->rowCount() : null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return float
|
||||
*/
|
||||
public function getTime()
|
||||
{
|
||||
return $this->time;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Normalizes result row.
|
||||
* @param array
|
||||
* @return array
|
||||
*/
|
||||
public function normalizeRow($row)
|
||||
{
|
||||
if ($this->types === null) {
|
||||
$this->types = (array) $this->supplementalDriver->getColumnTypes($this->pdoStatement);
|
||||
}
|
||||
|
||||
foreach ($this->types as $key => $type) {
|
||||
$value = $row[$key];
|
||||
if ($value === null || $value === false || $type === IStructure::FIELD_TEXT) {
|
||||
// do nothing
|
||||
} elseif ($type === IStructure::FIELD_INTEGER) {
|
||||
$row[$key] = is_float($tmp = $value * 1) ? $value : $tmp;
|
||||
|
||||
} elseif ($type === IStructure::FIELD_FLOAT) {
|
||||
if (($pos = strpos($value, '.')) !== false) {
|
||||
$value = rtrim(rtrim($pos === 0 ? "0$value" : $value, '0'), '.');
|
||||
}
|
||||
$float = (float) $value;
|
||||
$row[$key] = (string) $float === $value ? $float : $value;
|
||||
|
||||
} elseif ($type === IStructure::FIELD_BOOL) {
|
||||
$row[$key] = ((bool) $value) && $value !== 'f' && $value !== 'F';
|
||||
|
||||
} elseif ($type === IStructure::FIELD_DATETIME || $type === IStructure::FIELD_DATE || $type === IStructure::FIELD_TIME) {
|
||||
$row[$key] = new Nette\Utils\DateTime($value);
|
||||
|
||||
} elseif ($type === IStructure::FIELD_TIME_INTERVAL) {
|
||||
preg_match('#^(-?)(\d+)\D(\d+)\D(\d+)(\.\d+)?\z#', $value, $m);
|
||||
$row[$key] = new \DateInterval("PT$m[2]H$m[3]M$m[4]S");
|
||||
if (PHP_VERSION_ID >= 70100) {
|
||||
$row[$key]->f = isset($m[5]) ? (float) $m[5] : 0.0;
|
||||
}
|
||||
$row[$key]->invert = (int) (bool) $m[1];
|
||||
|
||||
} elseif ($type === IStructure::FIELD_UNIX_TIMESTAMP) {
|
||||
$row[$key] = Nette\Utils\DateTime::from($value);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->supplementalDriver->normalizeRow($row);
|
||||
}
|
||||
|
||||
|
||||
/********************* misc tools ****************d*g**/
|
||||
|
||||
|
||||
/**
|
||||
* Displays complete result set as HTML table for debug purposes.
|
||||
* @return void
|
||||
*/
|
||||
public function dump()
|
||||
{
|
||||
Helpers::dumpResult($this);
|
||||
}
|
||||
|
||||
|
||||
/********************* interface Iterator ****************d*g**/
|
||||
|
||||
|
||||
public function rewind()
|
||||
{
|
||||
if ($this->result === false) {
|
||||
throw new Nette\InvalidStateException('Nette\\Database\\ResultSet implements only one way iterator.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function current()
|
||||
{
|
||||
return $this->result;
|
||||
}
|
||||
|
||||
|
||||
public function key()
|
||||
{
|
||||
return $this->resultKey;
|
||||
}
|
||||
|
||||
|
||||
public function next()
|
||||
{
|
||||
$this->result = false;
|
||||
}
|
||||
|
||||
|
||||
public function valid()
|
||||
{
|
||||
if ($this->result) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->fetch() !== false;
|
||||
}
|
||||
|
||||
|
||||
/********************* interface IRowContainer ****************d*g**/
|
||||
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function fetch()
|
||||
{
|
||||
$data = $this->pdoStatement ? $this->pdoStatement->fetch() : null;
|
||||
if (!$data) {
|
||||
$this->pdoStatement->closeCursor();
|
||||
return false;
|
||||
|
||||
} elseif ($this->result === null && count($data) !== $this->pdoStatement->columnCount()) {
|
||||
$duplicates = Helpers::findDuplicates($this->pdoStatement);
|
||||
trigger_error("Found duplicate columns in database result set: $duplicates.", E_USER_NOTICE);
|
||||
}
|
||||
|
||||
$row = new Row;
|
||||
foreach ($this->normalizeRow($data) as $key => $value) {
|
||||
if ($key !== '') {
|
||||
$row->$key = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$this->resultKey++;
|
||||
return $this->result = $row;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fetches single field.
|
||||
* @param int
|
||||
* @return mixed|false
|
||||
*/
|
||||
public function fetchField($column = 0)
|
||||
{
|
||||
$row = $this->fetch();
|
||||
return $row ? $row[$column] : false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fetches array of fields.
|
||||
* @return array|null
|
||||
*/
|
||||
public function fetchFields()
|
||||
{
|
||||
$row = $this->fetch();
|
||||
return $row ? array_values((array) $row) : null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function fetchPairs($key = null, $value = null)
|
||||
{
|
||||
return Helpers::toPairs($this->fetchAll(), $key, $value);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function fetchAll()
|
||||
{
|
||||
if ($this->results === null) {
|
||||
$this->results = iterator_to_array($this);
|
||||
}
|
||||
return $this->results;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function fetchAssoc($path)
|
||||
{
|
||||
return Nette\Utils\Arrays::associate($this->fetchAll(), $path);
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
namespace Nette\Database;
|
||||
|
||||
use Nette;
|
||||
|
||||
|
||||
/**
|
||||
* Represents a single table row.
|
||||
*/
|
||||
class Row extends Nette\Utils\ArrayHash implements IRow
|
||||
{
|
||||
public function __get($key)
|
||||
{
|
||||
$hint = Nette\Utils\ObjectMixin::getSuggestion(array_keys((array) $this), $key);
|
||||
throw new Nette\MemberAccessException("Cannot read an undeclared column '$key'" . ($hint ? ", did you mean '$hint'?" : '.'));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a item.
|
||||
* @param mixed key or index
|
||||
* @return mixed
|
||||
*/
|
||||
public function offsetGet($key)
|
||||
{
|
||||
if (is_int($key)) {
|
||||
$arr = array_slice((array) $this, $key, 1);
|
||||
if (!$arr) {
|
||||
throw new Nette\MemberAccessException("Cannot read an undeclared column '$key'.");
|
||||
}
|
||||
return current($arr);
|
||||
}
|
||||
return $this->$key;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if $key exists.
|
||||
* @param mixed key or index
|
||||
* @return bool
|
||||
*/
|
||||
public function offsetExists($key)
|
||||
{
|
||||
if (is_int($key)) {
|
||||
return (bool) current(array_slice((array) $this, $key, 1));
|
||||
}
|
||||
return parent::offsetExists($key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
namespace Nette\Database;
|
||||
|
||||
use Nette;
|
||||
|
||||
|
||||
/**
|
||||
* SQL literal value.
|
||||
*/
|
||||
class SqlLiteral
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
/** @var string */
|
||||
private $value;
|
||||
|
||||
/** @var array */
|
||||
private $parameters;
|
||||
|
||||
|
||||
public function __construct($value, array $parameters = [])
|
||||
{
|
||||
$this->value = (string) $value;
|
||||
$this->parameters = $parameters;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getParameters()
|
||||
{
|
||||
return $this->parameters;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
namespace Nette\Database;
|
||||
|
||||
use Nette;
|
||||
|
||||
|
||||
/**
|
||||
* SQL preprocessor.
|
||||
*/
|
||||
class SqlPreprocessor
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
const ARRAY_MODES = [
|
||||
'INSERT' => 'values',
|
||||
'REPLACE' => 'values',
|
||||
'KEY UPDATE' => 'set',
|
||||
'SET' => 'set',
|
||||
'WHERE' => 'and',
|
||||
'HAVING' => 'and',
|
||||
'ORDER BY' => 'order',
|
||||
'GROUP BY' => 'order',
|
||||
];
|
||||
|
||||
const PARAMETRIC_COMMANDS = [
|
||||
'SELECT' => 1,
|
||||
'INSERT' => 1,
|
||||
'UPDATE' => 1,
|
||||
'DELETE' => 1,
|
||||
'REPLACE' => 1,
|
||||
'EXPLAIN' => 1,
|
||||
];
|
||||
|
||||
/** @var Connection */
|
||||
private $connection;
|
||||
|
||||
/** @var ISupplementalDriver */
|
||||
private $driver;
|
||||
|
||||
/** @var array of input parameters */
|
||||
private $params;
|
||||
|
||||
/** @var array of parameters to be processed by PDO */
|
||||
private $remaining;
|
||||
|
||||
/** @var int */
|
||||
private $counter;
|
||||
|
||||
/** @var bool */
|
||||
private $useParams;
|
||||
|
||||
/** @var string values|set|and|order */
|
||||
private $arrayMode;
|
||||
|
||||
|
||||
public function __construct(Connection $connection)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
$this->driver = $connection->getSupplementalDriver();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param array
|
||||
* @return array of [sql, params]
|
||||
*/
|
||||
public function process($params, $useParams = false)
|
||||
{
|
||||
$this->params = $params;
|
||||
$this->counter = 0;
|
||||
$prev = -1;
|
||||
$this->remaining = [];
|
||||
$this->arrayMode = null;
|
||||
$this->useParams = $useParams;
|
||||
$res = [];
|
||||
|
||||
while ($this->counter < count($params)) {
|
||||
$param = $params[$this->counter++];
|
||||
|
||||
if (($this->counter === 2 && count($params) === 2) || !is_scalar($param)) {
|
||||
$res[] = $this->formatValue($param, 'auto');
|
||||
$this->arrayMode = null;
|
||||
|
||||
} elseif (is_string($param) && $this->counter > $prev + 1) {
|
||||
$prev = $this->counter;
|
||||
$this->arrayMode = null;
|
||||
$res[] = Nette\Utils\Strings::replace(
|
||||
$param,
|
||||
'~\'[^\']*+\'|"[^"]*+"|\?[a-z]*|^\s*+(?:SELECT|INSERT|UPDATE|DELETE|REPLACE|EXPLAIN)\b|\b(?:SET|WHERE|HAVING|ORDER BY|GROUP BY|KEY UPDATE)(?=\s*\z|\s*\?)|/\*.*?\*/|--[^\n]*~si',
|
||||
[$this, 'callback']
|
||||
);
|
||||
} else {
|
||||
throw new Nette\InvalidArgumentException('There are more parameters than placeholders.');
|
||||
}
|
||||
}
|
||||
|
||||
return [implode(' ', $res), $this->remaining];
|
||||
}
|
||||
|
||||
|
||||
/** @internal */
|
||||
public function callback($m)
|
||||
{
|
||||
$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');
|
||||
|
||||
} elseif ($m[0] === "'" || $m[0] === '"' || $m[0] === '/' || $m[0] === '-') { // string or comment
|
||||
return $m;
|
||||
|
||||
} else { // command
|
||||
$cmd = ltrim(strtoupper($m));
|
||||
$tmp = self::ARRAY_MODES;
|
||||
$this->arrayMode = isset($tmp[$cmd]) ? $tmp[$cmd] : null;
|
||||
$tmp = self::PARAMETRIC_COMMANDS;
|
||||
$this->useParams = isset($tmp[$cmd]) || $this->useParams;
|
||||
return $m;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function formatValue($value, $mode = null)
|
||||
{
|
||||
if (!$mode || $mode === 'auto') {
|
||||
if (is_string($value) || is_resource($value)) {
|
||||
if ($this->useParams) {
|
||||
$this->remaining[] = $value;
|
||||
return '?';
|
||||
} else {
|
||||
if (is_resource($value)) {
|
||||
$value = stream_get_contents($value);
|
||||
}
|
||||
return $this->connection->quote($value);
|
||||
}
|
||||
|
||||
} elseif (is_int($value)) {
|
||||
return (string) $value;
|
||||
|
||||
} elseif (is_float($value)) {
|
||||
return rtrim(rtrim(number_format($value, 10, '.', ''), '0'), '.');
|
||||
|
||||
} elseif (is_bool($value)) {
|
||||
return $this->driver->formatBool($value);
|
||||
|
||||
} elseif ($value === null) {
|
||||
return 'NULL';
|
||||
|
||||
} elseif ($value instanceof Table\IRow) {
|
||||
return $this->formatValue($value->getPrimary());
|
||||
|
||||
} elseif ($value instanceof SqlLiteral) {
|
||||
$prep = clone $this;
|
||||
list($res, $params) = $prep->process(array_merge([$value->__toString()], $value->getParameters()), $this->useParams);
|
||||
$this->remaining = array_merge($this->remaining, $params);
|
||||
return $res;
|
||||
|
||||
} elseif ($value instanceof \DateTimeInterface) {
|
||||
return $this->driver->formatDateTime($value);
|
||||
|
||||
} elseif ($value instanceof \DateInterval) {
|
||||
return $this->driver->formatDateInterval($value);
|
||||
|
||||
} elseif (is_object($value) && method_exists($value, '__toString')) {
|
||||
return $this->formatValue((string) $value);
|
||||
}
|
||||
|
||||
} elseif ($mode === 'name') {
|
||||
if (!is_string($value)) {
|
||||
$type = gettype($value);
|
||||
throw new Nette\InvalidArgumentException("Placeholder ?$mode expects string, $type given.");
|
||||
}
|
||||
return $this->delimite($value);
|
||||
}
|
||||
|
||||
if ($value instanceof \Traversable && !$value instanceof Table\IRow) {
|
||||
$value = iterator_to_array($value);
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
$vx = $kx = [];
|
||||
if ($mode === 'auto') {
|
||||
$mode = $this->arrayMode;
|
||||
}
|
||||
|
||||
if ($mode === 'values') { // (key, key, ...) VALUES (value, value, ...)
|
||||
if (array_key_exists(0, $value)) { // multi-insert
|
||||
foreach ($value[0] as $k => $v) {
|
||||
$kx[] = $this->delimite($k);
|
||||
}
|
||||
foreach ($value as $val) {
|
||||
$vx2 = [];
|
||||
foreach ($val as $v) {
|
||||
$vx2[] = $this->formatValue($v);
|
||||
}
|
||||
$vx[] = implode(', ', $vx2);
|
||||
}
|
||||
$select = $this->driver->isSupported(ISupplementalDriver::SUPPORT_MULTI_INSERT_AS_SELECT);
|
||||
return '(' . implode(', ', $kx) . ($select ? ') SELECT ' : ') VALUES (')
|
||||
. implode($select ? ' UNION ALL SELECT ' : '), (', $vx) . ($select ? '' : ')');
|
||||
}
|
||||
|
||||
foreach ($value as $k => $v) {
|
||||
$kx[] = $this->delimite($k);
|
||||
$vx[] = $this->formatValue($v);
|
||||
}
|
||||
return '(' . implode(', ', $kx) . ') VALUES (' . implode(', ', $vx) . ')';
|
||||
|
||||
} elseif (!$mode || $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);
|
||||
} 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);
|
||||
} else { // key=value, key=value, ...
|
||||
$vx[] = $this->delimite($k) . '=' . $this->formatValue($v);
|
||||
}
|
||||
}
|
||||
return implode(', ', $vx);
|
||||
|
||||
} elseif ($mode === 'and' || $mode === 'or') { // (key [operator] value) AND ...
|
||||
foreach ($value as $k => $v) {
|
||||
if (is_int($k)) {
|
||||
$vx[] = $this->formatValue($v);
|
||||
continue;
|
||||
}
|
||||
list($k, $operator) = explode(' ', $k . ' ');
|
||||
$k = $this->delimite($k);
|
||||
if (is_array($v)) {
|
||||
if ($v) {
|
||||
$vx[] = $k . ' ' . ($operator ? $operator . ' ' : '') . 'IN (' . $this->formatValue(array_values($v)) . ')';
|
||||
} elseif ($operator === 'NOT') {
|
||||
} else {
|
||||
$vx[] = '1=0';
|
||||
}
|
||||
} else {
|
||||
$v = $this->formatValue($v);
|
||||
$vx[] = $k . ' ' . ($operator ?: ($v === 'NULL' ? 'IS' : '=')) . ' ' . $v;
|
||||
}
|
||||
}
|
||||
return $value ? '(' . implode(') ' . strtoupper($mode) . ' (', $vx) . ')' : '1=1';
|
||||
|
||||
} elseif ($mode === 'order') { // key, key DESC, ...
|
||||
foreach ($value as $k => $v) {
|
||||
$vx[] = $this->delimite($k) . ($v > 0 ? '' : ' DESC');
|
||||
}
|
||||
return implode(', ', $vx);
|
||||
|
||||
} else {
|
||||
throw new Nette\InvalidArgumentException("Unknown placeholder ?$mode.");
|
||||
}
|
||||
|
||||
} elseif (in_array($mode, ['and', 'or', 'set', 'values', 'order'], true)) {
|
||||
$type = gettype($value);
|
||||
throw new Nette\InvalidArgumentException("Placeholder ?$mode expects array or Traversable object, $type given.");
|
||||
|
||||
} elseif ($mode && $mode !== 'auto') {
|
||||
throw new Nette\InvalidArgumentException("Unknown placeholder ?$mode.");
|
||||
|
||||
} else {
|
||||
throw new Nette\InvalidArgumentException('Unexpected type of parameter: ' . (is_object($value) ? get_class($value) : gettype($value)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function delimite($name)
|
||||
{
|
||||
return implode('.', array_map([$this->driver, 'delimite'], explode('.', $name)));
|
||||
}
|
||||
}
|
||||
+284
@@ -0,0 +1,284 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
namespace Nette\Database;
|
||||
|
||||
use Nette;
|
||||
|
||||
|
||||
/**
|
||||
* Cached reflection of database structure.
|
||||
*/
|
||||
class Structure implements IStructure
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
/** @var Connection */
|
||||
protected $connection;
|
||||
|
||||
/** @var Nette\Caching\Cache */
|
||||
protected $cache;
|
||||
|
||||
/** @var array */
|
||||
protected $structure;
|
||||
|
||||
/** @var bool */
|
||||
protected $isRebuilt = false;
|
||||
|
||||
|
||||
public function __construct(Connection $connection, Nette\Caching\IStorage $cacheStorage)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
$this->cache = new Nette\Caching\Cache($cacheStorage, 'Nette.Database.Structure.' . md5($this->connection->getDsn()));
|
||||
}
|
||||
|
||||
|
||||
public function getTables()
|
||||
{
|
||||
$this->needStructure();
|
||||
return $this->structure['tables'];
|
||||
}
|
||||
|
||||
|
||||
public function getColumns($table)
|
||||
{
|
||||
$this->needStructure();
|
||||
$table = $this->resolveFQTableName($table);
|
||||
|
||||
return $this->structure['columns'][$table];
|
||||
}
|
||||
|
||||
|
||||
public function getPrimaryKey($table)
|
||||
{
|
||||
$this->needStructure();
|
||||
$table = $this->resolveFQTableName($table);
|
||||
|
||||
if (!isset($this->structure['primary'][$table])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->structure['primary'][$table];
|
||||
}
|
||||
|
||||
|
||||
public function getPrimaryAutoincrementKey($table)
|
||||
{
|
||||
$primaryKey = $this->getPrimaryKey($table);
|
||||
if (!$primaryKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Search for autoincrement key from multi primary key
|
||||
if (is_array($primaryKey)) {
|
||||
$keys = array_flip($primaryKey);
|
||||
foreach ($this->getColumns($table) as $column) {
|
||||
if (isset($keys[$column['name']]) && $column['autoincrement']) {
|
||||
return $column['name'];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Search for autoincrement key from simple primary key
|
||||
foreach ($this->getColumns($table) as $column) {
|
||||
if ($column['name'] == $primaryKey) {
|
||||
return $column['autoincrement'] ? $column['name'] : null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public function getPrimaryKeySequence($table)
|
||||
{
|
||||
$this->needStructure();
|
||||
$table = $this->resolveFQTableName($table);
|
||||
|
||||
if (!$this->connection->getSupplementalDriver()->isSupported(ISupplementalDriver::SUPPORT_SEQUENCE)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$autoincrementPrimaryKeyName = $this->getPrimaryAutoincrementKey($table);
|
||||
if (!$autoincrementPrimaryKeyName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Search for sequence from simple primary key
|
||||
foreach ($this->structure['columns'][$table] as $columnMeta) {
|
||||
if ($columnMeta['name'] === $autoincrementPrimaryKeyName) {
|
||||
return isset($columnMeta['vendor']['sequence']) ? $columnMeta['vendor']['sequence'] : null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public function getHasManyReference($table, $targetTable = null)
|
||||
{
|
||||
$this->needStructure();
|
||||
$table = $this->resolveFQTableName($table);
|
||||
|
||||
if ($targetTable) {
|
||||
$targetTable = $this->resolveFQTableName($targetTable);
|
||||
foreach ($this->structure['hasMany'][$table] as $key => $value) {
|
||||
if (strtolower($key) === $targetTable) {
|
||||
return $this->structure['hasMany'][$table][$key];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
} else {
|
||||
if (!isset($this->structure['hasMany'][$table])) {
|
||||
return [];
|
||||
}
|
||||
return $this->structure['hasMany'][$table];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function getBelongsToReference($table, $column = null)
|
||||
{
|
||||
$this->needStructure();
|
||||
$table = $this->resolveFQTableName($table);
|
||||
|
||||
if ($column) {
|
||||
$column = strtolower($column);
|
||||
if (!isset($this->structure['belongsTo'][$table][$column])) {
|
||||
return null;
|
||||
}
|
||||
return $this->structure['belongsTo'][$table][$column];
|
||||
|
||||
} else {
|
||||
if (!isset($this->structure['belongsTo'][$table])) {
|
||||
return [];
|
||||
}
|
||||
return $this->structure['belongsTo'][$table];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function rebuild()
|
||||
{
|
||||
$this->structure = $this->loadStructure();
|
||||
$this->cache->save('structure', $this->structure);
|
||||
}
|
||||
|
||||
|
||||
public function isRebuilt()
|
||||
{
|
||||
return $this->isRebuilt;
|
||||
}
|
||||
|
||||
|
||||
protected function needStructure()
|
||||
{
|
||||
if ($this->structure !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->structure = $this->cache->load('structure', [$this, 'loadStructure']);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function loadStructure()
|
||||
{
|
||||
$driver = $this->connection->getSupplementalDriver();
|
||||
|
||||
$structure = [];
|
||||
$structure['tables'] = $driver->getTables();
|
||||
|
||||
foreach ($structure['tables'] as $tablePair) {
|
||||
if (isset($tablePair['fullName'])) {
|
||||
$table = $tablePair['fullName'];
|
||||
$structure['aliases'][strtolower($tablePair['name'])] = strtolower($table);
|
||||
} else {
|
||||
$table = $tablePair['name'];
|
||||
}
|
||||
|
||||
$structure['columns'][strtolower($table)] = $columns = $driver->getColumns($table);
|
||||
|
||||
if (!$tablePair['view']) {
|
||||
$structure['primary'][strtolower($table)] = $this->analyzePrimaryKey($columns);
|
||||
$this->analyzeForeignKeys($structure, $table);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($structure['hasMany'])) {
|
||||
foreach ($structure['hasMany'] as &$table) {
|
||||
uksort($table, function ($a, $b) {
|
||||
return strlen($a) - strlen($b);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$this->isRebuilt = true;
|
||||
|
||||
return $structure;
|
||||
}
|
||||
|
||||
|
||||
protected function analyzePrimaryKey(array $columns)
|
||||
{
|
||||
$primary = [];
|
||||
foreach ($columns as $column) {
|
||||
if ($column['primary']) {
|
||||
$primary[] = $column['name'];
|
||||
}
|
||||
}
|
||||
|
||||
if (count($primary) === 0) {
|
||||
return null;
|
||||
} elseif (count($primary) === 1) {
|
||||
return reset($primary);
|
||||
} else {
|
||||
return $primary;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected function analyzeForeignKeys(&$structure, $table)
|
||||
{
|
||||
$lowerTable = strtolower($table);
|
||||
foreach ($this->connection->getSupplementalDriver()->getForeignKeys($table) as $row) {
|
||||
$structure['belongsTo'][$lowerTable][$row['local']] = $row['table'];
|
||||
$structure['hasMany'][strtolower($row['table'])][$table][] = $row['local'];
|
||||
}
|
||||
|
||||
if (isset($structure['belongsTo'][$lowerTable])) {
|
||||
uksort($structure['belongsTo'][$lowerTable], function ($a, $b) {
|
||||
return strlen($a) - strlen($b);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected function resolveFQTableName($table)
|
||||
{
|
||||
$name = strtolower($table);
|
||||
if (isset($this->structure['columns'][$name])) {
|
||||
return $name;
|
||||
}
|
||||
|
||||
if (isset($this->structure['aliases'][$name])) {
|
||||
return $this->structure['aliases'][$name];
|
||||
}
|
||||
|
||||
if (!$this->isRebuilt()) {
|
||||
$this->rebuild();
|
||||
return $this->resolveFQTableName($table);
|
||||
}
|
||||
|
||||
throw new Nette\InvalidArgumentException("Table '$name' does not exist.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
namespace Nette\Database\Table;
|
||||
|
||||
use Nette;
|
||||
|
||||
|
||||
/**
|
||||
* Single row representation.
|
||||
* ActiveRow is based on the great library NotORM http://www.notorm.com written by Jakub Vrana.
|
||||
*/
|
||||
class ActiveRow implements \IteratorAggregate, IRow
|
||||
{
|
||||
/** @var Selection */
|
||||
private $table;
|
||||
|
||||
/** @var array of row data */
|
||||
private $data;
|
||||
|
||||
/** @var bool */
|
||||
private $dataRefreshed = false;
|
||||
|
||||
|
||||
public function __construct(array $data, Selection $table)
|
||||
{
|
||||
$this->data = $data;
|
||||
$this->table = $table;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function setTable(Selection $table)
|
||||
{
|
||||
$this->table = $table;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getTable()
|
||||
{
|
||||
return $this->table;
|
||||
}
|
||||
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
try {
|
||||
return (string) $this->getPrimary();
|
||||
} catch (\Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
if (isset($e)) {
|
||||
if (func_num_args()) {
|
||||
throw $e;
|
||||
}
|
||||
trigger_error('Exception in ' . __METHOD__ . "(): {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}", E_USER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
$this->accessColumn(null);
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns primary key value.
|
||||
* @param bool
|
||||
* @return mixed possible int, string, array, object (Nette\Utils\DateTime)
|
||||
*/
|
||||
public function getPrimary($throw = true)
|
||||
{
|
||||
$primary = $this->table->getPrimary($throw);
|
||||
if ($primary === null) {
|
||||
return null;
|
||||
|
||||
} elseif (!is_array($primary)) {
|
||||
if (isset($this->data[$primary])) {
|
||||
return $this->data[$primary];
|
||||
} elseif ($throw) {
|
||||
throw new Nette\InvalidStateException("Row does not contain primary $primary column data.");
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
} else {
|
||||
$primaryVal = [];
|
||||
foreach ($primary as $key) {
|
||||
if (!isset($this->data[$key])) {
|
||||
if ($throw) {
|
||||
throw new Nette\InvalidStateException("Row does not contain primary $key column data.");
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
$primaryVal[$key] = $this->data[$key];
|
||||
}
|
||||
return $primaryVal;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns row signature (composition of primary keys)
|
||||
* @param bool
|
||||
* @return string
|
||||
*/
|
||||
public function getSignature($throw = true)
|
||||
{
|
||||
return implode('|', (array) $this->getPrimary($throw));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns referenced row.
|
||||
* @param string
|
||||
* @param string
|
||||
* @return self|null if the row does not exist
|
||||
*/
|
||||
public function ref($key, $throughColumn = null)
|
||||
{
|
||||
$row = $this->table->getReferencedTable($this, $key, $throughColumn);
|
||||
if ($row === false) {
|
||||
throw new Nette\MemberAccessException("No reference found for \${$this->table->getName()}->ref($key).");
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns referencing rows.
|
||||
* @param string
|
||||
* @param string
|
||||
* @return GroupedSelection
|
||||
*/
|
||||
public function related($key, $throughColumn = null)
|
||||
{
|
||||
$groupedSelection = $this->table->getReferencingTable($key, $throughColumn, $this[$this->table->getPrimary()]);
|
||||
if (!$groupedSelection) {
|
||||
throw new Nette\MemberAccessException("No reference found for \${$this->table->getName()}->related($key).");
|
||||
}
|
||||
|
||||
return $groupedSelection;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates row.
|
||||
* @param iterable (column => value)
|
||||
* @return bool
|
||||
*/
|
||||
public function update($data)
|
||||
{
|
||||
if ($data instanceof \Traversable) {
|
||||
$data = iterator_to_array($data);
|
||||
}
|
||||
|
||||
$primary = $this->getPrimary();
|
||||
if (!is_array($primary)) {
|
||||
$primary = [$this->table->getPrimary() => $primary];
|
||||
}
|
||||
|
||||
$selection = $this->table->createSelectionInstance()
|
||||
->wherePrimary($primary);
|
||||
|
||||
if ($selection->update($data)) {
|
||||
if ($tmp = array_intersect_key($data, $primary)) {
|
||||
$selection = $this->table->createSelectionInstance()
|
||||
->wherePrimary($tmp + $primary);
|
||||
}
|
||||
$selection->select('*');
|
||||
if (($row = $selection->fetch()) === false) {
|
||||
throw new Nette\InvalidStateException('Database refetch failed; row does not exist!');
|
||||
}
|
||||
$this->data = $row->data;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deletes row.
|
||||
* @return int number of affected rows
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
$res = $this->table->createSelectionInstance()
|
||||
->wherePrimary($this->getPrimary())
|
||||
->delete();
|
||||
|
||||
if ($res > 0 && ($signature = $this->getSignature(false))) {
|
||||
unset($this->table[$signature]);
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
||||
/********************* interface IteratorAggregate ****************d*g**/
|
||||
|
||||
|
||||
public function getIterator()
|
||||
{
|
||||
$this->accessColumn(null);
|
||||
return new \ArrayIterator($this->data);
|
||||
}
|
||||
|
||||
|
||||
/********************* interface ArrayAccess & magic accessors ****************d*g**/
|
||||
|
||||
|
||||
/**
|
||||
* Stores value in column.
|
||||
* @param string
|
||||
* @param mixed
|
||||
* @return void
|
||||
*/
|
||||
public function offsetSet($column, $value)
|
||||
{
|
||||
$this->__set($column, $value);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns value of column.
|
||||
* @param string
|
||||
* @return mixed
|
||||
*/
|
||||
public function offsetGet($column)
|
||||
{
|
||||
return $this->__get($column);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tests if column exists.
|
||||
* @param string
|
||||
* @return bool
|
||||
*/
|
||||
public function offsetExists($column)
|
||||
{
|
||||
return $this->__isset($column);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes column from data.
|
||||
* @param string
|
||||
* @return void
|
||||
*/
|
||||
public function offsetUnset($column)
|
||||
{
|
||||
$this->__unset($column);
|
||||
}
|
||||
|
||||
|
||||
public function __set($column, $value)
|
||||
{
|
||||
throw new Nette\DeprecatedException('ActiveRow is read-only; use update() method instead.');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string
|
||||
* @return ActiveRow|mixed
|
||||
* @throws Nette\MemberAccessException
|
||||
*/
|
||||
public function &__get($key)
|
||||
{
|
||||
if ($this->accessColumn($key)) {
|
||||
return $this->data[$key];
|
||||
}
|
||||
|
||||
$referenced = $this->table->getReferencedTable($this, $key);
|
||||
if ($referenced !== false) {
|
||||
$this->accessColumn($key, false);
|
||||
return $referenced;
|
||||
}
|
||||
|
||||
$this->removeAccessColumn($key);
|
||||
$hint = Nette\Utils\ObjectMixin::getSuggestion(array_keys($this->data), $key);
|
||||
throw new Nette\MemberAccessException("Cannot read an undeclared column '$key'" . ($hint ? ", did you mean '$hint'?" : '.'));
|
||||
}
|
||||
|
||||
|
||||
public function __isset($key)
|
||||
{
|
||||
if ($this->accessColumn($key)) {
|
||||
return isset($this->data[$key]);
|
||||
}
|
||||
|
||||
$referenced = $this->table->getReferencedTable($this, $key);
|
||||
if ($referenced !== false) {
|
||||
$this->accessColumn($key, false);
|
||||
return (bool) $referenced;
|
||||
}
|
||||
|
||||
$this->removeAccessColumn($key);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public function __unset($key)
|
||||
{
|
||||
throw new Nette\DeprecatedException('ActiveRow is read-only.');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function accessColumn($key, $selectColumn = true)
|
||||
{
|
||||
if ($this->table->accessColumn($key, $selectColumn) && !$this->dataRefreshed) {
|
||||
if (!isset($this->table[$this->getSignature()])) {
|
||||
throw new Nette\InvalidStateException("Database refetch failed; row with signature '{$this->getSignature()}' does not exist!");
|
||||
}
|
||||
$this->data = $this->table[$this->getSignature()]->data;
|
||||
$this->dataRefreshed = true;
|
||||
}
|
||||
return isset($this->data[$key]) || array_key_exists($key, $this->data);
|
||||
}
|
||||
|
||||
|
||||
protected function removeAccessColumn($key)
|
||||
{
|
||||
$this->table->removeAccessColumn($key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Database\Table;
|
||||
|
||||
use Nette\Caching\Cache;
|
||||
use Nette\Caching\IStorage;
|
||||
|
||||
|
||||
class ColumnAccessCache
|
||||
{
|
||||
/** @var Selection */
|
||||
protected $selection;
|
||||
|
||||
/** @var Cache */
|
||||
protected $cache;
|
||||
|
||||
/** @var string */
|
||||
protected $generalCacheKey;
|
||||
|
||||
/** @var string */
|
||||
protected $specificCacheKey;
|
||||
|
||||
/** @var array of touched columns */
|
||||
protected $accessedColumns = [];
|
||||
|
||||
/** @var array of earlier touched columns */
|
||||
protected $previousAccessedColumns;
|
||||
|
||||
/** @var Selection instance of observed accessed columns */
|
||||
protected $observeCache;
|
||||
|
||||
|
||||
public function __construct(Selection $selection, IStorage $cacheStorage = null)
|
||||
{
|
||||
$this->selection = $selection;
|
||||
$this->cache = $cacheStorage ? new Cache($cacheStorage, 'Nette.Database.' . md5($selection->getConnection()->getDsn())) : null;
|
||||
}
|
||||
|
||||
|
||||
public function getStorage(): ?IStorage
|
||||
{
|
||||
return $this->cache ? $this->cache->getStorage() : null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns general cache key independent on query parameters or sql limit
|
||||
* Used e.g. for previously accessed columns caching
|
||||
*/
|
||||
public function getGeneralCacheKey(): string
|
||||
{
|
||||
if ($this->generalCacheKey) {
|
||||
return $this->generalCacheKey;
|
||||
}
|
||||
|
||||
$key = [__CLASS__, $this->selection->getName(), $this->selection->getSqlBuilder()->getConditions()];
|
||||
$trace = [];
|
||||
foreach (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) as $item) {
|
||||
$trace[] = isset($item['file'], $item['line']) ? $item['file'] . $item['line'] : null;
|
||||
}
|
||||
|
||||
$key[] = $trace;
|
||||
return $this->generalCacheKey = md5(serialize($key));
|
||||
}
|
||||
|
||||
|
||||
public function setGeneralCacheKey(?string $key): void
|
||||
{
|
||||
$this->generalCacheKey = $key;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns object specific cache key dependent on query parameters
|
||||
* Used e.g. for reference memory caching
|
||||
*/
|
||||
public function getSpecificCacheKey(): string
|
||||
{
|
||||
if ($this->specificCacheKey) {
|
||||
return $this->specificCacheKey;
|
||||
}
|
||||
|
||||
return $this->specificCacheKey = $this->selection->getSqlBuilder()->getSelectQueryHash($this->getPreviousAccessedColumns());
|
||||
}
|
||||
|
||||
|
||||
public function setSpecificCacheKey(?string $key): void
|
||||
{
|
||||
$this->specificCacheKey = $key;
|
||||
}
|
||||
|
||||
|
||||
public function getAccessedColumns(): array
|
||||
{
|
||||
return $this->accessedColumns;
|
||||
}
|
||||
|
||||
|
||||
public function setAccessedColumns(array $accessedColumns): void
|
||||
{
|
||||
if ($this->cache) {
|
||||
$this->accessedColumns = $accessedColumns;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function setAccessedColumn(string $key, bool $value): void
|
||||
{
|
||||
if ($this->cache) {
|
||||
$this->accessedColumns[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Loads cache of previous accessed columns and returns it.
|
||||
*/
|
||||
public function getPreviousAccessedColumns(): array
|
||||
{
|
||||
if ($this->cache && $this->previousAccessedColumns === null) {
|
||||
$this->accessedColumns = $this->previousAccessedColumns = $this->cache->load($this->getGeneralCacheKey()) ?: [];
|
||||
}
|
||||
|
||||
return array_keys(array_filter((array) $this->previousAccessedColumns));
|
||||
}
|
||||
|
||||
|
||||
public function setPreviousAccessedColumns(array $previousAccessedColumns): void
|
||||
{
|
||||
$this->previousAccessedColumns = $previousAccessedColumns;
|
||||
}
|
||||
|
||||
|
||||
public function clearPreviousAccessedColumns(): void
|
||||
{
|
||||
$this->previousAccessedColumns = null;
|
||||
}
|
||||
|
||||
|
||||
public function saveState(): void
|
||||
{
|
||||
if ($this->observeCache === $this->selection && $this->cache && !$this->selection->getSqlBuilder()->getSelect() && $this->accessedColumns !== $this->previousAccessedColumns) {
|
||||
$previousAccessed = $this->cache->load($this->getGeneralCacheKey());
|
||||
$accessed = $this->accessedColumns;
|
||||
$needSave = is_array($previousAccessed)
|
||||
? array_intersect_key($accessed, $previousAccessed) !== $accessed
|
||||
: $accessed !== $previousAccessed;
|
||||
|
||||
if ($needSave) {
|
||||
$save = is_array($previousAccessed) ? $previousAccessed + $accessed : $accessed;
|
||||
$this->cache->save($this->getGeneralCacheKey(), $save);
|
||||
$this->previousAccessedColumns = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function &loadFromRefCache(&$referencing): string
|
||||
{
|
||||
$hash = $this->getSpecificCacheKey();
|
||||
$this->observeCache = &$referencing['observeCache'];
|
||||
$this->accessedColumns = &$referencing[$hash]['accessed'];
|
||||
$this->specificCacheKey = &$referencing[$hash]['specificCacheKey'];
|
||||
|
||||
if ($this->accessedColumns === null) {
|
||||
$this->accessedColumns = [];
|
||||
}
|
||||
|
||||
return $hash;
|
||||
}
|
||||
|
||||
|
||||
public function setObserveCache(Selection $observeCache): void
|
||||
{
|
||||
$this->observeCache = $observeCache;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function setSelection(Selection $selection): void
|
||||
{
|
||||
$this->selection = $selection;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
namespace Nette\Database\Table;
|
||||
|
||||
use Nette;
|
||||
use Nette\Database\Context;
|
||||
use Nette\Database\IConventions;
|
||||
|
||||
|
||||
/**
|
||||
* Representation of filtered table grouped by some column.
|
||||
* GroupedSelection is based on the great library NotORM http://www.notorm.com written by Jakub Vrana.
|
||||
*/
|
||||
class GroupedSelection extends Selection
|
||||
{
|
||||
/** @var Selection referenced table */
|
||||
protected $refTable;
|
||||
|
||||
/** @var mixed current assigned referencing array */
|
||||
protected $refCacheCurrent;
|
||||
|
||||
/** @var string grouping column name */
|
||||
protected $column;
|
||||
|
||||
/** @var int primary key */
|
||||
protected $active;
|
||||
|
||||
|
||||
/**
|
||||
* Creates filtered and grouped table representation.
|
||||
* @param Context
|
||||
* @param IConventions
|
||||
* @param string database table name
|
||||
* @param string joining column
|
||||
* @param Selection
|
||||
* @param Nette\Caching\IStorage|null
|
||||
*/
|
||||
public function __construct(Context $context, IConventions $conventions, $tableName, $column, Selection $refTable, Nette\Caching\IStorage $cacheStorage = null)
|
||||
{
|
||||
$this->refTable = $refTable;
|
||||
$this->column = $column;
|
||||
parent::__construct($context, $conventions, $tableName, $cacheStorage);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets active group.
|
||||
* @internal
|
||||
* @param int primary key of grouped rows
|
||||
* @return static
|
||||
*/
|
||||
public function setActive($active)
|
||||
{
|
||||
$this->active = $active;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
public function select($columns, ...$params)
|
||||
{
|
||||
if (!$this->sqlBuilder->getSelect()) {
|
||||
$this->sqlBuilder->addSelect("$this->name.$this->column");
|
||||
}
|
||||
|
||||
return parent::select($columns, ...$params);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
public function order($columns, ...$params)
|
||||
{
|
||||
if (!$this->sqlBuilder->getOrder()) {
|
||||
// improve index utilization
|
||||
$this->sqlBuilder->addOrder("$this->name.$this->column" . (preg_match('~\bDESC\z~i', $columns) ? ' DESC' : ''));
|
||||
}
|
||||
|
||||
return parent::order($columns, ...$params);
|
||||
}
|
||||
|
||||
|
||||
/********************* aggregations ****************d*g**/
|
||||
|
||||
|
||||
public function aggregation($function)
|
||||
{
|
||||
$aggregation = &$this->getRefTable($refPath)->aggregation[$refPath . $function . $this->sqlBuilder->getSelectQueryHash($this->getPreviousAccessedColumns())];
|
||||
|
||||
if ($aggregation === null) {
|
||||
$aggregation = [];
|
||||
|
||||
$selection = $this->createSelectionInstance();
|
||||
$selection->getSqlBuilder()->importConditions($this->getSqlBuilder());
|
||||
$selection->select($function);
|
||||
$selection->select("$this->name.$this->column");
|
||||
$selection->group("$this->name.$this->column");
|
||||
|
||||
foreach ($selection as $row) {
|
||||
$aggregation[$row[$this->column]] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($aggregation[$this->active])) {
|
||||
foreach ($aggregation[$this->active] as $val) {
|
||||
return $val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function count($column = null)
|
||||
{
|
||||
$return = parent::count($column);
|
||||
return isset($return) ? $return : 0;
|
||||
}
|
||||
|
||||
|
||||
/********************* internal ****************d*g**/
|
||||
|
||||
|
||||
protected function execute()
|
||||
{
|
||||
if ($this->rows !== null) {
|
||||
$this->observeCache = $this;
|
||||
return;
|
||||
}
|
||||
|
||||
$accessedColumns = $this->accessedColumns;
|
||||
$this->loadRefCache();
|
||||
|
||||
if (!isset($this->refCacheCurrent['data'])) {
|
||||
// we have not fetched any data yet => init accessedColumns by cached accessedColumns
|
||||
$this->accessedColumns = $accessedColumns;
|
||||
|
||||
$limit = $this->sqlBuilder->getLimit();
|
||||
$rows = count($this->refTable->rows);
|
||||
if ($limit && $rows > 1) {
|
||||
$this->sqlBuilder->setLimit(null, null);
|
||||
}
|
||||
parent::execute();
|
||||
$this->sqlBuilder->setLimit($limit, null);
|
||||
$data = [];
|
||||
$offset = [];
|
||||
$this->accessColumn($this->column);
|
||||
foreach ((array) $this->rows as $key => $row) {
|
||||
$ref = &$data[$row[$this->column]];
|
||||
$skip = &$offset[$row[$this->column]];
|
||||
if ($limit === null || $rows <= 1 || (count($ref ?: []) < $limit && $skip >= $this->sqlBuilder->getOffset())) {
|
||||
$ref[$key] = $row;
|
||||
} else {
|
||||
unset($this->rows[$key]);
|
||||
}
|
||||
$skip++;
|
||||
unset($ref, $skip);
|
||||
}
|
||||
|
||||
$this->refCacheCurrent['data'] = $data;
|
||||
$this->data = &$this->refCacheCurrent['data'][$this->active];
|
||||
}
|
||||
|
||||
$this->observeCache = $this;
|
||||
if ($this->data === null) {
|
||||
$this->data = [];
|
||||
} else {
|
||||
foreach ($this->data as $row) {
|
||||
$row->setTable($this); // injects correct parent GroupedSelection
|
||||
}
|
||||
reset($this->data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return Selection
|
||||
*/
|
||||
protected function getRefTable(&$refPath)
|
||||
{
|
||||
$refObj = $this->refTable;
|
||||
$refPath = $this->name . '.';
|
||||
while ($refObj instanceof self) {
|
||||
$refPath .= $refObj->name . '.';
|
||||
$refObj = $refObj->refTable;
|
||||
}
|
||||
|
||||
return $refObj;
|
||||
}
|
||||
|
||||
|
||||
protected function loadRefCache()
|
||||
{
|
||||
$hash = $this->getSpecificCacheKey();
|
||||
$referencing = &$this->refCache['referencing'][$this->getGeneralCacheKey()];
|
||||
$this->observeCache = &$referencing['observeCache'];
|
||||
$this->refCacheCurrent = &$referencing[$hash];
|
||||
$this->accessedColumns = &$referencing[$hash]['accessed'];
|
||||
$this->specificCacheKey = &$referencing[$hash]['specificCacheKey'];
|
||||
$this->rows = &$referencing[$hash]['rows'];
|
||||
|
||||
if (isset($referencing[$hash]['data'][$this->active])) {
|
||||
$this->data = &$referencing[$hash]['data'][$this->active];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected function emptyResultSet($saveCache = true, $deleteRererencedCache = true)
|
||||
{
|
||||
parent::emptyResultSet($saveCache, false);
|
||||
}
|
||||
|
||||
|
||||
/********************* manipulation ****************d*g**/
|
||||
|
||||
|
||||
public function insert($data)
|
||||
{
|
||||
if ($data instanceof \Traversable && !$data instanceof Selection) {
|
||||
$data = iterator_to_array($data);
|
||||
}
|
||||
|
||||
if (Nette\Utils\Arrays::isList($data)) {
|
||||
foreach (array_keys($data) as $key) {
|
||||
$data[$key][$this->column] = $this->active;
|
||||
}
|
||||
} else {
|
||||
$data[$this->column] = $this->active;
|
||||
}
|
||||
|
||||
return parent::insert($data);
|
||||
}
|
||||
|
||||
|
||||
public function update($data)
|
||||
{
|
||||
$builder = $this->sqlBuilder;
|
||||
|
||||
$this->sqlBuilder = clone $this->sqlBuilder;
|
||||
$this->where($this->column, $this->active);
|
||||
$return = parent::update($data);
|
||||
|
||||
$this->sqlBuilder = $builder;
|
||||
return $return;
|
||||
}
|
||||
|
||||
|
||||
public function delete()
|
||||
{
|
||||
$builder = $this->sqlBuilder;
|
||||
|
||||
$this->sqlBuilder = clone $this->sqlBuilder;
|
||||
$this->where($this->column, $this->active);
|
||||
$return = parent::delete();
|
||||
|
||||
$this->sqlBuilder = $builder;
|
||||
return $return;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
namespace Nette\Database\Table;
|
||||
|
||||
use Nette\Database;
|
||||
|
||||
|
||||
/**
|
||||
* Row interface.
|
||||
*/
|
||||
interface IRow extends Database\IRow
|
||||
{
|
||||
function setTable(Selection $name);
|
||||
|
||||
/**
|
||||
* @return Selection
|
||||
*/
|
||||
function getTable();
|
||||
|
||||
/**
|
||||
* Returns primary key value.
|
||||
* @param bool
|
||||
* @return mixed
|
||||
*/
|
||||
function getPrimary($throw = true);
|
||||
|
||||
/**
|
||||
* Returns row signature (composition of primary keys)
|
||||
* @param bool
|
||||
* @return string
|
||||
*/
|
||||
function getSignature($throw = true);
|
||||
|
||||
/**
|
||||
* Returns referencing rows.
|
||||
* @param string
|
||||
* @param string
|
||||
* @return GroupedSelection
|
||||
*/
|
||||
function related($key, $throughColumn = null);
|
||||
|
||||
/**
|
||||
* Returns referenced row.
|
||||
* @param string
|
||||
* @param string
|
||||
* @return IRow|null if the row does not exist
|
||||
*/
|
||||
function ref($key, $throughColumn = null);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
namespace Nette\Database\Table;
|
||||
|
||||
use Nette\Database;
|
||||
|
||||
|
||||
/**
|
||||
* Container of database result fetched into IRow objects.
|
||||
*
|
||||
* @method IRow|bool fetch() Fetches single row object.
|
||||
* @method IRow[] fetchAll() Fetches all rows.
|
||||
*/
|
||||
interface IRowContainer extends Database\IRowContainer
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Database\Table;
|
||||
|
||||
|
||||
class ReferenceCache
|
||||
{
|
||||
/** @var array */
|
||||
protected $referencingPrototype = [];
|
||||
|
||||
/** @var array */
|
||||
protected $referencing = [];
|
||||
|
||||
/** @var array */
|
||||
protected $referenced = [];
|
||||
|
||||
|
||||
public function &getReferencingPrototype(string $specificCacheKey, string $table, string $column)
|
||||
{
|
||||
return $this->referencingPrototype[$specificCacheKey]["$table.$column"];
|
||||
}
|
||||
|
||||
|
||||
public function clearReferencingPrototype(): void
|
||||
{
|
||||
$this->referencingPrototype = [];
|
||||
}
|
||||
|
||||
|
||||
public function &getReferencing(string $generalCacheKey): ?array
|
||||
{
|
||||
return $this->referencing[$generalCacheKey];
|
||||
}
|
||||
|
||||
|
||||
public function unsetReferencing(string $generalCacheKey, string $specificCacheKey): void
|
||||
{
|
||||
unset($this->referencing[$generalCacheKey][$specificCacheKey]);
|
||||
}
|
||||
|
||||
|
||||
public function &getReferenced(string $specificCacheKey, string $table, string $column): ?array
|
||||
{
|
||||
return $this->referenced[$specificCacheKey]["$table.$column"];
|
||||
}
|
||||
|
||||
|
||||
public function clearReferenced(): void
|
||||
{
|
||||
$this->referenced = [];
|
||||
}
|
||||
}
|
||||
+1102
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,897 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
namespace Nette\Database\Table;
|
||||
|
||||
use Nette;
|
||||
use Nette\Database\Context;
|
||||
use Nette\Database\IConventions;
|
||||
use Nette\Database\IStructure;
|
||||
use Nette\Database\ISupplementalDriver;
|
||||
use Nette\Database\SqlLiteral;
|
||||
|
||||
|
||||
/**
|
||||
* Builds SQL query.
|
||||
* SqlBuilder is based on great library NotORM http://www.notorm.com written by Jakub Vrana.
|
||||
*/
|
||||
class SqlBuilder
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
/** @var string */
|
||||
protected $tableName;
|
||||
|
||||
/** @var IConventions */
|
||||
protected $conventions;
|
||||
|
||||
/** @var string delimited table name */
|
||||
protected $delimitedTable;
|
||||
|
||||
/** @var array of column to select */
|
||||
protected $select = [];
|
||||
|
||||
/** @var array of where conditions */
|
||||
protected $where = [];
|
||||
|
||||
/** @var array of array of join conditions */
|
||||
protected $joinCondition = [];
|
||||
|
||||
/** @var array of where conditions for caching */
|
||||
protected $conditions = [];
|
||||
|
||||
/** @var array of parameters passed to where conditions */
|
||||
protected $parameters = [
|
||||
'select' => [],
|
||||
'joinCondition' => [],
|
||||
'where' => [],
|
||||
'group' => [],
|
||||
'having' => [],
|
||||
'order' => [],
|
||||
];
|
||||
|
||||
/** @var array or columns to order by */
|
||||
protected $order = [];
|
||||
|
||||
/** @var int number of rows to fetch */
|
||||
protected $limit;
|
||||
|
||||
/** @var int first row to fetch */
|
||||
protected $offset;
|
||||
|
||||
/** @var string columns to grouping */
|
||||
protected $group = '';
|
||||
|
||||
/** @var string grouping condition */
|
||||
protected $having = '';
|
||||
|
||||
/** @var array of reserved table names associated with chain */
|
||||
protected $reservedTableNames = [];
|
||||
|
||||
/** @var array of table aliases */
|
||||
protected $aliases = [];
|
||||
|
||||
/** @var string currently parsing alias for joins */
|
||||
protected $currentAlias;
|
||||
|
||||
/** @var ISupplementalDriver */
|
||||
private $driver;
|
||||
|
||||
/** @var IStructure */
|
||||
private $structure;
|
||||
|
||||
/** @var array */
|
||||
private $cacheTableList;
|
||||
|
||||
/** @var array of expanding joins */
|
||||
private $expandingJoins = [];
|
||||
|
||||
|
||||
public function __construct($tableName, Context $context)
|
||||
{
|
||||
$this->tableName = $tableName;
|
||||
$this->driver = $context->getConnection()->getSupplementalDriver();
|
||||
$this->conventions = $context->getConventions();
|
||||
$this->structure = $context->getStructure();
|
||||
$tableNameParts = explode('.', $tableName);
|
||||
$this->delimitedTable = implode('.', array_map([$this->driver, 'delimite'], $tableNameParts));
|
||||
$this->checkUniqueTableName(end($tableNameParts), $tableName);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTableName()
|
||||
{
|
||||
return $this->tableName;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string
|
||||
*/
|
||||
public function buildInsertQuery()
|
||||
{
|
||||
return "INSERT INTO {$this->delimitedTable}";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string
|
||||
*/
|
||||
public function buildUpdateQuery()
|
||||
{
|
||||
$query = "UPDATE {$this->delimitedTable} SET ?set" . $this->tryDelimite($this->buildConditions());
|
||||
|
||||
if ($this->order !== []) {
|
||||
$query .= ' ORDER BY ' . implode(', ', $this->order);
|
||||
}
|
||||
|
||||
if ($this->limit !== null || $this->offset) {
|
||||
$this->driver->applyLimit($query, $this->limit, $this->offset);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string
|
||||
*/
|
||||
public function buildDeleteQuery()
|
||||
{
|
||||
$query = "DELETE FROM {$this->delimitedTable}" . $this->tryDelimite($this->buildConditions());
|
||||
if ($this->limit !== null || $this->offset) {
|
||||
$this->driver->applyLimit($query, $this->limit, $this->offset);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns select query hash for caching.
|
||||
* @return string
|
||||
*/
|
||||
public function getSelectQueryHash(array $columns = null)
|
||||
{
|
||||
$parts = [
|
||||
'delimitedTable' => $this->delimitedTable,
|
||||
'queryCondition' => $this->buildConditions(),
|
||||
'queryEnd' => $this->buildQueryEnd(),
|
||||
$this->aliases,
|
||||
$this->limit, $this->offset,
|
||||
];
|
||||
if ($this->select) {
|
||||
$parts[] = $this->select;
|
||||
} elseif ($columns) {
|
||||
$parts[] = [$this->delimitedTable, $columns];
|
||||
} elseif ($this->group && !$this->driver->isSupported(ISupplementalDriver::SUPPORT_SELECT_UNGROUPED_COLUMNS)) {
|
||||
$parts[] = [$this->group];
|
||||
} else {
|
||||
$parts[] = "{$this->delimitedTable}.*";
|
||||
}
|
||||
return $this->getConditionHash(json_encode($parts), [
|
||||
$this->parameters['select'],
|
||||
$this->parameters['joinCondition'],
|
||||
$this->parameters['where'],
|
||||
$this->parameters['group'],
|
||||
$this->parameters['having'],
|
||||
$this->parameters['order'],
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns SQL query.
|
||||
* @param string[] list of columns
|
||||
* @return string
|
||||
*/
|
||||
public function buildSelectQuery(array $columns = null)
|
||||
{
|
||||
if (!$this->order && ($this->limit !== null || $this->offset)) {
|
||||
$this->order = array_map(
|
||||
function ($col) { return "$this->tableName.$col"; },
|
||||
(array) $this->conventions->getPrimary($this->tableName)
|
||||
);
|
||||
}
|
||||
|
||||
$queryJoinConditions = $this->buildJoinConditions();
|
||||
$queryCondition = $this->buildConditions();
|
||||
$queryEnd = $this->buildQueryEnd();
|
||||
|
||||
$joins = [];
|
||||
$finalJoinConditions = $this->parseJoinConditions($joins, $queryJoinConditions);
|
||||
$this->parseJoins($joins, $queryCondition);
|
||||
$this->parseJoins($joins, $queryEnd);
|
||||
|
||||
if ($this->select) {
|
||||
$querySelect = $this->buildSelect($this->select);
|
||||
$this->parseJoins($joins, $querySelect);
|
||||
|
||||
} elseif ($columns) {
|
||||
$prefix = $joins ? "{$this->delimitedTable}." : '';
|
||||
$cols = [];
|
||||
foreach ($columns as $col) {
|
||||
$cols[] = $prefix . $col;
|
||||
}
|
||||
$querySelect = $this->buildSelect($cols);
|
||||
|
||||
} elseif ($this->group && !$this->driver->isSupported(ISupplementalDriver::SUPPORT_SELECT_UNGROUPED_COLUMNS)) {
|
||||
$querySelect = $this->buildSelect([$this->group]);
|
||||
$this->parseJoins($joins, $querySelect);
|
||||
|
||||
} else {
|
||||
$prefix = $joins ? "{$this->delimitedTable}." : '';
|
||||
$querySelect = $this->buildSelect([$prefix . '*']);
|
||||
}
|
||||
|
||||
$queryJoins = $this->buildQueryJoins($joins, $finalJoinConditions);
|
||||
$query = "{$querySelect} FROM {$this->delimitedTable}{$queryJoins}{$queryCondition}{$queryEnd}";
|
||||
|
||||
$this->driver->applyLimit($query, $this->limit, $this->offset);
|
||||
|
||||
return $this->tryDelimite($query);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getParameters()
|
||||
{
|
||||
if (!isset($this->parameters['joinConditionSorted'])) {
|
||||
$this->buildSelectQuery();
|
||||
}
|
||||
return array_merge(
|
||||
$this->parameters['select'],
|
||||
$this->parameters['joinConditionSorted'] ? call_user_func_array('array_merge', $this->parameters['joinConditionSorted']) : [],
|
||||
$this->parameters['where'],
|
||||
$this->parameters['group'],
|
||||
$this->parameters['having'],
|
||||
$this->parameters['order']
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public function importConditions(self $builder)
|
||||
{
|
||||
$this->where = $builder->where;
|
||||
$this->joinCondition = $builder->joinCondition;
|
||||
$this->parameters['where'] = $builder->parameters['where'];
|
||||
$this->parameters['joinCondition'] = $builder->parameters['joinCondition'];
|
||||
$this->conditions = $builder->conditions;
|
||||
$this->aliases = $builder->aliases;
|
||||
$this->reservedTableNames = $builder->reservedTableNames;
|
||||
}
|
||||
|
||||
|
||||
/********************* SQL selectors ****************d*g**/
|
||||
|
||||
|
||||
public function addSelect($columns, ...$params)
|
||||
{
|
||||
if (is_array($columns)) {
|
||||
throw new Nette\InvalidArgumentException('Select column must be a string.');
|
||||
}
|
||||
$this->select[] = $columns;
|
||||
$this->parameters['select'] = array_merge($this->parameters['select'], $params);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getSelect()
|
||||
{
|
||||
return $this->select;
|
||||
}
|
||||
|
||||
|
||||
public function resetSelect()
|
||||
{
|
||||
$this->select = [];
|
||||
$this->parameters['select'] = [];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function addWhere($condition, ...$params)
|
||||
{
|
||||
return $this->addCondition($condition, $params, $this->where, $this->parameters['where']);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function addJoinCondition($tableChain, $condition, ...$params)
|
||||
{
|
||||
$this->parameters['joinConditionSorted'] = null;
|
||||
if (!isset($this->joinCondition[$tableChain])) {
|
||||
$this->joinCondition[$tableChain] = $this->parameters['joinCondition'][$tableChain] = [];
|
||||
}
|
||||
return $this->addCondition($condition, $params, $this->joinCondition[$tableChain], $this->parameters['joinCondition'][$tableChain]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
protected function addCondition($condition, array $params, array &$conditions, array &$conditionsParameters)
|
||||
{
|
||||
if (is_array($condition) && !empty($params[0]) && is_array($params[0])) {
|
||||
return $this->addConditionComposition($condition, $params[0], $conditions, $conditionsParameters);
|
||||
}
|
||||
|
||||
$hash = $this->getConditionHash($condition, $params);
|
||||
if (isset($this->conditions[$hash])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->conditions[$hash] = $condition;
|
||||
$placeholderCount = substr_count($condition, '?');
|
||||
if ($placeholderCount > 1 && count($params) === 1 && is_array($params[0])) {
|
||||
$params = $params[0];
|
||||
}
|
||||
|
||||
$condition = trim($condition);
|
||||
if ($placeholderCount === 0 && count($params) === 1) {
|
||||
$condition .= ' ?';
|
||||
} elseif ($placeholderCount !== count($params)) {
|
||||
throw new Nette\InvalidArgumentException('Argument count does not match placeholder count.');
|
||||
}
|
||||
|
||||
$replace = null;
|
||||
$placeholderNum = 0;
|
||||
foreach ($params as $arg) {
|
||||
preg_match('#(?:.*?\?.*?){' . $placeholderNum . '}(((?:&|\||^|~|\+|-|\*|/|%|\(|,|<|>|=|(?<=\W|^)(?:REGEXP|ALL|AND|ANY|BETWEEN|EXISTS|IN|[IR]?LIKE|OR|NOT|SOME|INTERVAL))\s*)?(?:\(\?\)|\?))#s', $condition, $match, PREG_OFFSET_CAPTURE);
|
||||
$hasOperator = ($match[1][0] === '?' && $match[1][1] === 0) ? true : !empty($match[2][0]);
|
||||
|
||||
if ($arg === null) {
|
||||
$replace = 'IS NULL';
|
||||
if ($hasOperator) {
|
||||
if (trim($match[2][0]) === 'NOT') {
|
||||
$replace = 'IS NOT NULL';
|
||||
} else {
|
||||
throw new Nette\InvalidArgumentException('Column operator does not accept null argument.');
|
||||
}
|
||||
}
|
||||
} elseif (is_array($arg) || $arg instanceof Selection) {
|
||||
if ($hasOperator) {
|
||||
if (trim($match[2][0]) === 'NOT') {
|
||||
$match[2][0] = rtrim($match[2][0]) . ' IN ';
|
||||
} elseif (trim($match[2][0]) !== 'IN') {
|
||||
throw new Nette\InvalidArgumentException('Column operator does not accept array argument.');
|
||||
}
|
||||
} else {
|
||||
$match[2][0] = 'IN ';
|
||||
}
|
||||
|
||||
if ($arg instanceof Selection) {
|
||||
$clone = clone $arg;
|
||||
if (!$clone->getSqlBuilder()->select) {
|
||||
try {
|
||||
$clone->select($clone->getPrimary());
|
||||
} catch (\LogicException $e) {
|
||||
throw new Nette\InvalidArgumentException('Selection argument must have defined a select column.', 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->driver->isSupported(ISupplementalDriver::SUPPORT_SUBSELECT)) {
|
||||
$arg = null;
|
||||
$replace = $match[2][0] . '(' . $clone->getSql() . ')';
|
||||
$conditionsParameters = array_merge($conditionsParameters, $clone->getSqlBuilder()->getParameters());
|
||||
} else {
|
||||
$arg = [];
|
||||
foreach ($clone as $row) {
|
||||
$arg[] = array_values(iterator_to_array($row));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($arg !== null) {
|
||||
if (!$arg) {
|
||||
$hasBrackets = strpos($condition, '(') !== false;
|
||||
$hasOperators = preg_match('#AND|OR#', $condition);
|
||||
$hasNot = strpos($condition, 'NOT') !== false;
|
||||
$hasPrefixNot = strpos($match[2][0], 'NOT') !== false;
|
||||
if (!$hasBrackets && ($hasOperators || ($hasNot && !$hasPrefixNot))) {
|
||||
throw new Nette\InvalidArgumentException('Possible SQL query corruption. Add parentheses around operators.');
|
||||
}
|
||||
if ($hasPrefixNot) {
|
||||
$replace = 'IS NULL OR TRUE';
|
||||
} else {
|
||||
$replace = 'IS NULL AND FALSE';
|
||||
}
|
||||
$arg = null;
|
||||
} else {
|
||||
$replace = $match[2][0] . '(?)';
|
||||
$conditionsParameters[] = $arg;
|
||||
}
|
||||
}
|
||||
} elseif ($arg instanceof SqlLiteral) {
|
||||
$conditionsParameters[] = $arg;
|
||||
} else {
|
||||
if (!$hasOperator) {
|
||||
$replace = '= ?';
|
||||
}
|
||||
$conditionsParameters[] = $arg;
|
||||
}
|
||||
|
||||
if ($replace) {
|
||||
$condition = substr_replace($condition, $replace, $match[1][1], strlen($match[1][0]));
|
||||
$replace = null;
|
||||
}
|
||||
|
||||
if ($arg !== null) {
|
||||
$placeholderNum++;
|
||||
}
|
||||
}
|
||||
|
||||
$conditions[] = $condition;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getConditions()
|
||||
{
|
||||
return array_values($this->conditions);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds alias.
|
||||
* @param string
|
||||
* @param string
|
||||
* @return void
|
||||
*/
|
||||
public function addAlias($chain, $alias)
|
||||
{
|
||||
if (isset($chain[0]) && $chain[0] !== '.' && $chain[0] !== ':') {
|
||||
$chain = '.' . $chain; // unified chain format
|
||||
}
|
||||
$this->checkUniqueTableName($alias, $chain);
|
||||
$this->aliases[$alias] = $chain;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string
|
||||
* @param string
|
||||
* @return void
|
||||
*/
|
||||
protected function checkUniqueTableName($tableName, $chain)
|
||||
{
|
||||
if (isset($this->aliases[$tableName]) && ($chain === '.' . $tableName)) {
|
||||
$chain = $this->aliases[$tableName];
|
||||
}
|
||||
if (isset($this->reservedTableNames[$tableName])) {
|
||||
if ($this->reservedTableNames[$tableName] === $chain) {
|
||||
return;
|
||||
}
|
||||
throw new Nette\InvalidArgumentException("Table alias '$tableName' from chain '$chain' is already in use by chain '{$this->reservedTableNames[$tableName]}'. Please add/change alias for one of them.");
|
||||
}
|
||||
$this->reservedTableNames[$tableName] = $chain;
|
||||
}
|
||||
|
||||
|
||||
public function addOrder($columns, ...$params)
|
||||
{
|
||||
$this->order[] = $columns;
|
||||
$this->parameters['order'] = array_merge($this->parameters['order'], $params);
|
||||
}
|
||||
|
||||
|
||||
public function setOrder(array $columns, array $parameters)
|
||||
{
|
||||
$this->order = $columns;
|
||||
$this->parameters['order'] = $parameters;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getOrder()
|
||||
{
|
||||
return $this->order;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param int|null
|
||||
* @param int|null
|
||||
* @return void
|
||||
*/
|
||||
public function setLimit($limit, $offset)
|
||||
{
|
||||
$this->limit = $limit;
|
||||
$this->offset = $offset;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
*/
|
||||
public function getLimit()
|
||||
{
|
||||
return $this->limit;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
*/
|
||||
public function getOffset()
|
||||
{
|
||||
return $this->offset;
|
||||
}
|
||||
|
||||
|
||||
public function setGroup($columns, ...$params)
|
||||
{
|
||||
$this->group = $columns;
|
||||
$this->parameters['group'] = $params;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getGroup()
|
||||
{
|
||||
return $this->group;
|
||||
}
|
||||
|
||||
|
||||
public function setHaving($having, ...$params)
|
||||
{
|
||||
$this->having = $having;
|
||||
$this->parameters['having'] = $params;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getHaving()
|
||||
{
|
||||
return $this->having;
|
||||
}
|
||||
|
||||
|
||||
/********************* SQL building ****************d*g**/
|
||||
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function buildSelect(array $columns)
|
||||
{
|
||||
return 'SELECT ' . implode(', ', $columns);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function parseJoinConditions(&$joins, $joinConditions)
|
||||
{
|
||||
$tableJoins = $leftJoinDependency = $finalJoinConditions = [];
|
||||
foreach ($joinConditions as $tableChain => &$joinCondition) {
|
||||
$fooQuery = $tableChain . '.foo';
|
||||
$requiredJoins = [];
|
||||
$this->parseJoins($requiredJoins, $fooQuery);
|
||||
$tableAlias = substr($fooQuery, 0, -4);
|
||||
$tableJoins[$tableAlias] = $requiredJoins;
|
||||
$leftJoinDependency[$tableAlias] = [];
|
||||
$finalJoinConditions[$tableAlias] = preg_replace_callback($this->getColumnChainsRegxp(), function (array $match) use ($tableAlias, &$tableJoins, &$leftJoinDependency) {
|
||||
$requiredJoins = [];
|
||||
$query = $this->parseJoinsCb($requiredJoins, $match);
|
||||
$queryParts = explode('.', $query);
|
||||
$tableJoins[$queryParts[0]] = $requiredJoins;
|
||||
if ($queryParts[0] !== $tableAlias) {
|
||||
foreach (array_keys($requiredJoins) as $requiredTable) {
|
||||
$leftJoinDependency[$tableAlias][$requiredTable] = $requiredTable;
|
||||
}
|
||||
}
|
||||
return $query;
|
||||
}, $joinCondition);
|
||||
}
|
||||
$this->parameters['joinConditionSorted'] = [];
|
||||
if (count($joinConditions)) {
|
||||
while (reset($tableJoins)) {
|
||||
$this->getSortedJoins(key($tableJoins), $leftJoinDependency, $tableJoins, $joins);
|
||||
}
|
||||
}
|
||||
return $finalJoinConditions;
|
||||
}
|
||||
|
||||
|
||||
protected function getSortedJoins($table, &$leftJoinDependency, &$tableJoins, &$finalJoins)
|
||||
{
|
||||
if (isset($this->expandingJoins[$table])) {
|
||||
$path = implode("' => '", array_map(function ($value) { return $this->reservedTableNames[$value]; }, array_merge(array_keys($this->expandingJoins), [$table])));
|
||||
throw new Nette\InvalidArgumentException("Circular reference detected at left join conditions (tables '$path').");
|
||||
}
|
||||
if (isset($tableJoins[$table])) {
|
||||
$this->expandingJoins[$table] = true;
|
||||
if (isset($leftJoinDependency[$table])) {
|
||||
foreach ($leftJoinDependency[$table] as $requiredTable) {
|
||||
if ($requiredTable === $table) {
|
||||
continue;
|
||||
}
|
||||
$this->getSortedJoins($requiredTable, $leftJoinDependency, $tableJoins, $finalJoins);
|
||||
}
|
||||
}
|
||||
if ($tableJoins[$table]) {
|
||||
foreach ($tableJoins[$table] as $requiredTable => $tmp) {
|
||||
if ($requiredTable === $table) {
|
||||
continue;
|
||||
}
|
||||
$this->getSortedJoins($requiredTable, $leftJoinDependency, $tableJoins, $finalJoins);
|
||||
}
|
||||
}
|
||||
$finalJoins += $tableJoins[$table];
|
||||
$key = isset($this->aliases[$table]) ? $table : $this->reservedTableNames[$table];
|
||||
$this->parameters['joinConditionSorted'] += isset($this->parameters['joinCondition'][$key])
|
||||
? [$table => $this->parameters['joinCondition'][$key]]
|
||||
: [];
|
||||
unset($tableJoins[$table], $this->expandingJoins[$table]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected function parseJoins(&$joins, &$query)
|
||||
{
|
||||
$query = preg_replace_callback($this->getColumnChainsRegxp(), function (array $match) use (&$joins) {
|
||||
return $this->parseJoinsCb($joins, $match);
|
||||
}, $query);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
private function getColumnChainsRegxp()
|
||||
{
|
||||
return '~
|
||||
(?P<chain> (?!\.) (?: [.:]? (?>[\w_]*[a-z][\w_]*) (\([\w_]*[a-z][\w_]*\))? ) *) \. (?P<column> (?>[\w_]*[a-z][\w_]*) | \* )
|
||||
~xi';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function parseJoinsCb(&$joins, $match)
|
||||
{
|
||||
$chain = $match['chain'];
|
||||
if (!empty($chain[0]) && ($chain[0] !== '.' && $chain[0] !== ':')) {
|
||||
$chain = '.' . $chain; // unified chain format
|
||||
}
|
||||
|
||||
preg_match_all('~
|
||||
(?P<del> [.:])?(?P<key> [\w_]*[a-z][\w_]* )(\((?P<throughColumn> [\w_]*[a-z][\w_]* )\))?
|
||||
~xi', $chain, $keyMatches, PREG_SET_ORDER);
|
||||
|
||||
$parent = $this->tableName;
|
||||
$parentAlias = preg_replace('#^(.*\.)?(.*)$#', '$2', $this->tableName);
|
||||
|
||||
// join schema keyMatch and table keyMatch to schema.table keyMatch
|
||||
if ($this->driver->isSupported(ISupplementalDriver::SUPPORT_SCHEMA) && count($keyMatches) > 1) {
|
||||
$tables = $this->getCachedTableList();
|
||||
if (!isset($tables[$keyMatches[0]['key']]) && isset($tables[$keyMatches[0]['key'] . '.' . $keyMatches[1]['key']])) {
|
||||
$keyMatch = array_shift($keyMatches);
|
||||
$keyMatches[0]['key'] = $keyMatch['key'] . '.' . $keyMatches[0]['key'];
|
||||
$keyMatches[0]['del'] = $keyMatch['del'];
|
||||
}
|
||||
}
|
||||
|
||||
// do not make a join when referencing to the current table column - inner conditions
|
||||
// check it only when not making backjoin on itself - outer condition
|
||||
if ($keyMatches[0]['del'] === '.') {
|
||||
if (count($keyMatches) > 1 && ($parent === $keyMatches[0]['key'] || $parentAlias === $keyMatches[0]['key'])) {
|
||||
throw new Nette\InvalidArgumentException("Do not prefix table chain with origin table name '{$keyMatches[0]['key']}'. If you want to make self reference, please add alias.");
|
||||
}
|
||||
if ($parent === $keyMatches[0]['key']) {
|
||||
return "{$parent}.{$match['column']}";
|
||||
} elseif ($parentAlias === $keyMatches[0]['key']) {
|
||||
return "{$parentAlias}.{$match['column']}";
|
||||
}
|
||||
}
|
||||
$tableChain = null;
|
||||
foreach ($keyMatches as $index => $keyMatch) {
|
||||
$isLast = !isset($keyMatches[$index + 1]);
|
||||
if (!$index && isset($this->aliases[$keyMatch['key']])) {
|
||||
if ($keyMatch['del'] === ':') {
|
||||
throw new Nette\InvalidArgumentException("You are using has many syntax with alias (':{$keyMatch['key']}'). You have to move it to alias definition.");
|
||||
} else {
|
||||
$previousAlias = $this->currentAlias;
|
||||
$this->currentAlias = $keyMatch['key'];
|
||||
$requiredJoins = [];
|
||||
$query = $this->aliases[$keyMatch['key']] . '.foo';
|
||||
$this->parseJoins($requiredJoins, $query);
|
||||
$aliasJoin = array_pop($requiredJoins);
|
||||
$joins += $requiredJoins;
|
||||
list($table, , $parentAlias, $column, $primary) = $aliasJoin;
|
||||
$this->currentAlias = $previousAlias;
|
||||
}
|
||||
} elseif ($keyMatch['del'] === ':') {
|
||||
if (isset($keyMatch['throughColumn'])) {
|
||||
$table = $keyMatch['key'];
|
||||
$belongsTo = $this->conventions->getBelongsToReference($table, $keyMatch['throughColumn']);
|
||||
if (!$belongsTo) {
|
||||
throw new Nette\InvalidArgumentException("No reference found for \${$parent}->{$keyMatch['key']}.");
|
||||
}
|
||||
list(, $primary) = $belongsTo;
|
||||
|
||||
} else {
|
||||
$hasMany = $this->conventions->getHasManyReference($parent, $keyMatch['key']);
|
||||
if (!$hasMany) {
|
||||
throw new Nette\InvalidArgumentException("No reference found for \${$parent}->related({$keyMatch['key']}).");
|
||||
}
|
||||
list($table, $primary) = $hasMany;
|
||||
}
|
||||
$column = $this->conventions->getPrimary($parent);
|
||||
|
||||
} else {
|
||||
$belongsTo = $this->conventions->getBelongsToReference($parent, $keyMatch['key']);
|
||||
if (!$belongsTo) {
|
||||
throw new Nette\InvalidArgumentException("No reference found for \${$parent}->{$keyMatch['key']}.");
|
||||
}
|
||||
list($table, $column) = $belongsTo;
|
||||
$primary = $this->conventions->getPrimary($table);
|
||||
}
|
||||
|
||||
if ($this->currentAlias && $isLast) {
|
||||
$tableAlias = $this->currentAlias;
|
||||
} elseif ($parent === $table) {
|
||||
$tableAlias = $parentAlias . '_ref';
|
||||
} elseif ($keyMatch['key']) {
|
||||
$tableAlias = $keyMatch['key'];
|
||||
} else {
|
||||
$tableAlias = preg_replace('#^(.*\.)?(.*)$#', '$2', $table);
|
||||
}
|
||||
|
||||
$tableChain .= $keyMatch[0];
|
||||
if (!$isLast || !$this->currentAlias) {
|
||||
$this->checkUniqueTableName($tableAlias, $tableChain);
|
||||
}
|
||||
$joins[$tableAlias] = [$table, $tableAlias, $parentAlias, $column, $primary];
|
||||
$parent = $table;
|
||||
$parentAlias = $tableAlias;
|
||||
}
|
||||
|
||||
return $tableAlias . ".{$match['column']}";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function buildQueryJoins(array $joins, array $leftJoinConditions = [])
|
||||
{
|
||||
$return = '';
|
||||
foreach ($joins as list($joinTable, $joinAlias, $table, $tableColumn, $joinColumn)) {
|
||||
$return .=
|
||||
" LEFT JOIN {$joinTable}" . ($joinTable !== $joinAlias ? " {$joinAlias}" : '') .
|
||||
" ON {$table}.{$tableColumn} = {$joinAlias}.{$joinColumn}" .
|
||||
(isset($leftJoinConditions[$joinAlias]) ? " {$leftJoinConditions[$joinAlias]}" : '');
|
||||
}
|
||||
return $return;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function buildJoinConditions()
|
||||
{
|
||||
$conditions = [];
|
||||
foreach ($this->joinCondition as $tableChain => $joinConditions) {
|
||||
$conditions[$tableChain] = 'AND (' . implode(') AND (', $joinConditions) . ')';
|
||||
}
|
||||
return $conditions;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function buildConditions()
|
||||
{
|
||||
return $this->where ? ' WHERE (' . implode(') AND (', $this->where) . ')' : '';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function buildQueryEnd()
|
||||
{
|
||||
$return = '';
|
||||
if ($this->group) {
|
||||
$return .= ' GROUP BY ' . $this->group;
|
||||
}
|
||||
if ($this->having) {
|
||||
$return .= ' HAVING ' . $this->having;
|
||||
}
|
||||
if ($this->order) {
|
||||
$return .= ' ORDER BY ' . implode(', ', $this->order);
|
||||
}
|
||||
return $return;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function tryDelimite($s)
|
||||
{
|
||||
return preg_replace_callback('#(?<=[^\w`"\[?]|^)[a-z_][a-z0-9_]*(?=[^\w`"(\]]|\z)#i', function ($m) {
|
||||
return strtoupper($m[0]) === $m[0] ? $m[0] : $this->driver->delimite($m[0]);
|
||||
}, $s);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
protected function addConditionComposition(array $columns, array $parameters, array &$conditions, array &$conditionsParameters)
|
||||
{
|
||||
if ($this->driver->isSupported(ISupplementalDriver::SUPPORT_MULTI_COLUMN_AS_OR_COND)) {
|
||||
$conditionFragment = '(' . implode(' = ? AND ', $columns) . ' = ?) OR ';
|
||||
$condition = substr(str_repeat($conditionFragment, count($parameters)), 0, -4);
|
||||
return $this->addCondition($condition, [Nette\Utils\Arrays::flatten($parameters)], $conditions, $conditionsParameters);
|
||||
} else {
|
||||
return $this->addCondition('(' . implode(', ', $columns) . ') IN', [$parameters], $conditions, $conditionsParameters);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
private function getConditionHash($condition, array $parameters)
|
||||
{
|
||||
foreach ($parameters as $key => &$parameter) {
|
||||
if ($parameter instanceof Selection) {
|
||||
$parameter = $this->getConditionHash($parameter->getSql(), $parameter->getSqlBuilder()->getParameters());
|
||||
} elseif ($parameter instanceof SqlLiteral) {
|
||||
$parameter = $this->getConditionHash($parameter->__toString(), $parameter->getParameters());
|
||||
} elseif (is_object($parameter) && method_exists($parameter, '__toString')) {
|
||||
$parameter = $parameter->__toString();
|
||||
} elseif (is_array($parameter) || $parameter instanceof \ArrayAccess) {
|
||||
$parameter = $this->getConditionHash($key, $parameter);
|
||||
}
|
||||
}
|
||||
return md5($condition . json_encode($parameters));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
private function getCachedTableList()
|
||||
{
|
||||
if (!$this->cacheTableList) {
|
||||
$this->cacheTableList = array_flip(array_map(function (array $pair) {
|
||||
return isset($pair['fullName']) ? $pair['fullName'] : $pair['name'];
|
||||
}, $this->structure->getTables()));
|
||||
}
|
||||
|
||||
return $this->cacheTableList;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
namespace Nette\Database;
|
||||
|
||||
|
||||
/**
|
||||
* Server connection related errors.
|
||||
*/
|
||||
class ConnectionException extends DriverException
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Base class for all constraint violation related exceptions.
|
||||
*/
|
||||
class ConstraintViolationException extends DriverException
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Exception for a foreign key constraint violation.
|
||||
*/
|
||||
class ForeignKeyConstraintViolationException extends ConstraintViolationException
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Exception for a NOT NULL constraint violation.
|
||||
*/
|
||||
class NotNullConstraintViolationException extends ConstraintViolationException
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Exception for a unique constraint violation.
|
||||
*/
|
||||
class UniqueConstraintViolationException extends ConstraintViolationException
|
||||
{
|
||||
}
|
||||
Reference in New Issue
Block a user