AC_services_website_design/system/Database/SQLSRV/Connection.php

564 lines
16 KiB
PHP

<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\SQLSRV;
use CodeIgniter\Database\BaseConnection;
use CodeIgniter\Database\Exceptions\DatabaseException;
use stdClass;
/**
* Connection for SQLSRV
*
* @extends BaseConnection<resource, resource>
*/
class Connection extends BaseConnection
{
/**
* Database driver
*
* @var string
*/
public $DBDriver = 'SQLSRV';
/**
* Database name
*
* @var string
*/
public $database;
/**
* Scrollable flag
*
* Determines what cursor type to use when executing queries.
*
* FALSE or SQLSRV_CURSOR_FORWARD would increase performance,
* but would disable num_rows() (and possibly insert_id())
*
* @var false|string
*/
public $scrollable;
/**
* Identifier escape character
*
* @var string
*/
public $escapeChar = '"';
/**
* Database schema
*
* @var string
*/
public $schema = 'dbo';
/**
* Quoted identifier flag
*
* Whether to use SQL-92 standard quoted identifier
* (double quotes) or brackets for identifier escaping.
*
* @var bool
*/
protected $_quoted_identifier = true;
/**
* List of reserved identifiers
*
* Identifiers that must NOT be escaped.
*
* @var string[]
*/
protected $_reserved_identifiers = ['*'];
/**
* Class constructor
*/
public function __construct(array $params)
{
parent::__construct($params);
// This is only supported as of SQLSRV 3.0
if ($this->scrollable === null) {
$this->scrollable = defined('SQLSRV_CURSOR_CLIENT_BUFFERED') ? SQLSRV_CURSOR_CLIENT_BUFFERED : false;
}
}
/**
* Connect to the database.
*
* @return false|resource
*
* @throws DatabaseException
*/
public function connect(bool $persistent = false)
{
$charset = in_array(strtolower($this->charset), ['utf-8', 'utf8'], true) ? 'UTF-8' : SQLSRV_ENC_CHAR;
$connection = [
'UID' => empty($this->username) ? '' : $this->username,
'PWD' => empty($this->password) ? '' : $this->password,
'Database' => $this->database,
'ConnectionPooling' => $persistent ? 1 : 0,
'CharacterSet' => $charset,
'Encrypt' => $this->encrypt === true ? 1 : 0,
'ReturnDatesAsStrings' => 1,
];
// If the username and password are both empty, assume this is a
// 'Windows Authentication Mode' connection.
if (empty($connection['UID']) && empty($connection['PWD'])) {
unset($connection['UID'], $connection['PWD']);
}
if (strpos($this->hostname, ',') === false && $this->port !== '') {
$this->hostname .= ', ' . $this->port;
}
sqlsrv_configure('WarningsReturnAsErrors', 0);
$this->connID = sqlsrv_connect($this->hostname, $connection);
if ($this->connID !== false) {
// Determine how identifiers are escaped
$query = $this->query('SELECT CASE WHEN (@@OPTIONS | 256) = @@OPTIONS THEN 1 ELSE 0 END AS qi');
$query = $query->getResultObject();
$this->_quoted_identifier = empty($query) ? false : (bool) $query[0]->qi;
$this->escapeChar = ($this->_quoted_identifier) ? '"' : ['[', ']'];
return $this->connID;
}
throw new DatabaseException($this->getAllErrorMessages());
}
/**
* For exception message
*
* @internal
*/
public function getAllErrorMessages(): string
{
$errors = [];
foreach (sqlsrv_errors() as $error) {
$errors[] = $error['message']
. ' SQLSTATE: ' . $error['SQLSTATE'] . ', code: ' . $error['code'];
}
return implode("\n", $errors);
}
/**
* Keep or establish the connection if no queries have been sent for
* a length of time exceeding the server's idle timeout.
*/
public function reconnect()
{
$this->close();
$this->initialize();
}
/**
* Close the database connection.
*/
protected function _close()
{
sqlsrv_close($this->connID);
}
/**
* Platform-dependant string escape
*/
protected function _escapeString(string $str): string
{
return str_replace("'", "''", remove_invisible_characters($str, false));
}
/**
* Insert ID
*/
public function insertID(): int
{
return $this->query('SELECT SCOPE_IDENTITY() AS insert_id')->getRow()->insert_id ?? 0;
}
/**
* Generates the SQL for listing tables in a platform-dependent manner.
*
* @param string|null $tableName If $tableName is provided will return only this table if exists.
*/
protected function _listTables(bool $prefixLimit = false, ?string $tableName = null): string
{
$sql = 'SELECT [TABLE_NAME] AS "name"'
. ' FROM [INFORMATION_SCHEMA].[TABLES] '
. ' WHERE '
. " [TABLE_SCHEMA] = '" . $this->schema . "' ";
if ($tableName !== null) {
return $sql .= ' AND [TABLE_NAME] LIKE ' . $this->escape($tableName);
}
if ($prefixLimit === true && $this->DBPrefix !== '') {
$sql .= " AND [TABLE_NAME] LIKE '" . $this->escapeLikeString($this->DBPrefix) . "%' "
. sprintf($this->likeEscapeStr, $this->likeEscapeChar);
}
return $sql;
}
/**
* Generates a platform-specific query string so that the column names can be fetched.
*/
protected function _listColumns(string $table = ''): string
{
return 'SELECT [COLUMN_NAME] '
. ' FROM [INFORMATION_SCHEMA].[COLUMNS]'
. ' WHERE [TABLE_NAME] = ' . $this->escape($this->DBPrefix . $table)
. ' AND [TABLE_SCHEMA] = ' . $this->escape($this->schema);
}
/**
* Returns an array of objects with index data
*
* @return stdClass[]
*
* @throws DatabaseException
*/
protected function _indexData(string $table): array
{
$sql = 'EXEC sp_helpindex ' . $this->escape($this->schema . '.' . $table);
if (($query = $this->query($sql)) === false) {
throw new DatabaseException(lang('Database.failGetIndexData'));
}
$query = $query->getResultObject();
$retVal = [];
foreach ($query as $row) {
$obj = new stdClass();
$obj->name = $row->index_name;
$_fields = explode(',', trim($row->index_keys));
$obj->fields = array_map(static fn ($v) => trim($v), $_fields);
if (strpos($row->index_description, 'primary key located on') !== false) {
$obj->type = 'PRIMARY';
} else {
$obj->type = (strpos($row->index_description, 'nonclustered, unique') !== false) ? 'UNIQUE' : 'INDEX';
}
$retVal[$obj->name] = $obj;
}
return $retVal;
}
/**
* Returns an array of objects with Foreign key data
* referenced_object_id parent_object_id
*
* @return stdClass[]
*
* @throws DatabaseException
*/
protected function _foreignKeyData(string $table): array
{
$sql = 'SELECT
f.name as constraint_name,
OBJECT_NAME (f.parent_object_id) as table_name,
COL_NAME(fc.parent_object_id,fc.parent_column_id) column_name,
OBJECT_NAME(f.referenced_object_id) foreign_table_name,
COL_NAME(fc.referenced_object_id,fc.referenced_column_id) foreign_column_name,
rc.delete_rule,
rc.update_rule,
rc.match_option
FROM
sys.foreign_keys AS f
INNER JOIN sys.foreign_key_columns AS fc ON f.OBJECT_ID = fc.constraint_object_id
INNER JOIN sys.tables t ON t.OBJECT_ID = fc.referenced_object_id
INNER JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc ON rc.CONSTRAINT_NAME = f.name
WHERE OBJECT_NAME (f.parent_object_id) = ' . $this->escape($table);
if (($query = $this->query($sql)) === false) {
throw new DatabaseException(lang('Database.failGetForeignKeyData'));
}
$query = $query->getResultObject();
$indexes = [];
foreach ($query as $row) {
$indexes[$row->constraint_name]['constraint_name'] = $row->constraint_name;
$indexes[$row->constraint_name]['table_name'] = $row->table_name;
$indexes[$row->constraint_name]['column_name'][] = $row->column_name;
$indexes[$row->constraint_name]['foreign_table_name'] = $row->foreign_table_name;
$indexes[$row->constraint_name]['foreign_column_name'][] = $row->foreign_column_name;
$indexes[$row->constraint_name]['on_delete'] = $row->delete_rule;
$indexes[$row->constraint_name]['on_update'] = $row->update_rule;
$indexes[$row->constraint_name]['match'] = $row->match_option;
}
return $this->foreignKeyDataToObjects($indexes);
}
/**
* Disables foreign key checks temporarily.
*
* @return string
*/
protected function _disableForeignKeyChecks()
{
return 'EXEC sp_MSforeachtable "ALTER TABLE ? NOCHECK CONSTRAINT ALL"';
}
/**
* Enables foreign key checks temporarily.
*
* @return string
*/
protected function _enableForeignKeyChecks()
{
return 'EXEC sp_MSforeachtable "ALTER TABLE ? WITH CHECK CHECK CONSTRAINT ALL"';
}
/**
* Returns an array of objects with field data
*
* @return stdClass[]
*
* @throws DatabaseException
*/
protected function _fieldData(string $table): array
{
$sql = 'SELECT
COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION,
COLUMN_DEFAULT, IS_NULLABLE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME= ' . $this->escape(($table));
if (($query = $this->query($sql)) === false) {
throw new DatabaseException(lang('Database.failGetFieldData'));
}
$query = $query->getResultObject();
$retVal = [];
for ($i = 0, $c = count($query); $i < $c; $i++) {
$retVal[$i] = new stdClass();
$retVal[$i]->name = $query[$i]->COLUMN_NAME;
$retVal[$i]->type = $query[$i]->DATA_TYPE;
$retVal[$i]->default = $query[$i]->COLUMN_DEFAULT;
$retVal[$i]->max_length = $query[$i]->CHARACTER_MAXIMUM_LENGTH > 0
? $query[$i]->CHARACTER_MAXIMUM_LENGTH
: $query[$i]->NUMERIC_PRECISION;
$retVal[$i]->nullable = $query[$i]->IS_NULLABLE !== 'NO';
}
return $retVal;
}
/**
* Begin Transaction
*/
protected function _transBegin(): bool
{
return sqlsrv_begin_transaction($this->connID);
}
/**
* Commit Transaction
*/
protected function _transCommit(): bool
{
return sqlsrv_commit($this->connID);
}
/**
* Rollback Transaction
*/
protected function _transRollback(): bool
{
return sqlsrv_rollback($this->connID);
}
/**
* Returns the last error code and message.
* Must return this format: ['code' => string|int, 'message' => string]
* intval(code) === 0 means "no error".
*
* @return array<string, int|string>
*/
public function error(): array
{
$error = [
'code' => '00000',
'message' => '',
];
$sqlsrvErrors = sqlsrv_errors(SQLSRV_ERR_ERRORS);
if (! is_array($sqlsrvErrors)) {
return $error;
}
$sqlsrvError = array_shift($sqlsrvErrors);
if (isset($sqlsrvError['SQLSTATE'])) {
$error['code'] = isset($sqlsrvError['code']) ? $sqlsrvError['SQLSTATE'] . '/' . $sqlsrvError['code'] : $sqlsrvError['SQLSTATE'];
} elseif (isset($sqlsrvError['code'])) {
$error['code'] = $sqlsrvError['code'];
}
if (isset($sqlsrvError['message'])) {
$error['message'] = $sqlsrvError['message'];
}
return $error;
}
/**
* Returns the total number of rows affected by this query.
*/
public function affectedRows(): int
{
return sqlsrv_rows_affected($this->resultID);
}
/**
* Select a specific database table to use.
*
* @return bool
*/
public function setDatabase(?string $databaseName = null)
{
if ($databaseName === null || $databaseName === '') {
$databaseName = $this->database;
}
if (empty($this->connID)) {
$this->initialize();
}
if ($this->execute('USE ' . $this->_escapeString($databaseName))) {
$this->database = $databaseName;
$this->dataCache = [];
return true;
}
return false;
}
/**
* Executes the query against the database.
*
* @return false|resource
*/
protected function execute(string $sql)
{
$stmt = ($this->scrollable === false || $this->isWriteType($sql)) ?
sqlsrv_query($this->connID, $sql) :
sqlsrv_query($this->connID, $sql, [], ['Scrollable' => $this->scrollable]);
if ($stmt === false) {
$error = $this->error();
log_message('error', $error['message']);
if ($this->DBDebug) {
throw new DatabaseException($error['message']);
}
}
return $stmt;
}
/**
* Returns the last error encountered by this connection.
*
* @return array<string, int|string>
*
* @deprecated Use `error()` instead.
*/
public function getError()
{
$error = [
'code' => '00000',
'message' => '',
];
$sqlsrvErrors = sqlsrv_errors(SQLSRV_ERR_ERRORS);
if (! is_array($sqlsrvErrors)) {
return $error;
}
$sqlsrvError = array_shift($sqlsrvErrors);
if (isset($sqlsrvError['SQLSTATE'])) {
$error['code'] = isset($sqlsrvError['code']) ? $sqlsrvError['SQLSTATE'] . '/' . $sqlsrvError['code'] : $sqlsrvError['SQLSTATE'];
} elseif (isset($sqlsrvError['code'])) {
$error['code'] = $sqlsrvError['code'];
}
if (isset($sqlsrvError['message'])) {
$error['message'] = $sqlsrvError['message'];
}
return $error;
}
/**
* The name of the platform in use (MySQLi, mssql, etc)
*/
public function getPlatform(): string
{
return $this->DBDriver;
}
/**
* Returns a string containing the version of the database being used.
*/
public function getVersion(): string
{
$info = [];
if (isset($this->dataCache['version'])) {
return $this->dataCache['version'];
}
if (! $this->connID || ($info = sqlsrv_server_info($this->connID)) === []) {
$this->initialize();
}
return isset($info['SQLServerVersion']) ? $this->dataCache['version'] = $info['SQLServerVersion'] : false;
}
/**
* Determines if a query is a "write" type.
*
* Overrides BaseConnection::isWriteType, adding additional read query types.
*
* @param string $sql
*/
public function isWriteType($sql): bool
{
if (preg_match('/^\s*"?(EXEC\s*sp_rename)\s/i', $sql)) {
return true;
}
return parent::isWriteType($sql);
}
}