1<?php
2
3/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
4
5/**
6 * The PEAR DB driver for PHP's mssql extension
7 * for interacting with Microsoft SQL Server databases
8 *
9 * PHP version 5
10 *
11 * LICENSE: This source file is subject to version 3.0 of the PHP license
12 * that is available through the world-wide-web at the following URI:
13 * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
14 * the PHP License and are unable to obtain it through the web, please
15 * send a note to license@php.net so we can mail you a copy immediately.
16 *
17 * @category   Database
18 * @package    DB
19 * @author     Sterling Hughes <sterling@php.net>
20 * @author     Daniel Convissor <danielc@php.net>
21 * @copyright  1997-2007 The PHP Group
22 * @license    http://www.php.net/license/3_0.txt  PHP License 3.0
23 * @version    CVS: $Id$
24 * @link       http://pear.php.net/package/DB
25 */
26
27/**
28 * Obtain the DB_common class so it can be extended from
29 */
30require_once 'DB/common.php';
31
32/**
33 * The methods PEAR DB uses to interact with PHP's mssql extension
34 * for interacting with Microsoft SQL Server databases
35 *
36 * These methods overload the ones declared in DB_common.
37 *
38 * DB's mssql driver is only for Microsfoft SQL Server databases.
39 *
40 * If you're connecting to a Sybase database, you MUST specify "sybase"
41 * as the "phptype" in the DSN.
42 *
43 * This class only works correctly if you have compiled PHP using
44 * --with-mssql=[dir_to_FreeTDS].
45 *
46 * @category   Database
47 * @package    DB
48 * @author     Sterling Hughes <sterling@php.net>
49 * @author     Daniel Convissor <danielc@php.net>
50 * @copyright  1997-2007 The PHP Group
51 * @license    http://www.php.net/license/3_0.txt  PHP License 3.0
52 * @version    Release: 1.11.0
53 * @link       http://pear.php.net/package/DB
54 */
55class DB_mssql extends DB_common
56{
57    // {{{ properties
58
59    /**
60     * The DB driver type (mysql, oci8, odbc, etc.)
61     * @var string
62     */
63    var $phptype = 'mssql';
64
65    /**
66     * The database syntax variant to be used (db2, access, etc.), if any
67     * @var string
68     */
69    var $dbsyntax = 'mssql';
70
71    /**
72     * The capabilities of this DB implementation
73     *
74     * The 'new_link' element contains the PHP version that first provided
75     * new_link support for this DBMS.  Contains false if it's unsupported.
76     *
77     * Meaning of the 'limit' element:
78     *   + 'emulate' = emulate with fetch row by number
79     *   + 'alter'   = alter the query
80     *   + false     = skip rows
81     *
82     * @var array
83     */
84    var $features = array(
85        'limit'         => 'emulate',
86        'new_link'      => false,
87        'numrows'       => true,
88        'pconnect'      => true,
89        'prepare'       => false,
90        'ssl'           => false,
91        'transactions'  => true,
92    );
93
94    /**
95     * A mapping of native error codes to DB error codes
96     * @var array
97     */
98    // XXX Add here error codes ie: 'S100E' => DB_ERROR_SYNTAX
99    var $errorcode_map = array(
100        102   => DB_ERROR_SYNTAX,
101        110   => DB_ERROR_VALUE_COUNT_ON_ROW,
102        155   => DB_ERROR_NOSUCHFIELD,
103        156   => DB_ERROR_SYNTAX,
104        170   => DB_ERROR_SYNTAX,
105        207   => DB_ERROR_NOSUCHFIELD,
106        208   => DB_ERROR_NOSUCHTABLE,
107        245   => DB_ERROR_INVALID_NUMBER,
108        319   => DB_ERROR_SYNTAX,
109        321   => DB_ERROR_NOSUCHFIELD,
110        325   => DB_ERROR_SYNTAX,
111        336   => DB_ERROR_SYNTAX,
112        515   => DB_ERROR_CONSTRAINT_NOT_NULL,
113        547   => DB_ERROR_CONSTRAINT,
114        1018  => DB_ERROR_SYNTAX,
115        1035  => DB_ERROR_SYNTAX,
116        1913  => DB_ERROR_ALREADY_EXISTS,
117        2209  => DB_ERROR_SYNTAX,
118        2223  => DB_ERROR_SYNTAX,
119        2248  => DB_ERROR_SYNTAX,
120        2256  => DB_ERROR_SYNTAX,
121        2257  => DB_ERROR_SYNTAX,
122        2627  => DB_ERROR_CONSTRAINT,
123        2714  => DB_ERROR_ALREADY_EXISTS,
124        3607  => DB_ERROR_DIVZERO,
125        3701  => DB_ERROR_NOSUCHTABLE,
126        7630  => DB_ERROR_SYNTAX,
127        8134  => DB_ERROR_DIVZERO,
128        9303  => DB_ERROR_SYNTAX,
129        9317  => DB_ERROR_SYNTAX,
130        9318  => DB_ERROR_SYNTAX,
131        9331  => DB_ERROR_SYNTAX,
132        9332  => DB_ERROR_SYNTAX,
133        15253 => DB_ERROR_SYNTAX,
134    );
135
136    /**
137     * The raw database connection created by PHP
138     * @var resource
139     */
140    var $connection;
141
142    /**
143     * The DSN information for connecting to a database
144     * @var array
145     */
146    var $dsn = array();
147
148
149    /**
150     * Should data manipulation queries be committed automatically?
151     * @var bool
152     * @access private
153     */
154    var $autocommit = true;
155
156    /**
157     * The quantity of transactions begun
158     *
159     * {@internal  While this is private, it can't actually be designated
160     * private in PHP 5 because it is directly accessed in the test suite.}}
161     *
162     * @var integer
163     * @access private
164     */
165    var $transaction_opcount = 0;
166
167    /**
168     * The database specified in the DSN
169     *
170     * It's a fix to allow calls to different databases in the same script.
171     *
172     * @var string
173     * @access private
174     */
175    var $_db = null;
176
177
178    // }}}
179    // {{{ constructor
180
181    /**
182     * This constructor calls <kbd>parent::__construct()</kbd>
183     *
184     * @return void
185     */
186    function __construct()
187    {
188        parent::__construct();
189    }
190
191    // }}}
192    // {{{ connect()
193
194    /**
195     * Connect to the database server, log in and open the database
196     *
197     * Don't call this method directly.  Use DB::connect() instead.
198     *
199     * @param array $dsn         the data source name
200     * @param bool  $persistent  should the connection be persistent?
201     *
202     * @return int  DB_OK on success. A DB_Error object on failure.
203     */
204    function connect($dsn, $persistent = false)
205    {
206        if (!PEAR::loadExtension('mssql') && !PEAR::loadExtension('sybase')
207            && !PEAR::loadExtension('sybase_ct'))
208        {
209            return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND);
210        }
211
212        $this->dsn = $dsn;
213        if ($dsn['dbsyntax']) {
214            $this->dbsyntax = $dsn['dbsyntax'];
215        }
216
217        $params = array(
218            $dsn['hostspec'] ? $dsn['hostspec'] : 'localhost',
219            $dsn['username'] ? $dsn['username'] : null,
220            $dsn['password'] ? $dsn['password'] : null,
221        );
222        if ($dsn['port']) {
223            $params[0] .= ((substr(PHP_OS, 0, 3) == 'WIN') ? ',' : ':')
224                        . $dsn['port'];
225        }
226
227        $connect_function = $persistent ? 'mssql_pconnect' : 'mssql_connect';
228
229        $this->connection = @call_user_func_array($connect_function, $params);
230
231        if (!$this->connection) {
232            return $this->raiseError(DB_ERROR_CONNECT_FAILED,
233                                     null, null, null,
234                                     @mssql_get_last_message());
235        }
236        if ($dsn['database']) {
237            if (!@mssql_select_db($dsn['database'], $this->connection)) {
238                return $this->raiseError(DB_ERROR_NODBSELECTED,
239                                         null, null, null,
240                                         @mssql_get_last_message());
241            }
242            $this->_db = $dsn['database'];
243        }
244        return DB_OK;
245    }
246
247    // }}}
248    // {{{ disconnect()
249
250    /**
251     * Disconnects from the database server
252     *
253     * @return bool  TRUE on success, FALSE on failure
254     */
255    function disconnect()
256    {
257        $ret = @mssql_close($this->connection);
258        $this->connection = null;
259        return $ret;
260    }
261
262    // }}}
263    // {{{ simpleQuery()
264
265    /**
266     * Sends a query to the database server
267     *
268     * @param string  the SQL query string
269     *
270     * @return mixed  + a PHP result resrouce for successful SELECT queries
271     *                + the DB_OK constant for other successful queries
272     *                + a DB_Error object on failure
273     */
274    function simpleQuery($query)
275    {
276        $ismanip = $this->_checkManip($query);
277        $this->last_query = $query;
278        if (!@mssql_select_db($this->_db, $this->connection)) {
279            return $this->mssqlRaiseError(DB_ERROR_NODBSELECTED);
280        }
281        $query = $this->modifyQuery($query);
282        if (!$this->autocommit && $ismanip) {
283            if ($this->transaction_opcount == 0) {
284                $result = @mssql_query('BEGIN TRAN', $this->connection);
285                if (!$result) {
286                    return $this->mssqlRaiseError();
287                }
288            }
289            $this->transaction_opcount++;
290        }
291        $result = @mssql_query($query, $this->connection);
292        if (!$result) {
293            return $this->mssqlRaiseError();
294        }
295        // Determine which queries that should return data, and which
296        // should return an error code only.
297        return $ismanip ? DB_OK : $result;
298    }
299
300    // }}}
301    // {{{ nextResult()
302
303    /**
304     * Move the internal mssql result pointer to the next available result
305     *
306     * @param a valid fbsql result resource
307     *
308     * @access public
309     *
310     * @return true if a result is available otherwise return false
311     */
312    function nextResult($result)
313    {
314        return @mssql_next_result($result);
315    }
316
317    // }}}
318    // {{{ fetchInto()
319
320    /**
321     * Places a row from the result set into the given array
322     *
323     * Formating of the array and the data therein are configurable.
324     * See DB_result::fetchInto() for more information.
325     *
326     * This method is not meant to be called directly.  Use
327     * DB_result::fetchInto() instead.  It can't be declared "protected"
328     * because DB_result is a separate object.
329     *
330     * @param resource $result    the query result resource
331     * @param array    $arr       the referenced array to put the data in
332     * @param int      $fetchmode how the resulting array should be indexed
333     * @param int      $rownum    the row number to fetch (0 = first row)
334     *
335     * @return mixed  DB_OK on success, NULL when the end of a result set is
336     *                 reached or on failure
337     *
338     * @see DB_result::fetchInto()
339     */
340    function fetchInto($result, &$arr, $fetchmode, $rownum = null)
341    {
342        if ($rownum !== null) {
343            if (!@mssql_data_seek($result, $rownum)) {
344                return null;
345            }
346        }
347        if ($fetchmode & DB_FETCHMODE_ASSOC) {
348            $arr = @mssql_fetch_assoc($result);
349            if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE && $arr) {
350                $arr = array_change_key_case($arr, CASE_LOWER);
351            }
352        } else {
353            $arr = @mssql_fetch_row($result);
354        }
355        if (!$arr) {
356            return null;
357        }
358        if ($this->options['portability'] & DB_PORTABILITY_RTRIM) {
359            $this->_rtrimArrayValues($arr);
360        }
361        if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) {
362            $this->_convertNullArrayValuesToEmpty($arr);
363        }
364        return DB_OK;
365    }
366
367    // }}}
368    // {{{ freeResult()
369
370    /**
371     * Deletes the result set and frees the memory occupied by the result set
372     *
373     * This method is not meant to be called directly.  Use
374     * DB_result::free() instead.  It can't be declared "protected"
375     * because DB_result is a separate object.
376     *
377     * @param resource $result  PHP's query result resource
378     *
379     * @return bool  TRUE on success, FALSE if $result is invalid
380     *
381     * @see DB_result::free()
382     */
383    function freeResult($result)
384    {
385        return is_resource($result) ? mssql_free_result($result) : false;
386    }
387
388    // }}}
389    // {{{ numCols()
390
391    /**
392     * Gets the number of columns in a result set
393     *
394     * This method is not meant to be called directly.  Use
395     * DB_result::numCols() instead.  It can't be declared "protected"
396     * because DB_result is a separate object.
397     *
398     * @param resource $result  PHP's query result resource
399     *
400     * @return int  the number of columns.  A DB_Error object on failure.
401     *
402     * @see DB_result::numCols()
403     */
404    function numCols($result)
405    {
406        $cols = @mssql_num_fields($result);
407        if (!$cols) {
408            return $this->mssqlRaiseError();
409        }
410        return $cols;
411    }
412
413    // }}}
414    // {{{ numRows()
415
416    /**
417     * Gets the number of rows in a result set
418     *
419     * This method is not meant to be called directly.  Use
420     * DB_result::numRows() instead.  It can't be declared "protected"
421     * because DB_result is a separate object.
422     *
423     * @param resource $result  PHP's query result resource
424     *
425     * @return int  the number of rows.  A DB_Error object on failure.
426     *
427     * @see DB_result::numRows()
428     */
429    function numRows($result)
430    {
431        $rows = @mssql_num_rows($result);
432        if ($rows === false) {
433            return $this->mssqlRaiseError();
434        }
435        return $rows;
436    }
437
438    // }}}
439    // {{{ autoCommit()
440
441    /**
442     * Enables or disables automatic commits
443     *
444     * @param bool $onoff  true turns it on, false turns it off
445     *
446     * @return int  DB_OK on success.  A DB_Error object if the driver
447     *               doesn't support auto-committing transactions.
448     */
449    function autoCommit($onoff = false)
450    {
451        // XXX if $this->transaction_opcount > 0, we should probably
452        // issue a warning here.
453        $this->autocommit = $onoff ? true : false;
454        return DB_OK;
455    }
456
457    // }}}
458    // {{{ commit()
459
460    /**
461     * Commits the current transaction
462     *
463     * @return int  DB_OK on success.  A DB_Error object on failure.
464     */
465    function commit()
466    {
467        if ($this->transaction_opcount > 0) {
468            if (!@mssql_select_db($this->_db, $this->connection)) {
469                return $this->mssqlRaiseError(DB_ERROR_NODBSELECTED);
470            }
471            $result = @mssql_query('COMMIT TRAN', $this->connection);
472            $this->transaction_opcount = 0;
473            if (!$result) {
474                return $this->mssqlRaiseError();
475            }
476        }
477        return DB_OK;
478    }
479
480    // }}}
481    // {{{ rollback()
482
483    /**
484     * Reverts the current transaction
485     *
486     * @return int  DB_OK on success.  A DB_Error object on failure.
487     */
488    function rollback()
489    {
490        if ($this->transaction_opcount > 0) {
491            if (!@mssql_select_db($this->_db, $this->connection)) {
492                return $this->mssqlRaiseError(DB_ERROR_NODBSELECTED);
493            }
494            $result = @mssql_query('ROLLBACK TRAN', $this->connection);
495            $this->transaction_opcount = 0;
496            if (!$result) {
497                return $this->mssqlRaiseError();
498            }
499        }
500        return DB_OK;
501    }
502
503    // }}}
504    // {{{ affectedRows()
505
506    /**
507     * Determines the number of rows affected by a data maniuplation query
508     *
509     * 0 is returned for queries that don't manipulate data.
510     *
511     * @return int  the number of rows.  A DB_Error object on failure.
512     */
513    function affectedRows()
514    {
515        if ($this->_last_query_manip) {
516            $res = @mssql_query('select @@rowcount', $this->connection);
517            if (!$res) {
518                return $this->mssqlRaiseError();
519            }
520            $ar = @mssql_fetch_row($res);
521            if (!$ar) {
522                $result = 0;
523            } else {
524                @mssql_free_result($res);
525                $result = $ar[0];
526            }
527        } else {
528            $result = 0;
529        }
530        return $result;
531    }
532
533    // }}}
534    // {{{ nextId()
535
536    /**
537     * Returns the next free id in a sequence
538     *
539     * @param string  $seq_name  name of the sequence
540     * @param boolean $ondemand  when true, the seqence is automatically
541     *                            created if it does not exist
542     *
543     * @return int  the next id number in the sequence.
544     *               A DB_Error object on failure.
545     *
546     * @see DB_common::nextID(), DB_common::getSequenceName(),
547     *      DB_mssql::createSequence(), DB_mssql::dropSequence()
548     */
549    function nextId($seq_name, $ondemand = true)
550    {
551        $seqname = $this->getSequenceName($seq_name);
552        if (!@mssql_select_db($this->_db, $this->connection)) {
553            return $this->mssqlRaiseError(DB_ERROR_NODBSELECTED);
554        }
555        $repeat = 0;
556        do {
557            $this->pushErrorHandling(PEAR_ERROR_RETURN);
558            $result = $this->query("INSERT INTO $seqname (vapor) VALUES (0)");
559            $this->popErrorHandling();
560            if ($ondemand && DB::isError($result) &&
561                ($result->getCode() == DB_ERROR || $result->getCode() == DB_ERROR_NOSUCHTABLE))
562            {
563                $repeat = 1;
564                $result = $this->createSequence($seq_name);
565                if (DB::isError($result)) {
566                    return $this->raiseError($result);
567                }
568            } elseif (!DB::isError($result)) {
569                $result = $this->query("SELECT IDENT_CURRENT('$seqname')");
570                if (DB::isError($result)) {
571                    /* Fallback code for MS SQL Server 7.0, which doesn't have
572                     * IDENT_CURRENT. This is *not* safe for concurrent
573                     * requests, and really, if you're using it, you're in a
574                     * world of hurt. Nevertheless, it's here to ensure BC. See
575                     * bug #181 for the gory details.*/
576                    $result = $this->query("SELECT @@IDENTITY FROM $seqname");
577                }
578                $repeat = 0;
579            } else {
580                $repeat = false;
581            }
582        } while ($repeat);
583        if (DB::isError($result)) {
584            return $this->raiseError($result);
585        }
586        $result = $result->fetchRow(DB_FETCHMODE_ORDERED);
587        return $result[0];
588    }
589
590    /**
591     * Creates a new sequence
592     *
593     * @param string $seq_name  name of the new sequence
594     *
595     * @return int  DB_OK on success.  A DB_Error object on failure.
596     *
597     * @see DB_common::createSequence(), DB_common::getSequenceName(),
598     *      DB_mssql::nextID(), DB_mssql::dropSequence()
599     */
600    function createSequence($seq_name)
601    {
602        return $this->query('CREATE TABLE '
603                            . $this->getSequenceName($seq_name)
604                            . ' ([id] [int] IDENTITY (1, 1) NOT NULL,'
605                            . ' [vapor] [int] NULL)');
606    }
607
608    // }}}
609    // {{{ dropSequence()
610
611    /**
612     * Deletes a sequence
613     *
614     * @param string $seq_name  name of the sequence to be deleted
615     *
616     * @return int  DB_OK on success.  A DB_Error object on failure.
617     *
618     * @see DB_common::dropSequence(), DB_common::getSequenceName(),
619     *      DB_mssql::nextID(), DB_mssql::createSequence()
620     */
621    function dropSequence($seq_name)
622    {
623        return $this->query('DROP TABLE ' . $this->getSequenceName($seq_name));
624    }
625
626    // }}}
627    // {{{ escapeSimple()
628
629    /**
630     * Escapes a string in a manner suitable for SQL Server.
631     *
632     * @param string $str  the string to be escaped
633     * @return string  the escaped string
634     *
635     * @see DB_common::quoteSmart()
636     * @since Method available since Release 1.6.0
637     */
638    function escapeSimple($str)
639    {
640        return str_replace(
641            array("'", "\\\r\n", "\\\n"),
642            array("''", "\\\\\r\n\r\n", "\\\\\n\n"),
643            $str
644        );
645    }
646
647    // }}}
648    // {{{ quoteIdentifier()
649
650    /**
651     * Quotes a string so it can be safely used as a table or column name
652     *
653     * @param string $str  identifier name to be quoted
654     *
655     * @return string  quoted identifier string
656     *
657     * @see DB_common::quoteIdentifier()
658     * @since Method available since Release 1.6.0
659     */
660    function quoteIdentifier($str)
661    {
662        return '[' . str_replace(']', ']]', $str) . ']';
663    }
664
665    // }}}
666    // {{{ mssqlRaiseError()
667
668    /**
669     * Produces a DB_Error object regarding the current problem
670     *
671     * @param int $errno  if the error is being manually raised pass a
672     *                     DB_ERROR* constant here.  If this isn't passed
673     *                     the error information gathered from the DBMS.
674     *
675     * @return object  the DB_Error object
676     *
677     * @see DB_common::raiseError(),
678     *      DB_mssql::errorNative(), DB_mssql::errorCode()
679     */
680    function mssqlRaiseError($code = null)
681    {
682        $message = @mssql_get_last_message();
683        if (!$code) {
684            $code = $this->errorNative();
685        }
686        return $this->raiseError($this->errorCode($code, $message),
687                                 null, null, null, "$code - $message");
688    }
689
690    // }}}
691    // {{{ errorNative()
692
693    /**
694     * Gets the DBMS' native error code produced by the last query
695     *
696     * @return int  the DBMS' error code
697     */
698    function errorNative()
699    {
700        $res = @mssql_query('select @@ERROR as ErrorCode', $this->connection);
701        if (!$res) {
702            return DB_ERROR;
703        }
704        $row = @mssql_fetch_row($res);
705        return $row[0];
706    }
707
708    // }}}
709    // {{{ errorCode()
710
711    /**
712     * Determines PEAR::DB error code from mssql's native codes.
713     *
714     * If <var>$nativecode</var> isn't known yet, it will be looked up.
715     *
716     * @param  mixed  $nativecode  mssql error code, if known
717     * @return integer  an error number from a DB error constant
718     * @see errorNative()
719     */
720    function errorCode($nativecode = null, $msg = '')
721    {
722        if (!$nativecode) {
723            $nativecode = $this->errorNative();
724        }
725        if (isset($this->errorcode_map[$nativecode])) {
726            if ($nativecode == 3701
727                && preg_match('/Cannot drop the index/i', $msg))
728            {
729                return DB_ERROR_NOT_FOUND;
730            }
731            return $this->errorcode_map[$nativecode];
732        } else {
733            return DB_ERROR;
734        }
735    }
736
737    // }}}
738    // {{{ tableInfo()
739
740    /**
741     * Returns information about a table or a result set
742     *
743     * NOTE: only supports 'table' and 'flags' if <var>$result</var>
744     * is a table name.
745     *
746     * @param object|string  $result  DB_result object from a query or a
747     *                                 string containing the name of a table.
748     *                                 While this also accepts a query result
749     *                                 resource identifier, this behavior is
750     *                                 deprecated.
751     * @param int            $mode    a valid tableInfo mode
752     *
753     * @return array  an associative array with the information requested.
754     *                 A DB_Error object on failure.
755     *
756     * @see DB_common::tableInfo()
757     */
758    function tableInfo($result, $mode = null)
759    {
760        if (is_string($result)) {
761            /*
762             * Probably received a table name.
763             * Create a result resource identifier.
764             */
765            if (!@mssql_select_db($this->_db, $this->connection)) {
766                return $this->mssqlRaiseError(DB_ERROR_NODBSELECTED);
767            }
768            $id = @mssql_query("SELECT * FROM $result WHERE 1=0",
769                               $this->connection);
770            $got_string = true;
771        } elseif (isset($result->result)) {
772            /*
773             * Probably received a result object.
774             * Extract the result resource identifier.
775             */
776            $id = $result->result;
777            $got_string = false;
778        } else {
779            /*
780             * Probably received a result resource identifier.
781             * Copy it.
782             * Deprecated.  Here for compatibility only.
783             */
784            $id = $result;
785            $got_string = false;
786        }
787
788        if (!is_resource($id)) {
789            return $this->mssqlRaiseError(DB_ERROR_NEED_MORE_DATA);
790        }
791
792        if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) {
793            $case_func = 'strtolower';
794        } else {
795            $case_func = 'strval';
796        }
797
798        $count = @mssql_num_fields($id);
799        $res   = array();
800
801        if ($mode) {
802            $res['num_fields'] = $count;
803        }
804
805        for ($i = 0; $i < $count; $i++) {
806            if ($got_string) {
807                $flags = $this->_mssql_field_flags($result,
808                        @mssql_field_name($id, $i));
809                if (DB::isError($flags)) {
810                    return $flags;
811                }
812            } else {
813                $flags = '';
814            }
815
816            $res[$i] = array(
817                'table' => $got_string ? $case_func($result) : '',
818                'name'  => $case_func(@mssql_field_name($id, $i)),
819                'type'  => @mssql_field_type($id, $i),
820                'len'   => @mssql_field_length($id, $i),
821                'flags' => $flags,
822            );
823            if ($mode & DB_TABLEINFO_ORDER) {
824                $res['order'][$res[$i]['name']] = $i;
825            }
826            if ($mode & DB_TABLEINFO_ORDERTABLE) {
827                $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i;
828            }
829        }
830
831        // free the result only if we were called on a table
832        if ($got_string) {
833            @mssql_free_result($id);
834        }
835        return $res;
836    }
837
838    // }}}
839    // {{{ _mssql_field_flags()
840
841    /**
842     * Get a column's flags
843     *
844     * Supports "not_null", "primary_key",
845     * "auto_increment" (mssql identity), "timestamp" (mssql timestamp),
846     * "unique_key" (mssql unique index, unique check or primary_key) and
847     * "multiple_key" (multikey index)
848     *
849     * mssql timestamp is NOT similar to the mysql timestamp so this is maybe
850     * not useful at all - is the behaviour of mysql_field_flags that primary
851     * keys are alway unique? is the interpretation of multiple_key correct?
852     *
853     * @param string $table   the table name
854     * @param string $column  the field name
855     *
856     * @return string  the flags
857     *
858     * @access private
859     * @author Joern Barthel <j_barthel@web.de>
860     */
861    function _mssql_field_flags($table, $column)
862    {
863        static $tableName = null;
864        static $flags = array();
865
866        if ($table != $tableName) {
867
868            $flags = array();
869            $tableName = $table;
870
871            // get unique and primary keys
872            $res = $this->getAll("EXEC SP_HELPINDEX $table", DB_FETCHMODE_ASSOC);
873            if (DB::isError($res)) {
874                return $res;
875            }
876
877            foreach ($res as $val) {
878                $keys = explode(', ', $val['index_keys']);
879
880                if (sizeof($keys) > 1) {
881                    foreach ($keys as $key) {
882                        $this->_add_flag($flags[$key], 'multiple_key');
883                    }
884                }
885
886                if (strpos($val['index_description'], 'primary key')) {
887                    foreach ($keys as $key) {
888                        $this->_add_flag($flags[$key], 'primary_key');
889                    }
890                } elseif (strpos($val['index_description'], 'unique')) {
891                    foreach ($keys as $key) {
892                        $this->_add_flag($flags[$key], 'unique_key');
893                    }
894                }
895            }
896
897            // get auto_increment, not_null and timestamp
898            $res = $this->getAll("EXEC SP_COLUMNS $table", DB_FETCHMODE_ASSOC);
899            if (DB::isError($res)) {
900                return $res;
901            }
902
903            foreach ($res as $val) {
904                $val = array_change_key_case($val, CASE_LOWER);
905                if ($val['nullable'] == '0') {
906                    $this->_add_flag($flags[$val['column_name']], 'not_null');
907                }
908                if (strpos($val['type_name'], 'identity')) {
909                    $this->_add_flag($flags[$val['column_name']], 'auto_increment');
910                }
911                if (strpos($val['type_name'], 'timestamp')) {
912                    $this->_add_flag($flags[$val['column_name']], 'timestamp');
913                }
914            }
915        }
916
917        if (array_key_exists($column, $flags)) {
918            return(implode(' ', $flags[$column]));
919        }
920        return '';
921    }
922
923    // }}}
924    // {{{ _add_flag()
925
926    /**
927     * Adds a string to the flags array if the flag is not yet in there
928     * - if there is no flag present the array is created
929     *
930     * @param array  &$array  the reference to the flag-array
931     * @param string $value   the flag value
932     *
933     * @return void
934     *
935     * @access private
936     * @author Joern Barthel <j_barthel@web.de>
937     */
938    function _add_flag(&$array, $value)
939    {
940        if (!is_array($array)) {
941            $array = array($value);
942        } elseif (!in_array($value, $array)) {
943            array_push($array, $value);
944        }
945    }
946
947    // }}}
948    // {{{ getSpecialQuery()
949
950    /**
951     * Obtains the query string needed for listing a given type of objects
952     *
953     * @param string $type  the kind of objects you want to retrieve
954     *
955     * @return string  the SQL query string or null if the driver doesn't
956     *                  support the object type requested
957     *
958     * @access protected
959     * @see DB_common::getListOf()
960     */
961    function getSpecialQuery($type)
962    {
963        switch ($type) {
964            case 'tables':
965                return "SELECT name FROM sysobjects WHERE type = 'U'"
966                       . ' ORDER BY name';
967            case 'views':
968                return "SELECT name FROM sysobjects WHERE type = 'V'";
969            default:
970                return null;
971        }
972    }
973
974    // }}}
975}
976
977/*
978 * Local variables:
979 * tab-width: 4
980 * c-basic-offset: 4
981 * End:
982 */
983
984?>
985