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