Merge pull request #10 from forkbb/add_SQLite

Add SQLite support
This commit is contained in:
Visman 2021-12-17 16:40:06 +07:00 committed by GitHub
commit 975ab6190f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 1026 additions and 75 deletions

1
.gitignore vendored
View file

@ -2,6 +2,7 @@
/index.php
/app/config/main.php
/app/config/_*
/app/config/db/*
/app/cache/**/*.php
/app/cache/**/*.lock
/app/log/*

View file

@ -56,22 +56,46 @@ class DB extends PDO
public function __construct(string $dsn, string $username = null, string $password = null, array $options = [], string $prefix = '')
{
$type = \strstr($dsn, ':', true);
$type = \strstr($dsn, ':', true);
$typeU = \ucfirst($type);
if (
! $type
|| ! \in_array($type, PDO::getAvailableDrivers(), true)
|| ! \is_file(__DIR__ . '/DB/' . \ucfirst($type) . '.php')
|| ! \is_file(__DIR__ . "/DB/{$typeU}.php")
) {
throw new PDOException("Driver isn't found for '$type'");
}
$this->dbType = $type;
$statement = $typeU . 'Statement' . (\PHP_MAJOR_VERSION < 8 ? '7' : '');
if (\is_file(__DIR__ . "/DB/{$typeU}.php")) {
$statement = 'ForkBB\\Core\\DB\\' . $statement;
} else {
$statement = DBStatement::class;
}
if ('sqlite' === $type) {
$dsn = \str_replace('!PATH!', \realpath(__DIR__ . '/../config/db') . '/', $dsn);
}
$this->dbType = $type;
$this->dbPrefix = $prefix;
if (isset($options['initSQLCommands'])) {
$initSQLCommands = implode(';', $options['initSQLCommands']);
unset($options['initSQLCommands']);
} else {
$initSQLCommands = null;
}
$options += [
self::ATTR_DEFAULT_FETCH_MODE => self::FETCH_ASSOC,
self::ATTR_EMULATE_PREPARES => false,
self::ATTR_STRINGIFY_FETCHES => false,
self::ATTR_ERRMODE => self::ERRMODE_EXCEPTION,
self::ATTR_STATEMENT_CLASS => [DBStatement::class, [$this]],
self::ATTR_STATEMENT_CLASS => [$statement, [$this]],
];
$start = \microtime(true);
@ -80,6 +104,10 @@ class DB extends PDO
$this->saveQuery('PDO::__construct()', \microtime(true) - $start, false);
if ($initSQLCommands) {
$this->exec($initSQLCommands);
}
$this->beginTransaction();
}
@ -144,6 +172,7 @@ class DB extends PDO
case 'i':
case 'b':
case 's':
case 'f':
$value = [1];
break;
default:
@ -214,6 +243,14 @@ class DB extends PDO
return $this->queries;
}
/**
* Возвращает тип базы данных указанный в DSN
*/
public function getType(): string
{
return $this->dbType;
}
/**
* Метод для сохранения статистики по выполненному запросу
*/

View file

@ -0,0 +1,88 @@
<?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\AbstractStatement;
use PDO;
abstract class AbstractSqliteStatement extends AbstractStatement
{
/**
* https://github.com/php/php-src/blob/master/ext/pdo_sqlite/sqlite_statement.c
*
* SQLite:
* native_type:
* null - для значения NULL, а не типа столбца
* integer - это INTEGER, NUMERIC(?), BOOLEAN // BOOLEAN тут как-то не к месту, его бы в отдельный тип
* string - это TEXT
* double - это REAL, NUMERIC(?) // NUMERIC может быть и double, и integer
* sqlite:decl_type:
* INTEGER
* TEXT
* REAL
* NUMERIC
* BOOLEAN
* ... (это те типы, которые прописаны в CREATE TABLE и полученные после перекодировки из {driver}::bTypeRepl)
*/
/**
* @var array
*/
protected $nativeTypeRepl = [
'integer' => self::INTEGER,
'double' => self::FLOAT,
];
public function getColumnsType(): array
{
if (isset($this->columnsType)) {
return $this->columnsType;
}
$this->columnsType = [];
$count = $this->columnCount();
$i = 0;
// $dbType = $this->db->getType();
for ($i = 0; $i < $count; $i++) {
$meta = $this->getColumnMeta($i);
$type = null;
// $declType = $meta[$dbType . ':decl_type'] ?? null;
$declType = $meta['sqlite:decl_type'] ?? null;
if (null === $declType) {
$type = $this->nativeTypeRepl[$meta['native_type']] ?? null;
} elseif (\preg_match('%INT%i', $declType)) {
$type = self::INTEGER;
} elseif (\preg_match('%BOOL%i', $declType)) {
$type = self::BOOLEAN;
// } elseif (\preg_match('%REAL|FLOA|DOUB|NUMERIC|DECIMAL%i', $declType)) {
// $type = self::FLOAT;
}
if ($type) {
$this->columnsType[$i] = $type;
if (isset($meta['name'])) { // ????? проверка на тип содержимого? только строки, не числа?
$this->columnsType[$meta['name']] = $type;
}
}
}
return $this->columnsType;
}
protected function convToBoolean(/* mixed */ $value): bool
{
return (bool) $value;
}
}

