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)', '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 sugarcrm.log for details."); } } 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) { 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 * * @see MssqlHelper::indexSQL() */ 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; } /** * @see DBManager::get_indices() */ public function get_indices($tableName) { //find all unique indexes and primary keys. $query = <<query($query); $indices = array(); while (($row=$this->fetchByAssoc($result)) != null) { $index_type = 'index'; if ($row['is_primary_key'] == '1') $index_type = 'primary'; elseif ($row['is_unique'] == 1 ) $index_type = 'unique'; $name = strtolower($row['index_name']); $indices[$name]['name'] = $name; $indices[$name]['type'] = $index_type; $indices[$name]['fields'][] = strtolower($row['column_name']); } return $indices; } /** * 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; } /** * (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 join("\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"); } }