no message

This commit is contained in:
BarboraFleg
2019-04-04 18:34:40 +02:00
commit ce48ca4a21
589 changed files with 82184 additions and 0 deletions
+41
View File
@@ -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
+41
View File
@@ -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
View File
@@ -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:
+60
View File
@@ -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)
+18
View File
@@ -0,0 +1,18 @@
Nette Database
==============
[![Downloads this Month](https://img.shields.io/packagist/dm/nette/database.svg)](https://packagist.org/packages/nette/database)
[![Build Status](https://travis-ci.org/nette/database.svg?branch=v2.4)](https://travis-ci.org/nette/database)
[![Build Status Windows](https://ci.appveyor.com/api/projects/status/github/nette/database?branch=v2.4&svg=true)](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();
}
}
@@ -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&nbsp;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>
@@ -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
View File
@@ -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
View File
@@ -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);
}
}
@@ -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;
}
}
+63
View File
@@ -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;
}
}
+155
View File
@@ -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
View File
@@ -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">&lt;binary&gt;</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)
. '>&lt;' . htmlspecialchars($type, ENT_NOQUOTES, 'UTF-8') . ' resource&gt;</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);
}
}
+46
View File
@@ -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
View File
@@ -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
{
}
+43
View File
@@ -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);
}
+91
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
}
+50
View File
@@ -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;
}
}
+279
View File
@@ -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
View File
@@ -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.");
}
}
+346
View File
@@ -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;
}
}
+54
View File
@@ -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 = [];
}
}
File diff suppressed because it is too large Load Diff
+897
View File
@@ -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;
}
}
+48
View File
@@ -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
{
}