View file

@ -0,0 +1,204 @@
<?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\DBStatement;
use PDO;
use PDOStatement;
use PDOException;
abstract class AbstractStatement extends DBStatement
{
/**
* Типы столбцов полученные через getColumnMeta()
* @var array
*/
protected $columnsType;
/**
* Режим выборки установленный через setFetchMode()/fetchAll()
* @var int
*/
protected $fetchMode;
/**
* colno, class или object из setFetchMode()/fetchAll()
* @var mixed
*/
protected $fetchArg;
/**
* constructorArgs из setFetchMode()/fetchAll()
* @var array
*/
protected $ctorArgs;
/**
* Флаг успешного завершения fetch() для PDO::FETCH_COLUMN
* @var bool
*/
protected $okFetchColumn;
abstract public function getColumnsType(): array;
abstract protected function convToBoolean(/* mixed */ $value): bool;
protected function setFetchVars(int $mode, ...$args): void
{
$this->fetchMode = $mode;
$this->fetchArg = null;
$this->ctorArgs = null;
switch ($mode) {
case PDO::FETCH_CLASS:
$this->ctorArgs = $args[1] ?? null;
case PDO::FETCH_INTO:
$this->fetchArg = $args[0];
break;
case PDO::FETCH_COLUMN:
$this->fetchArg = $args[0] ?? 0;
break;
}
}
protected function dbSetFetchMode(int $mode, ...$args): bool
{
$this->setFetchVars($mode, ...$args);
return parent::setFetchMode($mode, ...$args);
}
protected function dbFetch(int $mode = 0, int $orientation = PDO::FETCH_ORI_NEXT, int $offset = 0) /* : mixed */
{
$this->okFetchColumn = false;
if (0 === $mode) {
$mode = $this->fetchMode ?? 0;
$colNum = $this->fetchArg ?? 0;
} else {
$colNum = 0;
}
$data = parent::fetch(
PDO::FETCH_COLUMN === $mode ? PDO::FETCH_NUM : $mode,
$orientation,
$offset
);
if (! \is_array($data)) {
return $data;
}
$types = $this->getColumnsType();
foreach ($data as $key => &$value) {
if (
isset($types[$key])
&& \is_scalar($value)
) {
switch ($types[$key]) {
case self::INTEGER:
$value += 0; // If the string is not a number, then Warning/Notice
// It can return not an integer, but a float.
break;
case self::BOOLEAN:
$value = $this->convToBoolean($value);
break;
case self::FLOAT:
case self::STRING:
break;
default:
throw new PDOException("Unknown field type: '{$types[$key]}'");
}
}
}
unset($value);
if (PDO::FETCH_COLUMN === $mode) {
$this->okFetchColumn = true;
$data = $data[$colNum];
}
return $data;
}
protected function dbFetchAll(int $mode, ...$args): array
{
if (0 !== $mode) {
$this->setFetchVars($mode, ...$args);
}
$result = [];
switch ($this->fetchMode) {
case 0: /* PDO::FETCH_DEFAULT */
case PDO::FETCH_BOTH:
case PDO::FETCH_NUM:
case PDO::FETCH_ASSOC:
case PDO::FETCH_COLUMN:
while (false !== ($data = $this->dbFetch()) || $this->okFetchColumn) {
$result[] = $data;
}
break;
case PDO::FETCH_KEY_PAIR:
if (2 !== $this->columnCount()) {
throw new PDOException('General error: PDO::FETCH_KEY_PAIR fetch mode requires the result set to contain exactly 2 columns');
}
while (false !== ($data = $this->dbFetch(PDO::FETCH_NUM))) {
$result[$data[0]] = $data[1];
}
break;
case PDO::FETCH_UNIQUE:
case PDO::FETCH_UNIQUE | PDO::FETCH_BOTH:
case PDO::FETCH_UNIQUE | PDO::FETCH_NUM:
case PDO::FETCH_UNIQUE | PDO::FETCH_ASSOC:
$this->fetchMode ^= PDO::FETCH_UNIQUE;
while (false !== ($data = $this->dbFetch())) {
$key = \array_shift($data);
$result[$key] = $data;
}
break;
case PDO::FETCH_GROUP:
case PDO::FETCH_GROUP | PDO::FETCH_BOTH:
case PDO::FETCH_GROUP | PDO::FETCH_NUM:
case PDO::FETCH_GROUP | PDO::FETCH_ASSOC:
$this->fetchMode ^= PDO::FETCH_GROUP;
while (false !== ($data = $this->dbFetch())) {
$key = \array_shift($data);
if (PDO::FETCH_BOTH === $this->fetchMode) {
\array_shift($data);;
}
if (! isset($result[$key])) {
$result[$key] = [];
}
$result[$key][] = $data;
}
break;
default:
throw new PDOException('AbstractStatement class does not support this type for fetchAll(): ' . $this->fetchMode);
return parent::fetchAll($mode, ...$args);
}
return $result;
}
}

