Merge pull request #7 from forkbb/add_PostgreSQL

Add PostgreSQL support
This commit is contained in:
Visman 2021-12-11 21:35:35 +07:00 committed by GitHub
commit bc055d2e40
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 729 additions and 109 deletions

View file

@ -74,8 +74,12 @@ class DB extends PDO
self::ATTR_STATEMENT_CLASS => [DBStatement::class, [$this]],
];
$start = \microtime(true);
parent::__construct($dsn, $username, $password, $options);
$this->saveQuery('PDO::__construct()', \microtime(true) - $start, false);
$this->beginTransaction();
}

View file

@ -97,9 +97,10 @@ class Mysql
$value = "`{$value}`";
}
unset($value);
}
unset($value);
return \implode(',', $arr);
}
@ -132,22 +133,17 @@ class Mysql
*/
public function tableExists(string $table, bool $noPrefix = false): bool
{
$table = ($noPrefix ? '' : $this->dbPrefix) . $table;
$vars = [
':tname' => ($noPrefix ? '' : $this->dbPrefix) . $table,
];
$query = 'SELECT 1
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?s:tname';
try {
$vars = [
':table' => $table,
];
$query = 'SELECT 1
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?s:table';
$stmt = $this->db->query($query, $vars);
$result = $stmt->fetch();
$stmt = $this->db->query($query, $vars);
$result = $stmt->fetch();
$stmt->closeCursor();
} catch (PDOException $e) {
return false;
}
$stmt->closeCursor();
return ! empty($result);
}
@ -157,23 +153,18 @@ class Mysql
*/
public function fieldExists(string $table, string $field, bool $noPrefix = false): bool
{
$table = ($noPrefix ? '' : $this->dbPrefix) . $table;
$vars = [
':tname' => ($noPrefix ? '' : $this->dbPrefix) . $table,
':fname' => $field,
];
$query = 'SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?s:tname AND COLUMN_NAME = ?s:fname';
try {
$vars = [
':table' => $table,
':field' => $field,
];
$query = 'SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?s:table AND COLUMN_NAME = ?s:field';
$stmt = $this->db->query($query, $vars);
$result = $stmt->fetch();
$stmt = $this->db->query($query, $vars);
$result = $stmt->fetch();
$stmt->closeCursor();
} catch (PDOException $e) {
return false;
}
$stmt->closeCursor();
return ! empty($result);
}
@ -186,21 +177,18 @@ class Mysql
$table = ($noPrefix ? '' : $this->dbPrefix) . $table;
$index = 'PRIMARY' == $index ? $index : $table . '_' . $index;
try {
$vars = [
':table' => $table,
':index' => $index,
];
$query = 'SELECT 1
FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?s:table AND INDEX_NAME = ?s:index';
$vars = [
':tname' => $table,
':index' => $index,
];
$query = 'SELECT 1
FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?s:tname AND INDEX_NAME = ?s:index';
$stmt = $this->db->query($query, $vars);
$result = $stmt->fetch();
$stmt->closeCursor();
} catch (PDOException $e) {
return false;
}
$stmt = $this->db->query($query, $vars);
$result = $stmt->fetch();
$stmt->closeCursor();
return ! empty($result);
}
@ -213,8 +201,8 @@ class Mysql
$this->testStr($table);
$table = ($noPrefix ? '' : $this->dbPrefix) . $table;
$query = "CREATE TABLE IF NOT EXISTS `{$table}` (";
foreach ($schema['FIELDS'] as $field => $data) {
$this->testStr($field);
// имя и тип
@ -222,11 +210,13 @@ class Mysql
// сравнение
if (\preg_match('%^(?:CHAR|VARCHAR|TINYTEXT|TEXT|MEDIUMTEXT|LONGTEXT|ENUM|SET)%i', $data[0])) {
$query .= ' CHARACTER SET utf8mb4 COLLATE utf8mb4_';
if (
isset($data[3])
&& \is_string($data[3])
) {
$this->testStr($data[3]);
$query .= $data[3];
} else {
$query .= 'unicode_ci';
@ -240,11 +230,14 @@ class Mysql
if (isset($data[2])) {
$query .= ' DEFAULT ' . $this->convToStr($data[2]);
}
$query .= ', ';
}
if (isset($schema['PRIMARY KEY'])) {
$query .= 'PRIMARY KEY (' . $this->replIdxs($schema['PRIMARY KEY']) . '), ';
}
if (isset($schema['UNIQUE KEYS'])) {
foreach ($schema['UNIQUE KEYS'] as $key => $fields) {
$this->testStr($key);
@ -252,6 +245,7 @@ class Mysql
$query .= "UNIQUE `{$table}_{$key}` (" . $this->replIdxs($fields) . '), ';
}
}
if (isset($schema['INDEXES'])) {
foreach ($schema['INDEXES'] as $index => $fields) {
$this->testStr($index);
@ -259,13 +253,15 @@ class Mysql
$query .= "INDEX `{$table}_{$index}` (" . $this->replIdxs($fields) . '), ';
}
}
if (isset($schema['ENGINE'])) {
$engine = $schema['ENGINE'];
} else {
// при отсутствии типа таблицы он определяется на основании типов других таблиц в базе
$prefix = \str_replace('_', '\\_', $this->dbPrefix);
$stmt = $this->db->query("SHOW TABLE STATUS LIKE '{$prefix}%'");
$stmt = $this->db->query("SHOW TABLE STATUS LIKE '{$prefix}%'");
$engine = [];
while ($row = $stmt->fetch()) {
if (isset($engine[$row['Engine']])) {
++$engine[$row['Engine']];
@ -283,7 +279,9 @@ class Mysql
$engine = \array_shift($engine);
}
}
$this->testStr($engine);
$query = \rtrim($query, ', ') . ") ENGINE={$engine} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci";
return false !== $this->db->exec($query);
@ -335,14 +333,16 @@ class Mysql
}
$table = ($noPrefix ? '' : $this->dbPrefix) . $table;
$query = "ALTER TABLE `{$table}` ADD `{$field}` " . $this->replType($type);
if (! $allowNull) {
$query .= ' NOT NULL';
}
if (null !== $default) {
$query .= ' DEFAULT ' . $this->convToStr($default);
}
if (null !== $after) {
$this->testStr($after);
@ -361,14 +361,16 @@ class Mysql
$this->testStr($field);
$table = ($noPrefix ? '' : $this->dbPrefix) . $table;
$query = "ALTER TABLE `{$table}` MODIFY `{$field}` " . $this->replType($type);
if (! $allowNull) {
$query .= ' NOT NULL';
}
if (null !== $default) {
$query .= ' DEFAULT ' . $this->convToStr($default);
}
if (null !== $after) {
$this->testStr($after);
@ -405,38 +407,37 @@ class Mysql
$this->testStr($new);
if (
! $this->fieldExists($table, $old, $noPrefix)
|| $this->fieldExists($table, $new, $noPrefix)
$this->fieldExists($table, $new, $noPrefix)
&& ! $this->fieldExists($table, $old, $noPrefix)
) {
return false;
return true;
}
$table = ($noPrefix ? '' : $this->dbPrefix) . $table;
try {
$vars = [
':table' => $table,
':field' => $old,
];
$query = 'SELECT COLUMN_DEFAULT, IS_NULLABLE, COLUMN_TYPE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?s:table AND COLUMN_NAME = ?s:field';
$vars = [
':tname' => $table,
':fname' => $old,
];
$query = 'SELECT COLUMN_DEFAULT, IS_NULLABLE, COLUMN_TYPE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?s:tname AND COLUMN_NAME = ?s:fname';
$stmt = $this->db->query($query, $vars);
$result = $stmt->fetch();
$stmt->closeCursor();
$stmt = $this->db->query($query, $vars);
$result = $stmt->fetch();
$type = $result['COLUMN_TYPE'];
$allowNull = 'YES' == $result['IS_NULLABLE'];
$default = $result['COLUMN_DEFAULT'];
} catch (PDOException $e) {
return false;
}
$stmt->closeCursor();
$type = $result['COLUMN_TYPE'];
$allowNull = 'YES' == $result['IS_NULLABLE'];
$default = $result['COLUMN_DEFAULT'];
$query = "ALTER TABLE `{$table}` CHANGE COLUMN `{$old}` `{$new}` " . $this->replType($type);
if (! $allowNull) {
$query .= ' NOT NULL';
}
if (null !== $default) {
$query .= ' DEFAULT ' . $this->convToStr($default);
}
@ -456,8 +457,8 @@ class Mysql
}
$table = ($noPrefix ? '' : $this->dbPrefix) . $table;
$query = "ALTER TABLE `{$table}` ADD ";
if ('PRIMARY' == $index) {
$query .= 'PRIMARY KEY';
} else {
@ -471,6 +472,7 @@ class Mysql
$query .= "INDEX `{$index}`";
}
}
$query .= ' (' . $this->replIdxs($fields) . ')';
return false !== $this->db->exec($query);
@ -488,8 +490,8 @@ class Mysql
}
$table = ($noPrefix ? '' : $this->dbPrefix) . $table;
$query = "ALTER TABLE `{$table}` ";
if ('PRIMARY' == $index) {
$query .= "DROP PRIMARY KEY";
} else {
@ -551,7 +553,8 @@ class Mysql
}
return [
'db' => 'MySQL (PDO) ' . $this->db->getAttribute(PDO::ATTR_SERVER_VERSION) . ' : ' . implode(', ', $tmp),
'db' => 'MySQL (PDO) v.' . $this->db->getAttribute(PDO::ATTR_SERVER_VERSION),
'tables' => implode(', ', $tmp),
'records' => $records,
'size' => $size,
'server info' => $this->db->getAttribute(PDO::ATTR_SERVER_INFO),
@ -564,7 +567,7 @@ class Mysql
public function getMap(): array
{
$vars = [
"{$this->dbPrefix}%",
str_replace('_', '\\_', $this->dbPrefix) . '%',
];
$query = 'SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE
FROM INFORMATION_SCHEMA.COLUMNS
@ -573,12 +576,14 @@ class Mysql
$stmt = $this->db->query($query, $vars);
$result = [];
$table = null;
while ($row = $stmt->fetch()) {
if ($table !== $row['TABLE_NAME']) {
$table = $row['TABLE_NAME'];
$tableNoPref = \substr($table, \strlen($this->dbPrefix));
$table = $row['TABLE_NAME'];
$tableNoPref = \substr($table, \strlen($this->dbPrefix));
$result[$tableNoPref] = [];
}
$type = \strtolower($row['DATA_TYPE']);
$result[$tableNoPref][$row['COLUMN_NAME']] = $this->types[$type] ?? 's';
}

547
app/Core/DB/Pgsql.php Normal file
View file

@ -0,0 +1,547 @@
<?php
/**
* This file is part of the ForkBB <https://github.com/forkbb>.
*
* @copyright (c) Visman <mio.visman@yandex.ru, https://github.com/MioVisman>
* @license The MIT License (MIT)
*/
declare(strict_types=1);
namespace ForkBB\Core\DB;
use ForkBB\Core\DB;
use PDO;
use PDOStatement;
use PDOException;
class Pgsql
{
/**
* @var DB
*/
protected $db;
/**
* Префикс для таблиц базы
* @var string
*/
protected $dbPrefix;
/**
* Массив замены типов полей таблицы
* @var array
*/
protected $dbTypeRepl = [
'%^(?:TINY|SMALL)INT(?:\s*\(\d+\))?(?:\s*UNSIGNED)?$%i' => 'SMALLINT',
'%^(?:MEDIUM)?INT(?:\s*\(\d+\))?(?:\s*UNSIGNED)?$%i' => 'INTEGER',
'%^BIGINT(?:\s*\(\d+\))?(?:\s*UNSIGNED)?$%i' => 'BIGINT',
'%^(?:TINY|MEDIUM|LONG)TEXT$%i' => 'TEXT',
'%^DOUBLE(?:\s+PRECISION)?(?:\s*\([\d,]+\))?(?:\s*UNSIGNED)?$%i' => 'DOUBLE PRECISION',
'%^FLOAT(?:\s*\([\d,]+\))?(?:\s*UNSIGNED)?$%i' => 'REAL',
];
/**
* Подстановка типов полей для карты БД
* @var array
*/
protected $types = [
'bool' => 'b',
'boolean' => 'b',
'tinyint' => 'i',
'smallint' => 'i',
'mediumint' => 'i',
'int' => 'i',
'integer' => 'i',
'bigint' => 'i',
'decimal' => 'i',
'dec' => 'i',
'float' => 'i',
'real' => 'i',
'double' => 'i',
'double precision' => 'i',
];
public function __construct(DB $db, string $prefix)
{
$this->db = $db;
$this->testStr($prefix);
$this->dbPrefix = $prefix;
}
/**
* Перехват неизвестных методов
*/
public function __call(string $name, array $args)
{
throw new PDOException("Method '{$name}' not found in DB driver.");
}
/**
* Проверяет строку на допустимые символы
*/
protected function testStr(string $str): void
{
if (\preg_match('%[^a-zA-Z0-9_]%', $str)) {
throw new PDOException("Name '{$str}' have bad characters.");
}
}
/**
* Операции над полями индексов: проверка, замена
*/
protected function replIdxs(array $arr): string
{
foreach ($arr as &$value) {
if (\preg_match('%^(.*)\s*(\(\d+\))$%', $value, $matches)) {
$this->testStr($matches[1]);
$value = "\"{$matches[1]}\""; // {$matches[2]}
} else {
$this->testStr($value);
$value = "\"{$value}\"";
}
}
unset($value);
return \implode(',', $arr);
}
/**
* Замена типа поля в соответствии с dbTypeRepl
*/
protected function replType(string $type): string
{
return \preg_replace(\array_keys($this->dbTypeRepl), \array_values($this->dbTypeRepl), $type);
}
/**
* Конвертирует данные в строку для DEFAULT
*/
protected function convToStr(/* mixed */ $data): string
{
if (\is_string($data)) {
return $this->db->quote($data);
} elseif (\is_numeric($data)) {
return (string) $data;
} elseif (\is_bool($data)) {
return $data ? 'true' : 'false';
} else {
throw new PDOException('Invalid data type for DEFAULT.');
}
}
/**
* Проверяет наличие таблицы в базе
*/
public function tableExists(string $table, bool $noPrefix = false): bool
{
$vars = [
':schema' => 'public',
':tname' => ($noPrefix ? '' : $this->dbPrefix) . $table,
':ttype' => 'r',
];
$query = 'SELECT 1
FROM pg_class AS c
INNER JOIN pg_namespace AS n ON n.oid=c.relnamespace
WHERE c.relname=?s:tname AND c.relkind=?s:ttype AND n.nspname=?s:schema';
$stmt = $this->db->query($query, $vars);
$result = $stmt->fetch();
$stmt->closeCursor();
return ! empty($result);
}
/**
* Проверяет наличие поля в таблице
*/
public function fieldExists(string $table, string $field, bool $noPrefix = false): bool
{
$vars = [
':schema' => 'public',
':tname' => ($noPrefix ? '' : $this->dbPrefix) . $table,
':ttype' => 'r',
':fname' => $field,
];
$query = 'SELECT 1
FROM pg_attribute AS a
INNER JOIN pg_class AS c ON a.attrelid=c.oid
INNER JOIN pg_namespace AS n ON n.oid=c.relnamespace
WHERE a.attname=?s:fname AND c.relname=?s:tname AND c.relkind=?s:ttype AND n.nspname=?s:schema';
$stmt = $this->db->query($query, $vars);
$result = $stmt->fetch();
$stmt->closeCursor();
return ! empty($result);
}
/**
* Проверяет наличие индекса в таблице
*/
public function indexExists(string $table, string $index, bool $noPrefix = false): bool
{
$table = ($noPrefix ? '' : $this->dbPrefix) . $table;
$index = $table . '_' . ('PRIMARY' === $index ? 'pkey' : $index);
$vars = [
':schema' => 'public',
':tname' => $table,
':ttype' => 'r',
':iname' => $index,
':itype' => 'i',
];
$query = 'SELECT 1
FROM pg_class AS i
INNER JOIN pg_index AS ix ON ix.indexrelid=i.oid
INNER JOIN pg_class AS c ON c.oid=ix.indrelid
INNER JOIN pg_namespace AS n ON n.oid=c.relnamespace
WHERE i.relname=?s:iname
AND i.relkind=?s:itype
AND c.relname=?s:tname
AND c.relkind=?s:ttype
AND n.nspname=?s:schema';
$stmt = $this->db->query($query, $vars);
$result = $stmt->fetch();
$stmt->closeCursor();
return ! empty($result);
}
/**
* Создает таблицу
*/
public function createTable(string $table, array $schema, bool $noPrefix = false): bool
{
$this->testStr($table);
$table = ($noPrefix ? '' : $this->dbPrefix) . $table;
$query = "CREATE TABLE IF NOT EXISTS \"{$table}\" (";
foreach ($schema['FIELDS'] as $field => $data) {
$this->testStr($field);
// имя и тип
$query .= "\"{$field}\" " . $this->replType($data[0]);
if ('SERIAL' !== \strtoupper($data[0])) {
// не NULL
if (empty($data[1])) {
$query .= ' NOT NULL';
}
// значение по умолчанию
if (isset($data[2])) {
$query .= ' DEFAULT ' . $this->convToStr($data[2]);
}
}
$query .= ', ';
}
if (isset($schema['PRIMARY KEY'])) {
$query .= 'PRIMARY KEY (' . $this->replIdxs($schema['PRIMARY KEY']) . '), ';
}
$query = \rtrim($query, ', ') . ")";
$result = false !== $this->db->exec($query);
// вынесено отдельно для сохранения имен индексов
if ($result && isset($schema['UNIQUE KEYS'])) {
foreach ($schema['UNIQUE KEYS'] as $key => $fields) {
$result = $result && $this->addIndex($table, $key, $fields, true, true);
}
}
if ($result && isset($schema['INDEXES'])) {
foreach ($schema['INDEXES'] as $index => $fields) {
$result = $result && $this->addIndex($table, $index, $fields, false, true);
}
}
return $result;
}
/**
* Удаляет таблицу
*/
public function dropTable(string $table, bool $noPrefix = false): bool
{
$this->testStr($table);
$table = ($noPrefix ? '' : $this->dbPrefix) . $table;
return false !== $this->db->exec("DROP TABLE IF EXISTS \"{$table}\"");
}
/**
* Переименовывает таблицу
*/
public function renameTable(string $old, string $new, bool $noPrefix = false): bool
{
$this->testStr($old);
$this->testStr($new);
if (
$this->tableExists($new, $noPrefix)
&& ! $this->tableExists($old, $noPrefix)
) {
return true;
}
$old = ($noPrefix ? '' : $this->dbPrefix) . $old;
$new = ($noPrefix ? '' : $this->dbPrefix) . $new;
return false !== $this->db->exec("ALTER TABLE \"{$old}\" RENAME TO \"{$new}\"");
}
/**
* Добавляет поле в таблицу
*/
public function addField(string $table, string $field, string $type, bool $allowNull, /* mixed */ $default = null, string $after = null, bool $noPrefix = false): bool
{
$this->testStr($table);
$this->testStr($field);
if ($this->fieldExists($table, $field, $noPrefix)) {
return true;
}
$table = ($noPrefix ? '' : $this->dbPrefix) . $table;
$query = "ALTER TABLE \"{$table}\" ADD \"{$field}\" " . $this->replType($type);
if ('SERIAL' !== \strtoupper($type)) {
if (! $allowNull) {
$query .= ' NOT NULL';
}
if (null !== $default) {
$query .= ' DEFAULT ' . $this->convToStr($default);
}
}
return false !== $this->db->exec($query);
}
/**
* Модифицирует поле в таблице
*/
public function alterField(string $table, string $field, string $type, bool $allowNull, /* mixed */ $default = null, string $after = null, bool $noPrefix = false): bool
{
$this->testStr($table);
$this->testStr($field);
$table = ($noPrefix ? '' : $this->dbPrefix) . $table;
$query = "ALTER TABLE \"{$table}\"";
if ('SERIAL' === \strtoupper($type) || $allowNull) {
$query .= " ALTER COLUMN \"{$field}\" DROP NOT NULL,";
}
if ('SERIAL' === \strtoupper($type) || null === $default) {
$query .= " ALTER COLUMN \"{$field}\" DROP DEFAULT,";
}
$query = " ALTER COLUMN \"{$field}\" TYPE " . $this->replType($type) . ','; // ???? Использовать USING?
if ('SERIAL' !== \strtoupper($type) && ! $allowNull) {
$query .= " ALTER COLUMN \"{$field}\" SET NOT NULL,";
}
if ('SERIAL' !== \strtoupper($type) && null !== $default) {
$query .= " ALTER COLUMN \"{$field}\" SET DEFAULT " . $this->convToStr($default) . ',';
}
$query = \rtrim($query, ',');
return false !== $this->db->exec($query);
}
/**
* Удаляет поле из таблицы
*/
public function dropField(string $table, string $field, bool $noPrefix = false): bool
{
$this->testStr($table);
$this->testStr($field);
if (! $this->fieldExists($table, $field, $noPrefix)) {
return true;
}
$table = ($noPrefix ? '' : $this->dbPrefix) . $table;
return false !== $this->db->exec("ALTER TABLE \"{$table}\" DROP COLUMN \"{$field}\"");
}
/**
* Переименование поля в таблице
*/
public function renameField(string $table, string $old, string $new, bool $noPrefix = false): bool
{
$this->testStr($table);
$this->testStr($old);
$this->testStr($new);
if (
$this->fieldExists($table, $new, $noPrefix)
&& ! $this->fieldExists($table, $old, $noPrefix)
) {
return true;
}
$table = ($noPrefix ? '' : $this->dbPrefix) . $table;
return false !== $this->db->exec("ALTER TABLE \"{$table}\" RENAME COLUMN \"{$old}\" TO \"{$new}\"");
}
/**
* Добавляет индекс в таблицу
*/
public function addIndex(string $table, string $index, array $fields, bool $unique = false, bool $noPrefix = false): bool
{
$this->testStr($table);
if ($this->indexExists($table, $index, $noPrefix)) {
return true;
}
$table = ($noPrefix ? '' : $this->dbPrefix) . $table;
if ('PRIMARY' === $index) {
// ?????
} else {
$index = $table . '_' . $index;
$this->testStr($index);
$unique = $unique ? 'UNIQUE' : '';
$query = "CREATE {$unique} INDEX \"{$index}\" ON \"{$table}\" (" . $this->replIdxs($fields) . ')';
}
return false !== $this->db->exec($query);
}
/**
* Удаляет индекс из таблицы
*/
public function dropIndex(string $table, string $index, bool $noPrefix = false): bool
{
$this->testStr($table);
if (! $this->indexExists($table, $index, $noPrefix)) {
return true;
}
$table = ($noPrefix ? '' : $this->dbPrefix) . $table;
$index = $table . '_' . ('PRIMARY' === $index ? 'pkey' : $index);
$this->testStr($index);
return false !== $this->db->exec("DROP INDEX \"{$index}\"");
}
/**
* Очищает таблицу
*/
public function truncateTable(string $table, bool $noPrefix = false): bool
{
$this->testStr($table);
$table = ($noPrefix ? '' : $this->dbPrefix) . $table;
return false !== $this->db->exec("TRUNCATE TABLE ONLY \"{$table}\"");
}
/**
* Возвращает статистику
*/
public function statistics(): array
{
$records = $size = $tables = 0;
$vars = [
':schema' => 'public',
':tname' => str_replace('_', '\\_', $this->dbPrefix) . '%',
];
$query = 'SELECT c.relname, c.relpages, c.reltuples, c.relkind
FROM pg_class AS c
INNER JOIN pg_namespace AS n ON n.oid=c.relnamespace
WHERE n.nspname=?s:schema AND c.relname LIKE ?s:tname';
$stmt = $this->db->query($query, $vars);
while ($row = $stmt->fetch()) {
if ('r' === $row['relkind']) { // обычная таблица
++$tables;
$records += $row['reltuples'];
}
$size += $row['relpages'];
}
$other = [
'pg_database_size' => $this->db->query('SELECT pg_size_pretty(pg_database_size(current_database()))')->fetchColumn(),
]
+ $this->db->query('SELECT name, setting FROM pg_settings WHERE category ~ \'Locale\'')->fetchAll(PDO::FETCH_KEY_PAIR);
/*
$stmt = $this->db->query('SHOW ALL');
while ($row = $stmt->fetch()) {
if ('block_size' === $row['name']) {
$blockSize = (int) $row['setting'];
} elseif (\preg_match('%^lc_|_encoding%', $row['name'])) {
$other[$row['name']] = $row['setting'];
}
}
*/
$blockSize = $this->db->query('SELECT current_setting(\'block_size\')')->fetchColumn();
$size *= $blockSize ?: 8192;
return [
'db' => 'PostgreSQL (PDO) v.' . $this->db->getAttribute(PDO::ATTR_SERVER_VERSION),
'tables' => (string) $tables,
'records' => $records,
'size' => $size,
// 'server info' => $this->db->getAttribute(PDO::ATTR_SERVER_INFO),
] + $other;
}
/**
* Формирует карту базы данных
*/
public function getMap(): array
{
$vars = [
':schema' => 'public',
':tname' => str_replace('_', '\\_', $this->dbPrefix) . '%',
];
$query = 'SELECT table_name, column_name, data_type
FROM information_schema.columns
WHERE table_catalog = current_database() AND table_schema = ?s:schema AND table_name LIKE ?s:tname';
$stmt = $this->db->query($query, $vars);
$result = [];
$table = null;
while ($row = $stmt->fetch()) {
if ($table !== $row['table_name']) {
$table = $row['table_name'];
$tableNoPref = \substr($table, \strlen($this->dbPrefix));
$result[$tableNoPref] = [];
}
$type = \strtolower($row['data_type']);
$result[$tableNoPref][$row['column_name']] = $this->types[$type] ?? 's';
}
return $result;
}
}

View file

@ -253,8 +253,8 @@ class Groups extends Admin
'g_post_links' => 'required|integer|in:0,1',
'g_search' => 'required|integer|in:0,1',
'g_search_users' => 'required|integer|in:0,1',
'g_post_flood' => 'required|integer|min:0|max:999999',
'g_search_flood' => 'required|integer|min:0|max:999999',
'g_post_flood' => 'required|integer|min:0|max:32767',
'g_search_flood' => 'required|integer|min:0|max:32767',
]);
if (
@ -290,9 +290,9 @@ class Groups extends Admin
'g_deledit_interval' => 'required|integer|min:0|max:999999',
'g_set_title' => 'required|integer|in:0,1',
'g_send_email' => 'required|integer|in:0,1',
'g_email_flood' => 'required|integer|min:0|max:999999',
'g_report_flood' => 'required|integer|min:0|max:999999',
'g_sig_length' => 'required|integer|min:0|max:10000',
'g_email_flood' => 'required|integer|min:0|max:32767',
'g_report_flood' => 'required|integer|min:0|max:32767',
'g_sig_length' => 'required|integer|min:0|max:16000',
'g_sig_lines' => 'required|integer|min:0|max:255',
'g_pm' => 'required|integer|in:0,1',
'g_pm_limit' => 'required|integer|min:0|max:999999',
@ -654,7 +654,7 @@ class Groups extends Admin
$fieldset['g_post_flood'] = [
'type' => 'number',
'min' => '0',
'max' => '999999',
'max' => '32767',
'value' => $group->g_post_flood,
'caption' => 'Post flood label',
'help' => 'Post flood help',
@ -662,7 +662,7 @@ class Groups extends Admin
$fieldset['g_search_flood'] = [
'type' => 'number',
'min' => '0',
'max' => '999999',
'max' => '32767',
'value' => $group->g_search_flood,
'caption' => 'Search flood label',
'help' => 'Search flood help',
@ -680,7 +680,7 @@ class Groups extends Admin
$fieldset['g_email_flood'] = [
'type' => 'number',
'min' => '0',
'max' => '999999',
'max' => '32767',
'value' => $group->g_email_flood,
'caption' => 'E-mail flood label',
'help' => 'E-mail flood help',
@ -688,7 +688,7 @@ class Groups extends Admin
$fieldset['g_report_flood'] = [
'type' => 'number',
'min' => '0',
'max' => '999999',
'max' => '32767',
'value' => $group->g_report_flood,
'caption' => 'Report flood label',
'help' => 'Report flood help',

View file

@ -457,8 +457,8 @@ class Install extends Admin
if (! \preg_match('%^[a-z][a-z\d_]*$%i', $prefix)) {
$v->addError('Table prefix error');
} elseif (
'sqlite' === $v->dbtype
&& 'sqlite_' === \strtolower($prefix)
'sqlite_' === \strtolower($prefix)
|| 'pg_' === \strtolower($prefix)
) {
$v->addError('Prefix reserved');
}
@ -474,7 +474,7 @@ class Install extends Admin
{
$this->c->DB_USERNAME = $v->dbuser;
$this->c->DB_PASSWORD = $v->dbpass;
$this->c->DB_PREFIX = \is_string($v->dbprefix) ? $v->dbprefix : '';
$this->c->DB_PREFIX = $v->dbprefix;
$dbtype = $v->dbtype;
$dbname = $v->dbname;
@ -502,6 +502,16 @@ class Install extends Admin
case 'sqlite':
break;
case 'pgsql':
if (\preg_match('%^([^:]+):(\d+)$%', $dbhost, $matches)) {
$host = $matches[1];
$port = $matches[2];
} else {
$host = $dbhost;
$port = '5432';
}
$this->c->DB_DSN = "pgsql:host={$host} port={$port} dbname={$dbname} options='--client_encoding=UTF8'";
break;
default:
//????
@ -520,16 +530,10 @@ class Install extends Admin
}
// проверка наличия таблицы пользователей в БД
try {
$stmt = $this->c->DB->query('SELECT 1 FROM ::users LIMIT 1');
if ($this->c->DB->tableExists('users')) {
$v->addError(['Existing table error', $v->dbprefix, $v->dbname]);
if (! empty($stmt->fetch())) {
$v->addError(['Existing table error', $v->dbprefix, $v->dbname]);
return $dbhost;
}
} catch (PDOException $e) {
// все отлично, таблица пользователей не найдена
return $dbhost;
}
// база MySQL, кодировка базы отличается от UTF-8 (4 байта)
@ -540,6 +544,30 @@ class Install extends Admin
$v->addError('Bad database charset');
}
// база PostgreSQL, кодировка базы
if (
isset($stat['server_encoding'])
&& 'UTF8' !== $stat['server_encoding']
) {
$v->addError('Bad database encoding');
}
// база PostgreSQL, порядок сопоставления/сортировки
if (
isset($stat['lc_collate'])
&& 'C' !== $stat['lc_collate']
) {
$v->addError('Bad database collate');
}
// база PostgreSQL, тип символов
if (
isset($stat['lc_ctype'])
&& 'C' !== $stat['lc_ctype']
) {
$v->addError('Bad database ctype');
}
return $dbhost;
}
@ -1218,17 +1246,29 @@ class Install extends Admin
$ip = \filter_var($_SERVER['REMOTE_ADDR'], \FILTER_VALIDATE_IP) ?: '0.0.0.0';
$adminId = 1;
$catId = 1;
$forumId = 1;
$topicId = 1;
$postId = 1;
$this->c->DB->exec('INSERT INTO ::users (id, group_id, username, username_normal, password, email, email_normal, language, style, num_posts, last_post, registered, registration_ip, last_visit, signature, num_topics) VALUES (?i, ?i, ?s, ?s, ?s, ?s, ?s, ?s, ?s, 1, ?i, ?i, ?s, ?i, \'\', 1)', [$adminId, FORK_GROUP_ADMIN, $v->username, $this->c->users->normUsername($v->username), password_hash($v->password, \PASSWORD_DEFAULT), $v->email, $this->c->NormEmail->normalize($v->email), $v->defaultlang, $v->defaultstyle, $now, $now, $ip, $now]);
$this->c->DB->exec('INSERT INTO ::categories (id, cat_name, disp_position) VALUES (?i, ?s, ?i)', [$catId, __('Test category'), 1]);
$this->c->DB->exec('INSERT INTO ::forums (id, forum_name, forum_desc, num_topics, num_posts, last_post, last_post_id, last_poster, last_poster_id, last_topic, disp_position, cat_id, moderators) VALUES (?i, ?s, ?s, ?i, ?i, ?i, ?i, ?s, ?i, ?s, ?i, ?i, \'\')', [$forumId, __('Test forum'), __('This is just a test forum'), 1, 1, $now, $postId, $v->username, $adminId, __('Test post'), 1, $catId]);
$this->c->DB->exec('INSERT INTO ::topics (id, poster, poster_id, subject, posted, first_post_id, last_post, last_post_id, last_poster, last_poster_id, forum_id) VALUES(?i, ?s, ?i, ?s, ?i, ?i, ?i, ?i, ?s, ?i, ?i)', [$topicId, $v->username, $adminId, __('Test post'), $now, $postId, $now, $postId, $v->username, $adminId, $forumId]);
$this->c->DB->exec('INSERT INTO ::posts (id, poster, poster_id, poster_ip, message, posted, topic_id) VALUES(?i, ?s, ?i, ?s, ?s, ?i, ?i)', [$postId, $v->username, $adminId, $ip, __('Test message'), $now, $topicId]);
$this->c->DB->exec('INSERT INTO ::users (group_id, username, username_normal, password, email, email_normal, language, style, num_posts, last_post, registered, registration_ip, last_visit, signature, num_topics) VALUES (?i, ?s, ?s, ?s, ?s, ?s, ?s, ?s, 1, ?i, ?i, ?s, ?i, \'\', 1)', [FORK_GROUP_ADMIN, $v->username, $this->c->users->normUsername($v->username), password_hash($v->password, \PASSWORD_DEFAULT), $v->email, $this->c->NormEmail->normalize($v->email), $v->defaultlang, $v->defaultstyle, $now, $now, $ip, $now]);
$adminId = (int) $this->c->DB->lastInsertId();
$this->c->DB->exec('INSERT INTO ::categories (cat_name, disp_position) VALUES (?s, ?i)', [__('Test category'), 1]);
$catId = (int) $this->c->DB->lastInsertId();
$this->c->DB->exec('INSERT INTO ::posts (poster, poster_id, poster_ip, message, posted, topic_id) VALUES(?s, ?i, ?s, ?s, ?i, ?i)', [$v->username, $adminId, $ip, __('Test message'), $now, $topicId]);
$postId = (int) $this->c->DB->lastInsertId();
$this->c->DB->exec('INSERT INTO ::forums (forum_name, forum_desc, num_topics, num_posts, last_post, last_post_id, last_poster, last_poster_id, last_topic, disp_position, cat_id, moderators) VALUES (?s, ?s, ?i, ?i, ?i, ?i, ?s, ?i, ?s, ?i, ?i, \'\')', [__('Test forum'), __('This is just a test forum'), 1, 1, $now, $postId, $v->username, $adminId, __('Test post'), 1, $catId]);
$forumId = (int) $this->c->DB->lastInsertId();
$this->c->DB->exec('INSERT INTO ::topics (poster, poster_id, subject, posted, first_post_id, last_post, last_post_id, last_poster, last_poster_id, forum_id) VALUES(?s, ?i, ?s, ?i, ?i, ?i, ?i, ?s, ?i, ?i)', [$v->username, $adminId, __('Test post'), $now, $postId, $now, $postId, $v->username, $adminId, $forumId]);
$topicId = (int) $this->c->DB->lastInsertId();
$this->c->DB->exec('UPDATE ::posts SET topic_id=?i WHERE id=?i', [$topicId, $postId]);
$smilies = [
':)' => 'smile.png',

View file

@ -155,7 +155,8 @@ class Statistics extends Admin
$this->dbVersion = $stat['db'];
$this->tSize = $stat['size'];
$this->tRecords = $stat['records'];
unset($stat['db'], $stat['size'], $stat['records']);
$this->tTables = $stat['tables'];
unset($stat['db'], $stat['size'], $stat['records'], $stat['tables']);
$this->tOther = $stat;
// Check for the existence of various PHP opcode caches/optimizers

View file

@ -99,6 +99,9 @@ msgstr "Accelerator:"
msgid "Database label"
msgstr "Database"
msgid "Database data tables"
msgstr "Tables: %s"
msgid "Database data rows"
msgstr "Rows: %s"

View file

@ -151,7 +151,7 @@ msgid "Table prefix error"
msgstr "The table prefix '%s' does not match the format."
msgid "Prefix reserved"
msgstr "The table prefix 'sqlite_' is reserved for use by the SQLite engine."
msgstr "The 'sqlite_' and 'pg_' prefixes are reserved. Please choose a different prefix."
msgid "Existing table error"
msgstr "A table called '%1$susers' is already present in the database '%2$s'. This could mean that ForkBB is already installed or that another piece of software is installed and is occupying one or more of the table names ForkBB requires. If you want to install multiple copies of ForkBB in the same database, you must choose a different table prefix."
@ -219,6 +219,15 @@ msgstr "<b>File uploads appear to be disallowed on this server!</b> If you want
msgid "Bad database charset"
msgstr "The database must be created with the character encoding <b>utf8mb4</b> (compare <b>utf8mb4_unicode_ci</b>)."
msgid "Bad database encoding"
msgstr "The database must be created with the character encoding <b>UTF8</b>."
msgid "Bad database collate"
msgstr "The database must be created with Collation order <b>C</b> (LC_COLLATE)."
msgid "Bad database ctype"
msgstr "The database must be created with Character classification <b>C</b> (LC_CTYPE)."
msgid "Cookie setup"
msgstr "<b>Cookie setup</b>"

View file

@ -99,6 +99,9 @@ msgstr "Акселератор:"
msgid "Database label"
msgstr "База данных"
msgid "Database data tables"
msgstr "Таблиц: %s"
msgid "Database data rows"
msgstr "Строк: %s"

View file

@ -151,7 +151,7 @@ msgid "Table prefix error"
msgstr "Префикс '%s' не соответсвует формату."
msgid "Prefix reserved"
msgstr "Префикс 'sqlite_' зарезрвирован для SQLite."
msgstr "Префиксы 'sqlite_' и 'pg_' зарезервированы. Выберите другой префикс."
msgid "Existing table error"
msgstr "Таблица '%1$susers' уже существует в базе '%2$s'. Это может означать, что ForkBB был установлен в данную базу или другой программный пакет занимает таблицы, требуемые для работы ForkBB. Если вы хотите установить несколько копий форума в одну базу, выбирайте разные префиксы для них."
@ -219,6 +219,15 @@ msgstr "<b>Загрузка файлов, кажется, выключена н
msgid "Bad database charset"
msgstr "База данных должна быть создана с указанием кодировки символов <b>utf8mb4</b> (сравнение <b>utf8mb4_unicode_ci</b>)."
msgid "Bad database encoding"
msgstr "База данных должна быть создана с указанием кодировки символов <b>UTF8</b>."
msgid "Bad database collate"
msgstr "База данных должна быть создана с порядком сопоставления <b>C</b> (LC_COLLATE)."
msgid "Bad database ctype"
msgstr "База данных должна быть создана с типом символов <b>C</b> (LC_CTYPE)."
msgid "Cookie setup"
msgstr "<b>Настройки cookie</b>"

View file

@ -24,10 +24,9 @@
<dt>{!! __('Database label') !!}</dt>
<dd>
{{ $p->dbVersion }}
@if ($p->tRecords && $p->tSize)
<br>{!! __(['Database data tables', $p->tTables]) !!}
<br>{!! __(['Database data rows', num($p->tRecords)]) !!}
<br>{!! __(['Database data size', size($p->tSize)]) !!}
@endif
@if ($p->tOther)
<br><br>{!! __('Other')!!}
@foreach ($p->tOther as $key => $value)