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$)/', $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($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( $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($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 = <<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 = <<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) or !isset($app_strings['ERR_MSSQL_DB_CONTEXT']) or !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($sqlmsg, 'Changed database context to'); $sqlpos2 = strpos($sqlmsg, 'Warning:'); $sqlpos3 = strpos($sqlmsg, 'Checking identity information:'); if ($sqlpos !== false || $sqlpos2 !== false || $sqlpos3 !== false) { continue; } $sqlpos = strpos($sqlmsg, $app_strings['ERR_MSSQL_DB_CONTEXT']); $sqlpos2 = strpos($sqlmsg, $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"); } }