View file

@ -175,11 +175,10 @@ class Mysql
public function indexExists(string $table, string $index, bool $noPrefix = false): bool
{
$table = ($noPrefix ? '' : $this->dbPrefix) . $table;
$index = 'PRIMARY' == $index ? $index : $table . '_' . $index;
$vars = [
':tname' => $table,
':index' => $index,
':index' => 'PRIMARY' == $index ? $index : $table . '_' . $index,
];
$query = 'SELECT 1
FROM INFORMATION_SCHEMA.STATISTICS
@ -567,20 +566,22 @@ class Mysql
public function getMap(): array
{
$vars = [
str_replace('_', '\\_', $this->dbPrefix) . '%',
':tname' => str_replace('_', '\\_', $this->dbPrefix) . '%',
];
$query = 'SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME LIKE ?s';
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME LIKE ?s:tname
ORDER BY TABLE_NAME';
$stmt = $this->db->query($query, $vars);
$result = [];
$table = null;
$prfLen = \strlen($this->dbPrefix);
while ($row = $stmt->fetch()) {
if ($table !== $row['TABLE_NAME']) {
$table = $row['TABLE_NAME'];
$tableNoPref = \substr($table, \strlen($this->dbPrefix));
$tableNoPref = \substr($table, $prfLen);
$result[$tableNoPref] = [];
}

View file

@ -38,7 +38,7 @@ class Pgsql
'%^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',
'%^(?:FLOAT|REAL)(?:\s*\([\d,]+\))?(?:\s*UNSIGNED)?$%i' => 'REAL',
];
/**
@ -189,13 +189,12 @@ class Pgsql
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,
':iname' => $table . '_' . ('PRIMARY' === $index ? 'pkey' : $index),
':itype' => 'i',
];
$query = 'SELECT 1
@ -525,16 +524,18 @@ class Pgsql
];
$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';
WHERE table_catalog = current_database() AND table_schema = ?s:schema AND table_name LIKE ?s:tname
ORDER BY table_name';
$stmt = $this->db->query($query, $vars);
$result = [];
$table = null;
$prfLen = \strlen($this->dbPrefix);
while ($row = $stmt->fetch()) {
if ($table !== $row['table_name']) {
$table = $row['table_name'];
$tableNoPref = \substr($table, \strlen($this->dbPrefix));
$tableNoPref = \substr($table, $prfLen);
$result[$tableNoPref] = [];
}

501
app/Core/DB/Sqlite.php Normal file
View file

@ -0,0 +1,501 @@
<?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 Sqlite
{
/**
* @var DB
*/
protected $db;
/**
* Префикс для таблиц базы
* @var string
*/
protected $dbPrefix;
/**
* Массив замены типов полей таблицы
* @var array
*/
protected $dbTypeRepl = [
'%^.*?INT.*$%i' => 'INTEGER',
'%^.*?(?:CHAR|CLOB|TEXT).*$%i' => 'TEXT',
'%^.*?BLOB.*$%i' => 'BLOB',
'%^.*?(?:REAL|FLOA|DOUB).*$%i' => 'REAL',
'%^.*?(?:NUMERIC|DECIMAL).*$%i' => 'NUMERIC',
'%^.*?BOOL.*$%i' => 'BOOLEAN', // ???? не соответствует SQLite
'%^SERIAL$%i' => 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL',
];
/**
* Подстановка типов полей для карты БД
* @var array
*/
protected $types = [
'boolean' => 'b',
'integer' => 'i',
'real' => 'f',
'numeric' => 'f',
];
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 = [
':tname' => ($noPrefix ? '' : $this->dbPrefix) . $table,
':ttype' => 'table',
];
$query = 'SELECT 1 FROM sqlite_master WHERE tbl_name=?s:tname AND type=?s:ttype';
$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
{
$this->testStr($table);
$table = ($noPrefix ? '' : $this->dbPrefix) . $table;
$stmt = $this->db->query("PRAGMA table_info({$table})");
while ($row = $stmt->fetch()) {
if ($field === $row['name']) {
$stmt->closeCursor();
return true;
}
}
return false;
}
/**
* Проверяет наличие индекса в таблице
*/
public function indexExists(string $table, string $index, bool $noPrefix = false): bool
{
$table = ($noPrefix ? '' : $this->dbPrefix) . $table;
$vars = [
':tname' => $table,
':iname' => $table . '_' . $index, // ???? PRIMARY KEY искать нужно не в sqlite_master!
':itype' => 'index',
];
$query = 'SELECT 1 FROM sqlite_master WHERE name=?s:iname AND tbl_name=?s:tname AND type=?s:itype';
$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);
$prKey = true;
$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])) {
$prKey = false;
} else {
// сравнение
if (\preg_match('%^(?:CHAR|VARCHAR|TINYTEXT|TEXT|MEDIUMTEXT|LONGTEXT|ENUM|SET)%i', $data[0])) {
$query .= ' COLLATE ';
if (
isset($data[3])
&& \is_string($data[3])
&& \preg_match('%bin%i', $data[3])
) {
$query .= 'BINARY';
} else {
$query .= 'NOCASE';
}
}
// не NULL
if (empty($data[1])) {
$query .= ' NOT NULL';
}
// значение по умолчанию
if (isset($data[2])) {
$query .= ' DEFAULT ' . $this->convToStr($data[2]);
}
}
$query .= ', ';
}
if ($prKey && isset($schema['PRIMARY KEY'])) { // если не было поля с типом SERIAL
$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}\"");
}
/**
* Добавляет поле в таблицу // ???? нет COLLATE
*/
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 COLUMN \"{$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;
return true; // ???????????????????????????????????????
}
/**
* Удаляет поле из таблицы
*/
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}\""); // ???? add 2021-03-12 (3.35.0)
}
/**
* Переименование поля в таблице
*/
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}\""); // ???? add 2018-09-15 (3.25.0)
}
/**
* Добавляет индекс в таблицу
*/
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;
if (false !== $this->db->exec("DELETE FROM \"{$table}\"")) {
$vars = [
':tname' => $table,
];
$query = 'DELETE FROM SQLITE_SEQUENCE WHERE name=?s:tname';
return false !== $this->db->exec($query, $vars);
}
return false;
}
/**
* Возвращает статистику
*/
public function statistics(): array
{
$vars = [
':tname' => \str_replace('_', '\\_', $this->dbPrefix) . '%',
':ttype' => 'table',
];
$query = 'SELECT COUNT(*) FROM sqlite_master WHERE tbl_name LIKE ?s:tname ESCAPE \'\\\' AND type=?s:ttype';
$tables = $this->db->query($query, $vars)->fetchColumn();
$records = 0;
$size = (int) $this->db->query('PRAGMA page_count;')->fetchColumn();
$size *= (int) $this->db->query('PRAGMA page_size;')->fetchColumn();
return [
'db' => 'SQLite (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),
'encoding' => $this->db->query('PRAGMA encoding;')->fetchColumn(),
'journal_mode' => $this->db->query('PRAGMA journal_mode;')->fetchColumn(),
'synchronous' => $this->db->query('PRAGMA synchronous;')->fetchColumn(),
'busy_timeout' => $this->db->query('PRAGMA busy_timeout;')->fetchColumn(),
];
}
/**
* Формирует карту базы данных
*/
public function getMap(): array
{
$vars = [
':tname' => \str_replace('_', '\\_', $this->dbPrefix) . '%',
];
$query = 'SELECT m.name AS table_name, p.name AS column_name, p.type AS data_type
FROM sqlite_master AS m
INNER JOIN pragma_table_info(m.name) AS p
WHERE table_name LIKE ?s:tname ESCAPE \'\\\'
ORDER BY m.name, p.cid';
$stmt = $this->db->query($query, $vars);
$result = [];
$table = null;
$prfLen = \strlen($this->dbPrefix);
while ($row = $stmt->fetch()) {
if ($table !== $row['table_name']) {
$table = $row['table_name'];
$tableNoPref = \substr($table, $prfLen);
$result[$tableNoPref] = [];
}
$type = \strtolower($row['data_type']);
$result[$tableNoPref][$row['column_name']] = $this->types[$type] ?? 's';
}
return $result;
}
}

