]> CyberLeo.Net >> Repos - Github/sugarcrm.git/blob - include/database/MssqlManager.php
Release 6.5.11
[Github/sugarcrm.git] / include / database / MssqlManager.php
1 <?php
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-2013 SugarCRM Inc.
6  * 
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.
13  * 
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
17  * details.
18  * 
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
22  * 02110-1301 USA.
23  * 
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.
26  * 
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.
30  * 
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  ********************************************************************************/
37
38 /*********************************************************************************
39
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.
44 *
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.
47 *
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
55 *
56 * The field definition is an array with the following keys:
57 *
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:
60 *               int
61 *               long
62 *               varchar
63 *               text
64 *               date
65 *               datetime
66 *               double
67 *               float
68 *               uint
69 *               ulong
70 *               time
71 *               short
72 *               enum
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.
84 *
85 *
86 * Portions created by SugarCRM are Copyright (C) SugarCRM, Inc.
87 * All Rights Reserved.
88 * Contributor(s): ______________________________________..
89 ********************************************************************************/
90
91 /**
92  * SQL Server (mssql) manager
93  */
94 class MssqlManager extends DBManager
95 {
96     /**
97      * @see DBManager::$dbType
98      */
99     public $dbType = 'mssql';
100     public $dbName = 'MsSQL';
101     public $variant = 'mssql';
102     public $label = 'LBL_MSSQL';
103
104     protected $capabilities = array(
105         "affected_rows" => true,
106         "select_rows" => true,
107         'fulltext' => true,
108         'limit_subquery' => true,
109         "fix:expandDatabase" => true, // Support expandDatabase fix
110         "create_user" => true,
111         "create_db" => true,
112     );
113
114     /**
115      * Maximum length of identifiers
116      */
117     protected $maxNameLengths = array(
118         'table' => 128,
119         'column' => 128,
120         'index' => 128,
121         'alias' => 128
122     );
123
124     protected $type_map = array(
125             'int'      => 'int',
126             'double'   => 'float',
127             'float'    => 'float',
128             'uint'     => 'int',
129             'ulong'    => 'int',
130             'long'     => 'bigint',
131             'short'    => 'smallint',
132             'varchar'  => 'varchar',
133             'text'     => 'text',
134             'longtext' => 'text',
135             'date'     => 'datetime',
136             'enum'     => 'varchar',
137             'relate'   => 'varchar',
138             'multienum'=> 'text',
139             'html'     => 'text',
140                         'longhtml' => 'text',
141                 'datetime' => 'datetime',
142             'datetimecombo' => 'datetime',
143             'time'     => 'datetime',
144             'bool'     => 'bit',
145             'tinyint'  => 'tinyint',
146             'char'     => 'char',
147             'blob'     => 'image',
148             'longblob' => 'image',
149             'currency' => 'decimal(26,6)',
150             'decimal'  => 'decimal',
151             'decimal2' => 'decimal',
152             'id'       => 'varchar(36)',
153             'url'      => 'varchar',
154             'encrypt'  => 'varchar',
155             'file'     => 'varchar',
156                 'decimal_tpl' => 'decimal(%d, %d)',
157             );
158
159     protected $connectOptions = null;
160
161     /**
162      * @see DBManager::connect()
163      */
164     public function connect(array $configOptions = null, $dieOnError = false)
165     {
166         global $sugar_config;
167
168         if (is_null($configOptions))
169             $configOptions = $sugar_config['dbconfig'];
170
171         //SET DATEFORMAT to 'YYYY-MM-DD''
172         ini_set('mssql.datetimeconvert', '0');
173
174         //set the text size and textlimit to max number so that blob columns are not truncated
175         ini_set('mssql.textlimit','2147483647');
176         ini_set('mssql.textsize','2147483647');
177         ini_set('mssql.charset','UTF-8');
178
179         if(!empty($configOptions['db_host_instance'])) {
180             $configOptions['db_host_instance'] = trim($configOptions['db_host_instance']);
181         }
182         //set the connections parameters
183         if (empty($configOptions['db_host_instance'])) {
184             $connect_param = $configOptions['db_host_name'];
185         } else {
186             $connect_param = $configOptions['db_host_name']."\\".$configOptions['db_host_instance'];
187         }
188
189         //create persistent connection
190         if ($this->getOption('persistent')) {
191             $this->database =@mssql_pconnect(
192                 $connect_param ,
193                 $configOptions['db_user_name'],
194                 $configOptions['db_password']
195                 );
196         }
197         //if no persistent connection created, then create regular connection
198         if(!$this->database){
199             $this->database = mssql_connect(
200                     $connect_param ,
201                     $configOptions['db_user_name'],
202                     $configOptions['db_password']
203                     );
204             if(!$this->database){
205                 $GLOBALS['log']->fatal("Could not connect to server ".$configOptions['db_host_name'].
206                     " as ".$configOptions['db_user_name'].".");
207                 if($dieOnError) {
208                     sugar_die($GLOBALS['app_strings']['ERR_NO_DB']);
209                 } else {
210                     return false;
211                 }
212             }
213             if($this->database && $this->getOption('persistent')){
214                 $_SESSION['administrator_error'] = "<B>Severe Performance Degradation: Persistent Database Connections "
215                     . "not working.  Please set \$sugar_config['dbconfigoption']['persistent'] to false in your "
216                     . "config.php file</B>";
217             }
218         }
219         //make sure connection exists
220         if(!$this->database) {
221                 if($dieOnError) {
222                     sugar_die($GLOBALS['app_strings']['ERR_NO_DB']);
223                 } else {
224                     return false;
225                 }
226         }
227
228         //select database
229
230         //Adding sleep and retry for mssql connection. We have come across scenarios when
231         //an error is thrown.' Unable to select database'. Following will try to connect to
232         //mssql db maximum number of 5 times at the interval of .2 second. If can not connect
233         //it will throw an Unable to select database message.
234
235         if(!empty($configOptions['db_name']) && !@mssql_select_db($configOptions['db_name'], $this->database)){
236                         $connected = false;
237                         for($i=0;$i<5;$i++){
238                                 usleep(200000);
239                                 if(@mssql_select_db($configOptions['db_name'], $this->database)){
240                                         $connected = true;
241                                         break;
242                                 }
243                         }
244                         if(!$connected){
245                             $GLOBALS['log']->fatal( "Unable to select database {$configOptions['db_name']}");
246                 if($dieOnError) {
247                     if(isset($GLOBALS['app_strings']['ERR_NO_DB'])) {
248                         sugar_die($GLOBALS['app_strings']['ERR_NO_DB']);
249                     } else {
250                         sugar_die("Could not connect to the database. Please refer to sugarcrm.log for details.");
251                     }
252                 } else {
253                     return false;
254                 }
255                         }
256          }
257
258         if(!$this->checkError('Could Not Connect', $dieOnError))
259             $GLOBALS['log']->info("connected to db");
260
261         $this->connectOptions = $configOptions;
262
263         $GLOBALS['log']->info("Connect:".$this->database);
264         return true;
265     }
266
267         /**
268      * @see DBManager::version()
269      */
270     public function version()
271     {
272         return $this->getOne("SELECT @@VERSION as version");
273         }
274
275         /**
276      * @see DBManager::query()
277          */
278         public function query($sql, $dieOnError = false, $msg = '', $suppress = false, $keepResult = false)
279     {
280         if(is_array($sql)) {
281             return $this->queryArray($sql, $dieOnError, $msg, $suppress);
282         }
283         // Flag if there are odd number of single quotes
284         if ((substr_count($sql, "'") & 1))
285             $GLOBALS['log']->error("SQL statement[" . $sql . "] has odd number of single quotes.");
286
287                 $sql = $this->_appendN($sql);
288
289         $GLOBALS['log']->info('Query:' . $sql);
290         $this->checkConnection();
291         $this->countQuery($sql);
292         $this->query_time = microtime(true);
293
294         // Bug 34892 - Clear out previous error message by checking the @@ERROR global variable
295                 @mssql_query("SELECT @@ERROR", $this->database);
296
297         $result = $suppress?@mssql_query($sql, $this->database):mssql_query($sql, $this->database);
298
299         if (!$result) {
300             // awu Bug 10657: ignoring mssql error message 'Changed database context to' - an intermittent
301             //                            and difficult to reproduce error. The message is only a warning, and does
302             //                            not affect the functionality of the query
303             $sqlmsg = mssql_get_last_message();
304             $sqlpos = strpos($sqlmsg, 'Changed database context to');
305                         $sqlpos2 = strpos($sqlmsg, 'Warning:');
306                         $sqlpos3 = strpos($sqlmsg, 'Checking identity information:');
307
308                         if ($sqlpos !== false || $sqlpos2 !== false || $sqlpos3 !== false)              // if sqlmsg has 'Changed database context to', just log it
309                                 $GLOBALS['log']->debug($sqlmsg . ": " . $sql );
310                         else {
311                                 $GLOBALS['log']->fatal($sqlmsg . ": " . $sql );
312                                 if($dieOnError)
313                                         sugar_die('SQL Error : ' . $sqlmsg);
314                                 else
315                                         echo 'SQL Error : ' . $sqlmsg;
316                         }
317         }
318
319         $this->query_time = microtime(true) - $this->query_time;
320         $GLOBALS['log']->info('Query Execution Time:'.$this->query_time);
321
322
323         $this->checkError($msg.' Query Failed: ' . $sql, $dieOnError);
324
325         return $result;
326     }
327
328     /**
329      * This function take in the sql for a union query, the start and offset,
330      * and wraps it around an "mssql friendly" limit query
331      *
332      * @param  string $sql
333      * @param  int    $start record to start at
334      * @param  int    $count number of records to retrieve
335      * @return string SQL statement
336      */
337     private function handleUnionLimitQuery($sql, $start, $count)
338     {
339         //set the start to 0, no negs
340         if ($start < 0)
341             $start=0;
342
343         $GLOBALS['log']->debug(print_r(func_get_args(),true));
344
345         $this->lastsql = $sql;
346
347         //change the casing to lower for easier string comparison, and trim whitespaces
348         $sql = strtolower(trim($sql)) ;
349
350         //set default sql
351         $limitUnionSQL = $sql;
352         $order_by_str = 'order by';
353
354         //make array of order by's.  substring approach was proving too inconsistent
355         $orderByArray = explode($order_by_str, $sql);
356         $unionOrderBy = '';
357         $rowNumOrderBy = '';
358
359         //count the number of array elements
360         $unionOrderByCount = count($orderByArray);
361         $arr_count = 0;
362
363         //process if there are elements
364         if ($unionOrderByCount){
365             //we really want the last order by, so reconstruct string
366             //adding a 1 to count, as we dont wish to process the last element
367             $unionsql = '';
368             while ($unionOrderByCount>$arr_count+1) {
369                 $unionsql .= $orderByArray[$arr_count];
370                 $arr_count = $arr_count+1;
371                 //add an "order by" string back if we are coming into loop again
372                 //remember they were taken out when array was created
373                 if ($unionOrderByCount>$arr_count+1) {
374                     $unionsql .= "order by";
375                 }
376             }
377             //grab the last order by element, set both order by's'
378             $unionOrderBy = $orderByArray[$arr_count];
379             $rowNumOrderBy = $unionOrderBy;
380
381             //if last element contains a "select", then this is part of the union query,
382             //and there is no order by to use
383             if (strpos($unionOrderBy, "select")) {
384                 $unionsql = $sql;
385                 //with no guidance on what to use for required order by in rownumber function,
386                 //resort to using name column.
387                 $rowNumOrderBy = 'id';
388                 $unionOrderBy = "";
389             }
390         }
391         else {
392             //there are no order by elements, so just pass back string
393             $unionsql = $sql;
394             //with no guidance on what to use for required order by in rownumber function,
395             //resort to using name column.
396             $rowNumOrderBy = 'id';
397             $unionOrderBy = '';
398         }
399         //Unions need the column name being sorted on to match across all queries in Union statement
400         //so we do not want to strip the alias like in other queries.  Just add the "order by" string and
401         //pass column name as is
402         if ($unionOrderBy != '') {
403             $unionOrderBy = ' order by ' . $unionOrderBy;
404         }
405
406         //Bug 56560, use top query in conjunction with rownumber() function
407         //to create limit query when paging is needed. Otherwise,
408         //it shows duplicates when paging on activities subpanel.
409         //If not for paging, no need to use rownumber() function
410         if ($count == 1 && $start == 0)
411         {
412             $limitUnionSQL = "SELECT TOP $count * FROM (" .$unionsql .") as top_count ".$unionOrderBy;
413         }
414         else
415         {
416             $limitUnionSQL = "SELECT TOP $count * FROM( select ROW_NUMBER() OVER ( order by "
417             .$rowNumOrderBy.") AS row_number, * FROM ("
418             .$unionsql .") As numbered) "
419             . "As top_count_limit WHERE row_number > $start "
420             .$unionOrderBy;
421         }
422
423         return $limitUnionSQL;
424     }
425
426         /**
427          * FIXME: verify and thoroughly test this code, these regexps look fishy
428      * @see DBManager::limitQuery()
429      */
430     public function limitQuery($sql, $start, $count, $dieOnError = false, $msg = '', $execute = true)
431     {
432         $start = (int)$start;
433         $count = (int)$count;
434         $newSQL = $sql;
435         $distinctSQLARRAY = array();
436         if (strpos($sql, "UNION") && !preg_match("/(')(UNION).?(')/i", $sql))
437             $newSQL = $this->handleUnionLimitQuery($sql,$start,$count);
438         else {
439             if ($start < 0)
440                 $start = 0;
441             $GLOBALS['log']->debug(print_r(func_get_args(),true));
442             $this->lastsql = $sql;
443             $matches = array();
444             preg_match('/^(.*SELECT\b)(.*?\bFROM\b.*\bWHERE\b)(.*)$/isU',$sql, $matches);
445             if (!empty($matches[3])) {
446                 if ($start == 0) {
447                     $match_two = strtolower($matches[2]);
448                     if (!strpos($match_two, "distinct")> 0 && strpos($match_two, "distinct") !==0) {
449                         $orderByMatch = array();
450                         preg_match('/^(.*)(\bORDER BY\b)(.*)$/is',$matches[3], $orderByMatch);
451                         if (!empty($orderByMatch[3])) {
452                             $selectPart = array();
453                             preg_match('/^(.*)(\bFROM\b.*)$/isU', $matches[2], $selectPart);
454                             $newSQL = "SELECT TOP $count * FROM
455                                 (
456                                     " . $matches[1] . $selectPart[1] . ", ROW_NUMBER()
457                                     OVER (ORDER BY " . $this->returnOrderBy($sql, $orderByMatch[3]) . ") AS row_number
458                                     " . $selectPart[2] . $orderByMatch[1]. "
459                                 ) AS a
460                                 WHERE row_number > $start";
461                         }
462                         else {
463                             $newSQL = $matches[1] . " TOP $count " . $matches[2] . $matches[3];
464                         }
465                     }
466                     else {
467                         $distinct_o = strpos($match_two, "distinct");
468                         $up_to_distinct_str = substr($match_two, 0, $distinct_o);
469                         //check to see if the distinct is within a function, if so, then proceed as normal
470                         if (strpos($up_to_distinct_str,"(")) {
471                             //proceed as normal
472                             $newSQL = $matches[1] . " TOP $count " . $matches[2] . $matches[3];
473                         }
474                         else {
475                             //if distinct is not within a function, then parse
476                             //string contains distinct clause, "TOP needs to come after Distinct"
477                             //get position of distinct
478                             $match_zero = strtolower($matches[0]);
479                             $distinct_pos = strpos($match_zero , "distinct");
480                             //get position of where
481                             $where_pos = strpos($match_zero, "where");
482                             //parse through string
483                             $beg = substr($matches[0], 0, $distinct_pos+9 );
484                             $mid = substr($matches[0], strlen($beg), ($where_pos+5) - (strlen($beg)));
485                             $end = substr($matches[0], strlen($beg) + strlen($mid) );
486                             //repopulate matches array
487                             $matches[1] = $beg; $matches[2] = $mid; $matches[3] = $end;
488
489                             $newSQL = $matches[1] . " TOP $count " . $matches[2] . $matches[3];
490                         }
491                     }
492                 } else {
493                     $orderByMatch = array();
494                     preg_match('/^(.*)(\bORDER BY\b)(.*)$/is',$matches[3], $orderByMatch);
495
496                     //if there is a distinct clause, parse sql string as we will have to insert the rownumber
497                     //for paging, AFTER the distinct clause
498                     $grpByStr = '';
499                     $hasDistinct = strpos(strtolower($matches[0]), "distinct");
500                     if ($hasDistinct) {
501                         $matches_sql = strtolower($matches[0]);
502                         //remove reference to distinct and select keywords, as we will use a group by instead
503                         //we need to use group by because we are introducing rownumber column which would make every row unique
504
505                         //take out the select and distinct from string so we can reuse in group by
506                         $dist_str = ' distinct ';
507                         $distinct_pos = strpos($matches_sql, $dist_str);
508                         $matches_sql = substr($matches_sql,$distinct_pos+ strlen($dist_str));
509                         //get the position of where and from for further processing
510                         $from_pos = strpos($matches_sql , " from ");
511                         $where_pos = strpos($matches_sql, "where");
512                         //split the sql into a string before and after the from clause
513                         //we will use the columns being selected to construct the group by clause
514                         if ($from_pos>0 ) {
515                             $distinctSQLARRAY[0] = substr($matches_sql,0, $from_pos+1);
516                             $distinctSQLARRAY[1] = substr($matches_sql,$from_pos+1);
517                             //get position of order by (if it exists) so we can strip it from the string
518                             $ob_pos = strpos($distinctSQLARRAY[1], "order by");
519                             if ($ob_pos) {
520                                 $distinctSQLARRAY[1] = substr($distinctSQLARRAY[1],0,$ob_pos);
521                             }
522
523                             // strip off last closing parentheses from the where clause
524                             $distinctSQLARRAY[1] = preg_replace('/\)\s$/',' ',$distinctSQLARRAY[1]);
525                         }
526
527                         //place group by string into array
528                         $grpByArr = explode(',', $distinctSQLARRAY[0]);
529                         $first = true;
530                         //remove the aliases for each group by element, sql server doesnt like these in group by.
531                         foreach ($grpByArr as $gb) {
532                             $gb = trim($gb);
533
534                             //clean out the extra stuff added if we are concatenating first_name and last_name together
535                             //this way both fields are added in correctly to the group by
536                             $gb = str_replace("isnull(","",$gb);
537                             $gb = str_replace("'') + ' ' + ","",$gb);
538
539                             //remove outer reference if they exist
540                             if (strpos($gb,"'")!==false){
541                                 continue;
542                             }
543                             //if there is a space, then an alias exists, remove alias
544                             if (strpos($gb,' ')){
545                                 $gb = substr( $gb, 0,strpos($gb,' '));
546                             }
547
548                             //if resulting string is not empty then add to new group by string
549                             if (!empty($gb)) {
550                                 if ($first) {
551                                     $grpByStr .= " $gb";
552                                     $first = false;
553                                 } else {
554                                     $grpByStr .= ", $gb";
555                                 }
556                             }
557                         }
558                     }
559
560                     if (!empty($orderByMatch[3])) {
561                         //if there is a distinct clause, form query with rownumber after distinct
562                         if ($hasDistinct) {
563                             $newSQL = "SELECT TOP $count * FROM
564                                         (
565                                             SELECT ROW_NUMBER()
566                                                 OVER (ORDER BY " . preg_replace('/^' . ltrim($dist_str) . '/', '', $this->returnOrderBy($sql, $orderByMatch[3])) . ") AS row_number,
567                                                 count(*) counter, " . $distinctSQLARRAY[0] . "
568                                                 " . $distinctSQLARRAY[1] . "
569                                                 group by " . $grpByStr . "
570                                         ) AS a
571                                         WHERE row_number > $start";
572                         }
573                         else {
574                         $newSQL = "SELECT TOP $count * FROM
575                                     (
576                                         " . $matches[1] . " ROW_NUMBER()
577                                         OVER (ORDER BY " . $this->returnOrderBy($sql, $orderByMatch[3]) . ") AS row_number,
578                                         " . $matches[2] . $orderByMatch[1]. "
579                                     ) AS a
580                                     WHERE row_number > $start";
581                         }
582                     }else{
583                         //bug: 22231 Records in campaigns' subpanel may not come from
584                         //table of $_REQUEST['module']. Get it directly from query
585                         $upperQuery = strtoupper($matches[2]);
586                         if (!strpos($upperQuery,"JOIN")){
587                             $from_pos = strpos($upperQuery , "FROM") + 4;
588                             $where_pos = strpos($upperQuery, "WHERE");
589                             $tablename = trim(substr($upperQuery,$from_pos, $where_pos - $from_pos));
590                         }else{
591                             // FIXME: this looks really bad. Probably source for tons of bug
592                             // needs to be removed
593                             $tablename = $this->getTableNameFromModuleName($_REQUEST['module'],$sql);
594                         }
595                         $orderBy = $tablename;
596                         if (preg_match("/from\s+".$tablename."\s+([^\s]+)\s+(where|inner|left|join|outer|right)\s+/i", $sql, $table_alias))
597                         {
598                             $orderBy = $table_alias[1];
599                         }
600                         //if there is a distinct clause, form query with rownumber after distinct
601                         if ($hasDistinct) {
602                              $newSQL = "SELECT TOP $count * FROM
603                                             (
604                             SELECT ROW_NUMBER() OVER (ORDER BY ".$orderBy.".id) AS row_number, count(*) counter, " . $distinctSQLARRAY[0] . "
605                                                         " . $distinctSQLARRAY[1] . "
606                                                     group by " . $grpByStr . "
607                                             )
608                                             AS a
609                                             WHERE row_number > $start";
610                         }
611                         else {
612                              $newSQL = "SELECT TOP $count * FROM
613                                            (
614                                   " . $matches[1] . " ROW_NUMBER() OVER (ORDER BY ".$orderBy.".id) AS row_number, " . $matches[2] . $matches[3]. "
615                                            )
616                                            AS a
617                                            WHERE row_number > $start";
618                         }
619                     }
620                 }
621             }
622         }
623
624         $GLOBALS['log']->debug('Limit Query: ' . $newSQL);
625         if($execute) {
626             $result =  $this->query($newSQL, $dieOnError, $msg);
627             $this->dump_slow_queries($newSQL);
628             return $result;
629         } else {
630             return $newSQL;
631         }
632     }
633
634
635     /**
636      * Searches for begginning and ending characters.  It places contents into
637      * an array and replaces contents in original string.  This is used to account for use of
638      * nested functions while aliasing column names
639      *
640      * @param  string $p_sql     SQL statement
641      * @param  string $strip_beg Beginning character
642      * @param  string $strip_end Ending character
643      * @param  string $patt      Optional, pattern to
644      */
645     private function removePatternFromSQL($p_sql, $strip_beg, $strip_end, $patt = 'patt')
646     {
647         //strip all single quotes out
648         $count = substr_count ( $p_sql, $strip_beg);
649         $increment = 1;
650         if ($strip_beg != $strip_end)
651             $increment = 2;
652
653         $i=0;
654         $offset = 0;
655         $strip_array = array();
656         while ($i<$count && $offset<strlen($p_sql)) {
657             if ($offset > strlen($p_sql))
658             {
659                                 break;
660             }
661
662             $beg_sin = strpos($p_sql, $strip_beg, $offset);
663             if (!$beg_sin)
664             {
665                 break;
666             }
667             $sec_sin = strpos($p_sql, $strip_end, $beg_sin+1);
668             $strip_array[$patt.$i] = substr($p_sql, $beg_sin, $sec_sin - $beg_sin +1);
669             if ($increment > 1) {
670                 //we are in here because beginning and end patterns are not identical, so search for nesting
671                 $exists = strpos($strip_array[$patt.$i], $strip_beg );
672                 if ($exists>=0) {
673                     $nested_pos = (strrpos($strip_array[$patt.$i], $strip_beg ));
674                     $strip_array[$patt.$i] = substr($p_sql,$nested_pos+$beg_sin,$sec_sin - ($nested_pos+$beg_sin)+1);
675                     $p_sql = substr($p_sql, 0, $nested_pos+$beg_sin) . " ##". $patt.$i."## " . substr($p_sql, $sec_sin+1);
676                     $i = $i + 1;
677                     continue;
678                 }
679             }
680             $p_sql = substr($p_sql, 0, $beg_sin) . " ##". $patt.$i."## " . substr($p_sql, $sec_sin+1);
681             //move the marker up
682             $offset = $sec_sin+1;
683
684             $i = $i + 1;
685         }
686         $strip_array['sql_string'] = $p_sql;
687
688         return $strip_array;
689     }
690
691     /**
692      * adds a pattern
693      *
694      * @param  string $token
695      * @param  array  $pattern_array
696      * @return string
697      */
698         private function addPatternToSQL($token, array $pattern_array)
699     {
700         //strip all single quotes out
701         $pattern_array = array_reverse($pattern_array);
702
703         foreach ($pattern_array as $key => $replace) {
704             $token = str_replace( " ##".$key."## ", $replace,$token);
705         }
706
707         return $token;
708     }
709
710     /**
711      * gets an alias from the sql statement
712      *
713      * @param  string $sql
714      * @param  string $alias
715      * @return string
716      */
717         private function getAliasFromSQL($sql, $alias)
718     {
719         $matches = array();
720         preg_match('/^(.*SELECT)(.*?FROM.*WHERE)(.*)$/isU',$sql, $matches);
721         //parse all single and double  quotes out of array
722         $sin_array = $this->removePatternFromSQL($matches[2], "'", "'","sin_");
723         $new_sql = array_pop($sin_array);
724         $dub_array = $this->removePatternFromSQL($new_sql, "\"", "\"","dub_");
725         $new_sql = array_pop($dub_array);
726
727         //search for parenthesis
728         $paren_array = $this->removePatternFromSQL($new_sql, "(", ")", "par_");
729         $new_sql = array_pop($paren_array);
730
731         //all functions should be removed now, so split the array on commas
732         $mstr_sql_array = explode(",", $new_sql);
733         foreach($mstr_sql_array as $token ) {
734             if (strpos($token, $alias)) {
735                 //found token, add back comments
736                 $token = $this->addPatternToSQL($token, $paren_array);
737                 $token = $this->addPatternToSQL($token, $dub_array);
738                 $token = $this->addPatternToSQL($token, $sin_array);
739
740                 //log and break out of this function
741                 return $token;
742             }
743         }
744         return null;
745     }
746
747
748     /**
749      * Finds the alias of the order by column, and then return the preceding column name
750      *
751      * @param  string $sql
752      * @param  string $orderMatch
753      * @return string
754      */
755     private function findColumnByAlias($sql, $orderMatch)
756     {
757         //change case to lowercase
758         $sql = strtolower($sql);
759         $patt = '/\s+'.trim($orderMatch).'\s*(,|from)/';
760
761         //check for the alias, it should contain comma, may contain space, \n, or \t
762         $matches = array();
763         preg_match($patt, $sql, $matches, PREG_OFFSET_CAPTURE);
764         $found_in_sql = isset($matches[0][1]) ? $matches[0][1] : false;
765
766
767         //set default for found variable
768         $found = $found_in_sql;
769
770         //if still no match found, then we need to parse through the string
771         if (!$found_in_sql){
772             //get count of how many times the match exists in string
773             $found_count = substr_count($sql, $orderMatch);
774             $i = 0;
775             $first_ = 0;
776             $len = strlen($orderMatch);
777             //loop through string as many times as there is a match
778             while ($found_count > $i) {
779                 //get the first match
780                 $found_in_sql = strpos($sql, $orderMatch,$first_);
781                 //make sure there was a match
782                 if($found_in_sql){
783                     //grab the next 2 individual characters
784                     $str_plusone = substr($sql,$found_in_sql + $len,1);
785                     $str_plustwo = substr($sql,$found_in_sql + $len+1,1);
786                     //if one of those characters is a comma, then we have our alias
787                     if ($str_plusone === "," || $str_plustwo === ","){
788                         //keep track of this position
789                         $found = $found_in_sql;
790                     }
791                 }
792                 //set the offset and increase the iteration counter
793                 $first_ = $found_in_sql+$len;
794                 $i = $i+1;
795             }
796         }
797         //return $found, defaults have been set, so if no match was found it will be a negative number
798         return $found;
799     }
800
801
802     /**
803      * Return the order by string to use in case the column has been aliased
804      *
805      * @param  string $sql
806      * @param  string $orig_order_match
807      * @return string
808      */
809     private function returnOrderBy($sql, $orig_order_match)
810     {
811         $sql = strtolower($sql);
812         $orig_order_match = trim($orig_order_match);
813         if (strpos($orig_order_match, ".") != 0)
814             //this has a tablename defined, pass in the order match
815             return $orig_order_match;
816
817         // If there is no ordering direction (ASC/DESC), use ASC by default
818         if (strpos($orig_order_match, " ") === false) {
819                 $orig_order_match .= " ASC";
820         }
821
822         //grab first space in order by
823         $firstSpace = strpos($orig_order_match, " ");
824
825         //split order by into column name and ascending/descending
826         $orderMatch = " " . strtolower(substr($orig_order_match, 0, $firstSpace));
827         $asc_desc = trim(substr($orig_order_match,$firstSpace));
828
829         //look for column name as an alias in sql string
830         $found_in_sql = $this->findColumnByAlias($sql, $orderMatch);
831
832         if (!$found_in_sql) {
833             //check if this column needs the tablename prefixed to it
834             $orderMatch = ".".trim($orderMatch);
835             $colMatchPos = strpos($sql, $orderMatch);
836             if ($colMatchPos !== false) {
837                 //grab sub string up to column name
838                 $containsColStr = substr($sql,0, $colMatchPos);
839                 //get position of first space, so we can grab table name
840                 $lastSpacePos = strrpos($containsColStr, " ");
841                 //use positions of column name, space before name, and length of column to find the correct column name
842                 $col_name = substr($sql, $lastSpacePos, $colMatchPos-$lastSpacePos+strlen($orderMatch));
843                                 //bug 25485. When sorting by a custom field in Account List and then pressing NEXT >, system gives an error
844                                 $containsCommaPos = strpos($col_name, ",");
845                                 if($containsCommaPos !== false) {
846                                         $col_name = substr($col_name, $containsCommaPos+1);
847                                 }
848                 //add the "asc/desc" order back
849                 $col_name = $col_name. " ". $asc_desc;
850
851                 //return column name
852                 return $col_name;
853             }
854             //break out of here, log this
855             $GLOBALS['log']->debug("No match was found for order by, pass string back untouched as: $orig_order_match");
856             return $orig_order_match;
857         }
858         else {
859             //if found, then parse and return
860             //grab string up to the aliased column
861             $GLOBALS['log']->debug("order by found, process sql string");
862
863             $psql = (trim($this->getAliasFromSQL($sql, $orderMatch )));
864             if (empty($psql))
865                 $psql = trim(substr($sql, 0, $found_in_sql));
866
867             //grab the last comma before the alias
868             preg_match('/\s+' . trim($orderMatch). '/', $psql, $match, PREG_OFFSET_CAPTURE);
869             $comma_pos = $match[0][1];
870             //substring between the comma and the alias to find the joined_table alias and column name
871             $col_name = substr($psql,0, $comma_pos);
872
873             //make sure the string does not have an end parenthesis
874             //and is not part of a function (i.e. "ISNULL(leads.last_name,'') as name"  )
875             //this is especially true for unified search from home screen
876
877             $alias_beg_pos = 0;
878             if(strpos($psql, " as "))
879                 $alias_beg_pos = strpos($psql, " as ");
880
881             // Bug # 44923 - This breaks the query and does not properly filter isnull
882             // as there are other functions such as ltrim and rtrim.
883             /* else if (strncasecmp($psql, 'isnull', 6) != 0)
884                 $alias_beg_pos = strpos($psql, " "); */
885
886             if ($alias_beg_pos > 0) {
887                 $col_name = substr($psql,0, $alias_beg_pos );
888             }
889             //add the "asc/desc" order back
890             $col_name = $col_name. " ". $asc_desc;
891
892             //pass in new order by
893             $GLOBALS['log']->debug("order by being returned is " . $col_name);
894             return $col_name;
895         }
896     }
897
898     /**
899      * Take in a string of the module and retrieve the correspondent table name
900      *
901      * @param  string $module_str module name
902      * @param  string $sql        SQL statement
903      * @return string table name
904      */
905     private function getTableNameFromModuleName($module_str, $sql)
906     {
907
908         global $beanList, $beanFiles;
909         $GLOBALS['log']->debug("Module being processed is " . $module_str);
910         //get the right module files
911         //the module string exists in bean list, then process bean for correct table name
912         //note that we exempt the reports module from this, as queries from reporting module should be parsed for
913         //correct table name.
914         if (($module_str != 'Reports' && $module_str != 'SavedReport') && isset($beanList[$module_str])  &&  isset($beanFiles[$beanList[$module_str]])){
915             //if the class is not already loaded, then load files
916             if (!class_exists($beanList[$module_str]))
917                 require_once($beanFiles[$beanList[$module_str]]);
918
919             //instantiate new bean
920             $module_bean = new $beanList[$module_str]();
921             //get table name from bean
922             $tbl_name = $module_bean->table_name;
923             //make sure table name is not just a blank space, or empty
924             $tbl_name = trim($tbl_name);
925
926             if(empty($tbl_name)){
927                 $GLOBALS['log']->debug("Could not find table name for module $module_str. ");
928                 $tbl_name = $module_str;
929             }
930         }
931         else {
932             //since the module does NOT exist in beanlist, then we have to parse the string
933             //and grab the table name from the passed in sql
934             $GLOBALS['log']->debug("Could not find table name from module in request, retrieve from passed in sql");
935             $tbl_name = $module_str;
936             $sql = strtolower($sql);
937
938             // Bug #45625 : Getting Multi-part identifier (reports.id) could not be bound error when navigating to next page in reprots in mssql
939             // there is cases when sql string is multiline string and it we cannot find " from " string in it
940             $sql = str_replace(array("\n", "\r"), " ", $sql);
941
942             //look for the location of the "from" in sql string
943             $fromLoc = strpos($sql," from " );
944             if ($fromLoc>0){
945                 //found from, substring from the " FROM " string in sql to end
946                 $tableEnd = substr($sql, $fromLoc+6);
947                 //We know that tablename will be next parameter after from, so
948                 //grab the next space after table name.
949                 // MFH BUG #14009: Also check to see if there are any carriage returns before the next space so that we don't grab any arbitrary joins or other tables.
950                 $carriage_ret = strpos($tableEnd,"\n");
951                 $next_space = strpos($tableEnd," " );
952                 if ($carriage_ret < $next_space)
953                     $next_space = $carriage_ret;
954                 if ($next_space > 0) {
955                     $tbl_name= substr($tableEnd,0, $next_space);
956                     if(empty($tbl_name)){
957                         $GLOBALS['log']->debug("Could not find table name sql either, return $module_str. ");
958                         $tbl_name = $module_str;
959                     }
960                 }
961
962                 //grab the table, to see if it is aliased
963                 $aliasTableEnd = trim(substr($tableEnd, $next_space));
964                 $alias_space = strpos ($aliasTableEnd, " " );
965                 if ($alias_space > 0){
966                     $alias_tbl_name= substr($aliasTableEnd,0, $alias_space);
967                     strtolower($alias_tbl_name);
968                     if(empty($alias_tbl_name)
969                         || $alias_tbl_name == "where"
970                         || $alias_tbl_name == "inner"
971                         || $alias_tbl_name == "left"
972                         || $alias_tbl_name == "join"
973                         || $alias_tbl_name == "outer"
974                         || $alias_tbl_name == "right") {
975                         //not aliased, do nothing
976                     }
977                     elseif ($alias_tbl_name == "as") {
978                             //the next word is the table name
979                             $aliasTableEnd = trim(substr($aliasTableEnd, $alias_space));
980                             $alias_space = strpos ($aliasTableEnd, " " );
981                             if ($alias_space > 0) {
982                                 $alias_tbl_name= trim(substr($aliasTableEnd,0, $alias_space));
983                                 if (!empty($alias_tbl_name))
984                                     $tbl_name = $alias_tbl_name;
985                             }
986                     }
987                     else {
988                         //this is table alias
989                         $tbl_name = $alias_tbl_name;
990                     }
991                 }
992             }
993         }
994         //return table name
995         $GLOBALS['log']->debug("Table name for module $module_str is: ".$tbl_name);
996         return $tbl_name;
997     }
998
999
1000         /**
1001      * @see DBManager::getFieldsArray()
1002      */
1003         public function getFieldsArray($result, $make_lower_case = false)
1004         {
1005                 $field_array = array();
1006
1007                 if(! isset($result) || empty($result))
1008             return 0;
1009
1010         $i = 0;
1011         while ($i < mssql_num_fields($result)) {
1012             $meta = mssql_fetch_field($result, $i);
1013             if (!$meta)
1014                 return 0;
1015             if($make_lower_case==true)
1016                 $meta->name = strtolower($meta->name);
1017
1018             $field_array[] = $meta->name;
1019
1020             $i++;
1021         }
1022
1023         return $field_array;
1024         }
1025
1026     /**
1027      * @see DBManager::getAffectedRowCount()
1028      */
1029         public function getAffectedRowCount()
1030     {
1031         return $this->getOne("SELECT @@ROWCOUNT");
1032     }
1033
1034         /**
1035          * @see DBManager::fetchRow()
1036          */
1037         public function fetchRow($result)
1038         {
1039                 if (empty($result))     return false;
1040
1041         $row = mssql_fetch_assoc($result);
1042         //MSSQL returns a space " " when a varchar column is empty ("") and not null.
1043         //We need to iterate through the returned row array and strip empty spaces
1044         if(!empty($row)){
1045             foreach($row as $key => $column) {
1046                //notice we only strip if one space is returned.  we do not want to strip
1047                //strings with intentional spaces (" foo ")
1048                if (!empty($column) && $column ==" ") {
1049                    $row[$key] = '';
1050                }
1051             }
1052         }
1053         return $row;
1054         }
1055
1056     /**
1057      * @see DBManager::quote()
1058      */
1059     public function quote($string)
1060     {
1061         if(is_array($string)) {
1062             return $this->arrayQuote($string);
1063         }
1064         return str_replace("'","''", $this->quoteInternal($string));
1065     }
1066
1067     /**
1068      * @see DBManager::tableExists()
1069      */
1070     public function tableExists($tableName)
1071     {
1072         $GLOBALS['log']->info("tableExists: $tableName");
1073
1074         $this->checkConnection();
1075         $result = $this->getOne(
1076             "SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE='BASE TABLE' AND TABLE_NAME=".$this->quoted($tableName));
1077
1078         return !empty($result);
1079     }
1080
1081     /**
1082      * Get tables like expression
1083      * @param $like string
1084      * @return array
1085      */
1086     public function tablesLike($like)
1087     {
1088         if ($this->getDatabase()) {
1089             $tables = array();
1090             $r = $this->query('SELECT TABLE_NAME tn FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE=\'BASE TABLE\' AND TABLE_NAME LIKE '.$this->quoted($like));
1091             if (!empty($r)) {
1092                 while ($a = $this->fetchByAssoc($r)) {
1093                     $row = array_values($a);
1094                                         $tables[]=$row[0];
1095                 }
1096                 return $tables;
1097             }
1098         }
1099         return false;
1100     }
1101
1102     /**
1103      * @see DBManager::getTablesArray()
1104      */
1105     public function getTablesArray()
1106     {
1107         $GLOBALS['log']->debug('MSSQL fetching table list');
1108
1109         if($this->getDatabase()) {
1110             $tables = array();
1111             $r = $this->query('SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES');
1112             if (is_resource($r)) {
1113                 while ($a = $this->fetchByAssoc($r))
1114                     $tables[] = $a['TABLE_NAME'];
1115
1116                 return $tables;
1117             }
1118         }
1119
1120         return false; // no database available
1121     }
1122
1123
1124     /**
1125      * This call is meant to be used during install, when Full Text Search is enabled
1126      * Indexing would always occur after a fresh sql server install, so this code creates
1127      * a catalog and table with full text index.
1128      */
1129     public function full_text_indexing_setup()
1130     {
1131         $GLOBALS['log']->debug('MSSQL about to wakeup FTS');
1132
1133         if($this->getDatabase()) {
1134                 //create wakeup catalog
1135                 $FTSqry[] = "if not exists(  select * from sys.fulltext_catalogs where name ='wakeup_catalog' )
1136                 CREATE FULLTEXT CATALOG wakeup_catalog
1137                 ";
1138
1139                 //drop wakeup table if it exists
1140                 $FTSqry[] = "IF EXISTS(SELECT 'fts_wakeup' FROM sysobjects WHERE name = 'fts_wakeup' AND xtype='U')
1141                     DROP TABLE fts_wakeup
1142                 ";
1143                 //create wakeup table
1144                 $FTSqry[] = "CREATE TABLE fts_wakeup(
1145                     id varchar(36) NOT NULL CONSTRAINT pk_fts_wakeup_id PRIMARY KEY CLUSTERED (id ASC ),
1146                     body text NULL,
1147                     kb_index int IDENTITY(1,1) NOT NULL CONSTRAINT wakeup_fts_unique_idx UNIQUE NONCLUSTERED
1148                 )
1149                 ";
1150                 //create full text index
1151                  $FTSqry[] = "CREATE FULLTEXT INDEX ON fts_wakeup
1152                 (
1153                     body
1154                     Language 0X0
1155                 )
1156                 KEY INDEX wakeup_fts_unique_idx ON wakeup_catalog
1157                 WITH CHANGE_TRACKING AUTO
1158                 ";
1159
1160                 //insert dummy data
1161                 $FTSqry[] = "INSERT INTO fts_wakeup (id ,body)
1162                 VALUES ('".create_guid()."', 'SugarCRM Rocks' )";
1163
1164
1165                 //create queries to stop and restart indexing
1166                 $FTSqry[] = 'ALTER FULLTEXT INDEX ON fts_wakeup STOP POPULATION';
1167                 $FTSqry[] = 'ALTER FULLTEXT INDEX ON fts_wakeup DISABLE';
1168                 $FTSqry[] = 'ALTER FULLTEXT INDEX ON fts_wakeup ENABLE';
1169                 $FTSqry[] = 'ALTER FULLTEXT INDEX ON fts_wakeup SET CHANGE_TRACKING MANUAL';
1170                 $FTSqry[] = 'ALTER FULLTEXT INDEX ON fts_wakeup START FULL POPULATION';
1171                 $FTSqry[] = 'ALTER FULLTEXT INDEX ON fts_wakeup SET CHANGE_TRACKING AUTO';
1172
1173                 foreach($FTSqry as $q){
1174                     sleep(3);
1175                     $this->query($q);
1176                 }
1177                 $this->create_default_full_text_catalog();
1178         }
1179
1180         return false; // no database available
1181     }
1182
1183     protected $date_formats = array(
1184         '%Y-%m-%d' => 10,
1185         '%Y-%m' => 7,
1186         '%Y' => 4,
1187     );
1188
1189     /**
1190      * @see DBManager::convert()
1191      */
1192     public function convert($string, $type, array $additional_parameters = array())
1193     {
1194         // convert the parameters array into a comma delimited string
1195         if (!empty($additional_parameters)) {
1196             $additional_parameters_string = ','.implode(',',$additional_parameters);
1197         } else {
1198             $additional_parameters_string = '';
1199         }
1200         $all_parameters = $additional_parameters;
1201         if(is_array($string)) {
1202             $all_parameters = array_merge($string, $all_parameters);
1203         } elseif (!is_null($string)) {
1204             array_unshift($all_parameters, $string);
1205         }
1206
1207         switch (strtolower($type)) {
1208             case 'today':
1209                 return "GETDATE()";
1210             case 'left':
1211                 return "LEFT($string$additional_parameters_string)";
1212             case 'date_format':
1213                 if(!empty($additional_parameters[0]) && $additional_parameters[0][0] == "'") {
1214                     $additional_parameters[0] = trim($additional_parameters[0], "'");
1215                 }
1216                 if(!empty($additional_parameters) && isset($this->date_formats[$additional_parameters[0]])) {
1217                     $len = $this->date_formats[$additional_parameters[0]];
1218                     return "LEFT(CONVERT(varchar($len),". $string . ",120),$len)";
1219                 } else {
1220                    return "LEFT(CONVERT(varchar(10),". $string . ",120),10)";
1221                 }
1222             case 'ifnull':
1223                 if(empty($additional_parameters_string)) {
1224                     $additional_parameters_string = ",''";
1225                 }
1226                 return "ISNULL($string$additional_parameters_string)";
1227             case 'concat':
1228                 return implode("+",$all_parameters);
1229             case 'text2char':
1230                 return "CAST($string AS varchar(8000))";
1231             case 'quarter':
1232                 return "DATENAME(quarter, $string)";
1233             case "length":
1234                 return "LEN($string)";
1235             case 'month':
1236                 return "MONTH($string)";
1237             case 'add_date':
1238                 return "DATEADD({$additional_parameters[1]},{$additional_parameters[0]},$string)";
1239             case 'add_time':
1240                 return "DATEADD(hh, {$additional_parameters[0]}, DATEADD(mi, {$additional_parameters[1]}, $string))";
1241             case 'add_tz_offset' :
1242                 $getUserUTCOffset = $GLOBALS['timedate']->getUserUTCOffset();
1243                 $operation = $getUserUTCOffset < 0 ? '-' : '+';
1244                 return 'DATEADD(minute, ' . $operation . abs($getUserUTCOffset) . ', ' . $string. ')';
1245         }
1246
1247         return "$string";
1248     }
1249
1250     /**
1251      * @see DBManager::fromConvert()
1252      */
1253     public function fromConvert($string, $type)
1254     {
1255         switch($type) {
1256             case 'datetimecombo':
1257             case 'datetime': return substr($string, 0,19);
1258             case 'date': return substr($string, 0, 10);
1259             case 'time': return substr($string, 11);
1260                 }
1261                 return $string;
1262     }
1263
1264     /**
1265      * @see DBManager::createTableSQLParams()
1266      */
1267         public function createTableSQLParams($tablename, $fieldDefs, $indices)
1268     {
1269         if (empty($tablename) || empty($fieldDefs))
1270             return '';
1271
1272         $columns = $this->columnSQLRep($fieldDefs, false, $tablename);
1273         if (empty($columns))
1274             return '';
1275
1276         return "CREATE TABLE $tablename ($columns)";
1277     }
1278
1279     /**
1280      * Does this type represent text (i.e., non-varchar) value?
1281      * @param string $type
1282      */
1283     public function isTextType($type)
1284     {
1285         $type = strtolower($type);
1286         if(!isset($this->type_map[$type])) return false;
1287         return in_array($this->type_map[$type], array('ntext','text','image', 'nvarchar(max)'));
1288     }
1289
1290     /**
1291      * Return representation of an empty value depending on type
1292      * @param string $type
1293      */
1294     public function emptyValue($type)
1295     {
1296         $ctype = $this->getColumnType($type);
1297         if($ctype == "datetime") {
1298             return $this->convert($this->quoted("1970-01-01 00:00:00"), "datetime");
1299         }
1300         if($ctype == "date") {
1301             return $this->convert($this->quoted("1970-01-01"), "datetime");
1302         }
1303         if($ctype == "time") {
1304             return $this->convert($this->quoted("00:00:00"), "time");
1305         }
1306         return parent::emptyValue($type);
1307     }
1308
1309     public function renameColumnSQL($tablename, $column, $newname)
1310     {
1311         return "SP_RENAME '$tablename.$column', '$newname', 'COLUMN'";
1312     }
1313
1314     /**
1315      * Returns the SQL Alter table statment
1316      *
1317      * MSSQL has a quirky T-SQL alter table syntax. Pay special attention to the
1318      * modify operation
1319      * @param string $action
1320      * @param array  $def
1321      * @param bool   $ignorRequired
1322      * @param string $tablename
1323      */
1324     protected function alterSQLRep($action, array $def, $ignoreRequired, $tablename)
1325     {
1326         switch($action){
1327         case 'add':
1328              $f_def=$this->oneColumnSQLRep($def, $ignoreRequired,$tablename,false);
1329             return "ADD " . $f_def;
1330             break;
1331         case 'drop':
1332             return "DROP COLUMN " . $def['name'];
1333             break;
1334         case 'modify':
1335             //You cannot specify a default value for a column for MSSQL
1336             $f_def  = $this->oneColumnSQLRep($def, $ignoreRequired,$tablename, true);
1337             $f_stmt = "ALTER COLUMN ".$f_def['name'].' '.$f_def['colType'].' '.
1338                         $f_def['required'].' '.$f_def['auto_increment']."\n";
1339             if (!empty( $f_def['default']))
1340                 $f_stmt .= " ALTER TABLE " . $tablename .  " ADD  ". $f_def['default'] . " FOR " . $def['name'];
1341             return $f_stmt;
1342             break;
1343         default:
1344             return '';
1345         }
1346     }
1347
1348     /**
1349      * @see DBManager::changeColumnSQL()
1350      *
1351      * MSSQL uses a different syntax than MySQL for table altering that is
1352      * not quite as simplistic to implement...
1353      */
1354     protected function changeColumnSQL($tablename, $fieldDefs, $action, $ignoreRequired = false)
1355     {
1356         $sql=$sql2='';
1357         $constraints = $this->get_field_default_constraint_name($tablename);
1358         $columns = array();
1359         if ($this->isFieldArray($fieldDefs)) {
1360             foreach ($fieldDefs as $def)
1361                 {
1362                         //if the column is being modified drop the default value
1363                         //constraint if it exists. alterSQLRep will add the constraint back
1364                         if (!empty($constraints[$def['name']])) {
1365                                 $sql.=" ALTER TABLE " . $tablename . " DROP CONSTRAINT " . $constraints[$def['name']];
1366                         }
1367                         //check to see if we need to drop related indexes before the alter
1368                         $indices = $this->get_indices($tablename);
1369                 foreach ( $indices as $index ) {
1370                     if ( in_array($def['name'],$index['fields']) ) {
1371                         $sql  .= ' ' . $this->add_drop_constraint($tablename,$index,true).' ';
1372                         $sql2 .= ' ' . $this->add_drop_constraint($tablename,$index,false).' ';
1373                     }
1374                 }
1375
1376                         $columns[] = $this->alterSQLRep($action, $def, $ignoreRequired,$tablename);
1377                 }
1378         }
1379         else {
1380             //if the column is being modified drop the default value
1381                 //constraint if it exists. alterSQLRep will add the constraint back
1382                 if (!empty($constraints[$fieldDefs['name']])) {
1383                         $sql.=" ALTER TABLE " . $tablename . " DROP CONSTRAINT " . $constraints[$fieldDefs['name']];
1384                 }
1385                 //check to see if we need to drop related indexes before the alter
1386             $indices = $this->get_indices($tablename);
1387             foreach ( $indices as $index ) {
1388                 if ( in_array($fieldDefs['name'],$index['fields']) ) {
1389                     $sql  .= ' ' . $this->add_drop_constraint($tablename,$index,true).' ';
1390                     $sql2 .= ' ' . $this->add_drop_constraint($tablename,$index,false).' ';
1391                 }
1392             }
1393
1394
1395                 $columns[] = $this->alterSQLRep($action, $fieldDefs, $ignoreRequired,$tablename);
1396         }
1397
1398         $columns = implode(", ", $columns);
1399         $sql .= " ALTER TABLE $tablename $columns " . $sql2;
1400
1401         return $sql;
1402     }
1403
1404     protected function setAutoIncrement($table, $field_name)
1405     {
1406                 return "identity(1,1)";
1407         }
1408
1409     /**
1410      * @see DBManager::setAutoIncrementStart()
1411      */
1412     public function setAutoIncrementStart($table, $field_name, $start_value)
1413     {
1414         if($start_value > 1)
1415             $start_value -= 1;
1416                 $this->query("DBCC CHECKIDENT ('$table', RESEED, $start_value) WITH NO_INFOMSGS");
1417         return true;
1418     }
1419
1420         /**
1421      * @see DBManager::getAutoIncrement()
1422      */
1423     public function getAutoIncrement($table, $field_name)
1424     {
1425                 $result = $this->getOne("select IDENT_CURRENT('$table') + IDENT_INCR ( '$table' ) as 'Auto_increment'");
1426         return $result;
1427     }
1428
1429     /**
1430      * @see DBManager::get_indices()
1431      */
1432     public function get_indices($tableName)
1433     {
1434         //find all unique indexes and primary keys.
1435         $query = <<<EOSQL
1436 SELECT sys.tables.object_id, sys.tables.name as table_name, sys.columns.name as column_name,
1437                 sys.indexes.name as index_name, sys.indexes.is_unique, sys.indexes.is_primary_key
1438             FROM sys.tables, sys.indexes, sys.index_columns, sys.columns
1439             WHERE (sys.tables.object_id = sys.indexes.object_id
1440                     AND sys.tables.object_id = sys.index_columns.object_id
1441                     AND sys.tables.object_id = sys.columns.object_id
1442                     AND sys.indexes.index_id = sys.index_columns.index_id
1443                     AND sys.index_columns.column_id = sys.columns.column_id)
1444                 AND sys.tables.name = '$tableName'
1445 EOSQL;
1446         $result = $this->query($query);
1447
1448         $indices = array();
1449         while (($row=$this->fetchByAssoc($result)) != null) {
1450             $index_type = 'index';
1451             if ($row['is_primary_key'] == '1')
1452                 $index_type = 'primary';
1453             elseif ($row['is_unique'] == 1 )
1454                 $index_type = 'unique';
1455             $name = strtolower($row['index_name']);
1456             $indices[$name]['name']     = $name;
1457             $indices[$name]['type']     = $index_type;
1458             $indices[$name]['fields'][] = strtolower($row['column_name']);
1459         }
1460         return $indices;
1461     }
1462
1463     /**
1464      * @see DBManager::get_columns()
1465      */
1466     public function get_columns($tablename)
1467     {
1468         //find all unique indexes and primary keys.
1469         $result = $this->query("sp_columns $tablename");
1470
1471         $columns = array();
1472         while (($row=$this->fetchByAssoc($result)) !=null) {
1473             $column_name = strtolower($row['COLUMN_NAME']);
1474             $columns[$column_name]['name']=$column_name;
1475             $columns[$column_name]['type']=strtolower($row['TYPE_NAME']);
1476             if ( $row['TYPE_NAME'] == 'decimal' ) {
1477                 $columns[$column_name]['len']=strtolower($row['PRECISION']);
1478                 $columns[$column_name]['len'].=','.strtolower($row['SCALE']);
1479             }
1480                         elseif ( in_array($row['TYPE_NAME'],array('nchar','nvarchar')) )
1481                                 $columns[$column_name]['len']=strtolower($row['PRECISION']);
1482             elseif ( !in_array($row['TYPE_NAME'],array('datetime','text')) )
1483                 $columns[$column_name]['len']=strtolower($row['LENGTH']);
1484             if ( stristr($row['TYPE_NAME'],'identity') ) {
1485                 $columns[$column_name]['auto_increment'] = '1';
1486                 $columns[$column_name]['type']=str_replace(' identity','',strtolower($row['TYPE_NAME']));
1487             }
1488
1489             if (!empty($row['IS_NULLABLE']) && $row['IS_NULLABLE'] == 'NO' && (empty($row['KEY']) || !stristr($row['KEY'],'PRI')))
1490                 $columns[strtolower($row['COLUMN_NAME'])]['required'] = 'true';
1491
1492             $column_def = 1;
1493             if ( strtolower($tablename) == 'relationships' ) {
1494                 $column_def = $this->getOne("select cdefault from syscolumns where id = object_id('relationships') and name = '$column_name'");
1495             }
1496             if ( $column_def != 0 && ($row['COLUMN_DEF'] != null)) {    // NOTE Not using !empty as an empty string may be a viable default value.
1497                 $matches = array();
1498                 $row['COLUMN_DEF'] = html_entity_decode($row['COLUMN_DEF'],ENT_QUOTES);
1499                 if ( preg_match('/\([\(|\'](.*)[\)|\']\)/i',$row['COLUMN_DEF'],$matches) )
1500                     $columns[$column_name]['default'] = $matches[1];
1501                 elseif ( preg_match('/\(N\'(.*)\'\)/i',$row['COLUMN_DEF'],$matches) )
1502                     $columns[$column_name]['default'] = $matches[1];
1503                 else
1504                     $columns[$column_name]['default'] = $row['COLUMN_DEF'];
1505             }
1506         }
1507         return $columns;
1508     }
1509
1510
1511     /**
1512      * Get FTS catalog name for current DB
1513      */
1514     protected function ftsCatalogName()
1515     {
1516         if(isset($this->connectOptions['db_name'])) {
1517             return $this->connectOptions['db_name']."_fts_catalog";
1518         }
1519         return 'sugar_fts_catalog';
1520     }
1521
1522     /**
1523      * @see DBManager::add_drop_constraint()
1524      */
1525     public function add_drop_constraint($table, $definition, $drop = false)
1526     {
1527         $type         = $definition['type'];
1528         $fields       = is_array($definition['fields'])?implode(',',$definition['fields']):$definition['fields'];
1529         $name         = $definition['name'];
1530         $sql          = '';
1531
1532         switch ($type){
1533         // generic indices
1534         case 'index':
1535         case 'alternate_key':
1536             if ($drop)
1537                 $sql = "DROP INDEX {$name} ON {$table}";
1538             else
1539                 $sql = "CREATE INDEX {$name} ON {$table} ({$fields})";
1540             break;
1541         case 'clustered':
1542             if ($drop)
1543                 $sql = "DROP INDEX {$name} ON {$table}";
1544             else
1545                 $sql = "CREATE CLUSTERED INDEX $name ON $table ($fields)";
1546             break;
1547             // constraints as indices
1548         case 'unique':
1549             if ($drop)
1550                 $sql = "ALTER TABLE {$table} DROP CONSTRAINT $name";
1551             else
1552                 $sql = "ALTER TABLE {$table} ADD CONSTRAINT {$name} UNIQUE ({$fields})";
1553             break;
1554         case 'primary':
1555             if ($drop)
1556                 $sql = "ALTER TABLE {$table} DROP CONSTRAINT {$name}";
1557             else
1558                 $sql = "ALTER TABLE {$table} ADD CONSTRAINT {$name} PRIMARY KEY ({$fields})";
1559             break;
1560         case 'foreign':
1561             if ($drop)
1562                 $sql = "ALTER TABLE {$table} DROP FOREIGN KEY ({$fields})";
1563             else
1564                 $sql = "ALTER TABLE {$table} ADD CONSTRAINT {$name}  FOREIGN KEY ({$fields}) REFERENCES {$definition['foreignTable']}({$definition['foreignFields']})";
1565             break;
1566         case 'fulltext':
1567             if ($this->full_text_indexing_enabled() && $drop) {
1568                 $sql = "DROP FULLTEXT INDEX ON {$table}";
1569             } elseif ($this->full_text_indexing_enabled()) {
1570                 $catalog_name=$this->ftsCatalogName();
1571                 if ( isset($definition['catalog_name']) && $definition['catalog_name'] != 'default')
1572                     $catalog_name = $definition['catalog_name'];
1573
1574                 $language = "Language 1033";
1575                 if (isset($definition['language']) && !empty($definition['language']))
1576                     $language = "Language " . $definition['language'];
1577
1578                 $key_index = $definition['key_index'];
1579
1580                 $change_tracking = "auto";
1581                 if (isset($definition['change_tracking']) && !empty($definition['change_tracking']))
1582                     $change_tracking = $definition['change_tracking'];
1583
1584                 $sql = " CREATE FULLTEXT INDEX ON $table ($fields $language) KEY INDEX $key_index ON $catalog_name WITH CHANGE_TRACKING $change_tracking" ;
1585             }
1586             break;
1587         }
1588         return $sql;
1589     }
1590
1591     /**
1592      * Returns true if Full Text Search is installed
1593      *
1594      * @return bool
1595      */
1596     public function full_text_indexing_installed()
1597     {
1598         $ftsChckRes = $this->getOne("SELECT FULLTEXTSERVICEPROPERTY('IsFulltextInstalled') as fts");
1599         return !empty($ftsChckRes);
1600     }
1601
1602     /**
1603      * @see DBManager::full_text_indexing_enabled()
1604      */
1605     protected function full_text_indexing_enabled($dbname = null)
1606     {
1607         // check to see if we already have install setting in session
1608         if(!isset($_SESSION['IsFulltextInstalled']))
1609             $_SESSION['IsFulltextInstalled'] = $this->full_text_indexing_installed();
1610
1611         // check to see if FTS Indexing service is installed
1612         if(empty($_SESSION['IsFulltextInstalled']))
1613             return false;
1614
1615         // grab the dbname if it was not passed through
1616                 if (empty($dbname)) {
1617                         global $sugar_config;
1618                         $dbname = $sugar_config['dbconfig']['db_name'];
1619                 }
1620         //we already know that Indexing service is installed, now check
1621         //to see if it is enabled
1622                 $res = $this->getOne("SELECT DATABASEPROPERTY('$dbname', 'IsFulltextEnabled') ftext");
1623         return !empty($res);
1624         }
1625
1626     /**
1627      * Creates default full text catalog
1628      */
1629         protected function create_default_full_text_catalog()
1630     {
1631                 if ($this->full_text_indexing_enabled()) {
1632                     $catalog = $this->ftsCatalogName();
1633             $GLOBALS['log']->debug("Creating the default catalog for full-text indexing, $catalog");
1634
1635             //drop catalog if exists.
1636                         $ret = $this->query("
1637                 if not exists(
1638                     select *
1639                         from sys.fulltext_catalogs
1640                         where name ='$catalog'
1641                         )
1642                 CREATE FULLTEXT CATALOG $catalog");
1643
1644                         if (empty($ret)) {
1645                                 $GLOBALS['log']->error("Error creating default full-text catalog, $catalog");
1646                         }
1647                 }
1648         }
1649
1650     /**
1651      * Function returns name of the constraint automatically generated by sql-server.
1652      * We request this for default, primary key, required
1653      *
1654      * @param  string $table
1655      * @param  string $column
1656      * @return string
1657      */
1658         private function get_field_default_constraint_name($table, $column = null)
1659     {
1660         static $results = array();
1661
1662         if ( empty($column) && isset($results[$table]) )
1663             return $results[$table];
1664
1665         $query = <<<EOQ
1666 select s.name, o.name, c.name dtrt, d.name ctrt
1667     from sys.default_constraints as d
1668         join sys.objects as o
1669             on o.object_id = d.parent_object_id
1670         join sys.columns as c
1671             on c.object_id = o.object_id and c.column_id = d.parent_column_id
1672         join sys.schemas as s
1673             on s.schema_id = o.schema_id
1674     where o.name = '$table'
1675 EOQ;
1676         if ( !empty($column) )
1677             $query .= " and c.name = '$column'";
1678         $res = $this->query($query);
1679         if ( !empty($column) ) {
1680             $row = $this->fetchByAssoc($res);
1681             if (!empty($row))
1682                 return $row['ctrt'];
1683         }
1684         else {
1685             $returnResult = array();
1686             while ( $row = $this->fetchByAssoc($res) )
1687                 $returnResult[$row['dtrt']] = $row['ctrt'];
1688             $results[$table] = $returnResult;
1689             return $returnResult;
1690         }
1691
1692         return null;
1693         }
1694
1695     /**
1696      * @see DBManager::massageFieldDef()
1697      */
1698     public function massageFieldDef(&$fieldDef, $tablename)
1699     {
1700         parent::massageFieldDef($fieldDef,$tablename);
1701
1702         if ($fieldDef['type'] == 'int')
1703             $fieldDef['len'] = '4';
1704
1705         if(empty($fieldDef['len']))
1706         {
1707             switch($fieldDef['type']) {
1708                 case 'bit'      :
1709                 case 'bool'     : $fieldDef['len'] = '1'; break;
1710                 case 'smallint' : $fieldDef['len'] = '2'; break;
1711                 case 'float'    : $fieldDef['len'] = '8'; break;
1712                 case 'varchar'  :
1713                 case 'nvarchar' :
1714                                   $fieldDef['len'] = $this->isTextType($fieldDef['dbType']) ? 'max' : '255';
1715                                   break;
1716                 case 'image'    : $fieldDef['len'] = '2147483647'; break;
1717                 case 'ntext'    : $fieldDef['len'] = '2147483646'; break;   // Note: this is from legacy code, don't know if this is correct
1718             }
1719         }
1720         if($fieldDef['type'] == 'decimal'
1721            && empty($fieldDef['precision'])
1722            && !strpos($fieldDef['len'], ','))
1723         {
1724              $fieldDef['len'] .= ',0'; // Adding 0 precision if it is not specified
1725         }
1726
1727         if(empty($fieldDef['default'])
1728             && in_array($fieldDef['type'],array('bit','bool')))
1729         {
1730             $fieldDef['default'] = '0';
1731         }
1732                 if (isset($fieldDef['required']) && $fieldDef['required'] && !isset($fieldDef['default']) )
1733                         $fieldDef['default'] = '';
1734 //        if ($fieldDef['type'] == 'bit' && empty($fieldDef['len']) )
1735 //            $fieldDef['len'] = '1';
1736 //              if ($fieldDef['type'] == 'bool' && empty($fieldDef['len']) )
1737 //            $fieldDef['len'] = '1';
1738 //        if ($fieldDef['type'] == 'float' && empty($fieldDef['len']) )
1739 //            $fieldDef['len'] = '8';
1740 //        if ($fieldDef['type'] == 'varchar' && empty($fieldDef['len']) )
1741 //            $fieldDef['len'] = '255';
1742 //              if ($fieldDef['type'] == 'nvarchar' && empty($fieldDef['len']) )
1743 //            $fieldDef['len'] = '255';
1744 //        if ($fieldDef['type'] == 'image' && empty($fieldDef['len']) )
1745 //            $fieldDef['len'] = '2147483647';
1746 //        if ($fieldDef['type'] == 'ntext' && empty($fieldDef['len']) )
1747 //            $fieldDef['len'] = '2147483646';
1748 //        if ($fieldDef['type'] == 'smallint' && empty($fieldDef['len']) )
1749 //            $fieldDef['len'] = '2';
1750 //        if ($fieldDef['type'] == 'bit' && empty($fieldDef['default']) )
1751 //            $fieldDef['default'] = '0';
1752 //              if ($fieldDef['type'] == 'bool' && empty($fieldDef['default']) )
1753 //            $fieldDef['default'] = '0';
1754
1755     }
1756
1757     /**
1758      * @see DBManager::oneColumnSQLRep()
1759      */
1760     protected function oneColumnSQLRep($fieldDef, $ignoreRequired = false, $table = '', $return_as_array = false)
1761     {
1762         //Bug 25814
1763                 if(isset($fieldDef['name'])){
1764                     $colType = $this->getFieldType($fieldDef);
1765                 if(stristr($this->getFieldType($fieldDef), 'decimal') && isset($fieldDef['len'])){
1766                                 $fieldDef['len'] = min($fieldDef['len'],38);
1767                         }
1768                     //bug: 39690 float(8) is interpreted as real and this generates a diff when doing repair
1769                         if(stristr($colType, 'float') && isset($fieldDef['len']) && $fieldDef['len'] == 8){
1770                                 unset($fieldDef['len']);
1771                         }
1772                 }
1773
1774                 // always return as array for post-processing
1775                 $ref = parent::oneColumnSQLRep($fieldDef, $ignoreRequired, $table, true);
1776
1777                 // Bug 24307 - Don't add precision for float fields.
1778                 if ( stristr($ref['colType'],'float') )
1779                         $ref['colType'] = preg_replace('/(,\d+)/','',$ref['colType']);
1780
1781         if ( $return_as_array )
1782             return $ref;
1783         else
1784             return "{$ref['name']} {$ref['colType']} {$ref['default']} {$ref['required']} {$ref['auto_increment']}";
1785         }
1786
1787     /**
1788      * Saves changes to module's audit table
1789      *
1790      * @param object $bean    Sugarbean instance
1791      * @param array  $changes changes
1792      */
1793     public function save_audit_records(SugarBean $bean, $changes)
1794         {
1795                 //Bug 25078 fixed by Martin Hu: sqlserver haven't 'date' type, trim extra "00:00:00"
1796                 if($changes['data_type'] == 'date'){
1797                         $changes['before'] = str_replace(' 00:00:00','',$changes['before']);
1798                 }
1799                 parent::save_audit_records($bean,$changes);
1800         }
1801
1802     /**
1803      * Disconnects from the database
1804      *
1805      * Also handles any cleanup needed
1806      */
1807     public function disconnect()
1808     {
1809         $GLOBALS['log']->debug('Calling Mssql::disconnect()');
1810         if(!empty($this->database)){
1811             $this->freeResult();
1812             mssql_close($this->database);
1813             $this->database = null;
1814         }
1815     }
1816
1817     /**
1818      * @see DBManager::freeDbResult()
1819      */
1820     protected function freeDbResult($dbResult)
1821     {
1822         if(!empty($dbResult))
1823             mssql_free_result($dbResult);
1824     }
1825
1826         /**
1827          * (non-PHPdoc)
1828          * @see DBManager::lastDbError()
1829          */
1830     public function lastDbError()
1831     {
1832         $sqlmsg = mssql_get_last_message();
1833         if(empty($sqlmsg)) return false;
1834         global $app_strings;
1835         if (empty($app_strings)
1836                     or !isset($app_strings['ERR_MSSQL_DB_CONTEXT'])
1837                         or !isset($app_strings['ERR_MSSQL_WARNING']) ) {
1838         //ignore the message from sql-server if $app_strings array is empty. This will happen
1839         //only if connection if made before language is set.
1840                     return false;
1841         }
1842
1843         $sqlpos = strpos($sqlmsg, 'Changed database context to');
1844         $sqlpos2 = strpos($sqlmsg, 'Warning:');
1845         $sqlpos3 = strpos($sqlmsg, 'Checking identity information:');
1846         if ( $sqlpos !== false || $sqlpos2 !== false || $sqlpos3 !== false ) {
1847             return false;
1848         } else {
1849                 global $app_strings;
1850             //ERR_MSSQL_DB_CONTEXT: localized version of 'Changed database context to' message
1851             if (empty($app_strings) or !isset($app_strings['ERR_MSSQL_DB_CONTEXT'])) {
1852                 //ignore the message from sql-server if $app_strings array is empty. This will happen
1853                 //only if connection if made before languge is set.
1854                 $GLOBALS['log']->debug("Ignoring this database message: " . $sqlmsg);
1855                 return false;
1856             }
1857             else {
1858                 $sqlpos = strpos($sqlmsg, $app_strings['ERR_MSSQL_DB_CONTEXT']);
1859                 if ( $sqlpos !== false )
1860                     return false;
1861             }
1862         }
1863
1864         if ( strlen($sqlmsg) > 2 ) {
1865                 return "SQL Server error: " . $sqlmsg;
1866         }
1867
1868         return false;
1869     }
1870
1871     /**
1872      * (non-PHPdoc)
1873      * @see DBManager::getDbInfo()
1874      */
1875     public function getDbInfo()
1876     {
1877         return array("version" => $this->version());
1878     }
1879
1880     /**
1881      * (non-PHPdoc)
1882      * @see DBManager::validateQuery()
1883      */
1884     public function validateQuery($query)
1885     {
1886         if(!$this->isSelect($query)) {
1887             return false;
1888         }
1889         $this->query("SET SHOWPLAN_TEXT ON");
1890         $res = $this->getOne($query);
1891         $this->query("SET SHOWPLAN_TEXT OFF");
1892         return !empty($res);
1893     }
1894
1895     /**
1896      * This is a utility function to prepend the "N" character in front of SQL values that are
1897      * surrounded by single quotes.
1898      *
1899      * @param  $sql string SQL statement
1900      * @return string SQL statement with single quote values prepended with "N" character for nvarchar columns
1901      */
1902     protected function _appendN($sql)
1903     {
1904         // If there are no single quotes, don't bother, will just assume there is no character data
1905         if (strpos($sql, "'") === false)
1906             return $sql;
1907
1908         // Flag if there are odd number of single quotes, just continue without trying to append N
1909         if ((substr_count($sql, "'") & 1)) {
1910             $GLOBALS['log']->error("SQL statement[" . $sql . "] has odd number of single quotes.");
1911             return $sql;
1912         }
1913
1914         //The only location of three subsequent ' will be at the beginning or end of a value.
1915         $sql = preg_replace('/(?<!\')(\'{3})(?!\')/', "'<@#@#@PAIR@#@#@>", $sql);
1916
1917         // Remove any remaining '' and do not parse... replace later (hopefully we don't even have any)
1918         $pairs        = array();
1919         $regexp       = '/(\'{2})/';
1920         $pair_matches = array();
1921         preg_match_all($regexp, $sql, $pair_matches);
1922         if ($pair_matches) {
1923             foreach (array_unique($pair_matches[0]) as $key=>$value) {
1924                 $pairs['<@PAIR-'.$key.'@>'] = $value;
1925             }
1926             if (!empty($pairs)) {
1927                 $sql = str_replace($pairs, array_keys($pairs), $sql);
1928             }
1929         }
1930
1931         $regexp  = "/(N?'.+?')/is";
1932         $matches = array();
1933         preg_match_all($regexp, $sql, $matches);
1934         $replace = array();
1935         if (!empty($matches)) {
1936             foreach ($matches[0] as $value) {
1937                 // We are assuming that all nvarchar columns are no more than 200 characters in length
1938                 // One problem we face is the image column type in reports which cannot accept nvarchar data
1939                 if (!empty($value) && !is_numeric(trim(str_replace(array("'", ","), "", $value))) && !preg_match('/^\'[\,]\'$/', $value)) {
1940                     $replace[$value] = 'N' . trim($value, "N");
1941                 }
1942             }
1943         }
1944
1945         if (!empty($replace))
1946             $sql = str_replace(array_keys($replace), $replace, $sql);
1947
1948         if (!empty($pairs))
1949             $sql = str_replace(array_keys($pairs), $pairs, $sql);
1950
1951         if(strpos($sql, "<@#@#@PAIR@#@#@>"))
1952             $sql = str_replace(array('<@#@#@PAIR@#@#@>'), array("''"), $sql);
1953
1954         return $sql;
1955     }
1956
1957     /**
1958      * Quote SQL Server search term
1959      * @param string $term
1960      * @return string
1961      */
1962     protected function quoteTerm($term)
1963     {
1964         $term = str_replace("%", "*", $term); // Mssql wildcard is *
1965         return '"'.str_replace('"', '', $term).'"';
1966     }
1967
1968     /**
1969      * Generate fulltext query from set of terms
1970      * @param string $fields Field to search against
1971      * @param array $terms Search terms that may be or not be in the result
1972      * @param array $must_terms Search terms that have to be in the result
1973      * @param array $exclude_terms Search terms that have to be not in the result
1974      */
1975     public function getFulltextQuery($field, $terms, $must_terms = array(), $exclude_terms = array())
1976     {
1977         $condition = $or_condition = array();
1978         foreach($must_terms as $term) {
1979             $condition[] = $this->quoteTerm($term);
1980         }
1981
1982         foreach($terms as $term) {
1983             $or_condition[] = $this->quoteTerm($term);
1984         }
1985
1986         if(!empty($or_condition)) {
1987             $condition[] = "(".join(" | ", $or_condition).")";
1988         }
1989
1990         foreach($exclude_terms as $term) {
1991             $condition[] = " NOT ".$this->quoteTerm($term);
1992         }
1993         $condition = $this->quoted(join(" AND ",$condition));
1994         return "CONTAINS($field, $condition)";
1995     }
1996
1997     /**
1998      * Check if certain database exists
1999      * @param string $dbname
2000      */
2001     public function dbExists($dbname)
2002     {
2003         $db = $this->getOne("SELECT name FROM master..sysdatabases WHERE name = N".$this->quoted($dbname));
2004         return !empty($db);
2005     }
2006
2007     /**
2008      * Select database
2009      * @param string $dbname
2010      */
2011     protected function selectDb($dbname)
2012     {
2013         return mssql_select_db($dbname);
2014     }
2015
2016     /**
2017      * Check if certain DB user exists
2018      * @param string $username
2019      */
2020     public function userExists($username)
2021     {
2022         $this->selectDb("master");
2023         $user = $this->getOne("select count(*) from sys.sql_logins where name =".$this->quoted($username));
2024         // FIXME: go back to the original DB
2025         return !empty($user);
2026     }
2027
2028     /**
2029      * Create DB user
2030      * @param string $database_name
2031      * @param string $host_name
2032      * @param string $user
2033      * @param string $password
2034      */
2035     public function createDbUser($database_name, $host_name, $user, $password)
2036     {
2037         $qpassword = $this->quote($password);
2038         $this->selectDb($database_name);
2039         $this->query("CREATE LOGIN $user WITH PASSWORD = '$qpassword'", true);
2040         $this->query("CREATE USER $user FOR LOGIN $user", true);
2041         $this->query("EXEC sp_addRoleMember 'db_ddladmin ', '$user'", true);
2042         $this->query("EXEC sp_addRoleMember 'db_datareader','$user'", true);
2043         $this->query("EXEC sp_addRoleMember 'db_datawriter','$user'", true);
2044     }
2045
2046     /**
2047      * Create a database
2048      * @param string $dbname
2049      */
2050     public function createDatabase($dbname)
2051     {
2052         return $this->query("CREATE DATABASE $dbname", true);
2053     }
2054
2055     /**
2056      * Drop a database
2057      * @param string $dbname
2058      */
2059     public function dropDatabase($dbname)
2060     {
2061         return $this->query("DROP DATABASE $dbname", true);
2062     }
2063
2064     /**
2065      * Check if this driver can be used
2066      * @return bool
2067      */
2068     public function valid()
2069     {
2070         return function_exists("mssql_connect");
2071     }
2072
2073     /**
2074      * Check if this DB name is valid
2075      *
2076      * @param string $name
2077      * @return bool
2078      */
2079     public function isDatabaseNameValid($name)
2080     {
2081         // No funny chars, does not begin with number
2082         return preg_match('/^[0-9#@]+|[\"\'\*\/\\?\:\\<\>\-\ \&\!\(\)\[\]\{\}\;\,\.\`\~\|\\\\]+/', $name)==0;
2083     }
2084
2085     public function installConfig()
2086     {
2087         return array(
2088                 'LBL_DBCONFIG_MSG3' =>  array(
2089                 "setup_db_database_name" => array("label" => 'LBL_DBCONF_DB_NAME', "required" => true),
2090             ),
2091             'LBL_DBCONFIG_MSG2' =>  array(
2092                 "setup_db_host_name" => array("label" => 'LBL_DBCONF_HOST_NAME', "required" => true),
2093                 "setup_db_host_instance" => array("label" => 'LBL_DBCONF_HOST_INSTANCE'),
2094             ),
2095             'LBL_DBCONF_TITLE_USER_INFO' => array(),
2096             'LBL_DBCONFIG_B_MSG1' => array(
2097                 "setup_db_admin_user_name" => array("label" => 'LBL_DBCONF_DB_ADMIN_USER', "required" => true),
2098                 "setup_db_admin_password" => array("label" => 'LBL_DBCONF_DB_ADMIN_PASSWORD', "type" => "password"),
2099             )
2100         );
2101     }
2102
2103     /**
2104      * Returns a DB specific FROM clause which can be used to select against functions.
2105      * Note that depending on the database that this may also be an empty string.
2106      * @return string
2107      */
2108     public function getFromDummyTable()
2109     {
2110         return '';
2111     }
2112
2113     /**
2114      * Returns a DB specific piece of SQL which will generate GUID (UUID)
2115      * This string can be used in dynamic SQL to do multiple inserts with a single query.
2116      * I.e. generate a unique Sugar id in a sub select of an insert statement.
2117      * @return string
2118      */
2119
2120         public function getGuidSQL()
2121     {
2122         return 'NEWID()';
2123     }
2124 }