salesagility_SuiteCRM/include/database/SqlsrvManager.php

635 lines
22 KiB
PHP
Executable File

<?php
/**
*
* SugarCRM Community Edition is a customer relationship management program developed by
* SugarCRM, Inc. Copyright (C) 2004-2013 SugarCRM Inc.
*
* SuiteCRM is an extension to SugarCRM Community Edition developed by SalesAgility Ltd.
* Copyright (C) 2011 - 2018 SalesAgility Ltd.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License version 3 as published by the
* Free Software Foundation with the addition of the following permission added
* to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
* IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY
* OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along with
* this program; if not, see http://www.gnu.org/licenses or write to the Free
* Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301 USA.
*
* You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road,
* SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "Powered by
* SugarCRM" logo and "Supercharged by SuiteCRM" logo. If the display of the logos is not
* reasonably feasible for technical reasons, the Appropriate Legal Notices must
* display the words "Powered by SugarCRM" and "Supercharged by SuiteCRM".
*/
if (!defined('sugarEntry') || !sugarEntry) {
die('Not A Valid Entry Point');
}
/*********************************************************************************
* Description: This file handles the Data base functionality for the application.
* It acts as the DB abstraction layer for the application. It depends on helper classes
* which generate the necessary SQL. This sql is then passed to PEAR DB classes.
* The helper class is chosen in DBManagerFactory, which is driven by 'db_type' in 'dbconfig' under config.php.
*
* All the functions in this class will work with any bean which implements the meta interface.
* The passed bean is passed to helper class which uses these functions to generate correct sql.
*
* The meta interface has the following functions:
* getTableName() Returns table name of the object.
* getFieldDefinitions() Returns a collection of field definitions in order.
* getFieldDefintion(name) Return field definition for the field.
* getFieldValue(name) Returns the value of the field identified by name.
* If the field is not set, the function will return boolean FALSE.
* getPrimaryFieldDefinition() Returns the field definition for primary key
*
* The field definition is an array with the following keys:
*
* name This represents name of the field. This is a required field.
* type This represents type of the field. This is a required field and valid values are:
* int
* long
* varchar
* text
* date
* datetime
* double
* float
* uint
* ulong
* time
* short
* enum
* length This is used only when the type is varchar and denotes the length of the string.
* The max value is 255.
* enumvals This is a list of valid values for an enum separated by "|".
* It is used only if the type is ?enum?;
* required This field dictates whether it is a required value.
* The default value is ?FALSE?.
* isPrimary This field identifies the primary key of the table.
* If none of the fields have this flag set to ?TRUE?,
* the first field definition is assume to be the primary key.
* Default value for this field is ?FALSE?.
* default This field sets the default value for the field definition.
*
*
* Portions created by SugarCRM are Copyright (C) SugarCRM, Inc.
* All Rights Reserved.
* Contributor(s): ______________________________________..
********************************************************************************/
include_once('include/database/MssqlManager.php');
/**
* SQL Server (sqlsrv) manager
*/
class SqlsrvManager extends MssqlManager
{
public $dbName = 'SQL Server';
public $variant = 'sqlsrv';
public $priority = 10;
public $label = 'LBL_MSSQL_SQLSRV';
protected $capabilities = array(
"affected_rows" => true,
'fulltext' => true,
'limit_subquery' => true,
'create_user' => true,
"create_db" => true,
);
protected $type_map = array(
'int' => 'int',
'double' => 'float',
'float' => 'float',
'uint' => 'int',
'ulong' => 'int',
'long' => 'bigint',
'short' => 'smallint',
'varchar' => 'nvarchar',
'text' => 'nvarchar(max)',
'longtext' => 'nvarchar(max)',
'date' => 'datetime',
'enum' => 'nvarchar',
'relate' => 'nvarchar',
'multienum' => 'nvarchar(max)',
'html' => 'nvarchar(max)',
'emailbody' => 'nvarchar(max)',
'longhtml' => 'nvarchar(max)',
'datetime' => 'datetime',
'datetimecombo' => 'datetime',
'time' => 'datetime',
'bool' => 'bit',
'tinyint' => 'tinyint',
'char' => 'char',
'blob' => 'nvarchar(max)',
'longblob' => 'nvarchar(max)',
'currency' => 'decimal(26,6)',
'decimal' => 'decimal',
'decimal2' => 'decimal',
'id' => 'varchar(36)',
'url' => 'nvarchar',
'encrypt' => 'nvarchar',
'file' => 'nvarchar',
'decimal_tpl' => 'decimal(%d, %d)',
);
/**
* @see DBManager::connect()
*/
public function connect(array $configOptions = null, $dieOnError = false)
{
global $sugar_config;
if (is_null($configOptions)) {
$configOptions = $sugar_config['dbconfig'];
}
//set the connections parameters
$connect_param = '';
$configOptions['db_host_instance'] = trim($configOptions['db_host_instance']);
if (empty($configOptions['db_host_instance'])) {
$connect_param = $configOptions['db_host_name'];
} else {
$connect_param = $configOptions['db_host_name'] . "\\" . $configOptions['db_host_instance'];
}
/*
* Don't try to specifically use a persistent connection
* since the driver will handle that for us
*/
$options = array(
"UID" => $configOptions['db_user_name'],
"PWD" => $configOptions['db_password'],
"CharacterSet" => "UTF-8",
"ReturnDatesAsStrings" => true,
"MultipleActiveResultSets" => true,
);
if (!empty($configOptions['db_name'])) {
$options["Database"] = $configOptions['db_name'];
}
$this->database = sqlsrv_connect($connect_param, $options);
if (empty($this->database)) {
$GLOBALS['log']->fatal("Could not connect to server " . $configOptions['db_host_name'] . " as " . $configOptions['db_user_name'] . ".");
if ($dieOnError) {
if (isset($GLOBALS['app_strings']['ERR_NO_DB'])) {
sugar_die($GLOBALS['app_strings']['ERR_NO_DB']);
} else {
sugar_die("Could not connect to the database. Please refer to suitecrm.log for details (4).");
}
} else {
return false;
}
}
if ($this->checkError('Could Not Connect:', $dieOnError)) {
$GLOBALS['log']->info("connected to db");
}
sqlsrv_query($this->database, 'SET DATEFORMAT mdy');
$this->connectOptions = $configOptions;
$GLOBALS['log']->info("Connect:" . $this->database);
return true;
}
/**
* @see DBManager::query()
*/
public function query($sql, $dieOnError = false, $msg = '', $suppress = false, $keepResult = false)
{
if (is_array($sql)) {
return $this->queryArray($sql, $dieOnError, $msg, $suppress);
}
$sql = $this->_appendN($sql);
$this->countQuery($sql);
$GLOBALS['log']->info('Query:' . $sql);
$this->checkConnection();
$this->query_time = microtime(true);
$result = $suppress ? @sqlsrv_query($this->database, $sql) : sqlsrv_query($this->database, $sql);
$this->query_time = microtime(true) - $this->query_time;
$GLOBALS['log']->info('Query Execution Time:' . $this->query_time);
$this->checkError($msg . ' Query Failed:' . $sql . '::', $dieOnError);
//suppress non error messages
sqlsrv_configure('WarningsReturnAsErrors', false);
return $result;
}
/**
* @see DBManager::getFieldsArray()
*/
public function getFieldsArray($result, $make_lower_case = false)
{
$field_array = array();
if (!$result) {
return false;
}
foreach (sqlsrv_field_metadata($result) as $fieldMetadata) {
$key = $fieldMetadata['Name'];
if ($make_lower_case == true) {
$key = strtolower($key);
}
$field_array[] = $key;
}
return $field_array;
}
/**
* @see DBManager::fetchRow()
*/
public function fetchRow($result)
{
if (empty($result)) {
return false;
}
$row = sqlsrv_fetch_array($result, SQLSRV_FETCH_ASSOC);
if (empty($row)) {
return false;
}
foreach ($row as $key => $column) {
// MSSQL returns a space " " when a varchar column is empty ("") and not null.
// We need to strip empty spaces
// notice we only strip if one space is returned. we do not want to strip
// strings with intentional spaces (" foo ")
if (!empty($column) && $column == " ") {
$row[$key] = '';
}
}
return $row;
}
/**
* @see DBManager::convert()
*/
public function convert($string, $type, array $additional_parameters = array())
{
if ($type == 'datetime') { // see http://msdn.microsoft.com/en-us/library/ms187928.aspx for details
return "CONVERT(datetime,$string,120)";
} else {
return parent::convert($string, $type, $additional_parameters);
}
}
/**
* Compares two vardefs. Overriding 39098 due to bug: 39098 . IN 6.0 we changed the id columns to dbType = 'id'
* for example emails_beans. In 554 the field email_id was nvarchar but in 6.0 since it id dbType = 'id' we would want to alter
* it to varchar. This code will prevent it.
*
* @param array $fielddef1
* @param array $fielddef2
* @return bool true if they match, false if they don't
*/
public function compareVarDefs($fielddef1, $fielddef2, $ignoreName = false)
{
if ((isset($fielddef2['dbType']) && $fielddef2['dbType'] == 'id') || preg_match(
'/(_id$|^id$)/',
(string) $fielddef2['name']
)
) {
if (isset($fielddef1['type']) && isset($fielddef2['type'])) {
$fielddef2['type'] = $fielddef1['type'];
}
}
return parent::compareVarDefs($fielddef1, $fielddef2);
}
/**
* Disconnects from the database
*
* Also handles any cleanup needed
*/
public function disconnect()
{
$GLOBALS['log']->debug('Calling Mssql::disconnect()');
if (!empty($this->database)) {
$this->freeResult();
sqlsrv_close($this->database);
$this->database = null;
}
}
/**
* @see DBManager::freeDbResult()
*/
protected function freeDbResult($dbResult)
{
if (!empty($dbResult)) {
sqlsrv_free_stmt($dbResult);
}
}
/**
* Detect if no clustered index has been created for a table; if none created then just pick the first index and make it that
*/
public function getConstraintSql($indices, $table)
{
if ($this->doesTableHaveAClusteredIndexDefined($table)) {
return parent::getConstraintSql($indices, $table);
}
// check to see if one of the passed in indices is a primary one; if so we can bail as well
foreach ($indices as $index) {
if ($index['type'] == 'primary') {
return parent::getConstraintSql($indices, $table);
}
}
// Change the first index listed to be a clustered one instead ( so we have at least one for the table )
if (isset($indices[0])) {
$indices[0]['type'] = 'clustered';
}
return parent::getConstraintSql($indices, $table);
}
/**
* @see DBManager::get_columns()
*/
public function get_columns($tablename)
{
//find all unique indexes and primary keys.
$result = $this->query("sp_columns_90 $tablename");
$columns = array();
while (($row = $this->fetchByAssoc($result)) != null) {
$column_name = strtolower($row['COLUMN_NAME']);
$columns[$column_name]['name'] = $column_name;
$columns[$column_name]['type'] = strtolower($row['TYPE_NAME']);
if ($row['TYPE_NAME'] == 'decimal') {
$columns[$column_name]['len'] = strtolower($row['PRECISION']);
$columns[$column_name]['len'] .= ',' . strtolower($row['SCALE']);
} elseif (in_array($row['TYPE_NAME'], array('nchar', 'nvarchar'))) {
$columns[$column_name]['len'] = strtolower($row['PRECISION']);
if ($row['TYPE_NAME'] == 'nvarchar' && $row['PRECISION'] == '0') {
$columns[$column_name]['len'] = 'max';
}
} elseif (!in_array($row['TYPE_NAME'], array('datetime', 'text'))) {
$columns[$column_name]['len'] = strtolower($row['LENGTH']);
}
if (stristr((string) $row['TYPE_NAME'], 'identity')) {
$columns[$column_name]['auto_increment'] = '1';
$columns[$column_name]['type'] = str_replace(' identity', '', strtolower($row['TYPE_NAME']));
}
if (!empty($row['IS_NULLABLE']) && $row['IS_NULLABLE'] == 'NO' && (empty($row['KEY']) || !stristr(
(string) $row['KEY'],
'PRI'
))
) {
$columns[strtolower($row['COLUMN_NAME'])]['required'] = 'true';
}
$column_def = 1;
if (strtolower($tablename) == 'relationships') {
$column_def = $this->getOne("select cdefault from syscolumns where id = object_id('relationships') and name = '$column_name'");
}
if ($column_def != 0 && ($row['COLUMN_DEF'] != null)) { // NOTE Not using !empty as an empty string may be a viable default value.
$matches = array();
$row['COLUMN_DEF'] = html_entity_decode((string) $row['COLUMN_DEF'], ENT_QUOTES);
if (preg_match('/\([\(|\'](.*)[\)|\']\)/i', $row['COLUMN_DEF'], $matches)) {
$columns[$column_name]['default'] = $matches[1];
} elseif (preg_match('/\(N\'(.*)\'\)/i', $row['COLUMN_DEF'], $matches)) {
$columns[$column_name]['default'] = $matches[1];
} else {
$columns[$column_name]['default'] = $row['COLUMN_DEF'];
}
}
}
return $columns;
}
/**
* protected function to return true if the given tablename has any clustered indexes defined.
*
* @param string $tableName
* @return bool
*/
protected function doesTableHaveAClusteredIndexDefined($tableName)
{
$query = <<<EOSQL
SELECT IST.TABLE_NAME
FROM INFORMATION_SCHEMA.TABLES IST
WHERE objectProperty(object_id(IST.TABLE_NAME), 'IsUserTable') = 1
AND objectProperty(object_id(IST.TABLE_NAME), 'TableHasClustIndex') = 1
AND IST.TABLE_NAME = '{$tableName}'
EOSQL;
$result = $this->getOne($query);
if (!$result) {
return false;
}
return true;
}
/**
* protected function to return true if the given tablename has any fulltext indexes defined.
*
* @param string $tableName
* @return bool
*/
protected function doesTableHaveAFulltextIndexDefined($tableName)
{
$query = <<<EOSQL
SELECT 1
FROM sys.fulltext_indexes i
JOIN sys.objects o ON i.object_id = o.object_id
WHERE o.name = '{$tableName}'
EOSQL;
$result = $this->getOne($query);
if (!$result) {
return false;
}
return true;
}
/**
* Override method to add support for detecting and dropping fulltext indices.
*
* @see DBManager::changeColumnSQL()
* @see MssqlHelper::changeColumnSQL()
*/
protected function changeColumnSQL($tablename, $fieldDefs, $action, $ignoreRequired = false)
{
$sql = '';
if ($action == 'drop' && $this->doesTableHaveAFulltextIndexDefined($tablename)) {
$sql .= "DROP FULLTEXT INDEX ON {$tablename}";
}
$sql .= parent::changeColumnSQL($tablename, $fieldDefs, $action, $ignoreRequired);
return $sql;
}
/**
* Truncate table
* @param $name
* @return string
*/
public function truncateTableSQL($name)
{
return "TRUNCATE TABLE $name";
}
/**
* (non-PHPdoc)
* @see DBManager::lastDbError()
*/
public function lastDbError()
{
$errors = sqlsrv_errors(SQLSRV_ERR_ERRORS);
if (empty($errors)) {
return false;
}
global $app_strings;
if (empty($app_strings) || !isset($app_strings['ERR_MSSQL_DB_CONTEXT']) || !isset($app_strings['ERR_MSSQL_WARNING'])
) {
//ignore the message from sql-server if $app_strings array is empty. This will happen
//only if connection if made before languge is set.
return false;
}
$messages = array();
foreach ($errors as $error) {
$sqlmsg = $error['message'];
$sqlpos = strpos((string) $sqlmsg, 'Changed database context to');
$sqlpos2 = strpos((string) $sqlmsg, 'Warning:');
$sqlpos3 = strpos((string) $sqlmsg, 'Checking identity information:');
if ($sqlpos !== false || $sqlpos2 !== false || $sqlpos3 !== false) {
continue;
}
$sqlpos = strpos((string) $sqlmsg, (string) $app_strings['ERR_MSSQL_DB_CONTEXT']);
$sqlpos2 = strpos((string) $sqlmsg, (string) $app_strings['ERR_MSSQL_WARNING']);
if ($sqlpos !== false || $sqlpos2 !== false) {
continue;
}
$messages[] = $sqlmsg;
}
if (!empty($messages)) {
return implode("\n", $messages);
}
return false;
}
/**
* (non-PHPdoc)
* @see DBManager::getDbInfo()
* @return array
*/
public function getDbInfo()
{
$info = array_merge(sqlsrv_client_info(), sqlsrv_server_info());
return $info;
}
/**
* Execute data manipulation statement, then roll it back
* @param $type
* @param $table
* @param $query
* @return string
*/
protected function verifyGenericQueryRollback($type, $table, $query)
{
$this->log->debug("verifying $type statement");
if (!sqlsrv_begin_transaction($this->database)) {
return "Failed to create transaction";
}
$this->query($query, false);
$error = $this->lastError();
sqlsrv_rollback($this->database);
return $error;
}
/**
* Tests an INSERT INTO query
* @param string table The table name to get DDL
* @param string query The query to test.
* @return string Non-empty if error found
*/
public function verifyInsertInto($table, $query)
{
return $this->verifyGenericQueryRollback("INSERT", $table, $query);
}
/**
* Tests an UPDATE query
* @param string table The table name to get DDL
* @param string query The query to test.
* @return string Non-empty if error found
*/
public function verifyUpdate($table, $query)
{
return $this->verifyGenericQueryRollback("UPDATE", $table, $query);
}
/**
* Tests an DELETE FROM query
* @param string table The table name to get DDL
* @param string query The query to test.
* @return string Non-empty if error found
*/
public function verifyDeleteFrom($table, $query)
{
return $this->verifyGenericQueryRollback("DELETE", $table, $query);
}
/**
* Select database
* @param string $dbname
*/
protected function selectDb($dbname)
{
return $this->query("USE " . $this->quoted($dbname));
}
/**
* Check if this driver can be used
* @return bool
*/
public function valid()
{
return function_exists("sqlsrv_connect");
}
}