View file

@ -0,0 +1,35 @@
<?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\AbstractSqliteStatement;
use PDO;
/**
* For PHP 8
*/
class SqliteStatement extends AbstractSqliteStatement
{
public function fetch(int $mode = 0 /* PDO::FETCH_DEFAULT */, int $orientation = PDO::FETCH_ORI_NEXT, int $offset = 0): mixed
{
return $this->dbFetch($mode, $orientation, $offset);
}
public function fetchAll(int $mode = 0 /* PDO::FETCH_DEFAULT */, ...$args): array
{
return $this->dbFetchAll($mode, ...$args);
}
public function setFetchMode(int $mode, ...$args): bool
{
return $this->dbSetFetchMode($mode, ...$args);
}
}

View file

@ -0,0 +1,59 @@
<?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\AbstractSqliteStatement;
use PDO;
/**
* For PHP 7
*/
class SqliteStatement7 extends AbstractSqliteStatement
{
public function fetch($mode = null, $orientation = null, $offset = null)
{
$mode = $mode ?? 0;
$orientation = $orientation ?? PDO::FETCH_ORI_NEXT;
$offset = $offset ?? 0;
return $this->dbFetch($mode, $orientation, $offset);
}
public function fetchAll($mode = null, $fetchArg = null, $ctorArgs = null)
{
$mode = $mode ?? 0;
$args = $this->returnArgs($fetchArg, $ctorArgs);
return $this->dbFetchAll($mode, ...$args);
}
public function setFetchMode($mode, $fetchArg = null, $ctorArgs = null): bool
{
$args = $this->returnArgs($fetchArg, $ctorArgs);
return $this->dbSetFetchMode($mode, ...$args);
}
protected function returnArgs($fetchArg, $ctorArgs): array
{
$args = [];
if (isset($fetchArg)) {
$args[] = $fetchArg;
if (isset($ctorArgs)) {
$args[] = $ctorArgs;
}
}
return $args;
}
}

