2 if(!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');
3 /*********************************************************************************
4 * SugarCRM Community Edition is a customer relationship management program developed by
5 * SugarCRM, Inc. Copyright (C) 2004-2012 SugarCRM Inc.
7 * This program is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU Affero General Public License version 3 as published by the
9 * Free Software Foundation with the addition of the following permission added
10 * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
11 * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY
12 * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
14 * This program is distributed in the hope that it will be useful, but WITHOUT
15 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16 * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
19 * You should have received a copy of the GNU Affero General Public License along with
20 * this program; if not, see http://www.gnu.org/licenses or write to the Free
21 * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
24 * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road,
25 * SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com.
27 * The interactive user interfaces in modified source and object code versions
28 * of this program must display Appropriate Legal Notices, as required under
29 * Section 5 of the GNU Affero General Public License version 3.
31 * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
32 * these Appropriate Legal Notices must retain the display of the "Powered by
33 * SugarCRM" logo. If the display of the logo is not reasonably feasible for
34 * technical reasons, the Appropriate Legal Notices must display the words
35 * "Powered by SugarCRM".
36 ********************************************************************************/
38 /*********************************************************************************
40 * Description: This file handles the Data base functionality for the application.
41 * It acts as the DB abstraction layer for the application. It depends on helper classes
42 * which generate the necessary SQL. This sql is then passed to PEAR DB classes.
43 * The helper class is chosen in DBManagerFactory, which is driven by 'db_type' in 'dbconfig' under config.php.
45 * All the functions in this class will work with any bean which implements the meta interface.
46 * The passed bean is passed to helper class which uses these functions to generate correct sql.
48 * The meta interface has the following functions:
49 * getTableName() Returns table name of the object.
50 * getFieldDefinitions() Returns a collection of field definitions in order.
51 * getFieldDefintion(name) Return field definition for the field.
52 * getFieldValue(name) Returns the value of the field identified by name.
53 * If the field is not set, the function will return boolean FALSE.
54 * getPrimaryFieldDefinition() Returns the field definition for primary key
56 * The field definition is an array with the following keys:
58 * name This represents name of the field. This is a required field.
59 * type This represents type of the field. This is a required field and valid values are:
73 * length This is used only when the type is varchar and denotes the length of the string.
74 * The max value is 255.
75 * enumvals This is a list of valid values for an enum separated by "|".
76 * It is used only if the type is ?enum?;
77 * required This field dictates whether it is a required value.
78 * The default value is ?FALSE?.
79 * isPrimary This field identifies the primary key of the table.
80 * If none of the fields have this flag set to ?TRUE?,
81 * the first field definition is assume to be the primary key.
82 * Default value for this field is ?FALSE?.
83 * default This field sets the default value for the field definition.
86 * Portions created by SugarCRM are Copyright (C) SugarCRM, Inc.
87 * All Rights Reserved.
88 * Contributor(s): ______________________________________..
89 ********************************************************************************/
91 include_once('include/database/MssqlManager.php');
94 * SQL Server (sqlsrv) manager
96 class SqlsrvManager extends MssqlManager
98 public $dbName = 'SQL Server';
99 public $variant = 'sqlsrv';
100 public $priority = 10;
101 public $label = 'LBL_MSSQL_SQLSRV';
103 protected $capabilities = array(
104 "affected_rows" => true,
106 'limit_subquery' => true,
107 'create_user' => true,
111 protected $type_map = array(
118 'short' => 'smallint',
119 'varchar' => 'nvarchar',
120 'text' => 'nvarchar(max)',
121 'longtext' => 'nvarchar(max)',
122 'date' => 'datetime',
123 'enum' => 'nvarchar',
124 'relate' => 'nvarchar',
125 'multienum'=> 'nvarchar(max)',
126 'html' => 'nvarchar(max)',
127 'longhtml' => 'nvarchar(max)',
128 'datetime' => 'datetime',
129 'datetimecombo' => 'datetime',
130 'time' => 'datetime',
132 'tinyint' => 'tinyint',
134 'blob' => 'nvarchar(max)',
135 'longblob' => 'nvarchar(max)',
136 'currency' => 'decimal(26,6)',
137 'decimal' => 'decimal',
138 'decimal2' => 'decimal',
139 'id' => 'varchar(36)',
141 'encrypt' => 'nvarchar',
142 'file' => 'nvarchar',
143 'decimal_tpl' => 'decimal(%d, %d)',
147 * @see DBManager::connect()
149 public function connect(array $configOptions = null, $dieOnError = false)
151 global $sugar_config;
153 if (is_null($configOptions))
154 $configOptions = $sugar_config['dbconfig'];
156 //set the connections parameters
158 $configOptions['db_host_instance'] = trim($configOptions['db_host_instance']);
159 if (empty($configOptions['db_host_instance']))
160 $connect_param = $configOptions['db_host_name'];
162 $connect_param = $configOptions['db_host_name']."\\".$configOptions['db_host_instance'];
165 * Don't try to specifically use a persistent connection
166 * since the driver will handle that for us
169 "UID" => $configOptions['db_user_name'],
170 "PWD" => $configOptions['db_password'],
171 "CharacterSet" => "UTF-8",
172 "ReturnDatesAsStrings" => true,
173 "MultipleActiveResultSets" => true,
175 if(!empty($configOptions['db_name'])) {
176 $options["Database"] = $configOptions['db_name'];
178 $this->database = sqlsrv_connect($connect_param, $options);
179 if(empty($this->database)) {
180 $GLOBALS['log']->fatal("Could not connect to server ".$configOptions['db_host_name']." as ".$configOptions['db_user_name'].".");
182 if(isset($GLOBALS['app_strings']['ERR_NO_DB'])) {
183 sugar_die($GLOBALS['app_strings']['ERR_NO_DB']);
185 sugar_die("Could not connect to the database. Please refer to sugarcrm.log for details.");
192 if($this->checkError('Could Not Connect:', $dieOnError))
193 $GLOBALS['log']->info("connected to db");
195 sqlsrv_query($this->database, 'SET DATEFORMAT mdy');
197 $this->connectOptions = $configOptions;
199 $GLOBALS['log']->info("Connect:".$this->database);
204 * @see DBManager::query()
206 public function query($sql, $dieOnError = false, $msg = '', $suppress = false, $keepResult = false)
209 return $this->queryArray($sql, $dieOnError, $msg, $suppress);
211 $sql = $this->_appendN($sql);
213 $this->countQuery($sql);
214 $GLOBALS['log']->info('Query:' . $sql);
215 $this->checkConnection();
216 $this->query_time = microtime(true);
218 $result = $suppress?@sqlsrv_query($this->database, $sql):sqlsrv_query($this->database, $sql);
220 $this->query_time = microtime(true) - $this->query_time;
221 $GLOBALS['log']->info('Query Execution Time:'.$this->query_time);
224 $this->checkError($msg.' Query Failed:' . $sql . '::', $dieOnError);
226 //suppress non error messages
227 sqlsrv_configure('WarningsReturnAsErrors',false);
233 * @see DBManager::getFieldsArray()
235 public function getFieldsArray($result, $make_lower_case = false)
237 $field_array = array();
243 foreach ( sqlsrv_field_metadata($result) as $fieldMetadata ) {
244 $key = $fieldMetadata['Name'];
245 if($make_lower_case==true)
246 $key = strtolower($key);
248 $field_array[] = $key;
255 * @see DBManager::fetchRow()
257 public function fetchRow($result)
259 if (empty($result)) return false;
261 $row = sqlsrv_fetch_array($result,SQLSRV_FETCH_ASSOC);
266 foreach($row as $key => $column) {
267 // MSSQL returns a space " " when a varchar column is empty ("") and not null.
268 // We need to strip empty spaces
269 // notice we only strip if one space is returned. we do not want to strip
270 // strings with intentional spaces (" foo ")
271 if (!empty($column) && $column == " ") {
280 * @see DBManager::convert()
282 public function convert($string, $type, array $additional_parameters = array())
284 if ( $type == 'datetime')
285 // see http://msdn.microsoft.com/en-us/library/ms187928.aspx for details
286 return "CONVERT(datetime,$string,120)";
288 return parent::convert($string, $type, $additional_parameters);
292 * Compares two vardefs. Overriding 39098 due to bug: 39098 . IN 6.0 we changed the id columns to dbType = 'id'
293 * 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
294 * it to varchar. This code will prevent it.
296 * @param array $fielddef1
297 * @param array $fielddef2
298 * @return bool true if they match, false if they don't
300 public function compareVarDefs($fielddef1,$fielddef2)
302 if((isset($fielddef2['dbType']) && $fielddef2['dbType'] == 'id') || preg_match('/(_id$|^id$)/', $fielddef2['name'])){
303 if(isset($fielddef1['type']) && isset($fielddef2['type'])){
304 $fielddef2['type'] = $fielddef1['type'];
307 return parent::compareVarDefs($fielddef1, $fielddef2);
311 * Disconnects from the database
313 * Also handles any cleanup needed
315 public function disconnect()
317 $GLOBALS['log']->debug('Calling Mssql::disconnect()');
318 if(!empty($this->database)){
320 sqlsrv_close($this->database);
321 $this->database = null;
326 * @see DBManager::freeDbResult()
328 protected function freeDbResult($dbResult)
330 if(!empty($dbResult))
331 sqlsrv_free_stmt($dbResult);
336 * Detect if no clustered index has been created for a table; if none created then just pick the first index and make it that
338 * @see MssqlHelper::indexSQL()
340 public function getConstraintSql($indices, $table)
342 if ( $this->doesTableHaveAClusteredIndexDefined($table) ) {
343 return parent::getConstraintSql($indices, $table);
346 // check to see if one of the passed in indices is a primary one; if so we can bail as well
347 foreach ( $indices as $index ) {
348 if ( $index['type'] == 'primary' ) {
349 return parent::getConstraintSql($indices, $table);
353 // Change the first index listed to be a clustered one instead ( so we have at least one for the table )
354 if ( isset($indices[0]) ) {
355 $indices[0]['type'] = 'clustered';
358 return parent::getConstraintSql($indices, $table);
362 * @see DBManager::get_columns()
364 public function get_columns($tablename)
366 //find all unique indexes and primary keys.
367 $result = $this->query("sp_columns_90 $tablename");
370 while (($row=$this->fetchByAssoc($result)) !=null) {
371 $column_name = strtolower($row['COLUMN_NAME']);
372 $columns[$column_name]['name']=$column_name;
373 $columns[$column_name]['type']=strtolower($row['TYPE_NAME']);
374 if ( $row['TYPE_NAME'] == 'decimal' ) {
375 $columns[$column_name]['len']=strtolower($row['PRECISION']);
376 $columns[$column_name]['len'].=','.strtolower($row['SCALE']);
378 elseif ( in_array($row['TYPE_NAME'],array('nchar','nvarchar')) ) {
379 $columns[$column_name]['len']=strtolower($row['PRECISION']);
380 if ( $row['TYPE_NAME'] == 'nvarchar' && $row['PRECISION'] == '0' ) {
381 $columns[$column_name]['len']='max';
384 elseif ( !in_array($row['TYPE_NAME'],array('datetime','text')) ) {
385 $columns[$column_name]['len']=strtolower($row['LENGTH']);
387 if ( stristr($row['TYPE_NAME'],'identity') ) {
388 $columns[$column_name]['auto_increment'] = '1';
389 $columns[$column_name]['type']=str_replace(' identity','',strtolower($row['TYPE_NAME']));
392 if (!empty($row['IS_NULLABLE']) && $row['IS_NULLABLE'] == 'NO' && (empty($row['KEY']) || !stristr($row['KEY'],'PRI')))
393 $columns[strtolower($row['COLUMN_NAME'])]['required'] = 'true';
396 if ( strtolower($tablename) == 'relationships' ) {
397 $column_def = $this->getOne("select cdefault from syscolumns where id = object_id('relationships') and name = '$column_name'");
399 if ( $column_def != 0 && ($row['COLUMN_DEF'] != null)) { // NOTE Not using !empty as an empty string may be a viable default value.
401 $row['COLUMN_DEF'] = html_entity_decode($row['COLUMN_DEF'],ENT_QUOTES);
402 if ( preg_match('/\([\(|\'](.*)[\)|\']\)/i',$row['COLUMN_DEF'],$matches) )
403 $columns[$column_name]['default'] = $matches[1];
404 elseif ( preg_match('/\(N\'(.*)\'\)/i',$row['COLUMN_DEF'],$matches) )
405 $columns[$column_name]['default'] = $matches[1];
407 $columns[$column_name]['default'] = $row['COLUMN_DEF'];
414 * protected function to return true if the given tablename has any clustered indexes defined.
416 * @param string $tableName
419 protected function doesTableHaveAClusteredIndexDefined($tableName)
422 SELECT IST.TABLE_NAME
423 FROM INFORMATION_SCHEMA.TABLES IST
424 WHERE objectProperty(object_id(IST.TABLE_NAME), 'IsUserTable') = 1
425 AND objectProperty(object_id(IST.TABLE_NAME), 'TableHasClustIndex') = 1
426 AND IST.TABLE_NAME = '{$tableName}'
429 $result = $this->getOne($query);
438 * protected function to return true if the given tablename has any fulltext indexes defined.
440 * @param string $tableName
443 protected function doesTableHaveAFulltextIndexDefined($tableName)
447 FROM sys.fulltext_indexes i
448 JOIN sys.objects o ON i.object_id = o.object_id
449 WHERE o.name = '{$tableName}'
452 $result = $this->getOne($query);
461 * Override method to add support for detecting and dropping fulltext indices.
463 * @see DBManager::changeColumnSQL()
464 * @see MssqlHelper::changeColumnSQL()
466 protected function changeColumnSQL($tablename,$fieldDefs, $action, $ignoreRequired = false)
469 if ( $action == 'drop' && $this->doesTableHaveAFulltextIndexDefined($tablename) ) {
470 $sql .= "DROP FULLTEXT INDEX ON {$tablename}";
473 $sql .= parent::changeColumnSQL($tablename, $fieldDefs, $action, $ignoreRequired);
483 public function truncateTableSQL($name)
485 return "TRUNCATE TABLE $name";
490 * @see DBManager::lastDbError()
492 public function lastDbError()
494 $errors = sqlsrv_errors(SQLSRV_ERR_ERRORS);
495 if(empty($errors)) return false;
497 if (empty($app_strings)
498 or !isset($app_strings['ERR_MSSQL_DB_CONTEXT'])
499 or !isset($app_strings['ERR_MSSQL_WARNING']) ) {
500 //ignore the message from sql-server if $app_strings array is empty. This will happen
501 //only if connection if made before languge is set.
505 foreach($errors as $error) {
506 $sqlmsg = $error['message'];
507 $sqlpos = strpos($sqlmsg, 'Changed database context to');
508 $sqlpos2 = strpos($sqlmsg, 'Warning:');
509 $sqlpos3 = strpos($sqlmsg, 'Checking identity information:');
510 if ( $sqlpos !== false || $sqlpos2 !== false || $sqlpos3 !== false ) {
513 $sqlpos = strpos($sqlmsg, $app_strings['ERR_MSSQL_DB_CONTEXT']);
514 $sqlpos2 = strpos($sqlmsg, $app_strings['ERR_MSSQL_WARNING']);
515 if ( $sqlpos !== false || $sqlpos2 !== false) {
518 $messages[] = $sqlmsg;
521 if(!empty($messages)) {
522 return join("\n", $messages);
529 * @see DBManager::getDbInfo()
532 public function getDbInfo()
534 $info = array_merge(sqlsrv_client_info(), sqlsrv_server_info());
539 * Execute data manipulation statement, then roll it back
545 protected function verifyGenericQueryRollback($type, $table, $query)
547 $this->log->debug("verifying $type statement");
548 if(!sqlsrv_begin_transaction($this->database)) {
549 return "Failed to create transaction";
551 $this->query($query, false);
552 $error = $this->lastError();
553 sqlsrv_rollback($this->database);
558 * Tests an INSERT INTO query
559 * @param string table The table name to get DDL
560 * @param string query The query to test.
561 * @return string Non-empty if error found
563 public function verifyInsertInto($table, $query)
565 return $this->verifyGenericQueryRollback("INSERT", $table, $query);
569 * Tests an UPDATE query
570 * @param string table The table name to get DDL
571 * @param string query The query to test.
572 * @return string Non-empty if error found
574 public function verifyUpdate($table, $query)
576 return $this->verifyGenericQueryRollback("UPDATE", $table, $query);
580 * Tests an DELETE FROM query
581 * @param string table The table name to get DDL
582 * @param string query The query to test.
583 * @return string Non-empty if error found
585 public function verifyDeleteFrom($table, $query)
587 return $this->verifyGenericQueryRollback("DELETE", $table, $query);
592 * @param string $dbname
594 protected function selectDb($dbname)
596 return $this->query("USE ".$this->quoted($dbname));
600 * Check if this driver can be used
603 public function valid()
605 return function_exists("sqlsrv_connect");