View file

@ -16,6 +16,11 @@ use PDOException;
class DBStatement extends PDOStatement
{
const BOOLEAN = 'b';
const FLOAT = 'f';
const INTEGER = 'i';
const STRING = 's';
/**
* Префикс для таблиц базы
* @var PDO
@ -33,12 +38,13 @@ class DBStatement extends PDOStatement
* @var array
*/
protected $types = [
's' => PDO::PARAM_STR,
'i' => PDO::PARAM_INT,
'b' => PDO::PARAM_BOOL,
'f' => PDO::PARAM_STR,
'i' => PDO::PARAM_INT,
's' => PDO::PARAM_STR,
'a' => PDO::PARAM_STR,
'as' => PDO::PARAM_STR,
'ai' => PDO::PARAM_INT,
'as' => PDO::PARAM_STR,
];
protected function __construct(PDO $db)

View file

@ -168,7 +168,10 @@ class ErrorHandler
$useErrLog = true;
try {
if (! $this->c->Log instanceof NullLogger) {
if (
$this->c instanceof Container
&& ! $this->c->Log instanceof NullLogger
) {
$context = [];
$method = $this->type[$error['type']][1] ?? $this->type[0][1];

View file

@ -32,7 +32,7 @@ class CalcStat extends Method
FROM ::topics AS t
WHERE t.forum_id=?i:fid AND t.moved_to!=0';
$moved = $this->c->DB->query($query, $vars)->fetchColumn();
$moved = (int) $this->c->DB->query($query, $vars)->fetchColumn();
$query = 'SELECT COUNT(t.id) as num_topics, SUM(t.num_replies) as num_replies
FROM ::topics AS t

View file

@ -53,7 +53,7 @@ class CalcStat extends Method
FROM ::pm_posts AS pp
WHERE pp.topic_id=?i:tid';
$this->model->num_replies = $this->c->DB->query($query, $vars)->fetchColumn();
$this->model->num_replies = (int) $this->c->DB->query($query, $vars)->fetchColumn();
return $this->model;
}

View file

@ -238,8 +238,8 @@ class PPost extends DataModel
ORDER BY pp.id DESC
LIMIT 1";
$id = $this->c->DB->query($query, $vars)->fetchColumn();
$id = (int) $this->c->DB->query($query, $vars)->fetchColumn();
return empty($id) ? null : $id;
return $id ?: null;
}
}

View file

@ -176,9 +176,7 @@ class PTopic extends DataModel
FROM ::pm_posts AS pp
WHERE pp.topic_id=?i:tid AND pp.posted>?i:visit';
$pid = $this->c->DB->query($query, $vars)->fetchColumn();
return $pid ?: 0;
return (int) $this->c->DB->query($query, $vars)->fetchColumn();
}
protected function setsender(User $user): void

View file

@ -20,7 +20,9 @@ use function \ForkBB\__;
class Install extends Admin
{
const PHP_MIN = '7.3.0';
const PHP_MIN = '7.3.0';
const MYSQL_MIN = '5.5.3';
const SQLITE_MIN = '3.25.0';
const JSON_OPTIONS = \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE | \JSON_THROW_ON_ERROR;
@ -109,6 +111,7 @@ class Install extends Admin
// доступность папок на запись
$folders = [
$this->c->DIR_APP . '/config',
$this->c->DIR_APP . '/config/db',
$this->c->DIR_CACHE,
$this->c->DIR_PUBLIC . '/img/avatars',
];
@ -159,7 +162,7 @@ class Install extends Admin
'dbname' => 'required|string:trim',
'dbuser' => 'string:trim',
'dbpass' => 'string:trim',
'dbprefix' => 'required|string:trim|max:40|check_prefix',
'dbprefix' => 'required|string:trim|min:1|max:40|check_prefix',
'username' => 'required|string:trim|min:2|max:25',
'password' => 'required|string|min:16|max:100000|password',
'email' => 'required|string:trim|email',
@ -472,11 +475,13 @@ class Install extends Admin
*/
public function vCheckHost(Validator $v, $dbhost)
{
$this->c->DB_USERNAME = $v->dbuser;
$this->c->DB_PASSWORD = $v->dbpass;
$this->c->DB_PREFIX = $v->dbprefix;
$dbtype = $v->dbtype;
$dbname = $v->dbname;
$this->c->DB_USERNAME = $v->dbuser;
$this->c->DB_PASSWORD = $v->dbpass;
$this->c->DB_OPTIONS = [];
$this->c->DB_OPTS_AS_STR = '';
$this->c->DB_PREFIX = $v->dbprefix;
$dbtype = $v->dbtype;
$dbname = $v->dbname;
// есть ошибки, ни чего не проверяем
if (! empty($v->getErrors())) {
@ -500,6 +505,15 @@ class Install extends Admin
break;
case 'sqlite':
$this->c->DB_DSN = "sqlite:!PATH!{$dbname}";
$this->c->DB_OPTS_AS_STR = '\\PDO::ATTR_TIMEOUT => 5, /* \'initSQLCommands\' => [\'PRAGMA journal_mode=WAL\',], */';
$this->c->DB_OPTIONS = [
PDO::ATTR_TIMEOUT => 5,
'initSQLCommands' => [
'PRAGMA journal_mode=WAL',
],
];
break;
case 'pgsql':
if (\preg_match('%^([^:]+):(\d+)$%', $dbhost, $matches)) {
@ -518,8 +532,6 @@ class Install extends Admin
break;
}
$this->c->DB_OPTIONS = [];
// подключение к БД
try {
$stat = $this->c->DB->statistics();
@ -529,6 +541,28 @@ class Install extends Admin
return $dbhost;
}
$version = $versionNeed = $this->c->DB->getAttribute(PDO::ATTR_SERVER_VERSION);
switch ($dbtype) {
case 'mysql_innodb':
case 'mysql':
$versionNeed = self::MYSQL_MIN;
$progName = 'MySQL';
break;
case 'sqlite':
$versionNeed = self::SQLITE_MIN;
$progName = 'SQLite';
break;
}
if (\version_compare($version, $versionNeed, '<')) {
$v->addError(['You are running error', $progName, $version, $this->c->FORK_REVISION, $versionNeed]);
return $dbhost;
}
// проверка наличия таблицы пользователей в БД
if ($this->c->DB->tableExists('users')) {
$v->addError(['Existing table error', $v->dbprefix, $v->dbname]);
@ -549,7 +583,7 @@ class Install extends Admin
isset($stat['server_encoding'])
&& 'UTF8' !== $stat['server_encoding']
) {
$v->addError('Bad database encoding');
$v->addError(['Bad database encoding', 'UTF8']);
}
// база PostgreSQL, порядок сопоставления/сортировки
@ -568,6 +602,14 @@ class Install extends Admin
$v->addError('Bad database ctype');
}
// база SQLite, кодировка базы
if (
isset($stat['encoding'])
&& 'UTF-8' !== $stat['encoding']
) {
$v->addError(['Bad database encoding', 'UTF-8']);
}
return $dbhost;
}
@ -840,16 +882,12 @@ class Install extends Admin
'id' => ['SERIAL', false],
'word' => ['VARCHAR(20)', false, '' , 'bin'],
],
'PRIMARY KEY' => ['word'],
'INDEXES' => [
'id_idx' => ['id'],
'PRIMARY KEY' => ['id'],
'UNIQUE KEYS' => [
'word_idx' => ['word']
],
'ENGINE' => $this->DBEngine,
];
if ('sqlite' === $v->dbtype) { //????
$schema['PRIMARY KEY'] = ['id'];
$schema['UNIQUE KEYS'] = ['word_idx' => ['word']];
}
$this->c->DB->createTable('search_words', $schema);
// topic_subscriptions
@ -1338,6 +1376,7 @@ class Install extends Admin
$config = \str_replace($key, \addslashes($val), $config);
}
$config = \str_replace('_DB_OPTIONS_', $this->c->DB_OPTS_AS_STR, $config);
$result = \file_put_contents($this->c->DIR_APP . '/config/main.php', $config);
if (false === $result) {

View file

@ -31,8 +31,8 @@ class PreviousPost extends Action
ORDER BY p.id DESC
LIMIT 1";
$id = $this->c->DB->query($query, $vars)->fetchColumn();
$id = (int) $this->c->DB->query($query, $vars)->fetchColumn();
return empty($id) ? null : $id;
return $id ?: null;
}
}

View file

@ -35,7 +35,7 @@ class CalcStat extends Method
FROM ::posts AS p
WHERE p.topic_id=?i:tid';
$numReplies = $this->c->DB->query($query, $vars)->fetchColumn();
$numReplies = (int) $this->c->DB->query($query, $vars)->fetchColumn();
$query = 'SELECT p.id, p.poster, p.poster_id, p.posted
FROM ::posts AS p

View file

@ -256,9 +256,7 @@ class Topic extends DataModel
FROM ::posts AS p
WHERE p.topic_id=?i:tid AND p.posted>?i:visit';
$pid = $this->c->DB->query($query, $vars)->fetchColumn();
return $pid ?: 0;
return (int) $this->c->DB->query($query, $vars)->fetchColumn();
}
/**
@ -280,9 +278,7 @@ class Topic extends DataModel
FROM ::posts AS p
WHERE p.topic_id=?i:tid AND p.posted>?i:visit';
$pid = $this->c->DB->query($query, $vars)->fetchColumn();
return $pid ?: 0;
return (int) $this->c->DB->query($query, $vars)->fetchColumn();
}
/**

View file

@ -26,7 +26,7 @@ class Stats extends Action
FROM ::users AS u
WHERE u.group_id!=?i:gid';
$total = $this->c->DB->query($query, $vars)->fetchColumn();
$total = (int) $this->c->DB->query($query, $vars)->fetchColumn();
$query = 'SELECT u.id, u.username
FROM ::users AS u

View file

@ -34,6 +34,6 @@ class UsersNumber extends Action
FROM ::users AS u
WHERE u.group_id=?i:gid';
return $this->c->DB->query($query, $vars)->fetchColumn();
return (int) $this->c->DB->query($query, $vars)->fetchColumn();
}
}

0
app/config/db/.gitkeep Normal file
View file

View file

@ -19,7 +19,7 @@ return [
'DB_DSN' => '_DB_DSN_',
'DB_USERNAME' => '_DB_USERNAME_',
'DB_PASSWORD' => '_DB_PASSWORD_',
'DB_OPTIONS' => [],
'DB_OPTIONS' => [_DB_OPTIONS_],
'DB_PREFIX' => '_DB_PREFIX_',
'COOKIE' => [
'prefix' => '_COOKIE_PREFIX_',

View file

@ -207,20 +207,11 @@ msgstr "Test forum"
msgid "This is just a test forum"
msgstr "This is just a test forum"
msgid "Alert cache"
msgstr "<b>The cache directory is currently not writable!</b> In order for ForkBB to function properly, the directory <em>%s</em> must be writable by PHP. Use chmod to set the appropriate directory permissions. If in doubt, chmod to 0777."
msgid "Alert avatar"
msgstr "<b>The avatar directory is currently not writable!</b> If you want users to be able to upload their own avatar images you must see to it that the directory <em>%s</em> is writable by PHP. You can later choose to save avatar images in a different directory (see Admin/Options). Use chmod to set the appropriate directory permissions. If in doubt, chmod to 0777."
msgid "Alert upload"
msgstr "<b>File uploads appear to be disallowed on this server!</b> If you want users to be able to upload their own avatar images you must enable the file_uploads configuration setting in PHP. Once file uploads have been enabled, avatar uploads can be enabled in Administration/Options/Features."
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>."
msgstr "The database must be created with the character encoding <b>%s</b>."
msgid "Bad database collate"
msgstr "The database must be created with Collation order <b>C</b> (LC_COLLATE)."

View file

@ -207,20 +207,11 @@ msgstr "Тестовый раздел"
msgid "This is just a test forum"
msgstr "Этот раздел создан при установке форума"
msgid "Alert cache"
msgstr "<b>Папка кэша заблокирована для записи!</b> Для правильного функционирования ForkBB директория <em>%s</em> должна быть открыта для записи из PHP. Используйте chmod для установки прав на директорию. Если сомневаетесь, то установите права 0777."
msgid "Alert avatar"
msgstr "<b>Папка для аватар заблокирована для записи!</b> Если вы хотите, чтобы пользователи форума использовали аватары, вы должны разрешить запись в директорию <em>%s</em> для PHP. Позже вы можете сменить директорию хранения аватар (смотрите Админка/Опции). Используйте chmod для установки прав на директорию. Если сомневаетесь, то установите права 0777."
msgid "Alert upload"
msgstr "<b>Загрузка файлов, кажется, выключена на этом сервере!</b> Если вы хотите, чтобы пользователи форума использовали аватары, вы должны разрешить file_uploads в настройках вашего PHP. После разрешения загрузки файлов на сервер, вы можете разрешить использования аватар для пользователей форума (смотрите Админка/Опции)."
msgid "Bad database charset"
msgstr "База данных должна быть создана с указанием кодировки символов <b>utf8mb4</b> (сравнение <b>utf8mb4_unicode_ci</b>)."
msgid "Bad database encoding"
msgstr "База данных должна быть создана с указанием кодировки символов <b>UTF8</b>."
msgstr "База данных должна быть создана с указанием кодировки символов <b>%s</b>."
msgid "Bad database collate"
msgstr "База данных должна быть создана с порядком сопоставления <b>C</b> (LC_COLLATE)."

View file

@ -15,7 +15,7 @@ No: plugins/extensions system, ...
* PHP 7.3+
* PHP extensions: pdo, intl, json, mbstring, fileinfo
* PHP extensions (desirable): gd or imagick (for upload avatars and other images)
* A database such as MySQL 5.5.3+, PostgreSQL 10+(?) (_Drivers for other databases are not realized now_)
* A database such as MySQL 5.5.3+, SQLite 3.25+, PostgreSQL 10+(?)
## Install