root

Changeset 562

Show
Ignore:
Timestamp:
05/05/09 13:55:03 (1 year ago)
Author:
wbond
Message:

BackwardsCompatibilityBreak - removed fORMSchema::enableSmartCaching(), fORM::enableSchemaCaching() now provides equivalent functionality. fSchema::setCacheFile() changed to fSchema::enableCaching() and now requires an fCache object. fSchema::flushInfo() renamed to fSchema::clearCache(). Fixed tickets #139, #144, #173 and #185. Added Oracle support and the fCache class. Added a whole slew of new database tests.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • fDatabase.php

    r514 r562 Hide Line Numbers
    1010 *  - [http://microsoft.com/sql/ MSSQL] 
    1111 *  - [http://mysql.com MySQL] 
     12 *  - [http://oracle.com Oracle] 
    1213 *  - [http://postgresql.org PostgreSQL] 
    1314 *  - [http://sqlite.org SQLite] 
     
    2627 *   - [http://php.net/mysqli mysqli] 
    2728 *   - [http://php.net/pdo_mysql pdo_mysql] 
     29 *  - Oracle (via ODBC) 
     30 *   - [http://php.net/pdo_odbc pdo_odbc] 
     31 *   - [http://php.net/odbc odbc] 
     32 *  - Oracle 
     33 *   - [http://php.net/oci8 oci8] 
     34 *   - [http://php.net/pdo_oci pdo_oci] 
    2835 *  - PostgreSQL 
    2936 *   - [http://php.net/pgsql pgsql] 
     
    4047 * @link       http://flourishlib.com/fDatabase 
    4148 *  
    42  * @version    1.0.0b7 
     49 * @version    1.0.0b8 
     50 * @changes    1.0.0b8  Added Oracle support, change PostgreSQL code to no longer cause lastval() warnings, added support for arrays of values to ::escape() [wb, 2009-05-03] 
    4351 * @changes    1.0.0b7  Updated for new fCore API [wb, 2009-02-16] 
    4452 * @changes    1.0.0b6  Fixed a bug with executing transaction queries when using the mysqli extension [wb, 2009-02-12] 
     
    7583     
    7684    /** 
    77      * The character set that data is coming back as 
    78      *  
    79      * @var string  
    80      */ 
    81     private $character_set
     85     * An fCache object to cache the schema info to 
     86     *  
     87     * @var fCache 
     88     */ 
     89    private $cache
    8290     
    8391    /** 
     
    110118     *  - `'mysql'` 
    111119     *  - `'mysqli'` 
     120     *  - `'oci8'` 
    112121     *  - `'odbc'` 
    113122     *  - `'pgsql'` 
     
    156165     
    157166    /** 
     167     * A cache of database-specific code 
     168     *  
     169     * @var array  
     170     */ 
     171    private $schema_info; 
     172     
     173    /** 
    158174     * The millisecond threshold for triggering a warning about SQL performance 
    159175     *  
     
    170186     
    171187    /** 
    172      * The database type: `'mssql'`, `'mysql'`, `'postgresql'`, or `'sqlite'` 
     188     * The database type: `'mssql'`, `'mysql'`, `'oracle'`, `'postgresql'`, or `'sqlite'` 
    173189     *  
    174190     * @var string 
     
    194210     * Configures the connection to a database - connection is not made until the first query is executed 
    195211     *  
    196      * @param  string  $type      The type of the database: `'mssql'`, `'mysql'`, `'postgresql'`, `'sqlite'` 
     212     * @param  string  $type      The type of the database: `'mssql'`, `'mysql'`, `'oracle'`, `'postgresql'`, `'sqlite'` 
    197213     * @param  string  $database  Name of the database. If an ODBC connection `'dsn:'` concatenated with the DSN, if SQLite the path to the database file. 
    198      * @param  string  $username  Database username, required for all databases except SQLite 
    199      * @param  string  $password  The password for the username specified 
    200      * @param  string  $host      Database server host or ip, defaults to localhost for all databases except SQLite 
    201      * @param  integer $port      The port to connect to, defaults to the standard port for the database type specified 
     214     * @param  string  $username  Database username - not used for SQLite 
     215     * @param  string  $password  The password for the username specified - not used for SQLite 
     216     * @param  string  $host      Database server host or IP, defaults to localhost - not used for SQLite or ODBC connections. MySQL socket connection can be made by entering `'sock:'` followed by the socket path. PostgreSQL socket connection can be made by passing just `'sock:'`.  
     217     * @param  integer $port      The port to connect to, defaults to the standard port for the database type specified - not used for SQLite or ODBC connections  
    202218     * @return fDatabase 
    203219     */ 
    204220    public function __construct($type, $database, $username=NULL, $password=NULL, $host=NULL, $port=NULL) 
    205221    { 
    206         $valid_types = array('mssql', 'mysql', 'postgresql', 'sqlite'); 
     222        $valid_types = array('mssql', 'mysql', 'oracle', 'postgresql', 'sqlite'); 
    207223        if (!in_array($type, $valid_types)) { 
    208224            throw new fProgrammerException( 
     
    217233        } 
    218234         
    219         if ($type != 'sqlite') { 
    220             if (empty($username)) { 
    221                 throw new fProgrammerException('No username was specified'); 
    222             } 
    223             if ($host === NULL) { 
    224                 $host = 'localhost'; 
    225             } 
     235        if ($host === NULL) { 
     236            $host = 'localhost'; 
    226237        } 
    227238         
     
    233244        $this->port     = $port; 
    234245         
    235         $this->character_set = NULL
     246        $this->schema_info = array()
    236247         
    237248        $this->determineExtension(); 
     
    255266        } elseif ($this->extension == 'mysqli') { 
    256267            mysqli_close($this->connection); 
     268        } elseif ($this->extension == 'oci8') { 
     269            oci_close($this->connection); 
    257270        } elseif ($this->extension == 'odbc') { 
    258271            odbc_close($this->connection); 
     
    284297     * Checks to see if an SQL error occured 
    285298     *  
    286      * @param  fResult|fUnbufferedResult $result                The result object for the query 
    287      * @param  string                    $sqlite_error_message  If we are using the sqlite extension, this will contain an error message if one exists 
     299     * @param  fResult|fUnbufferedResult $result      The result object for the query 
     300     * @param  mixed                     $extra_info  The sqlite extension will pass a string error message, the oci8 extension will pass the statement resource 
    288301     * @return void 
    289302     */ 
    290     private function checkForError($result, $sqlite_error_message=NULL) 
     303    private function checkForError($result, $extra_info=NULL) 
    291304    { 
    292305        if ($result->getResult() === FALSE) { 
     
    298311            } elseif ($this->extension == 'mysqli') { 
    299312                $message = mysqli_error($this->connection); 
     313            } elseif ($this->extension == 'oci8') { 
     314                $error_info = oci_error($extra_info); 
     315                $message = $error_info['message']; 
    300316            } elseif ($this->extension == 'odbc') { 
    301317                $message = odbc_errormsg($this->connection); 
     
    303319                $message = pg_last_error($this->connection); 
    304320            } elseif ($this->extension == 'sqlite') { 
    305                 $message = $sqlite_error_message
     321                $message = $extra_info
    306322            } elseif ($this->extension == 'sqlsrv') { 
    307323                $error_info = sqlsrv_errors(SQLSRV_ERR_ALL); 
     
    315331                'mssql'      => 'MSSQL', 
    316332                'mysql'      => 'MySQL', 
     333                'oracle'     => 'Oracle', 
    317334                'postgresql' => 'PostgreSQL', 
    318335                'sqlite'     => 'SQLite' 
     
    330347     
    331348    /** 
     349     * Clears all of the schema info out of the object and, if set, the fCache object 
     350     *  
     351     * @return void 
     352     */ 
     353    public function clearCache() 
     354    { 
     355        $this->schema_info = array(); 
     356        if ($this->cache) { 
     357            $this->cache->delete($this->makeCachePrefix() . 'schema_info'); 
     358        } 
     359        if ($this->type == 'mssql') { 
     360            $this->determineCharacterSet();      
     361        } 
     362    } 
     363     
     364     
     365    /** 
    332366     * Connects to the database specified if no connection exists 
    333367     *  
     
    341375        // Establish a connection to the database 
    342376        if ($this->extension == 'pdo') { 
     377            $odbc = strtolower(substr($this->database, 0, 4)) == 'dsn:'; 
    343378            if ($this->type == 'mssql') { 
    344                 $odbc = strtolower(substr($this->database, 0, 4)) == 'dsn:'; 
    345                 if ($odbc && in_array('odbc', PDO::getAvailableDrivers())) { 
    346                    try
    347                        $this->connection = new PDO('odbc:' . substr($this->database, 4), $this->username, $this->password)
    348                     } catch (PDOException $e) { 
    349                        $this->connection = FALSE
    350                     } 
     379                if ($odbc) { 
     380                   $dsn = 'odbc:' . substr($this->database, 4); 
     381                } else
     382                    $separator = (fCore::checkOS('windows')) ? ',' : ':'
     383                    $port      = ($this->port) ? $separator . $this->port : ''; 
     384                    $driver    = (fCore::checkOs('windows')) ? 'mssql' : 'dblib'
     385                    $dsn = $driver . ':host=' . $this->host . $port . ';dbname=' . $this->database; 
    351386                } 
    352                 if (!$odbc && in_array('mssql', PDO::getAvailableDrivers())) { 
    353                     try { 
    354                         $separator = (fCore::checkOS('windows')) ? ',' : ':'; 
    355                         $port      = ($this->port) ? $separator . $this->port : ''; 
    356                         $this->connection = new PDO('mssql:host=' . $this->host . $port . ';dbname=' . $this->database, $this->username, $this->password); 
    357                     } catch (PDOException $e) { 
    358                         $this->connection = FALSE; 
    359                     } 
     387                 
     388            } elseif ($this->type == 'mysql') { 
     389                if (substr($this->host, 0, 5) == 'sock:') { 
     390                    $dsn = 'mysql:unix_socket=' . substr($this->host, 5) . ';dbname=' . $this->database;     
     391                } else { 
     392                    $port = ($this->port) ? ';port=' . $this->port : ''; 
     393                    $dsn  = 'mysql:host=' . $this->host . ';dbname=' . $this->database . $port; 
    360394                } 
    361395                 
    362             } elseif ($this->type == 'mysql') { 
    363                 try
    364                     $this->connection = new PDO('mysql:host=' . $this->host . ';dbname=' . $this->database . (($this->port) ? ';port=' . $this->port : ''), $this->username, $this->password); 
    365                    $this->connection->setAttribute(PDO::MYSQL_ATTR_DIRECT_QUERY, 1); 
    366                 } catch (PDOException $e) { 
    367                     $this->connection = FALSE
     396            } elseif ($this->type == 'oracle') { 
     397                if ($odbc)
     398                    $dsn = 'odbc:' . substr($this->database, 4); 
     399                } else { 
     400                   $port = ($this->port) ? ':' . $this->port : ''; 
     401                    $dsn  = 'oci:dbname=' . $this->host . $port . '/' . $this->database . ';charset=AL32UTF8'
    368402                } 
    369403                 
    370404            } elseif ($this->type == 'postgresql') { 
    371                 try { 
    372                    $this->connection = new PDO('pgsql:host=' . $this->host . ' dbname=' . $this->database, $this->username, $this->password)
    373                 } catch (PDOException $e) { 
    374                     $this->connection = FALSE; 
     405                 
     406                $dsn = 'pgsql:dbname=' . $this->database
     407                if ($this->host && $this->host != 'sock:') { 
     408                    $dsn .= ' host=' . $this->host;     
    375409                } 
     410                if ($this->port) { 
     411                    $dsn .= ' port=' . $this->port;  
     412                } 
    376413                 
    377414            } elseif ($this->type == 'sqlite') { 
    378                 try { 
    379                     $this->connection = new PDO('sqlite:' . $this->database); 
    380                 } catch (PDOException $e) { 
    381                     $this->connection = FALSE; 
     415                $dsn = 'sqlite:' . $this->database; 
     416            } 
     417             
     418            try { 
     419                $this->connection = new PDO($dsn, $this->username, $this->password);     
     420                if ($this->type == 'mysql') { 
     421                    $this->connection->setAttribute(PDO::MYSQL_ATTR_DIRECT_QUERY, 1);    
    382422                } 
     423            } catch (PDOException $e) { 
     424                $this->connection = FALSE; 
    383425            } 
    384426        } 
     
    397439         
    398440        if ($this->extension == 'mysql') { 
    399             $this->connection = mysql_connect(($this->port) ? $this->host . ':' . $this->port : $this->host, $this->username, $this->password); 
     441            if (substr($this->host, 0, 5) == 'sock:') { 
     442                $host = substr($this->host, 4); 
     443            } elseif ($this->port) { 
     444                $host = $this->host . ':' . $this->port;     
     445            } else { 
     446                $host = $this->host;     
     447            } 
     448            $this->connection = mysql_connect($host, $this->username, $this->password); 
    400449            if ($this->connection !== FALSE && mysql_select_db($this->database, $this->connection) === FALSE) { 
    401450                $this->connection = FALSE; 
     
    404453             
    405454        if ($this->extension == 'mysqli') { 
    406             if ($this->port) { 
     455            if (substr($this->host, 0, 5) == 'sock:') { 
     456                $this->connection = mysqli_connect('localhost', $this->username, $this->password, $this->database, $this->port, substr($this->host, 5)); 
     457            } elseif ($this->port) { 
    407458                $this->connection = mysqli_connect($this->host, $this->username, $this->password, $this->database, $this->port); 
    408459            } else { 
     
    411462        } 
    412463         
     464        if ($this->extension == 'oci8') { 
     465            $this->connection = oci_connect($this->username, $this->password, $this->host . ($this->port ? ':' . $this->port : '') . '/' . $this->database, 'AL32UTF8'); 
     466        } 
     467         
    413468        if ($this->extension == 'odbc') { 
    414469            $this->connection = odbc_connect(substr($this->database, 4), $this->username, $this->password); 
     
    416471             
    417472        if ($this->extension == 'pgsql') { 
    418             $this->connection = pg_connect("host='" . addslashes($this->host) . "' 
    419                                             dbname='" . addslashes($this->database) . "' 
    420                                             user='" . addslashes($this->username) . "' 
    421                                             password='" . addslashes($this->password) . "'" . 
    422                                             (($this->port) ? " port='" . $this->port . "'" : '')); 
     473            $connection_string = "dbname='" . addslashes($this->database) . "'"; 
     474            if ($this->host && $this->host != 'sock:') { 
     475                $connection_string .= " host='" . addslashes($this->host) . "'";     
     476            } 
     477            if ($this->username) { 
     478                $connection_string .= " user='" . addslashes($this->username) . "'"; 
     479            } 
     480            if ($this->password) { 
     481                $connection_string .= " password='" . addslashes($this->password) . "'"; 
     482            } 
     483            if ($this->port) { 
     484                $connection_string .= " port='" . $this->port . "'"; 
     485            } 
     486            $this->connection = pg_connect($connection_string); 
    423487        } 
    424488         
     
    441505        // Make MySQL act more strict and use UTF-8 
    442506        if ($this->type == 'mysql') { 
    443             $this->query("SET SQL_MODE = 'ANSI'"); 
     507            $this->query("SET SQL_MODE = 'REAL_AS_FLOAT,PIPES_AS_CONCAT,ANSI_QUOTES,IGNORE_SPACE'"); 
    444508            $this->query("SET NAMES 'utf8'"); 
    445509            $this->query("SET CHARACTER SET utf8"); 
     
    453517        // Fix some issues with mssql 
    454518        if ($this->type == 'mssql') { 
     519            if (!isset($this->schema_info['character_set'])) { 
     520                $this->determineCharacterSet(); 
     521            } 
    455522            $this->query('SET TEXTSIZE 65536'); 
    456             $this->character_set = $this->query("SELECT 'WINDOWS-' + CONVERT(VARCHAR, COLLATIONPROPERTY(CONVERT(NVARCHAR, DATABASEPROPERTYEX(%s, 'Collation')), 'CodePage')) AS charset", $this->database)->fetchScalar(); 
    457523        } 
    458524         
     
    461527            $this->query("SET NAMES 'UTF8'"); 
    462528        } 
     529         
     530        // Oracle has different date and timestamp defaults 
     531        if ($this->type == 'oracle') { 
     532            $this->query("ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD'"); 
     533            $this->query("ALTER SESSION SET NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SS'"); 
     534            $this->query("ALTER SESSION SET NLS_TIMESTAMP_TZ_FORMAT = 'YYYY-MM-DD HH24:MI:SS TZR'"); 
     535            $this->query("ALTER SESSION SET NLS_TIME_FORMAT = 'HH24:MI:SS'"); 
     536            $this->query("ALTER SESSION SET NLS_TIME_TZ_FORMAT = 'HH24:MI:SS TZR'"); 
     537        } 
     538    } 
     539     
     540     
     541    /** 
     542     * Determines the character set of a SQL Server database 
     543     *  
     544     * @return void 
     545     */ 
     546    private function determineCharacterSet() 
     547    { 
     548        $this->schema_info['character_set'] = 'WINDOWS-1252'; 
     549        $this->schema_info['character_set'] = $this->query("SELECT 'WINDOWS-' + CONVERT(VARCHAR, COLLATIONPROPERTY(CONVERT(NVARCHAR, DATABASEPROPERTYEX(DB_NAME(), 'Collation')), 'CodePage')) AS charset")->fetchScalar(); 
     550        if ($this->cache) { 
     551            $this->cache->set($this->makeCachePrefix() . 'schema_info', $this->schema_info);     
     552        } 
    463553    } 
    464554     
     
    478568                 
    479569                if ($odbc) { 
    480                     if (class_exists('PDO', FALSE) && in_array('odbc', PDO::getAvailableDrivers())) { 
     570                    if (extension_loaded('odbc')) { 
     571                        $this->extension = 'odbc'; 
     572                         
     573                    } elseif (class_exists('PDO', FALSE) && in_array('odbc', PDO::getAvailableDrivers())) { 
    481574                        $this->extension = 'pdo'; 
    482                          
    483                     } elseif (extension_loaded('odbc')) { 
    484                         $this->extension = 'odbc'; 
    485575                         
    486576                    } else { 
    487577                        $type = 'MSSQL (ODBC)'; 
    488                         $exts = 'pdo_odbc, odbc'; 
     578                        $exts = 'odbc, pdo_odbc'; 
    489579                    } 
    490580                     
     
    493583                        $this->extension = 'sqlsrv'; 
    494584                         
    495                     } elseif (class_exists('PDO', FALSE) && in_array('mssql', PDO::getAvailableDrivers())) { 
    496                         $this->extension = 'pdo'; 
    497                          
    498585                    } elseif (extension_loaded('mssql')) { 
    499586                        $this->extension = 'mssql'; 
    500587                         
     588                    } elseif (class_exists('PDO', FALSE) && (in_array('dblib', PDO::getAvailableDrivers()) || in_array('mssql', PDO::getAvailableDrivers()))) { 
     589                        $this->extension = 'pdo'; 
     590                         
    501591                    } else { 
    502592                        $type = 'MSSQL'; 
    503                         $exts = 'sqlsrv, pdo_dblib, mssql'; 
     593                        $exts = 'mssql, sqlsrv, pdo_dblib (linux), pdo_mssql (windows)'; 
    504594                    } 
    505595                } 
     
    509599            case 'mysql': 
    510600             
    511                 if (class_exists('PDO', FALSE) && in_array('mysql', PDO::getAvailableDrivers())) { 
     601                if (extension_loaded('mysql')) { 
     602                    $this->extension = 'mysql'; 
     603                     
     604                } elseif (class_exists('PDO', FALSE) && in_array('mysql', PDO::getAvailableDrivers())) { 
    512605                    $this->extension = 'pdo'; 
    513606                     
     
    515608                    $this->extension = 'mysqli'; 
    516609                     
    517                 } elseif (extension_loaded('mysql')) { 
    518                     $this->extension = 'mysql'; 
     610                } else { 
     611                    $type = 'MySQL'; 
     612                    $exts = 'mysql, pdo_mysql, mysqli'; 
     613                } 
     614                break; 
     615                 
     616                 
     617            case 'oracle': 
     618             
     619                $odbc = strtolower(substr($this->database, 0, 4)) == 'dsn:'; 
     620                 
     621                if ($odbc) { 
     622                    if (extension_loaded('odbc')) { 
     623                        $this->extension = 'odbc'; 
     624                         
     625                    } elseif (class_exists('PDO', FALSE) && in_array('odbc', PDO::getAvailableDrivers())) { 
     626                        $this->extension = 'pdo'; 
     627                         
     628                    } else { 
     629                        $type = 'Oracle (ODBC)'; 
     630                        $exts = 'odbc, pdo_odbc'; 
     631                    } 
    519632                     
    520633                } else { 
    521                     $type = 'MySQL'; 
    522                     $exts = 'mysql, mysqli, pdo_mysql'; 
     634                    if (extension_loaded('oci8')) { 
     635                        $this->extension = 'oci8'; 
     636                         
     637                    } elseif (class_exists('PDO', FALSE) && in_array('oci', PDO::getAvailableDrivers())) { 
     638                        $this->extension = 'pdo'; 
     639                         
     640                    } else { 
     641                        $type = 'Oracle'; 
     642                        $exts = 'oci8, pdo_oci'; 
     643                    } 
    523644                } 
    524645                break; 
    525                 
    526                 
     646             
     647             
    527648            case 'postgresql': 
    528649             
    529                 if (class_exists('PDO', FALSE) && in_array('pgsql', PDO::getAvailableDrivers())) { 
     650                if (extension_loaded('pgsql')) { 
     651                    $this->extension = 'pgsql'; 
     652                     
     653                } elseif (class_exists('PDO', FALSE) && in_array('pgsql', PDO::getAvailableDrivers())) { 
    530654                    $this->extension = 'pdo'; 
    531                      
    532                 } elseif (extension_loaded('pgsql')) { 
    533                     $this->extension = 'pgsql'; 
    534655                     
    535656                } else { 
     
    573694                    ); 
    574695                 
    575                 } elseif ($sqlite_version == 2 && extension_loaded('sqlite')) { 
     696                } elseif ((!$sqlite_version || $sqlite_version == 2) && extension_loaded('sqlite')) { 
    576697                    $this->extension = 'sqlite'; 
    577698                     
     
    601722     
    602723    /** 
     724     * Sets the schema info to be cached to the fCache object specified 
     725     *  
     726     * @param  fCache $cache  The cache to cache to 
     727     * @return void 
     728     */ 
     729    public function enableCaching($cache) 
     730    { 
     731        $this->cache = $cache; 
     732         
     733        $this->schema_info = $this->cache->get($this->makeCachePrefix() . 'schema_info', array()); 
     734    } 
     735     
     736     
     737    /** 
    603738     * Sets if debug messages should be shown 
    604739     *  
     
    609744    { 
    610745        $this->debug = (boolean) $flag; 
    611         if ($this->translation) { 
    612             $this->translation->enableDebugging($this->debug); 
    613         } 
    614746    } 
    615747     
     
    660792     *  
    661793     * @param  string $sql_or_type  This can either be the data type to escape or an SQL string with a data type placeholder - see method description 
    662      * @param  mixed  $value        The value to escape - you should pass a single value if a data type is specified or a value for each placeholder 
     794     * @param  mixed  $value        The value to escape - you should pass a single value or an array of values if a data type is specified, or a value for each placeholder 
    663795     * @param  mixed  ... 
    664      * @return string  The escaped value/SQL 
     796     * @return mixed  The escaped value/SQL or an array of the escaped values 
    665797     */ 
    666798    public function escape($sql_or_type, $value) 
     
    686818        $values = $new_values; 
    687819         
     820        $value = array_shift($values); 
     821         
    688822        // Handle single value escaping 
    689         $value = array_shift($values)
     823        $callback = NULL
    690824         
    691825        switch ($sql_or_type) { 
    692826            case 'blob': 
    693827            case '%l': 
    694                 return $this->escapeBlob($value); 
     828                $callback = $this->escapeBlob; 
     829                break; 
    695830            case 'boolean': 
    696831            case '%b': 
    697                 return $this->escapeBoolean($value); 
     832                $callback = $this->escapeBoolean; 
     833                break; 
    698834            case 'date': 
    699835            case '%d': 
    700                 return $this->escapeDate($value); 
     836                $callback = $this->escapeDate; 
     837                break; 
    701838            case 'float': 
    702839            case '%f': 
    703                 return $this->escapeFloat($value); 
     840                $callback = $this->escapeFloat; 
     841                break; 
    704842            case 'integer': 
    705843            case '%i': 
    706                 return $this->escapeInteger($value); 
     844                $callback = $this->escapeInteger; 
     845                break; 
    707846            case 'string': 
    708847            case 'varchar': 
     
    710849            case 'text': 
    711850            case '%s': 
    712                 return $this->escapeString($value); 
     851                $callback = $this->escapeString; 
     852                break; 
    713853            case 'time': 
    714854            case '%t': 
    715                 return $this->escapeTime($value); 
     855                $callback = $this->escapeTime; 
     856                break; 
    716857            case 'timestamp': 
    717858            case '%p': 
    718                 return $this->escapeTimestamp($value); 
     859                $callback = $this->escapeTimestamp; 
     860                break; 
     861        } 
     862         
     863        if ($callback) { 
     864            if (is_array($value)) { 
     865                return array_map($callback, $value);         
     866            } 
     867            return call_user_func($callback, $value); 
    719868        }    
    720869         
    721         // Handle SQL escaping 
    722         preg_match_all("#(?:'(?:''|\\\\'|\\\\[^']|[^'\\\\]+)*')|(?:[^']+)#", $sql_or_type, $matches); 
     870        // Fix \' in MySQL and PostgreSQL 
     871        if(($this->type == 'mysql' || $this->type == 'postgresql') && strpos($sql_or_type, '\\') !== FALSE) { 
     872            $sql_or_type = preg_replace("#(?<!\\\\)((\\\\{2})*)\\\\'#", "\\1''", $sql_or_type);  
     873        } 
     874         
     875        // Separate the SQL from quoted values 
     876        preg_match_all("#(?:'([^']*(?:'')*)*?')|(?:[^']+)#", $sql_or_type, $matches); 
    723877         
    724878        $temp_sql = ''; 
     
    818972        $this->connectToDatabase(); 
    819973         
    820         if ($this->extension == 'mysql') { 
    821             return "'" . mysql_real_escape_string($value, $this->connection) . "'"; 
    822         } elseif ($this->extension == 'mysqli') { 
    823             return "'" . mysqli_real_escape_string($this->connection, $value) . "'"; 
    824         } elseif ($this->extension == 'pgsql') { 
    825             return "'" . pg_escape_bytea($this->connection, $value) . "'"; 
     974        if ($this->type == 'mysql') { 
     975            return "x'" . bin2hex($value) . "'"; 
     976             
     977        } elseif ($this->type == 'postgresql') { 
     978            $output = ''; 
     979            for ($i=0; $i<strlen($value); $i++) { 
     980                $output .= '\\\\' . str_pad(decoct(ord($value[$i])), 3, '0', STR_PAD_LEFT); 
     981            } 
     982            return "E'" . $output . "'"; 
     983             
     984        } elseif ($this->extension == 'sqlite') { 
     985            return "'" . bin2hex($value) . "'"; 
     986             
    826987        } elseif ($this->type == 'sqlite') { 
    827988            return "X'" . bin2hex($value) . "'"; 
     989             
    828990        } elseif ($this->type == 'mssql') { 
    829991            return '0x' . bin2hex($value); 
    830         } elseif ($this->extension == 'pdo') { 
    831             return $this->connection->quote($value, PDO::PARAM_LOB); 
     992             
     993        } elseif ($this->type == 'oracle') { 
     994            return "'" . bin2hex($value) . "'"; 
    832995        } 
    833996    } 
     
    8521015        } elseif (in_array($this->type, array('mssql', 'sqlite'))) { 
    8531016            return ($value) ? "'1'" : "'0'"; 
     1017        } elseif ($this->type == 'oracle') { 
     1018            return ($value) ? '1' : '0';     
    8541019        } 
    8551020    } 
     
    9461111        } elseif ($this->extension == 'sqlite') { 
    9471112            return "'" . sqlite_escape_string($value) . "'"; 
    948          
     1113        } elseif ($this->type == 'oracle') { 
     1114            return "'" . str_replace("'", "''", $value) . "'"; 
     1115             
    9491116        } elseif ($this->type == 'mssql') { 
    9501117             
    9511118            // If there are any non-ASCII characters, we need to escape 
    9521119            if (preg_match('#[^\x00-\x7F]#', $value)) { 
    953                 $characters = preg_split('##u', $value); 
    954                 $output = "'"; 
    955                 foreach ($characters as $character) { 
     1120                preg_match_all('#.|^\z#us', $value, $characters); 
     1121                $output    = ""; 
     1122                $last_type = NULL; 
     1123                foreach ($characters[0] as $character) { 
    9561124                    if (strlen($character) > 1) { 
    9571125                        $b = array_map('ord', str_split($character)); 
     
    9741142                                continue; 
    9751143                        } 
    976                         $output .= "'+NCHAR(" . bindec($bin) . ")+'"; 
     1144                        if ($last_type == 'nchar') { 
     1145                            $output .= '+'; 
     1146                        } elseif ($last_type == 'char') { 
     1147                            $output .= "'+"; 
     1148                        }        
     1149                        $output .= "NCHAR(" . bindec($bin) . ")"; 
     1150                        $last_type = 'nchar'; 
    9771151                    } else { 
     1152                        if (!$last_type) { 
     1153                            $output .= "'"; 
     1154                        } elseif ($last_type == 'nchar') { 
     1155                            $output .= "+'";     
     1156                        } 
    9781157                        $output .= $character; 
    9791158                        // Escape single quotes 
    980                         if ($character = "'") { 
     1159                        if ($character == "'") { 
    9811160                            $output .= "'"; 
    9821161                        } 
     1162                        $last_type = 'char'; 
    9831163                    } 
    9841164                } 
    985                 $output .= "'"; 
     1165                if ($last_type == 'char') { 
     1166                    $output .= "'"; 
     1167                } elseif (!$last_type) { 
     1168                    $output .= "''";     
     1169                } 
    9861170             
    9871171            // ASCII text is normal 
     
    10151199            return 'NULL'; 
    10161200        } 
     1201        if ($this->type == 'mssql' || $this->type == 'oracle') { 
     1202            return "'" . date('1970-01-01 H:i:s', strtotime($value)) . "'";  
     1203        } 
    10171204        return "'" . date('H:i:s', strtotime($value)) . "'"; 
    10181205    } 
     
    10471234    private function executeQuery($result) 
    10481235    { 
     1236        // We don't want errors and an exception 
    10491237        $old_level = error_reporting(error_reporting() & ~E_WARNING); 
    10501238         
    10511239        if ($this->extension == 'mssql') { 
    10521240            $result->setResult(mssql_query($result->getSQL(), $this->connection)); 
     1241             
    10531242        } elseif ($this->extension == 'mysql') { 
    10541243            $result->setResult(mysql_query($result->getSQL(), $this->connection)); 
     1244             
    10551245        } elseif ($this->extension == 'mysqli') { 
    10561246            $result->setResult(mysqli_query($this->connection, $result->getSQL())); 
     1247             
     1248        } elseif ($this->extension == 'oci8') { 
     1249            $oci_statement = oci_parse($this->connection, $result->getSQL()); 
     1250            if (oci_execute($oci_statement, $this->inside_transaction ? OCI_DEFAULT : OCI_COMMIT_ON_SUCCESS)) { 
     1251                oci_fetch_all($oci_statement, $rows, 0, -1, OCI_FETCHSTATEMENT_BY_ROW + OCI_ASSOC); 
     1252                $result->setResult($rows); 
     1253                unset($rows);    
     1254            } else { 
     1255                $result->setResult(FALSE); 
     1256            } 
     1257             
    10571258        } elseif ($this->extension == 'odbc') { 
    1058             $rows = array(); 
    10591259            $resource = odbc_exec($this->connection, $result->getSQL()); 
    10601260            if (is_resource($resource)) { 
     1261                $rows = array(); 
    10611262                // Allow up to 1MB of binary data 
    10621263                odbc_longreadlen($resource, 1048576); 
     
    10661267                } 
    10671268                $result->setResult($rows); 
     1269                unset($rows); 
    10681270            } else { 
    10691271                $result->setResult($resource); 
    10701272            } 
     1273             
    10711274        } elseif ($this->extension == 'pgsql') { 
    10721275            $result->setResult(pg_query($this->connection, $result->getSQL())); 
     1276             
    10731277        } elseif ($this->extension == 'sqlite') { 
    10741278            $result->setResult(sqlite_query($this->connection, $result->getSQL(), SQLITE_ASSOC, $sqlite_error_message)); 
     1279             
    10751280        } elseif ($this->extension == 'sqlsrv') { 
    1076             $rows = array(); 
    10771281            $resource = sqlsrv_query($this->connection, $result->getSQL()); 
    10781282            if (is_resource($resource)) { 
     1283                $rows = array(); 
    10791284                while ($row = sqlsrv_fetch_array($resource, SQLSRV_FETCH_ASSOC)) { 
    10801285                    $rows[] = $row; 
    10811286                } 
    10821287                $result->setResult($rows); 
     1288                unset($rows); 
    10831289            } else { 
    10841290                $result->setResult($resource); 
    10851291            } 
     1292             
    10861293        } elseif ($this->extension == 'pdo') { 
    1087             $pdo_statement = $this->connection->query($result->getSQL()); 
    1088             $result->setResult((is_object($pdo_statement)) ? $pdo_statement->fetchAll(PDO::FETCH_ASSOC) : $pdo_statement); 
     1294            if (preg_match('#^\s*CREATE(\s+OR\s+REPLACE)?\s+TRIGGER#i', $result->getSQL())) { 
     1295                $this->connection->exec($result->getSQL()); 
     1296                $pdo_statement = FALSE; 
     1297                $returned_rows = array(); 
     1298            } else { 
     1299                $pdo_statement = $this->connection->query($result->getSQL()); 
     1300                $returned_rows = (is_object($pdo_statement)) ? $pdo_statement->fetchAll(PDO::FETCH_ASSOC) : $pdo_statement; 
     1301                 
     1302                // The pdo_pgsql driver likes to return empty rows equal to the number of affected rows for insert and deletes 
     1303                if ($this->type == 'postgresql' && $returned_rows && $returned_rows[0] == array()) { 
     1304                    $returned_rows = array();        
     1305                } 
     1306            } 
     1307             
     1308            $result->setResult($returned_rows); 
    10891309        } 
    10901310         
    10911311        error_reporting($old_level); 
     1312         
    10921313         
    10931314        if ($this->extension == 'sqlite') { 
    10941315            $this->checkForError($result, $sqlite_error_message); 
     1316        } elseif ($this->extension == 'oci8') { 
     1317            $this->checkForError($result, $oci_statement); 
    10951318        } else { 
    10961319            $this->checkForError($result); 
    10971320        } 
    10981321         
     1322         
    10991323        if ($this->extension == 'pdo') { 
    11001324            $this->setAffectedRows($result, $pdo_statement); 
    1101             $pdo_statement->closeCursor(); 
     1325            if ($pdo_statement) { 
     1326                $pdo_statement->closeCursor(); 
     1327            } 
    11021328            unset($pdo_statement); 
     1329        } elseif ($this->extension == 'oci8') { 
     1330            $this->setAffectedRows($result, $oci_statement); 
     1331            oci_free_statement($oci_statement); 
    11031332        } elseif ($this->extension == 'odbc') { 
    11041333            $this->setAffectedRows($result, $resource); 
     
    11311360        } elseif ($this->extension == 'mysql') { 
    11321361            $result->setResult(mysql_unbuffered_query($result->getSQL(), $this->connection)); 
    1133         } elseif ($this->extension == 'mysqli') { 
     1362        } elseif ($this->extension == 'mysqli') {  
    11341363            $result->setResult(mysqli_query($this->connection, $result->getSQL(), MYSQLI_USE_RESULT)); 
     1364        } elseif ($this->extension == 'oci8') { 
     1365            $oci_statement = oci_parse($this->connection, $result->getSQL()); 
     1366            $result->setResult(oci_execute($oci_statement, $this->inside_transaction ? OCI_DEFAULT : OCI_COMMIT_ON_SUCCESS) ? $oci_statement : FALSE); 
    11351367        } elseif ($this->extension == 'odbc') { 
    11361368            $result->setResult(odbc_exec($this->connection, $result->getSQL())); 
     
    11491381        if ($this->extension == 'sqlite') { 
    11501382            $this->checkForError($result, $sqlite_error_message); 
     1383        } elseif ($this->extension == 'oci8') { 
     1384            $this->checkForError($result, $oci_statement); 
    11511385        } else { 
    11521386            $this->checkForError($result); 
     
    11661400         
    11671401        // Separate the SQL from quoted values 
    1168         preg_match_all("#(?:'(?:''|\\\\'|\\\\[^']|[^'\\\\]*)*')|(?:[^']+)#", $sql, $matches); 
     1402        preg_match_all("#(?:'([^']*(?:'')*)*?')|(?:[^']+)#", $sql, $matches); 
    11691403         
    11701404        $cur_sql = ''; 
     
    11751409                $cur_sql .= $match; 
    11761410             
    1177             // Handle the SQL 
     1411            // Handle the SQL, exploding on any ; that isn't escaped with a \ 
    11781412            } else { 
    1179                 $sql_strings = explode(';', $match); 
     1413                $sql_strings = preg_split('#(?<!\\\\);#', $match); 
    11801414                $cur_sql .= $sql_strings[0]; 
    11811415                for ($i=1; $i < sizeof($sql_strings); $i++) { 
    1182                     // SQLite triggers have a ; before and after the "end" 
    1183                     if (strtolower(trim($sql_strings[$i])) == 'end') { 
    1184                         $cur_sql .= "; END"; 
    1185                         $i++; 
    1186                         if ($i >= sizeof($sql_strings)) { 
    1187                             break; 
    1188                         } 
    1189                     } 
    11901416                    $cur_sql = trim($cur_sql); 
    11911417                    if ($cur_sql) { 
     
    12051431     
    12061432    /** 
     1433     * Returns the database connection resource or object 
     1434     *  
     1435     * @return mixed  The database connection 
     1436     */ 
     1437    public function getConnection() 
     1438    { 
     1439        $this->connectToDatabase(); 
     1440        return $this->connection; 
     1441    } 
     1442     
     1443     
     1444    /** 
    12071445     * Gets the name of the database currently connected to 
    12081446     *  
     
    12291467     
    12301468    /** 
     1469     * Gets the host for this database 
     1470     *  
     1471     * @return string  The host 
     1472     */ 
     1473    public function getHost() 
     1474    { 
     1475        return $this->host; 
     1476    } 
     1477     
     1478     
     1479    /** 
     1480     * Gets the port for this database 
     1481     *  
     1482     * @return string  The port 
     1483     */ 
     1484    public function getPort() 
     1485    { 
     1486        return $this->port; 
     1487    } 
     1488     
     1489     
     1490    /** 
     1491     * Gets the fSQLTranslation object used for translated queries 
     1492     *  
     1493     * @return fSQLTranslation  The SQL translation object 
     1494     */ 
     1495    public function getSQLTranslation() 
     1496    { 
     1497        if (!$this->translation) { new fSQLTranslation($this); } 
     1498        return $this->translation;   
     1499    } 
     1500     
     1501     
     1502    /** 
    12311503     * Gets the database type 
    12321504     *  
     
    12361508    { 
    12371509        return $this->type; 
     1510    } 
     1511     
     1512     
     1513    /** 
     1514     * Gets the username for this database 
     1515     *  
     1516     * @return string  The username 
     1517     */ 
     1518    public function getUsername() 
     1519    { 
     1520        return $this->username; 
    12381521    } 
    12391522     
     
    12471530    private function handleAutoIncrementedValue($result) 
    12481531    { 
    1249         if (!preg_match('#^\s*INSERT#i', $result->getSQL())) { 
     1532        if (!preg_match('#^\s*INSERT\s+INTO\s+(\w+)#i', $result->getSQL(), $table_match)) { 
    12501533            $result->setAutoIncrementedValue(NULL); 
    12511534            return; 
    12521535        } 
     1536        $table = strtolower($table_match[1]); 
    12531537         
    12541538        $insert_id = NULL; 
     1539         
     1540        if ($this->type == 'oracle') { 
     1541            if (!isset($this->schema_info['sequences'])) { 
     1542                $sql = "SELECT 
     1543                                TABLE_NAME, 
     1544                                TRIGGER_BODY 
     1545                            FROM 
     1546                                USER_TRIGGERS 
     1547                            WHERE 
     1548                                TRIGGERING_EVENT = 'INSERT' AND 
     1549                                STATUS = 'ENABLED' AND 
     1550                                TRIGGER_NAME NOT LIKE 'BIN\$%'"; 
     1551                                 
     1552                $this->schema_info['sequences'] = array(); 
     1553                 
     1554                foreach ($this->query($sql) as $row) { 
     1555                    if (preg_match('#SELECT\s+(\w+).nextval\s+INTO\s+:new\.(\w+)\s+FROM\s+dual#i', $row['trigger_body'], $matches)) { 
     1556                        $this->schema_info['sequences'][strtolower($row['table_name'])] = array('sequence' => $matches[1], 'column' => $matches[2]); 
     1557                    } 
     1558                } 
     1559                 
     1560                if ($this->cache) { 
     1561                    $this->cache->set($this->makeCachePrefix() . 'schema_info', $this->schema_info);     
     1562                } 
     1563            } 
     1564             
     1565            if (!isset($this->schema_info['sequences'][$table]) || preg_match('#INSERT\s+INTO\s+' . preg_quote($table, '#') . '\s+VALUES\s+\([^\)]*?\b' . preg_quote($this->schema_info['sequences'][$table]['column'], '#') . '\b#i', $result->getSQL())) { 
     1566                return;  
     1567            } 
     1568             
     1569            $insert_id_sql = "SELECT " . $this->schema_info['sequences'][$table]['sequence'] . ".currval AS INSERT_ID FROM dual"; 
     1570        } 
     1571         
     1572        if ($this->type == 'postgresql') { 
     1573            if (!isset($this->schema_info['sequences'])) { 
     1574                $sql = "SELECT 
     1575                                pg_class.relname AS table_name, 
     1576                                pg_attribute.attname AS column 
     1577                            FROM 
     1578                                pg_attribute INNER JOIN 
     1579                                pg_class ON pg_attribute.attrelid = pg_class.oid INNER JOIN 
     1580                                pg_attrdef ON pg_class.oid = pg_attrdef.adrelid AND pg_attribute.attnum = pg_attrdef.adnum 
     1581                            WHERE 
     1582                                NOT pg_attribute.attisdropped AND 
     1583                                pg_attrdef.adsrc LIKE 'nextval(%'"; 
     1584                                 
     1585                $this->schema_info['sequences'] = array(); 
     1586                 
     1587                foreach ($this->query($sql) as $row) { 
     1588                    $this->schema_info['sequences'][strtolower($row['table_name'])] = $row['column']; 
     1589                } 
     1590                 
     1591                if ($this->cache) { 
     1592                    $this->cache->set($this->makeCachePrefix() . 'schema_info', $this->schema_info);     
     1593                }    
     1594            } 
     1595             
     1596            if (!isset($this->schema_info['sequences'][$table]) || preg_match('#INSERT\s+INTO\s+' . preg_quote($table, '#') . '\s+VALUES\s+\([^\)]*?\b' . preg_quote($this->schema_info['sequences'][$table], '#') . '\b#i', $result->getSQL())) { 
     1597                return; 
     1598            }        
     1599        } 
    12551600         
    12561601        if ($this->extension == 'mssql') { 
     
    12651610            $insert_id     = mysqli_insert_id($this->connection); 
    12661611         
    1267         } elseif ($this->extension == 'odbc') { 
    1268             $insert_id_res = odbc_exec("SELECT @@IDENTITY AS insert_id", $this->connection); 
     1612        } elseif ($this->extension == 'oci8') { 
     1613            $oci_statement = oci_parse($this->connection, $insert_id_sql); 
     1614            oci_execute($oci_statement); 
     1615            $insert_id_row = oci_fetch_array($oci_statement, OCI_ASSOC); 
     1616            $insert_id = $insert_id_row['INSERT_ID']; 
     1617            oci_free_statement($oci_statement); 
     1618         
     1619        } elseif ($this->extension == 'odbc' && $this->type == 'mssql') { 
     1620            $insert_id_res = odbc_exec($this->connection, "SELECT @@IDENTITY AS insert_id"); 
    12691621            $insert_id     = odbc_result($insert_id_res, 'insert_id'); 
    12701622            odbc_free_result($insert_id_res); 
    12711623         
     1624        } elseif ($this->extension == 'odbc' && $this->type == 'oracle') { 
     1625            $insert_id_res = odbc_exec($this->connection, $insert_id_sql); 
     1626            $insert_id     = odbc_result($insert_id_res, 'insert_id'); 
     1627            odbc_free_result($insert_id_res); 
     1628         
    12721629        } elseif ($this->extension == 'pgsql') { 
    12731630             
    1274             if (!$this->isInsideTransaction()) { 
    1275                 pg_query($this->connection, "BEGIN"); 
    1276             } else { 
    1277                 pg_query($this->connection, "SAVEPOINT get_last_val"); 
    1278             } 
    1279              
    1280             $old_level = error_reporting(error_reporting() & ~E_WARNING); 
    12811631            $insert_id_res = pg_query($this->connection, "SELECT lastval()"); 
    1282             error_reporting($old_level); 
    1283              
    1284             if (is_resource($insert_id_res)) { 
    1285                 $insert_id_row = pg_fetch_assoc($insert_id_res); 
    1286                 $insert_id = array_shift($insert_id_row); 
    1287                 pg_free_result($insert_id_res); 
    1288                  
    1289                 if (!$this->isInsideTransaction()) { 
    1290                     pg_query($this->connection, "COMMIT"); 
    1291                 } 
    1292                  
    1293             } else { 
    1294                 if (!$this->isInsideTransaction()) { 
    1295                     pg_query($this->connection, "ROLLBACK"); 
    1296                 } else { 
    1297                     pg_query($this->connection, "ROLLBACK TO get_last_val"); 
    1298                 } 
    1299             } 
     1632            $insert_id_row = pg_fetch_assoc($insert_id_res); 
     1633            $insert_id = array_shift($insert_id_row); 
     1634            pg_free_result($insert_id_res); 
    13001635         
    13011636        } elseif ($this->extension == 'sqlite') { 
     
    13041639        } elseif ($this->extension == 'sqlsrv') { 
    13051640            $insert_id_res = sqlsrv_query($this->connection, "SELECT @@IDENTITY AS insert_id"); 
    1306             $insert_id     = sqlsrv_get_field($insert_id_res, 0); 
     1641            $insert_id_row = sqlsrv_fetch_array($insert_id_res, SQLSRV_FETCH_ASSOC); 
     1642            $insert_id     = $insert_id_row['insert_id']; 
    13071643            sqlsrv_free_stmt($insert_id_res); 
    13081644         
     
    13241660                        // If there was an error we don't have an insert id 
    13251661                    } 
    1326                  
    1327                 case 'postgresql': 
     1662                    break; 
     1663                     
     1664                case 'oracle': 
    13281665                    try { 
    1329                          
    1330                         if (!$this->isInsideTransaction()) { 
    1331                             $this->connection->beginTransaction(); 
    1332                         } else { 
    1333                             $this->connection->query("SAVEPOINT get_last_val"); 
    1334                         } 
    1335                          
    1336                         $insert_id_statement = $this->connection->query("SELECT lastval()"); 
     1666                        $insert_id_statement = $this->connection->query($insert_id_sql); 
    13371667                        if (!$insert_id_statement) { 
    13381668                            throw new Exception(); 
     
    13421672                        $insert_id = array_shift($insert_id_row); 
    13431673                         
    1344                         if (!$this->isInsideTransaction()) { 
    1345                             $this->connection->commit(); 
    1346                         } 
    1347                          
    13481674                    } catch (Exception $e) { 
    1349                          
    1350                         if (!$this->isInsideTransaction()) { 
    1351                             $this->connection->rollBack(); 
    1352                         } else { 
    1353                             $this->connection->exec("ROLLBACK TO get_last_val"); 
    1354                         } 
    1355                          
     1675                        // If there was an error we don't have an insert id 
    13561676                    } 
     1677                    break; 
     1678                 
     1679                case 'postgresql': 
     1680                     
     1681                    $insert_id_statement = $this->connection->query("SELECT lastval()"); 
     1682                    $insert_id_row = $insert_id_statement->fetch(PDO::FETCH_ASSOC); 
     1683                    $insert_id = array_shift($insert_id_row); 
     1684                    $insert_id_statement->closeCursor(); 
     1685                    unset($insert_id_statement); 
     1686                     
    13571687                    break; 
    13581688         
     
    13721702     
    13731703    /** 
    1374      * Will hand off a transaction query to the PDO method if the current DB connection is via PDO 
    1375      *  
    1376      * @param  string $sql           The SQL to check for a transaction query 
     1704     * Makes sure each database and extension handles BEGIN, COMMIT and ROLLBACK  
     1705     *  
     1706     * @param  string &$sql          The SQL to check for a transaction query 
    13771707     * @param  string $result_class  The type of result object to create 
    1378      * @return mixed  If the connection is not via PDO will return `FALSE`, otherwise an object of the type $result_class 
    1379      */ 
    1380     private function handleTransactionQueries($sql, $result_class) 
    1381     { 
    1382         if (!is_object($this->connection) || get_class($this->connection) != 'PDO') { 
     1708     * @return mixed  `FALSE` if normal processing should continue, otherwise an object of the type $result_class 
     1709     */ 
     1710    private function handleTransactionQueries(&$sql, $result_class) 
     1711    { 
     1712        // SQL Server supports transactions, but starts then with BEGIN TRANSACTION 
     1713        if ($this->type == 'mssql' && preg_match('#^\s*(begin|start(\s+transaction)?)\s*#i', $sql)) { 
     1714            $sql = 'BEGIN TRANSACTION'; 
     1715        } 
     1716         
     1717        $begin    = FALSE; 
     1718        $commit   = FALSE; 
     1719        $rollback = FALSE; 
     1720         
     1721        // Track transactions since most databases don't support nesting 
     1722        if (preg_match('#^\s*(begin|start)(\s+(transaction|work))?\s*$#iD', $sql)) { 
     1723            if ($this->inside_transaction) { 
     1724                throw new fProgrammerException('A transaction is already in progress'); 
     1725            } 
     1726            $this->inside_transaction = TRUE; 
     1727            $begin = TRUE; 
     1728             
     1729        } elseif (preg_match('#^\s*(commit)(\s+(transaction|work))?\s*$#iD', $sql)) { 
     1730            if (!$this->inside_transaction) { 
     1731                throw new fProgrammerException('There is no transaction in progress'); 
     1732            } 
     1733            $this->inside_transaction = FALSE; 
     1734            $commit = TRUE; 
     1735             
     1736        } elseif (preg_match('#^\s*(rollback)(\s+(transaction|work))?\s*$#iD', $sql)) { 
     1737            if (!$this->inside_transaction) { 
     1738                throw new fProgrammerException('There is no transaction in progress'); 
     1739            } 
     1740            $this->inside_transaction = FALSE; 
     1741            $rollback = TRUE; 
     1742        } 
     1743         
     1744        if (!$begin && !$commit && !$rollback) { 
     1745            return FALSE;    
     1746        } 
     1747         
     1748        // The PDO, OCI8, ODBC and SQLSRV extensions require special handling through methods and functions 
     1749        $is_pdo    = $this->extension == 'pdo'; 
     1750        $is_oci    = $this->extension == 'oci8'; 
     1751        $is_odbc   = $this->extension == 'odbc'; 
     1752        $is_sqlsrv = $this->extension == 'sqlsrv'; 
     1753         
     1754        if (!$is_pdo && !$is_oci && !$is_odbc && !$is_sqlsrv) { 
    13831755            return FALSE; 
    13841756        } 
    13851757         
    1386         $success = FALSE; 
    1387          
    1388         try { 
    1389             if (preg_match('#^\s*(begin|start)(\s+transaction)?\s*$#iD', $sql)) { 
    1390                 $this->connection->beginTransaction(); 
    1391                 $success = TRUE; 
    1392             } 
    1393             if (preg_match('#^\s*(commit)(\s+transaction)?\s*$#iD', $sql)) { 
    1394                 $this->connection->commit(); 
    1395                 $success = TRUE; 
    1396             } 
    1397             if (preg_match('#^\s*(rollback)(\s+transaction)?\s*$#iD', $sql)) { 
    1398                 $this->connection->rollBack(); 
    1399                 $success = TRUE; 
    1400             } 
    1401         } catch (Exception $e) { 
    1402             $db_type_map = array( 
    1403                 'mssql'      => 'MSSQL', 
    1404                 'mysql'      => 'MySQL', 
    1405                 'postgresql' => 'PostgreSQL', 
    1406                 'sqlite'     => 'SQLite' 
    1407             ); 
    1408              
    1409             throw new fSQLException( 
    1410                 '%1$s error (%2$s) in %3$s', 
    1411                 $db_type_map[$this->type], 
    1412                 $e->getMessage(), 
    1413                 $sql 
    1414             ); 
    1415         } 
    1416          
    1417         if ($success) { 
    1418             $result = new $result_class($this->type, $this->extension); 
    1419             $result->setSQL($sql); 
    1420             $result->setResult(TRUE); 
    1421             return $result; 
    1422         } 
    1423          
    1424         return FALSE; 
     1758        // PDO seems to act weird if you try to start transactions through a normal query call 
     1759        if ($is_pdo) { 
     1760            try { 
     1761                $is_mssql  = $this->type == 'mssql'  && substr($this->database, 0, 4) != 'dsn:'; 
     1762                $is_oracle = $this->type == 'oracle' && substr($this->database, 0, 4) != 'dsn:'; 
     1763                if ($begin) { 
     1764                    // The SQL Server PDO object hasn't implemented transactions 
     1765                    if ($is_mssql) { 
     1766                        $this->connection->exec('BEGIN TRANSACTION'); 
     1767                    } elseif ($is_oracle) { 
     1768                        $this->connection->setAttribute(PDO::ATTR_AUTOCOMMIT, FALSE); 
     1769                    } else { 
     1770                        $this->connection->beginTransaction(); 
     1771                    } 
     1772                 
     1773                } elseif ($commit) { 
     1774                    if ($is_mssql) { 
     1775                        $this->connection->exec('COMMIT'); 
     1776                    } elseif ($is_oracle) { 
     1777                        $this->connection->exec('COMMIT'); 
     1778                        $this->connection->setAttribute(PDO::ATTR_AUTOCOMMIT, TRUE); 
     1779                    } else  { 
     1780                        $this->connection->commit(); 
     1781                    } 
     1782                 
     1783                } elseif ($rollback) { 
     1784                    if ($is_mssql) { 
     1785                        $this->connection->exec('ROLLBACK'); 
     1786                    } elseif ($is_oracle) {                  
     1787                        $this->connection->exec('ROLLBACK'); 
     1788                        $this->connection->setAttribute(PDO::ATTR_AUTOCOMMIT, TRUE); 
     1789                    } else { 
     1790                        $this->connection->rollBack(); 
     1791                    } 
     1792                } 
     1793                 
     1794            } catch (Exception $e) { 
     1795                 
     1796                $db_type_map = array( 
     1797                    'mssql'      => 'MSSQL', 
     1798                    'mysql'      => 'MySQL', 
     1799                    'oracle'     => 'Oracle', 
     1800                    'postgresql' => 'PostgreSQL', 
     1801                    'sqlite'     => 'SQLite' 
     1802                ); 
     1803                 
     1804                throw new fSQLException( 
     1805                    '%1$s error (%2$s) in %3$s', 
     1806                    $db_type_map[$this->type], 
     1807                    $e->getMessage(), 
     1808                    $sql 
     1809                ); 
     1810            } 
     1811         
     1812        } elseif ($is_oci) { 
     1813            if ($commit) { 
     1814                oci_commit($this->connection); 
     1815            } elseif ($rollback) { 
     1816                oci_rollback($this->connection); 
     1817            } 
     1818         
     1819        } elseif ($is_odbc) { 
     1820            if ($begin) { 
     1821                odbc_autocommit($this->connection, FALSE); 
     1822            } elseif ($commit) { 
     1823                odbc_commit($this->connection); 
     1824                odbc_autocommit($this->connection, TRUE); 
     1825            } elseif ($rollback) { 
     1826                odbc_rollback($this->connection); 
     1827                odbc_autocommit($this->connection, TRUE); 
     1828            } 
     1829             
     1830        } elseif ($is_sqlsrv) { 
     1831            if ($begin) { 
     1832                sqlsrv_begin_transaction($this->connection); 
     1833            } elseif ($commit) { 
     1834                sqlsrv_commit($this->connection); 
     1835            } elseif ($rollback) { 
     1836                sqlsrv_rollback($this->connection); 
     1837            }    
     1838        } 
     1839         
     1840        $result = new $result_class($this->type, $this->extension); 
     1841        $result->setSQL($sql); 
     1842        $result->setResult(TRUE); 
     1843        return $result; 
     1844    } 
     1845     
     1846     
     1847    /** 
     1848     * Injects an fSQLTranslation object to handle translation 
     1849     *  
     1850     * @internal 
     1851     *  
     1852     * @param  fSQLTranslation $sql_translation  The SQL translation object 
     1853     * @return void 
     1854     */ 
     1855    public function inject($sql_translation) 
     1856    { 
     1857        $this->translation = $sql_translation; 
    14251858    } 
    14261859     
     
    14341867    { 
    14351868        return $this->inside_transaction; 
     1869    } 
     1870     
     1871     
     1872    /** 
     1873     * Creates a unique cache prefix to help prevent cache conflicts 
     1874     *  
     1875     * @return void 
     1876     */ 
     1877    private function makeCachePrefix() 
     1878    { 
     1879        $prefix  = 'fDatabase::' . $this->type . '::'; 
     1880        if ($this->host) { 
     1881            $prefix .= $this->host . '::';   
     1882        } 
     1883        if ($this->port) { 
     1884            $prefix .= $this->port . '::';   
     1885        } 
     1886        $prefix .= $this->database . '::'; 
     1887        if ($this->username) { 
     1888            $prefix .= $this->username . '::';   
     1889        } 
     1890        return $prefix;  
     1891    } 
     1892     
     1893     
     1894    /** 
     1895     * Prepares the SQL by escaping values, spliting queries, cleaning escaped semicolons, fixing backslashed single quotes and translating 
     1896     *  
     1897     * @param  string  $sql        The SQL to prepare 
     1898     * @param  array   $values     Literal values to escape into the SQL 
     1899     * @param  boolean $translate  If the SQL should be translated 
     1900     * @return array  The split out SQL queries, queries that have been translated will have a string key of the original SQL, non-translated SQL will have a numeric key 
     1901     */ 
     1902    private function prepareSQL($sql, $values, $translate) 
     1903    { 
     1904        $this->connectToDatabase(); 
     1905         
     1906        // Ensure an SQL statement was passed 
     1907        if (empty($sql)) { 
     1908            throw new fProgrammerException('No SQL statement passed'); 
     1909        } 
     1910         
     1911        if ($values) { 
     1912            $sql = call_user_func_array( 
     1913                $this->escape, 
     1914                array_merge(array($sql), $values) 
     1915            );   
     1916        } 
     1917         
     1918        // Fix \' in MySQL and PostgreSQL 
     1919        if(($this->type == 'mysql' || $this->type == 'postgresql') && strpos($sql, '\\') !== FALSE) { 
     1920            $sql = preg_replace("#(?<!\\\\)((\\\\{2})*)\\\\'#", "\\1''", $sql);  
     1921        } 
     1922         
     1923        $strings = array(array()); 
     1924        $queries = array(''); 
     1925        $number  = 0; 
     1926         
     1927        // Separate the SQL from quoted values 
     1928        preg_match_all("#(?:'([^']*(?:'')*)*?')|(?:[^']+)#", $sql, $matches); 
     1929         
     1930        foreach ($matches[0] as $match) { 
     1931            if ($match[0] == "'") { 
     1932                $queries[$number]  .= ':string_' . sizeof($strings[$number]); 
     1933                $strings[$number][] = $match;    
     1934            } else { 
     1935                $split_queries = preg_split('#(?<!\\\\);#', $match); 
     1936                 
     1937                $queries[$number] .= $split_queries[0]; 
     1938                 
     1939                for ($i=1; $i < sizeof($split_queries); $i++) { 
     1940                    $queries[$number] = trim($queries[$number]); 
     1941                    $number++; 
     1942                    $strings[$number] = array(); 
     1943                    $queries[$number] = $split_queries[$i]; 
     1944                }    
     1945            }        
     1946        } 
     1947        if (!trim($queries[$number])) { 
     1948            unset($queries[$number]); 
     1949            unset($strings[$number]);    
     1950        } else { 
     1951            $queries[$number] = trim($queries[$number]);     
     1952        } 
     1953         
     1954        // Translate the SQL queries, this takes care of unescaping and reinserting strings 
     1955        if ($translate) { 
     1956            $output = $this->getSQLTranslation()->translate($queries, $strings); 
     1957             
     1958        // For untranslated queries we need to unescape and reinsert strings 
     1959        } else { 
     1960            $output = array(); 
     1961            foreach ($queries as $number => $query) { 
     1962                // Unescape literal semicolons in the queries 
     1963                $query = preg_replace('#(?<!\\\\)\\\\;#', ';', $query); 
     1964                // Put the strings back into the SQL 
     1965                foreach ($strings[$number] as $index => $string) { 
     1966                    $query = preg_replace('#:string_' . $index . '\b#', str_replace('\\', '\\\\', $string), $query, 1); 
     1967                } 
     1968                $output[] = $query; 
     1969            }    
     1970        } 
     1971         
     1972        return $output; 
    14361973    } 
    14371974     
     
    14471984    public function query($sql) 
    14481985    { 
    1449         $this->connectToDatabase(); 
    1450          
    1451         // Ensure an SQL statement was passed 
    1452         if (empty($sql)) { 
    1453             throw new fProgrammerException('No SQL statement passed'); 
    1454         } 
    1455          
    1456         if (func_num_args() > 1) { 
    1457             $args = func_get_args(); 
    1458             $sql  = call_user_func_array($this->escape, $args); 
    1459         } 
    1460          
    1461         // Split multiple queries 
    1462         if (strpos($sql, ';') !== FALSE) { 
    1463             $sql_queries = $this->explodeQueries($sql); 
    1464             $sql = array_shift($sql_queries); 
    1465         } 
    1466          
    1467         $start_time = microtime(TRUE); 
    1468          
    1469         $this->trackTransactions($sql); 
    1470         if (!$result = $this->handleTransactionQueries($sql, 'fResult')) { 
    1471             $result = new fResult($this->type, $this->extension, $this->character_set); 
     1986        $args    = func_get_args(); 
     1987        $queries = $this->prepareSQL( 
     1988            $sql, 
     1989            array_slice($args, 1), 
     1990            FALSE 
     1991        ); 
     1992         
     1993        $output = array(); 
     1994        foreach ($queries as $query) { 
     1995            $output[] = $this->runQuery($query, 'fResult');  
     1996        } 
     1997         
     1998        return sizeof($output) == 1 ? $output[0] : $output; 
     1999    } 
     2000     
     2001     
     2002    /** 
     2003     * Runs a single query and times it, removes any old unbuffered queries before starting 
     2004     *  
     2005     * @param  string $sql          The SQL statement to execute 
     2006     * @param  string $result_type  The type of result object to return, fResult or fUnbufferedResult 
     2007     * @return fResult|fUnbufferedResult  The result for the query 
     2008     */ 
     2009    private function runQuery($sql, $result_type) 
     2010    { 
     2011        if ($this->unbuffered_result) { 
     2012            $this->unbuffered_result->__destruct(); 
     2013            $this->unbuffered_result = NULL; 
     2014        } 
     2015         
     2016        $start_time = microtime(TRUE);   
     2017             
     2018        if (!$result = $this->handleTransactionQueries($sql, $result_type)) { 
     2019            $result = new $result_type($this->type, $this->extension, $this->type == 'mssql' ? $this->schema_info['character_set'] : NULL); 
    14722020            $result->setSQL($sql); 
    14732021             
    1474             $this->executeQuery($result); 
     2022            if ($result_type == 'fResult') { 
     2023                $this->executeQuery($result); 
     2024            } else { 
     2025                $this->executeUnbufferedQuery($result);  
     2026            } 
    14752027        } 
    14762028         
     
    14992051        } 
    15002052         
    1501         // Handle multiple SQL queries 
    1502         if (!empty($sql_queries)) { 
    1503             $result = array($result); 
    1504             foreach ($sql_queries as $sql_query) { 
    1505                 $result[] = $this->query($sql_query); 
    1506             } 
    1507         } 
    1508          
    15092053        return $result; 
    15102054    } 
     
    15152059     *  
    15162060     * @param  fResult $result    The result object for the query 
    1517      * @param  mixed   $resource  Only applicable for `pdo`, `odbc` and `sqlsrv` extentions, this is either the `PDOStatement` object or `odbc` or `sqlsrv` resource 
     2061     * @param  mixed   $resource  Only applicable for `pdo`, `oci8`, `odbc` and `sqlsrv` extentions, this is either the `PDOStatement` object or the `oci8`, `odbc` or `sqlsrv` resource 
    15182062     * @return void 
    15192063     */ 
     
    15272071        } elseif ($this->extension == 'mysqli') { 
    15282072            $result->setAffectedRows(mysqli_affected_rows($this->connection)); 
     2073        } elseif ($this->extension == 'oci8') { 
     2074            $result->setAffectedRows(oci_num_rows($resource)); 
    15292075        } elseif ($this->extension == 'odbc') { 
    15302076            $result->setAffectedRows(odbc_num_rows($resource)); 
     
    15382084            // This fixes the fact that rowCount is not reset for non INSERT/UPDATE/DELETE statements 
    15392085            try { 
    1540                 if (!$resource->fetch()) { 
     2086                if (!$resource || !$resource->fetch()) { 
    15412087                    throw new PDOException(); 
    15422088                } 
    15432089                $result->setAffectedRows(0); 
    15442090            } catch (PDOException $e) { 
    1545                 $result->setAffectedRows($resource->rowCount()); 
     2091                // The SQLite PDO driver seems to return 1 when no rows are returned from a SELECT statement 
     2092                if ($this->type == 'sqlite' && $this->extension == 'pdo' && preg_match('#^\s*SELECT#i', $result->getSQL())) { 
     2093                    $result->setAffectedRows(0);     
     2094                } elseif (!$resource) { 
     2095                    $result->setAffectedRows(0); 
     2096                } else { 
     2097                    $result->setAffectedRows($resource->rowCount()); 
     2098                } 
    15462099            } 
    15472100        } 
     
    15762129     
    15772130    /** 
    1578      * Keeps track to see if a transaction is being started or stopped 
    1579      *  
    1580      * @param  string $sql  The SQL to check for a transaction query 
    1581      * @return void 
    1582      */ 
    1583     private function trackTransactions($sql) 
    1584     { 
    1585         if (preg_match('#^\s*(begin|start)(\s+transaction)?\s*$#iD', $sql)) { 
    1586             if ($this->inside_transaction) { 
    1587                 throw new fProgrammerException('A transaction is already in progress'); 
    1588             } 
    1589             $this->inside_transaction = TRUE; 
    1590              
    1591         } elseif (preg_match('#^\s*(commit)(\s+transaction)?\s*$#iD', $sql)) { 
    1592             if (!$this->inside_transaction) { 
    1593                 throw new fProgrammerException('There is no transaction in progress'); 
    1594             } 
    1595             $this->inside_transaction = FALSE; 
    1596              
    1597         } elseif (preg_match('#^\s*(rollback)(\s+transaction)?\s*$#iD', $sql)) { 
    1598             if (!$this->inside_transaction) { 
    1599                 throw new fProgrammerException('There is no transaction in progress'); 
    1600             } 
    1601             $this->inside_transaction = FALSE; 
    1602         } 
    1603     } 
    1604      
    1605      
    1606     /** 
    16072131     * Translates the SQL statement using fSQLTranslation and executes it 
    16082132     *  
     
    16142138    public function translatedQuery($sql) 
    16152139    { 
    1616         if (!$this->translation) { 
    1617             $this->connectToDatabase(); 
    1618             $this->translation = new fSQLTranslation($this, $this->connection); 
    1619             $this->translation->enableDebugging($this->debug); 
    1620         } 
    1621          
    1622         if (func_num_args() > 1) { 
    1623             $args = func_get_args(); 
    1624             $sql  = call_user_func_array($this->escape, $args); 
    1625         } 
    1626          
    1627         $result = $this->query($this->translation->translate($sql)); 
    1628         $result->setUntranslatedSQL($sql); 
    1629         return $result; 
     2140        $args    = func_get_args(); 
     2141        $queries = $this->prepareSQL( 
     2142            $sql, 
     2143            array_slice($args, 1), 
     2144            TRUE 
     2145        ); 
     2146         
     2147        $output = array(); 
     2148        foreach ($queries as $original_query => $query) { 
     2149            $result = $this->runQuery($query, 'fResult'); 
     2150            if (!is_numeric($original_query)) { 
     2151                $result->setUntranslatedSQL($original_query);    
     2152            } 
     2153            $output[] = $result; 
     2154        } 
     2155         
     2156        return sizeof($output) == 1 ? $output[0] : $output; 
    16302157    } 
    16312158     
     
    16452172    public function unbufferedQuery($sql) 
    16462173    { 
    1647         $this->connectToDatabase(); 
    1648          
    1649         // Ensure an SQL statement was passed 
    1650         if (empty($sql)) { 
    1651             throw new fProgrammerException('No SQL statement passed'); 
    1652         } 
    1653          
    1654         if (func_num_args() > 1) { 
    1655             $args = func_get_args(); 
    1656             $sql  = call_user_func_array($this->escape, $args); 
    1657         } 
    1658          
    1659         if ($this->unbuffered_result) { 
    1660             $this->unbuffered_result->__destruct(); 
    1661         } 
    1662          
    1663         $start_time = microtime(TRUE); 
    1664          
    1665         $this->trackTransactions($sql); 
    1666         if (!$result = $this->handleTransactionQueries($sql, 'fUnbufferedRequest')) { 
    1667             $result = new fUnbufferedResult($this->type, $this->extension, $this->character_set); 
    1668             $result->setSQL($sql); 
    1669              
    1670             $this->executeUnbufferedQuery($result); 
    1671         } 
    1672          
    1673         // Write some debugging info 
    1674         $query_time = microtime(TRUE) - $start_time; 
    1675         $this->query_time += $query_time; 
    1676         fCore::debug( 
    1677             self::compose( 
    1678                 'Query time was %1$s seconds for (unbuffered):%2$s', 
    1679                 $query_time, 
    1680                 "\n" . $result->getSQL() 
    1681             ), 
    1682             $this->debug 
     2174        $args    = func_get_args(); 
     2175        $queries = $this->prepareSQL( 
     2176            $sql, 
     2177            array_slice($args, 1), 
     2178            FALSE 
    16832179        ); 
    16842180         
    1685         if ($this->slow_query_threshold && $query_time > $this->slow_query_threshold) { 
    1686             trigger_error( 
    1687                 self::compose( 
    1688                     'The following query took %1$s milliseconds, which is above the slow query threshold of %2$s:%3$s', 
    1689                     $query_time, 
    1690                     $this->slow_query_threshold, 
    1691                     "\n" . $result->getSQL() 
    1692                 ), 
    1693                 E_USER_WARNING 
     2181        if (sizeof($queries) > 1) { 
     2182            throw new fProgrammerException( 
     2183                'Only a single unbuffered query can be run at a time, however %d were passed', 
     2184                sizeof($queries)     
    16942185            ); 
    16952186        } 
     2187         
     2188        $result = $this->runQuery($queries[0], 'fUnbufferedResult'); 
    16962189         
    16972190        $this->unbuffered_result = $result; 
     
    17152208    public function unbufferedTranslatedQuery($sql) 
    17162209    { 
    1717         if (!$this->translation) { 
    1718             $this->connectToDatabase(); 
    1719             $this->translation = new fSQLTranslation($this, $this->connection); 
    1720         } 
    1721          
    1722         if (func_num_args() > 1) { 
    1723             $args = func_get_args(); 
    1724             $sql  = call_user_func_array($this->escape, $args); 
    1725         } 
    1726          
    1727         $result = $this->unbufferedQuery($this->translation->translate($sql)); 
    1728         $result->setUntranslatedSQL($sql); 
     2210        $args    = func_get_args(); 
     2211        $queries = $this->prepareSQL( 
     2212            $sql, 
     2213            array_slice($args, 1), 
     2214            TRUE 
     2215        ); 
     2216         
     2217        if (sizeof($queries) > 1) { 
     2218            throw new fProgrammerException( 
     2219                'Only a single unbuffered query can be run at a time, however %d were passed', 
     2220                sizeof($queries)     
     2221            ); 
     2222        } 
     2223         
     2224        $query_keys     = array_keys($queries); 
     2225        $original_query = $query_keys[0]; 
     2226         
     2227        $result = $this->runQuery($queries[$original_query], 'fUnbufferedResult'); 
     2228        $result->setUntranslatedSQL($original_query); 
     2229         
     2230        $this->unbuffered_result = $result; 
     2231         
    17292232        return $result; 
    17302233    } 
     
    17462249     *  
    17472250     * @param  string $data_type  The data type being unescaped - see method description for valid values 
    1748      * @param  mixed  $value      The value to unescape 
     2251     * @param  mixed  $value      The value or array of values to unescape 
    17492252     * @return mixed  The unescaped value 
    17502253     */ 
    17512254    public function unescape($data_type, $value) 
    17522255    { 
     2256        $callback = NULL; 
     2257         
    17532258        switch ($data_type) { 
    17542259            case 'blob': 
    17552260            case '%l': 
    1756                 return $this->unescapeBlob($value); 
     2261                $callback = $this->unescapeBlob; 
     2262                break; 
    17572263            case 'boolean': 
    17582264            case '%b': 
    1759                 return $this->unescapeBoolean($value); 
     2265                $callback = $this->unescapeBoolean; 
     2266                break; 
    17602267            case 'date': 
    17612268            case '%d': 
    1762                 return $this->unescapeDate($value); 
     2269                $callback = $this->unescapeDate; 
     2270                break; 
    17632271            case 'float': 
    17642272            case '%f': 
    1765                 return $this->unescapeFloat($value); 
     2273                $callback = $this->unescapeFloat; 
     2274                break; 
    17662275            case 'integer': 
    17672276            case '%i': 
    1768                 return $this->unescapeInteger($value); 
     2277                $callback = $this->unescapeInteger; 
     2278                break; 
    17692279            case 'string': 
    17702280            case 'varchar': 
     
    17722282            case 'text': 
    17732283            case '%s': 
    1774                 return $this->unescapeString($value); 
     2284                $callback = $this->unescapeString; 
     2285                break; 
    17752286            case 'time': 
    17762287            case '%t': 
    1777                 return $this->unescapeTime($value); 
     2288                $callback = $this->unescapeTime; 
     2289                break; 
    17782290            case 'timestamp': 
    17792291            case '%p': 
    1780                 return $this->unescapeTimestamp($value); 
     2292                $callback = $this->unescapeTimestamp; 
     2293                break; 
     2294        } 
     2295         
     2296        if ($callback) { 
     2297            if (is_array($value)) { 
     2298                return array_map($callback, $value);     
     2299            } 
     2300            return call_user_func($callback, $value); 
    17812301        }    
    17822302         
     
    18002320         
    18012321        if ($this->extension == 'pgsql') { 
    1802             return pg_unescape_bytea($this->connection, $value); 
    1803         } if ($this->extension == 'odbc') { 
     2322            return pg_unescape_bytea($value); 
     2323        } elseif ($this->extension == 'pdo' && is_resource($value)) { 
     2324            return stream_get_contents($value); 
     2325        } elseif ($this->type == 'mssql' && (substr($this->database, 0, 4) == 'dsn:')) { 
    18042326            return pack('H*', $value); 
    1805         } else  { 
     2327        } elseif ($this->extension == 'sqlite') { 
     2328            return pack('H*', $value); 
     2329        } else { 
    18062330            return $value; 
    18072331        } 
     
    18292353    private function unescapeDate($value) 
    18302354    { 
     2355        if ($this->extension == 'sqlsrv' && $value instanceof DateTime) { 
     2356            return $value->format('Y-m-d'); 
     2357        } elseif ($this->type == 'mssql') { 
     2358            $value = preg_replace('#:\d{3}#', '', $value); 
     2359        } 
    18312360        return date('Y-m-d', strtotime($value)); 
    18322361    } 
     
    18772406    private function unescapeTime($value) 
    18782407    { 
     2408        if ($this->extension == 'sqlsrv' && $value instanceof DateTime) { 
     2409            return $value->format('H:i:s'); 
     2410        } elseif ($this->type == 'mssql') { 
     2411            $value = preg_replace('#:\d{3}#', '', $value); 
     2412        } 
    18792413        return date('H:i:s', strtotime($value)); 
    18802414    } 
     
    18892423    private function unescapeTimestamp($value) 
    18902424    { 
     2425        if ($this->extension == 'sqlsrv' && $value instanceof DateTime) { 
     2426            return $value->format('Y-m-d H:i:s'); 
     2427        } elseif ($this->type == 'mssql') { 
     2428            $value = preg_replace('#:\d{3}#', '', $value); 
     2429        } 
    18912430        return date('Y-m-d H:i:s', strtotime($value)); 
    18922431    } 
  • fORM.php

    r517 r562 Hide Line Numbers
    1010 * @link       http://flourishlib.com/fORM 
    1111 *  
    12  * @version    1.0.0b6 
     12 * @version    1.0.0b7 
     13 * @changes    1.0.0b7  Added ::enableSchemaCaching() to replace fORMSchema::enableSmartCaching() [wb, 2009-05-04] 
    1314 * @changes    1.0.0b6  Added the ability to pass a class instance to ::addCustomClassTableMapping() [wb, 2009-02-23] 
    1415 * @changes    1.0.0b5  Backwards compatibility break - renamed ::addCustomTableClassMapping() to ::addCustomClassTableMapping() and swapped the parameters [wb, 2009-01-26] 
     
    2728    const classize                   = 'fORM::classize'; 
    2829    const defineActiveRecordClass    = 'fORM::defineActiveRecordClass'; 
     30    const enableSchemaCaching        = 'fORM::enableSchemaCaching'; 
    2931    const getActiveRecordMethod      = 'fORM::getActiveRecordMethod'; 
    3032    const getClass                   = 'fORM::getClass'; 
     
    313315            $class 
    314316        ); 
     317    } 
     318     
     319     
     320    /** 
     321     * Enables caching on the fDatabase, fSQLTranslation and fSchema objects used for the ORM 
     322     *  
     323     * This method will cache database schema information to the three objects 
     324     * that use it during normal ORM operation: fDatabase, fSQLTranslation and 
     325     * fSchema. To allow for schema changes without having to manually clear 
     326     * the cache, all cached information will be cleared if any 
     327     * fUnexpectedException objects are thrown. 
     328     *  
     329     * This method should be called right after fORMDatabase::attach(). 
     330     *           
     331     * @param  fCache $cache  The object to cache schema information to 
     332     * @return void 
     333     */ 
     334    static public function enableSchemaCaching($cache) 
     335    { 
     336        $db = fORMDatabase::retrieve(); 
     337        $db->enableCaching($cache); 
     338        fException::registerCallback($db->clearCache, 'fUnexpectedException'); 
     339         
     340        $sql_translation = $db->getSQLTranslation(); 
     341        $sql_translation->enableCaching($cache); 
     342        fException::registerCallback($sql_translation->clearCache, 'fUnexpectedException'); 
     343         
     344        $schema = fORMSchema::retrieve(); 
     345        $schema->enableCaching($cache); 
     346        fException::registerCallback($schema->clearCache, 'fUnexpectedException');   
    315347    } 
    316348     
  • fORMSchema.php

    r458 r562 Hide Line Numbers
    33 * Provides fSchema class related functions for ORM code 
    44 *  
    5  * @copyright  Copyright (c) 2007-2008 Will Bond 
     5 * @copyright  Copyright (c) 2007-2009 Will Bond 
    66 * @author     Will Bond [wb] <will@flourishlib.com> 
    77 * @license    http://flourishlib.com/license 
     
    1010 * @link       http://flourishlib.com/fORMSchema 
    1111 *  
    12  * @version    1.0.0b 
    13  * @changes    1.0.0b  The initial implementation [wb, 2007-06-14] 
     12 * @version    1.0.0b2 
     13 * @changes    1.0.0b2  Backwards Compatiblity Break - removed ::enableSmartCaching(), fORM::enableSchemaCaching() now provides equivalent functionality [wb, 2009-05-04] 
     14 * @changes    1.0.0b   The initial implementation [wb, 2007-06-14] 
    1415 */ 
    1516class fORMSchema 
     
    1718    // The following constants allow for nice looking callbacks to static methods 
    1819    const attach                       = 'fORMSchema::attach'; 
    19     const enableSmartCaching           = 'fORMSchema::enableSmartCaching'; 
    2020    const getRoute                     = 'fORMSchema::getRoute'; 
    2121    const getRouteName                 = 'fORMSchema::getRouteName'; 
     
    4343    { 
    4444        self::$schema_object = $schema; 
    45     } 
    46      
    47      
    48     /** 
    49      * Turns on schema caching, using fUnexpectedException flushing 
    50      * 
    51      * @param  string  $cache_file  The file to use for caching 
    52      * @return void 
    53      */ 
    54     static public function enableSmartCaching($cache_file) 
    55     { 
    56         if (!self::retrieve() instanceof fSchema) { 
    57             throw new fProgrammerException( 
    58                 'Smart caching is only available (and most likely only applicable) if you are using the fSchema object' 
    59             ); 
    60         } 
    61         self::retrieve()->setCacheFile($cache_file); 
    62         fException::registerCallback(array(self::retrieve(), 'flushInfo'), 'fUnexpectedException'); 
    6345    } 
    6446     
     
    309291 
    310292/** 
    311  * Copyright (c) 2007-2008 Will Bond <will@flourishlib.com> 
     293 * Copyright (c) 2007-2009 Will Bond <will@flourishlib.com> 
    312294 *  
    313295 * Permission is hereby granted, free of charge, to any person obtaining a copy 
  • fResult.php

    r514 r562 Hide Line Numbers
    1010 * @link       http://flourishlib.com/fResult 
    1111 *  
    12  * @version    1.0.0b2 
     12 * @version    1.0.0b3 
     13 * @changes    1.0.0b3  Added support for Oracle, various bug fixes [wb, 2009-05-04] 
    1314 * @changes    1.0.0b2  Updated for new fCore API [wb, 2009-02-16] 
    1415 * @changes    1.0.0b   The initial implementation [wb, 2007-09-25] 
     
    122123     * @internal 
    123124     *  
    124      * @param  string $type           The type of database: `'mssql'`, `'mysql'`, `'postgresql'`, `'sqlite'` 
     125     * @param  string $type           The type of database: `'mssql'`, `'mysql'`, `'oracle'`, `'postgresql'`, `'sqlite'` 
    125126     * @param  string $extension      The database extension used: `'array'`, `'mssql'`, `'mysql'`, `'mysqli'`, `'pgsql'`, `'sqlite'` 
    126127     * @param  string $character_set  MSSQL only: the character set to transcode from since MSSQL doesn't do UTF-8 
     
    129130    public function __construct($type, $extension, $character_set=NULL) 
    130131    { 
    131         $valid_types = array('mssql', 'mysql', 'postgresql', 'sqlite'); 
     132        $valid_types = array('mssql', 'mysql', 'oracle', 'postgresql', 'sqlite'); 
    132133        if (!in_array($type, $valid_types)) { 
    133134            throw new fProgrammerException( 
     
    139140         
    140141        // Certain extensions don't offer a buffered query, so it is emulated using an array 
    141         if (in_array($extension, array('odbc', 'pdo', 'sqlsrv'))) { 
     142        if (in_array($extension, array('oci8', 'odbc', 'pdo', 'sqlsrv'))) { 
    142143            $extension = 'array'; 
    143144        } 
     
    182183            // SQLite doesn't have a way to free a result 
    183184        } 
     185         
     186        $this->result = NULL; 
    184187    } 
    185188     
     
    206209        if ($this->extension == 'mssql') { 
    207210            $row = mssql_fetch_assoc($this->result); 
    208             $row = $this->fixDblibMSSQLDriver($row); 
    209              
    210             // This is an unfortunate fix that required for databases that don't support limit 
    211             // clauses with an offset. It prevents unrequested columns from being returned. 
    212             if ($this->untranslated_sql !== NULL && isset($row['__flourish_limit_offset_row_num'])) { 
    213                 unset($row['__flourish_limit_offset_row_num']); 
     211            if (!empty($row)) { 
     212                $row = $this->fixDblibMSSQLDriver($row); 
    214213            } 
    215214                 
     
    226225        } 
    227226         
     227        // Fix uppercase column names to lowercase 
     228        if ($row && $this->type == 'oracle') { 
     229            $new_row = array(); 
     230            foreach ($row as $column => $value) { 
     231                $new_row[strtolower($column)] = $value; 
     232            }    
     233            $row = $new_row; 
     234        } 
     235         
     236        // This is an unfortunate fix that required for databases that don't support limit 
     237        // clauses with an offset. It prevents unrequested columns from being returned. 
     238        if ($row && ($this->type == 'mssql' || $this->type == 'oracle')) { 
     239            if ($this->untranslated_sql !== NULL && isset($row['flourish__row__num'])) { 
     240                unset($row['flourish__row__num']); 
     241            }    
     242        } 
     243         
    228244        // This decodes the data coming out of MSSQL into UTF-8 
    229         if ($this->type == 'mssql') { 
     245        if ($row && $this->type == 'mssql') { 
    230246            if ($this->character_set) { 
    231247                foreach ($row as $key => $value) { 
    232                     if (!is_string($value) || strpos($key, '__flourish_mssqln_') === 0) { 
     248                    if (!is_string($value) || strpos($key, 'fmssqln__') === 0 || isset($row['fmssqln__' . $key]) || preg_match('#[\x0-\x8\xB\xC\xD-\x1F]#', $value)) { 
    233249                        continue; 
    234250                    }        
     
    302318    private function decodeMSSQLNationalColumns($row) 
    303319    { 
    304         if (strpos($this->sql, '__flourish_mssqln_') === FALSE) { 
     320        if (strpos($this->sql, 'fmssqln__') === FALSE) { 
    305321            return $row; 
    306322        } 
     
    309325         
    310326        foreach ($columns as $column) { 
    311             if (substr($column, 0, 18) != '__flourish_mssqln_') { 
     327            if (substr($column, 0, 9) != 'fmssqln__') { 
    312328                continue; 
    313329            }    
    314330             
    315             $real_column = substr($column, 18); 
     331            $real_column = substr($column, 9); 
    316332             
    317333            $row[$real_column] = iconv('ucs-2le', 'utf-8', $row[$column]); 
     
    394410                ob_end_clean(); 
    395411                 
    396                 $using_dblib = preg_match('#FreeTDS#ims', $module_info, $match); 
     412                $using_dblib = !preg_match('#FreeTDS#ims', $module_info, $match); 
    397413            } 
    398414        } 
     
    596612    public function setAffectedRows($affected_rows) 
    597613    { 
     614        if ($affected_rows === -1) { $affected_rows = 0; } 
    598615        $this->affected_rows = (int) $affected_rows; 
    599616    } 
  • fSQLTranslation.php

    r515 r562 Hide Line Numbers
    11<?php 
    22/** 
    3  * Takes a subset of SQL from MySQL, PostgreSQL, SQLite and MSSQL and translates into the various dialects allowing for cross-database code 
     3 * Takes a subset of SQL from MySQL, PostgreSQL, Oracle, SQLite and MSSQL and translates into the various dialects allowing for cross-database code 
    44 *  
    55 * @copyright  Copyright (c) 2007-2009 Will Bond 
     
    1010 * @link       http://flourishlib.com/fSQLTranslation 
    1111 *  
    12  * @internal 
    13  *  
    14  * @version    1.0.0b2 
     12 * @version    1.0.0b3 
     13 * @changes    1.0.0b3  Added Oracle and caching support, various bug fixes [wb, 2009-05-04] 
    1514 * @changes    1.0.0b2  Fixed a notice with SQLite foreign key constraints having no `ON` clauses [wb, 2009-02-21] 
    1615 * @changes    1.0.0b   The initial implementation [wb, 2007-09-25] 
     
    4645    } 
    4746     
    48      
    49     /** 
    50      * Takes a Flourish SQL `SELECT` query and parses it into clauses. 
    51      *  
    52      * The select statement must be of the format: 
    53      *  
    54      * {{{ 
    55      * SELECT [ table_name. | alias. ]* 
    56      * FROM table [ AS alias ] [ [ INNER | OUTER ] [ LEFT | RIGHT ] JOIN other_table ON condition | , ] ... 
    57      * [ WHERE condition [ , condition ]... ] 
    58      * [ GROUP BY conditions ] 
    59      * [ HAVING conditions ] 
    60      * [ ORDER BY [ column | expression ] [ ASC | DESC ] [ , [ column | expression ] [ ASC | DESC ] ] ... ] 
    61      * [ LIMIT integer [ OFFSET integer ] ] 
    62      * }}} 
    63      *  
    64      * The returned array will contain the following keys, which may have a `NULL` or non-empty string value: 
    65      *  
    66      *  - `'SELECT'` 
    67      *  - `'FROM'` 
    68      *  - `'WHERE'` 
    69      *  - `'GROUP BY'` 
    70      *  - `'HAVING'` 
    71      *  - `'ORDER BY'` 
    72      *  - `'LIMIT'` 
    73      *  
    74      * @param  string $sql  The SQL to parse 
    75      * @return array  The various clauses of the `SELECT` statement - see method description for details 
    76      */ 
    77     static private function parseSelectSQL($sql) 
    78     { 
    79         // Split the strings out of the sql so parsing doesn't get messed up by quoted values 
    80         preg_match_all("#(?:'(?:''|\\\\'|\\\\[^']|[^'\\\\]+)*')|(?:[^']+)#", $sql, $matches); 
    81          
    82         $possible_clauses = array('SELECT', 'FROM', 'WHERE', 'GROUP BY', 'HAVING', 'ORDER BY', 'LIMIT'); 
    83         $found_clauses    = array(); 
    84         foreach ($possible_clauses as $possible_clause) { 
    85             $found_clauses[$possible_clause] = NULL; 
    86         } 
    87          
    88         $current_clause = 0; 
    89          
    90         foreach ($matches[0] as $match) { 
    91             // This is a quoted string value, don't do anything to it 
    92             if ($match[0] == "'") { 
    93                 $found_clauses[$possible_clauses[$current_clause]] .= $match; 
    94              
    95             // Non-quoted strings should be checked for clause markers 
    96             } else { 
    97                  
    98                 // Look to see if a new clause starts in this string 
    99                 $i = 1; 
    100                 while ($current_clause+$i < sizeof($possible_clauses)) { 
    101                     // If the next clause is found in this string 
    102                     if (stripos($match, $possible_clauses[$current_clause+$i]) !== FALSE) { 
    103                         list($before, $after) = preg_split('#\s*' . $possible_clauses[$current_clause+$i] . '\s*#i', $match); 
    104                         $found_clauses[$possible_clauses[$current_clause]] .= preg_replace('#\s*' . $possible_clauses[$current_clause] . '\s*#i', '', $before); 
    105                         $match = $after; 
    106                         $current_clause = $current_clause + $i; 
    107                         $i = 0; 
    108                     } 
    109                     $i++; 
    110                 } 
    111                  
    112                 // Otherwise just add on to the current clause 
    113                 if (!empty($match)) { 
    114                     $found_clauses[$possible_clauses[$current_clause]] .= preg_replace('#\s*' . $possible_clauses[$current_clause] . '\s*#i', '', $match); 
    115                 } 
    116             } 
    117         } 
    118          
    119         return $found_clauses; 
    120     } 
    121      
    12247         
    12348    /** 
     
    13055    { 
    13156        $aliases = array(); 
    132          
    133         preg_match_all("#(?:'(?:''|\\\\'|\\\\[^']|[^'\\\\]+)*')|(?:[^']+)#", $sql, $matches); 
    134          
    135         $sql = ''; 
    136         // Replace strings with two single quotes 
    137         foreach ($matches[0] as $match) { 
    138             if ($match[0] == "'") { 
    139                 $match = "''"; 
    140             } 
    141             $sql .= $match; 
    142         } 
    14357         
    14458        // Turn comma joins into cross joins 
     
    14761        } 
    14862         
    149         // Error out if we can't figure out the join structure 
    150         if (!preg_match('#^(?:\w+(?:\s+(?:as\s+)?(?:\w+))?)(?:\s+(?:(?:CROSS|INNER|OUTER|LEFT|RIGHT)?\s+)*JOIN\s+(?:\w+(?:\s+(?:as\s+)?(?:\w+))?)(?:\s+ON\s+.*)?)*$#isD', $sql)) { 
    151             throw new fProgrammerException( 
    152                 'Unable to parse FROM clause, does not appears to be in comma style or join style' 
    153             ); 
    154         } 
    155          
    15663        $tables = preg_split('#\s+((?:(?:CROSS|INNER|OUTER|LEFT|RIGHT)?\s+)*?JOIN)\s+#i', $sql); 
    15764         
    15865        foreach ($tables as $table) { 
    15966            // This grabs the table name and alias (if there is one) 
    160             preg_match('#\s*([\w.]+)(?:\s+(?:as\s+)?((?!ON)[\w.]+))?\s*(?:ON\s+(.*))?#im', $table, $parts); 
     67            preg_match('#^\s*([\w.]+|\(((?:[^()]+|\((?2)\))*)\))(?:\s+(?:as\s+)?((?!ON|USING)[\w.]+))?\s*(?:(?:ON|USING)\s+(.*))?\s*$#im', $table, $parts); 
    16168             
    16269            $table_name  = $parts[1]; 
    163             $table_alias = (isset($parts[2])) ? $parts[2] : $parts[1]; 
     70            $table_alias = (isset($parts[2])) ? $parts[2] : $parts[1];  
    16471             
    16572            $aliases[$table_alias] = $table_name; 
     
    18188    { 
    18289        return 1/tan($x); 
     90    } 
     91     
     92     
     93    /** 
     94     * Callback for custom SQLite function; returns the current date 
     95     *  
     96     * @internal 
     97     *  
     98     * @return string  The current date 
     99     */ 
     100    static public function sqliteDate() 
     101    { 
     102        return date('Y-m-d'); 
    183103    } 
    184104     
     
    220140     
    221141    /** 
    222      * The database connection resource or PDO object 
    223      *  
    224      * @var mixed 
    225      */ 
    226     private $connection; 
     142     * Callback for custom SQLite function; returns the current time 
     143     *  
     144     * @internal 
     145     *  
     146     * @return string  The current time 
     147     */ 
     148    static public function sqliteTime() 
     149    { 
     150        return date('H:i:s'); 
     151    } 
     152     
     153     
     154    /** 
     155     * Callback for custom SQLite function; returns the current timestamp 
     156     *  
     157     * @internal 
     158     *  
     159     * @return string  The current date 
     160     */ 
     161    static public function sqliteTimestamp() 
     162    { 
     163        return date('Y-m-d H:i:s'); 
     164    } 
     165     
     166     
     167    /** 
     168     * The fCache object to cache schema info and, optionally, translated queries to 
     169     *  
     170     * @var fCache 
     171     */ 
     172    private $cache; 
    227173     
    228174    /** 
     
    240186    private $debug; 
    241187     
     188    /** 
     189     * Database-specific schema information needed for translation 
     190     *  
     191     * @var array 
     192     */ 
     193    private $schema_info; 
     194     
    242195     
    243196    /** 
    244197     * Sets up the class and creates functions for SQLite databases 
    245      *  
    246      * @internal 
    247198     *  
    248199     * @param  fDatabase $database    The database being translated for 
     
    250201     * @return fSQLTranslation 
    251202     */ 
    252     public function __construct($database, $connection) 
    253     { 
    254         if (!is_resource($connection) && !is_object($connection)) { 
    255             throw new fProgrammerException( 
    256                 'The connection specified, %s, is not a valid database connection', 
    257                 $connection 
    258             ); 
    259         } 
    260          
    261         $this->connection = $connection; 
    262         $this->database   = $database; 
     203    public function __construct($database) 
     204    { 
     205        $this->database = $database; 
     206        $this->database->inject($this); 
    263207         
    264208        if ($database->getType() == 'sqlite') { 
    265209            $this->createSQLiteFunctions(); 
    266210        } 
     211         
     212        $this->schema_info = array(); 
     213    } 
     214     
     215     
     216    /** 
     217     * Clears all of the schema info out of the object and, if set, the fCache object 
     218     *  
     219     * @return void 
     220     */ 
     221    public function clearCache() 
     222    { 
     223        $this->schema_info = array(); 
     224        if ($this->cache) { 
     225            $prefix = $this->makeCachePrefix(); 
     226            $this->cache->delete($prefix . 'schema_info'); 
     227        } 
    267228    } 
    268229     
     
    271232     * Creates a trigger for SQLite that handles an on delete clause 
    272233     *  
     234     * @param  array  &$extra_statements   An array of extra SQL statements to be added to the SQL 
    273235     * @param  string $referencing_table   The table that contains the foreign key 
    274236     * @param  string $referencing_column  The column the foriegn key constraint is on 
     
    278240     * @return string  The trigger 
    279241     */ 
    280     private function createSQLiteForeignKeyTriggerOnDelete($referencing_table, $referencing_column, $referenced_table, $referenced_column, $delete_clause) 
     242    private function createSQLiteForeignKeyTriggerOnDelete(&$extra_statements, $referencing_table, $referencing_column, $referenced_table, $referenced_column, $delete_clause) 
    281243    { 
    282244        switch (strtolower($delete_clause)) { 
    283245            case 'no action': 
    284246            case 'restrict': 
    285                 $sql = "\nCREATE TRIGGER fkd_res_" . $referencing_table . "_" . $referencing_column . " 
     247                $extra_statements[] = "CREATE TRIGGER fkd_res_" . $referencing_table . "_" . $referencing_column . " 
    286248                             BEFORE DELETE ON " . $referenced_table . " 
    287249                             FOR EACH ROW BEGIN 
    288250                                 SELECT RAISE(ROLLBACK, 'delete on table \"" . $referenced_table . "\" can not be executed because it would violate the foreign key constraint on column \"" . $referencing_column . "\" of table \"" . $referencing_table . "\"') 
    289251                                 WHERE (SELECT " . $referencing_column . " FROM " . $referencing_table . " WHERE " . $referencing_column . " = OLD." . $referenced_table . ") IS NOT NULL; 
    290                              END;"; 
     252                             END"; 
    291253                break; 
    292254             
    293255            case 'set null': 
    294                 $sql = "\nCREATE TRIGGER fkd_nul_" . $referencing_table . "_" . $referencing_column . " 
     256                $extra_statements[] = "CREATE TRIGGER fkd_nul_" . $referencing_table . "_" . $referencing_column . " 
    295257                             BEFORE DELETE ON " . $referenced_table . " 
    296258                             FOR EACH ROW BEGIN 
    297259                                 UPDATE " . $referencing_table . " SET " . $referencing_column . " = NULL WHERE " . $referencing_column . " = OLD." . $referenced_column . "; 
    298                              END;"; 
     260                             END"; 
    299261                break; 
    300262                 
    301263            case 'cascade': 
    302                 $sql = "\nCREATE TRIGGER fkd_cas_" . $referencing_table . "_" . $referencing_column . " 
     264                $extra_statements[] = "CREATE TRIGGER fkd_cas_" . $referencing_table . "_" . $referencing_column . " 
    303265                             BEFORE DELETE ON " . $referenced_table . " 
    304266                             FOR EACH ROW BEGIN 
    305267                                 DELETE FROM " . $referencing_table . " WHERE " . $referencing_column . " = OLD." . $referenced_column . "; 
    306                              END;"; 
     268                             END"; 
    307269                break; 
    308270        } 
    309         return $sql; 
    310271    } 
    311272     
     
    314275     * Creates a trigger for SQLite that handles an on update clause 
    315276     *  
     277     * @param  array  &$extra_statements   An array of extra SQL statements to be added to the SQL 
    316278     * @param  string $referencing_table   The table that contains the foreign key 
    317279     * @param  string $referencing_column  The column the foriegn key constraint is on 
     
    321283     * @return string  The trigger 
    322284     */ 
    323     private function createSQLiteForeignKeyTriggerOnUpdate($referencing_table, $referencing_column, $referenced_table, $referenced_column, $update_clause) 
     285    private function createSQLiteForeignKeyTriggerOnUpdate(&$extra_statements, $referencing_table, $referencing_column, $referenced_table, $referenced_column, $update_clause) 
    324286    { 
    325287        switch (strtolower($update_clause)) { 
    326288            case 'no action': 
    327289            case 'restrict': 
    328                 $sql = "\nCREATE TRIGGER fku_res_" . $referencing_table . "_" . $referencing_column . " 
     290                $extra_statements[] = "\nCREATE TRIGGER fku_res_" . $referencing_table . "_" . $referencing_column . " 
    329291                             BEFORE UPDATE ON " . $referenced_table . " 
    330292                             FOR EACH ROW BEGIN 
    331293                                 SELECT RAISE(ROLLBACK, 'update on table \"" . $referenced_table . "\" can not be executed because it would violate the foreign key constraint on column \"" . $referencing_column . "\" of table \"" . $referencing_table . "\"') 
    332294                                 WHERE (SELECT " . $referencing_column . " FROM " . $referencing_table . " WHERE " . $referencing_column . " = OLD." . $referenced_column . ") IS NOT NULL; 
    333                              END;"; 
     295                             END"; 
    334296                break; 
    335297             
    336298            case 'set null': 
    337                 $sql = "\nCREATE TRIGGER fku_nul_" . $referencing_table . "_" . $referencing_column . " 
     299                $extra_statements[] = "\nCREATE TRIGGER fku_nul_" . $referencing_table . "_" . $referencing_column . " 
    338300                             BEFORE UPDATE ON " . $referenced_table . " 
    339301                             FOR EACH ROW BEGIN 
    340302                                 UPDATE " . $referencing_table . " SET " . $referencing_column . " = NULL WHERE OLD." . $referenced_column . " <> NEW." . $referenced_column . " AND " . $referencing_column . " = OLD." . $referenced_column . "; 
    341                              END;"; 
     303                             END"; 
    342304                break; 
    343305                 
    344306            case 'cascade': 
    345                 $sql = "\nCREATE TRIGGER fku_cas_" . $referencing_table . "_" . $referencing_column . " 
     307                $extra_statements[] = "\nCREATE TRIGGER fku_cas_" . $referencing_table . "_" . $referencing_column . " 
    346308                             BEFORE UPDATE ON " . $referenced_table . " 
    347309                             FOR EACH ROW BEGIN 
    348310                                 UPDATE " . $referencing_table . " SET " . $referencing_column . " = NEW." . $referenced_column . " WHERE OLD." . $referenced_column . " <> NEW." . $referenced_column . " AND " . $referencing_column . " = OLD." . $referenced_column . "; 
    349                              END;"; 
     311                             END"; 
    350312                break; 
    351313        } 
    352         return $sql; 
    353314    } 
    354315     
     
    357318     * Creates a trigger for SQLite that prevents inserting or updating to values the violate a `FOREIGN KEY` constraint 
    358319     *  
     320     * @param  array  &$extra_statements   An array of extra SQL statements to be added to the SQL 
    359321     * @param  string  $referencing_table     The table that contains the foreign key 
    360322     * @param  string  $referencing_column    The column the foriegn key constraint is on 
     
    364326     * @return string  The trigger 
    365327     */ 
    366     private function createSQLiteForeignKeyTriggerValidInsertUpdate($referencing_table, $referencing_column, $referenced_table, $referenced_column, $referencing_not_null) 
     328    private function createSQLiteForeignKeyTriggerValidInsertUpdate(&$extra_statements, $referencing_table, $referencing_column, $referenced_table, $referenced_column, $referencing_not_null) 
    367329    { 
    368330        // Verify key on inserts 
     
    376338        } 
    377339        $sql .= " (SELECT " . $referenced_column . " FROM " . $referenced_table . " WHERE " . $referenced_column . " = NEW." . $referencing_column . ") IS NULL; 
    378                       END;"; 
     340                      END"; 
     341                       
     342        $extra_statements[] = $sql; 
    379343                     
    380344        // Verify key on updates 
    381         $sql .= "\nCREATE TRIGGER fku_ver_" . $referencing_table . "_" . $referencing_column . " 
     345        $sql = "\nCREATE TRIGGER fku_ver_" . $referencing_table . "_" . $referencing_column . " 
    382346                      BEFORE UPDATE ON " . $referencing_table . " 
    383347                      FOR EACH ROW BEGIN 
     
    388352        } 
    389353        $sql .= " (SELECT " . $referenced_column . " FROM " . $referenced_table . " WHERE " . $referenced_column . " = NEW." . $referencing_column . ") IS NULL; 
    390                       END;"; 
    391          
    392         return $sql; 
     354                      END"; 
     355         
     356        $extra_statements[] = $sql; 
    393357    } 
    394358     
     
    415379        $functions[] = array('ln',       'log',                                          1); 
    416380        $functions[] = array('log',      array('fSQLTranslation', 'sqliteLogBaseFirst'), 2); 
    417         $functions[] = array('pi',       'pi',                                           1); 
    418         $functions[] = array('power',    'pow',                                          1); 
     381        $functions[] = array('ltrim',    'ltrim',                                        1); 
     382        $functions[] = array('pi',       'pi',                                           0); 
     383        $functions[] = array('power',    'pow',                                          2); 
    419384        $functions[] = array('radians',  'deg2rad',                                      1); 
     385        $functions[] = array('rtrim',    'rtrim',                                        1); 
    420386        $functions[] = array('sign',     array('fSQLTranslation', 'sqliteSign'),         1); 
    421387        $functions[] = array('sqrt',     'sqrt',                                         1); 
    422388        $functions[] = array('sin',      'sin',                                          1); 
    423389        $functions[] = array('tan',      'tan',                                          1); 
     390        $functions[] = array('trim',     'trim',                                         1); 
     391         
     392        if ($this->database->getExtension() == 'sqlite') { 
     393            $functions[] = array('current_date',      array('fSQLTranslation', 'sqliteDate'), 0); 
     394            $functions[] = array('current_time',      array('fSQLTranslation', 'sqliteTime'), 0); 
     395            $functions[] = array('current_timestamp', array('fSQLTranslation', 'sqliteTimestamp'), 0);   
     396        } 
    424397         
    425398        foreach ($functions as $function) { 
    426399            if ($this->database->getExtension() == 'pdo') { 
    427                 $this->connection->sqliteCreateFunction($function[0], $function[1], $function[2]); 
     400                $this->database->getConnection()->sqliteCreateFunction($function[0], $function[1], $function[2]); 
    428401            } else { 
    429                 sqlite_create_function($this->connection, $function[0], $function[1], $function[2]); 
    430             } 
    431         } 
     402                sqlite_create_function($this->database->getConnection(), $function[0], $function[1], $function[2]); 
     403            } 
     404        } 
     405    } 
     406     
     407     
     408    /** 
     409     * Sets the schema info to be cached to the fCache object specified 
     410     *  
     411     * @param  fCache  $cache  The cache to cache to 
     412     * @return void 
     413     */ 
     414    public function enableCaching($cache) 
     415    { 
     416        $this->cache       = $cache; 
     417        $this->schema_info = $this->cache->get($this->makeCachePrefix() . 'schema_info', array()); 
    432418    } 
    433419     
     
    436422     * Sets if debug messages should be shown 
    437423     * 
    438      * @internal 
    439      *   
    440424     * @param  boolean $flag  If debugging messages should be shown 
    441425     * @return void 
     
    455439    private function fixMSSQLNationalColumns($sql) 
    456440    { 
    457         if (!preg_match_all('#^\s*(select.*)$|\(\s*(select(?:\s*(?:[^()\']+|\'(?:\'\'|\\\\\'|\\\\[^\']|[^\'\\\\]+)*\'|\((?2)\)|\(\))+\s*))\s*\)\s*(?= union)|\s+union(?:\s+all)?\s+\(\s*(select(?:\s*(?:[^()\']+|\'(?:\'\'|\\\\\'|\\\\[^\']|[^\'\\\\]+)*\'|\((?3)\)|\(\))+\s*))\s*\)#iD', $sql, $matches)) { 
     441        if (!preg_match_all('#select((?:(?:(?!\sfrom\s)[^()])+|\(((?:[^()]+|\((?2)\))*)\))*\s)from((?:(?:(?!\sunion\s|\swhere\s|\sgroup by\s|\slimit\s|\sorder by\s)[^()])+|\(((?:[^()]+|\((?4)\))*)\))*)(?=\swhere\s|\sgroup by\s|\slimit\s|\sorder by\s|\sunion\s|\)|$)#i', $sql, $matches, PREG_SET_ORDER)) { 
    458442            return $sql; 
    459443        } 
    460444         
    461         static $national_columns = NULL; 
    462         static $national_types   = NULL; 
    463          
    464         if ($national_columns === NULL) { 
     445        if (!isset($this->schema_info['national_columns'])) { 
    465446            $result = $this->database->query( 
    466447                "SELECT 
     
    474455                         c.data_type = 'ntext' OR 
    475456                         c.data_type = 'nchar') AND 
    476                         c.table_catalog = 'flourish
     457                        c.table_catalog = '" . $this->database->getDatabase() . "
    477458                    ORDER BY 
    478459                        lower(c.table_name) ASC, 
     
    481462             
    482463            $national_columns = array(); 
     464            $national_types   = array(); 
    483465             
    484466            foreach ($result as $row) { 
     
    490472                $national_types[$row['table']][$row['column']] = $row['type']; 
    491473            } 
    492         } 
    493          
    494         $selects = array_merge( 
    495             array_filter($matches[1]), 
    496             array_filter($matches[2]), 
    497             array_filter($matches[3]) 
    498         ); 
     474             
     475            $this->schema_info['national_columns'] = $national_columns; 
     476            $this->schema_info['national_types']   = $national_types; 
     477             
     478            if ($this->cache) { 
     479                $this->cache->set($this->makeCachePrefix() . 'schema_info', $this->schema_info);         
     480            } 
     481             
     482        } else { 
     483            $national_columns = $this->schema_info['national_columns']; 
     484            $national_types   = $this->schema_info['national_types'];    
     485        } 
    499486         
    500487        $additions = array(); 
    501488         
    502         foreach ($selects as $select) { 
    503             $clauses       = self::parseSelectSQL($select); 
    504             $table_aliases = self::parseTableAliases($clauses['FROM']); 
    505              
    506             preg_match_all('#([^,()\']+|\'(?>\'\'|\\\\\'|\\\\[^\']|[^\'\\\\]+)*\'|\((?:(?1)|,)*\)|\(\))+#i', $clauses['SELECT'], $selections); 
     489        foreach ($matches as $select) { 
     490            $select_clause = trim($select[1]); 
     491            $from_clause   = trim($select[3]); 
     492             
     493            // This recursively fixes sub-selects 
     494            if (preg_match('#\bselect\b#', $from_clause)) { 
     495                $from_clause = $this->fixMSSQLNationalColumns($from_clause);     
     496            } 
     497             
     498            $table_aliases = self::parseTableAliases($from_clause); 
     499             
     500            preg_match_all('#([^,()]+|\((?:(?1)|,)*\))+#i', $select_clause, $selections); 
    507501            $selections    = array_map('trim', $selections[0]); 
    508502            $to_fix        = array(); 
     
    601595                    if (is_array($column)) { 
    602596                        if (isset($column['alias'])) { 
    603                             $as = ' AS __flourish_mssqln_' . $column['alias']; 
     597                            $as = ' AS fmssqln__' . $column['alias']; 
    604598                        } else { 
    605                             $as = ' AS __flourish_mssqln_' . $column['column'];    
     599                            $as = ' AS fmssqln__' . $column['column'];     
    606600                        } 
    607601                        if (isset($column['expression'])) { 
     
    612606                        $column = $column['column']; 
    613607                    } else { 
    614                         $as     = ' AS __flourish_mssqln_' . $column; 
     608                        $as     = ' AS fmssqln__' . $column; 
    615609                        $expression = $alias . '.' . $column; 
    616610                    } 
     
    618612                        $cast = 'CAST(' . $expression . ' AS IMAGE)';    
    619613                    } else { 
    620                         $cast = 'CAST(' . $expression . ' AS VARBINARY(8000))'; 
     614                        $cast = 'CAST(' . $expression . ' AS VARBINARY(MAX))'; 
    621615                    } 
    622616                    $additions[] = $cast . $as; 
     
    624618            } 
    625619             
    626             $replace = preg_replace('#\bselect\s+' . preg_quote($clauses['SELECT'], '#') . '#i', 'SELECT ' . join(', ', array_merge($selections, $additions)), $select); 
     620            $replace = preg_replace('#\bselect\s+' . preg_quote($select_clause, '#') . '#i', 'SELECT ' . join(', ', array_merge($selections, $additions)), $select); 
    627621            $sql = str_replace($select, $replace, $sql);     
    628622        } 
     
    633627     
    634628    /** 
     629     * Fixes pulling unicode data out of national data type MSSQL columns 
     630     *  
     631     * @param  string $sql       The SQL to fix 
     632     * @param  array  &$strings  The strings from the SQL 
     633     * @return string  The fixed SQL 
     634     */ 
     635    private function fixOracleEmptyStrings($sql, &$strings) 
     636    { 
     637        if (preg_match('#^(UPDATE\s+(?:\w+\.)?\w+\s+)(SET((?:(?:(?!\bwhere\b|\breturning\b)[^()])+|\(((?:[^()]+|\((?3)\))*)\))*))(.*)$#i', $sql, $set_match)) { 
     638            $sql        = $set_match[1] . ':set_clause ' . $set_match[5]; 
     639            $set_clause = $set_match[2]; 
     640        } else { 
     641            $set_clause = FALSE; 
     642        }    
     643         
     644        foreach ($strings as $number => &$string) { 
     645            if ($string == "''") { 
     646                $sql = preg_replace('#(\s)=(?=\s+:string_' . $number . '\b)#',       '\1IS',     $sql, 1, $count_equal); 
     647                $sql = preg_replace('#(\s)(!=|<>)(?=\s+:string_' . $number . '\b)#', '\1IS NOT', $sql, 1, $count_not_equal); 
     648                if ($count_equal || $count_not_equal) { 
     649                    $string = 'NULL'; 
     650                }    
     651            } 
     652        } 
     653         
     654        if ($set_clause) { 
     655            $sql = preg_replace('#:set_clause\b#', $set_clause, $sql, 1);    
     656        } 
     657         
     658        return $sql; 
     659    } 
     660     
     661     
     662    /** 
     663     * Creates a unique cache prefix to help prevent cache conflicts 
     664     *  
     665     * @return void 
     666     */ 
     667    private function makeCachePrefix() 
     668    { 
     669        $prefix  = 'fSQLTranslation::' . $this->database->getType() . '::'; 
     670        if ($this->database->getHost()) { 
     671            $prefix .= $this->database->getHost() . '::';    
     672        } 
     673        if ($this->database->getPort()) { 
     674            $prefix .= $this->database->getPort() . '::';    
     675        } 
     676        $prefix .= $this->database->getDatabase() . '::'; 
     677        if ($this->database->getUsername()) { 
     678            $prefix .= $this->database->getUsername() . '::';    
     679        } 
     680        return $prefix;  
     681    } 
     682     
     683     
     684    /** 
    635685     * Translates Flourish SQL into the dialect for the current database 
    636686     *  
    637687     * @internal 
     688     *  
     689     * @param  array $statements  The SQL statements to translate 
     690     * @param  array $strings     The strings to interpolate back into the SQL statements 
     691     * @return array  The translated SQL statements all ready for execution. Statements that have been translated will have string key of the original SQL, all other will have a numeric key. 
     692     */ 
     693    public function translate($statements, $strings) 
     694    { 
     695        $output = array(); 
     696         
     697        foreach ($statements as $number => $sql) { 
     698             
     699            // These fixes don't need to know about strings 
     700            $new_sql = $this->translateBasicSyntax($sql); 
     701             
     702            if ($this->database->getType() == 'mssql' || $this->database->getType() == 'oracle') { 
     703                $new_sql = $this->translateLimitOffsetToRowNumber($new_sql);     
     704            } 
     705             
     706            // SQL Server does not like to give unicode results back to PHP without some coersion 
     707            if ($this->database->getType() == 'mssql') { 
     708                $new_sql = $this->fixMSSQLNationalColumns($new_sql);     
     709            } 
     710             
     711            // Oracle has this nasty habit of silently translating empty strings to null 
     712            if ($this->database->getType() == 'oracle') { 
     713                $new_sql = $this->fixOracleEmptyStrings($new_sql, $strings[$number]);    
     714            } 
     715             
     716            // Unescape literal semicolons in the queries 
     717            $sql     = preg_replace('#(?<!\\\\)\\\\;#', ';', $sql); 
     718            $new_sql = preg_replace('#(?<!\\\\)\\\\;#', ';', $new_sql); 
     719                 
     720            $extra_statements = array(); 
     721            $new_sql = $this->translateCreateTableStatements($new_sql, $extra_statements); 
     722                 
     723            // Put the strings back into the SQL 
     724            foreach ($strings[$number] as $index => $string) { 
     725                $sql     = preg_replace('#:string_' . $index . '\b#', $string, $sql, 1); 
     726                $new_sql = preg_replace('#:string_' . $index . '\b#', $string, $new_sql, 1);     
     727            } 
     728             
     729            $new_sql = $this->translateDateFunctions($new_sql); 
     730             
     731            if ($sql != $new_sql || $extra_statements) { 
     732                fCore::debug( 
     733                    self::compose( 
     734                        "Original SQL:%s", 
     735                        "\n" . $sql 
     736                    ), 
     737                    $this->debug 
     738                ); 
     739                $translated_sql = $new_sql; 
     740                if ($extra_statements) { 
     741                    $translated_sql .= '; ' . join('; ', $extra_statements);     
     742                } 
     743                fCore::debug( 
     744                    self::compose( 
     745                        "Translated SQL:%s", 
     746                        "\n" . $translated_sql 
     747                    ), 
     748                    $this->debug 
     749                ); 
     750            } 
     751             
     752            $output = array_merge($output, array($sql => $new_sql), $extra_statements);  
     753        } 
     754         
     755        return $output; 
     756    } 
     757     
     758     
     759    /** 
     760     * Translates basic syntax differences of the current database 
    638761     *  
    639762     * @param  string $sql  The SQL to translate 
    640763     * @return string  The translated SQL 
    641764     */ 
    642     public function translate($sql) 
    643     { 
    644         // Separate the SQL from quoted values 
    645         preg_match_all("#(?:'(?:''|\\\\'|\\\\[^']|[^'\\\\]+)*')|(?:[^']+)#", $sql, $matches); 
    646          
    647         $new_sql = ''; 
    648         foreach ($matches[0] as $match) { 
    649             // This is a quoted string value, don't do anything to it 
    650             if ($match[0] == "'") { 
    651                 $new_sql .= $match; 
    652              
    653             // Raw SQL should be run through the fixes 
    654             } else { 
    655                 $new_sql .= $this->translateBasicSyntax($match); 
    656             } 
    657         } 
    658          
    659         // Fix stuff that includes sql and quotes values 
    660         $new_sql = $this->translateDateFunctions($new_sql); 
    661         $new_sql = $this->translateComplicatedSyntax($new_sql); 
    662         $new_sql = $this->translateCreateTableStatements($new_sql); 
    663          
    664         if ($this->database->getType() == 'mssql') { 
    665             $new_sql = $this->fixMSSQLNationalColumns($new_sql);     
    666         } 
    667          
    668         if ($sql != $new_sql) { 
    669             fCore::debug( 
    670                 self::compose( 
    671                     "Original SQL:%s", 
    672                     "\n" .$sql 
    673                 ), 
    674                 $this->debug 
    675             ); 
    676             fCore::debug( 
    677                 self::compose( 
    678                     "Translated SQL:%s", 
    679                     "\n" . $new_sql 
    680                 ), 
    681                 $this->debug 
    682             ); 
    683         } 
    684          
    685         return $new_sql; 
    686     } 
    687      
    688      
    689     /** 
    690      * Translates basic syntax differences of the current database 
    691      *  
    692      * @param  string $sql  The SQL to translate 
    693      * @return string  The translated SQL 
    694      */ 
    695765    private function translateBasicSyntax($sql) 
    696766    { 
     
    699769             
    700770            if ($this->database->getType() == 'sqlite' && $this->database->getExtension() == 'pdo') { 
    701                 static $regex_sqlite = array( 
    702                     '#\binteger\s+autoincrement\s+primary\s+key\b#i'  => 'INTEGER PRIMARY KEY AUTOINCREMENT', 
    703                     '#\bcurrent_timestamp\b#i'                        => "datetime(CURRENT_TIMESTAMP, 'localtime')", 
    704                     '#\btrue\b#i'                                     => "'1'", 
    705                     '#\bfalse\b#i'                                    => "'0'" 
     771                $regex_sqlite = array( 
     772                    '#\binteger(?:\(\d+\))?\s+autoincrement\s+primary\s+key\b#i'  => 'INTEGER PRIMARY KEY AUTOINCREMENT', 
     773                    '#\bcurrent_timestamp\b#i'                                    => "datetime(CURRENT_TIMESTAMP, 'localtime')", 
     774                    '#\btrue\b#i'                                                 => "'1'", 
     775                    '#\bfalse\b#i'                                                => "'0'", 
     776                    '#\brandom\(\)#i'                                             => '(ABS(RANDOM())/9223372036854775807)' 
    706777                ); 
    707778            } else { 
    708                 static $regex_sqlite = array( 
    709                     '#\binteger\s+autoincrement\s+primary\s+key\b#i'  => 'INTEGER PRIMARY KEY', 
    710                     '#\bcurrent_timestamp\b#i'       => "datetime(CURRENT_TIMESTAMP, 'localtime')", 
    711                     '#\btrue\b#i'                    => "'1'", 
    712                     '#\bfalse\b#i'                   => "'0'" 
     779                $regex_sqlite = array( 
     780                    '#\binteger(?:\(\d+\))?\s+autoincrement\s+primary\s+key\b#i'  => 'INTEGER PRIMARY KEY', 
     781                    '#\bcurrent_timestamp\b#i'                                    => "CURRENT_TIMESTAMP()", 
     782                    '#\bcurrent_time\b#i'                                         => "CURRENT_TIME()", 
     783                    '#\bcurrent_date\b#i'                                         => "CURRENT_DATE()", 
     784                    '#\btrue\b#i'                                                 => "'1'", 
     785                    '#\bfalse\b#i'                                                => "'0'", 
     786                    '#\brandom\(\)#i'                                             => '(ABS(RANDOM())/9223372036854775807)', 
     787                    // SQLite 2 doesn't support CAST, but is also type-less, so we remove it 
     788                    '#\bcast\(\s*((?:[^()\s]+|\(((?:[^()]+|\((?2)\))*)\))+)\s+as\s+(?:[^()\s]+|\(((?:[^()]+|\((?3)\))*)\))+\s*\)#i' => '\1' 
    713789                ); 
    714790            } 
     
    720796        if ($this->database->getType() == 'postgresql') { 
    721797            static $regex_postgresql = array( 
    722                 '#\blike\b#i'                    => 'ILIKE', 
    723                 '#\bblob\b#i'                    => 'bytea', 
    724                 '#\binteger\s+autoincrement\b#i' => 'serial' 
     798                '#\b([\w.]+)\s+like\b#i'                     => 'CAST(\1 AS VARCHAR) ILIKE', 
     799                '#\blike\b#i'                                => 'ILIKE', 
     800                '#\bblob\b#i'                                => 'BYTEA', 
     801                '#\binteger(?:\(\d+\))?\s+autoincrement\b#i' => 'SERIAL' 
    725802            ); 
    726803             
     
    731808        if ($this->database->getType() == 'mysql') { 
    732809            static $regex_mysql = array( 
    733                 '#\brandom\(#i'                  => 'rand(', 
    734                 '#\btext\b#i'                    => 'MEDIUMTEXT', 
    735                 '#\bblob\b#i'                    => 'LONGBLOB', 
    736                 '#\btimestamp\b#i'               => 'DATETIME', 
    737                 '#\binteger\s+autoincrement\b#i' => 'INTEGER AUTO_INCREMENT' 
     810                '#\brandom\(#i'                              => 'rand(', 
     811                '#\btext\b#i'                                => 'MEDIUMTEXT', 
     812                '#\bblob\b#i'                                => 'LONGBLOB', 
     813                '#\btimestamp\b#i'                           => 'DATETIME', 
     814                '#\binteger(?:\(\d+\))?\s+autoincrement\b#i' => 'INTEGER AUTO_INCREMENT', 
     815                '#\bpi\(\)#i'                                => '(pi()+0.0000000000000)' 
    738816            ); 
    739817         
     
    744822        if ($this->database->getType() == 'mssql') { 
    745823            static $regex_mssql = array( 
    746                 '#\bbegin\s*(?!tran)#i'          => 'BEGIN TRANSACTION ', 
    747                 '#\brandom\(#i'                  => 'RAND(', 
    748                 '#\batan2\(#i'                   => 'ATN2(', 
    749                 '#\bceil\(#i'                    => 'CEILING(', 
    750                 '#\bln\(#i'                      => 'LOG(', 
    751                 '#\blength\(#i'                  => 'LEN(', 
    752                 '#\bsubstr\(#i'                  => 'SUBSTRING(', 
    753                 '#\bblob\b#i'                    => 'IMAGE', 
    754                 '#\btimestamp\b#i'               => 'DATETIME', 
    755                 '#\btime\b#i'                    => 'DATETIME', 
    756                 '#\bdate\b#i'                    => 'DATETIME', 
    757                 '#\binteger\s+autoincrement\b#i' => 'INTEGER IDENTITY(1,1)', 
    758                 '#\bboolean\b#i'                 => 'BIT', 
    759                 '#\btrue\b#i'                    => "'1'", 
    760                 '#\bfalse\b#i'                   => "'0'", 
    761                 '#\|\|#i'                      => '+' 
     824                '#\bbegin\s*(?!tran)#i'                      => 'BEGIN TRANSACTION ', 
     825                '#\brandom\(#i'                              => 'RAND(', 
     826                '#\batan2\(#i'                               => 'ATN2(', 
     827                '#\bceil\(#i'                                => 'CEILING(', 
     828                '#\bln\(#i'                                  => 'LOG(', 
     829                '#\blength\(#i'                              => 'LEN(', 
     830                '#\bsubstr\(#i'                              => 'SUBSTRING(', 
     831                '#\bblob\b#i'                                => 'IMAGE', 
     832                '#\btimestamp\b#i'                           => 'DATETIME', 
     833                '#\btime\b#i'                                => 'DATETIME', 
     834                '#\bdate\b#i'                                => 'DATETIME', 
     835                '#\binteger(?:\(\d+\))?\s+autoincrement\b#i' => 'INTEGER IDENTITY(1,1)', 
     836                '#\bboolean\b#i'                             => 'BIT', 
     837                '#\bvarchar\b#i'                             => 'NVARCHAR', 
     838                '#\bchar\b#i'                                => 'NCHAR', 
     839                '#\btext\b#i'                                => 'NTEXT', 
     840                '#\btrue\b#i'                                => "'1'", 
     841                '#\bfalse\b#i'                               => "'0'", 
     842                '#\|\|#i'                                    => '+', 
     843                // These wrap multiple mssql functions to accomplish another function 
     844                '#\blog\(\s*((?>[^(),]+|\((?1)(?:,(?1))?\)|\(\))+)\s*,\s*((?>[^(),]+|\((?2)(?:,(?2))?\)|\(\))+)\s*\)#i' => '(LOG(\2)/LOG(\1))', 
     845                '#\btrim\(\s*((?>[^(),]+|\((?1)\)|\(\))+)\s*\)#i'                                                       => 'RTRIM(LTRIM(\1))', 
     846                '#\bround\(\s*((?>[^(),]+|\((?1)\)|\(\))+)\s*\)#i'                                                      => 'round(\1, 0)' 
    762847            ); 
    763848         
    764849            return preg_replace(array_keys($regex_mssql), array_values($regex_mssql), $sql); 
    765850        } 
    766     } 
    767      
    768      
    769     /** 
    770      * Translates more complicated inconsistencies 
    771      *  
    772      * @param  string $sql  The SQL to translate 
     851         
     852        // Oracle fixes 
     853        if ($this->database->getType() == 'oracle') { 
     854            static $regex_oracle = array( 
     855                '#\bbigint\b#i'                  => 'INTEGER', 
     856                '#\bboolean\b#i'                 => 'NUMBER(1)', 
     857                '#\btext\b#i'                    => 'CLOB', 
     858                '#\bvarchar\b#i'                 => 'VARCHAR2', 
     859                '#\btime\b#i'                    => 'TIMESTAMP', 
     860                '#\btrue\b#i'                    => '1', 
     861                '#\bfalse\b#i'                   => '0', 
     862                '#(?:\b|^)((?>[^()%\s]+|\(((?:[^()]+|\((?2)\))*)\))+)\s*%\s*((?>[^()\s]+|\(((?:[^()]+|\((?4)\))*)\))+)(?:\b|$)#i'   => 'MOD(\1, \3)', 
     863                '#(?:\b|^)((?>[^()\s]+|\(((?:[^()]+|\((?2)\))*)\))+)\s+LIKE\s+((?>[^()\s]+|\(((?:[^()]+|\((?4)\))*)\))+)(?:\b|$)#i' => 'LOWER(\1) LIKE LOWER(\3)', 
     864                '#\bcot\(\s*((?>[^()\s]+|\(((?:[^()]+|\((?2)\))*)\))+)\s*\)#i'                                                      => '(1/TAN(\1))', 
     865                '#\bceiling\(#i'                 => 'CEIL(', 
     866                '#\brandom\(\)#i'                => '(ABS(DBMS_RANDOM.RANDOM)/2147483647)', 
     867                '#\bpi\(\)#i'                    => '3.14159265358979', 
     868                '#\bdegrees\(\s*((?>[^()\s]+|\(((?:[^()]+|\((?2)\))*)\))+)\s*\)#i' => '(\1 * 57.295779513083)', 
     869                '#\bradians\(\s*((?>[^()\s]+|\(((?:[^()]+|\((?2)\))*)\))+)\s*\)#i' => '(\1 * 0.017453292519943)', 
     870            ); 
     871         
     872            return preg_replace(array_keys($regex_oracle), array_values($regex_oracle), $sql); 
     873        } 
     874    } 
     875     
     876     
     877    /** 
     878     * Translates the structure of `CREATE TABLE` statements to the database specific syntax 
     879     *  
     880     * @param  string $sql                The SQL to translate 
     881     * @param  array  &$extra_statements  Any extra SQL statements that need to be added 
    773882     * @return string  The translated SQL 
    774883     */ 
    775     private function translateComplicatedSyntax($sql) 
    776     { 
    777         if ($this->database->getType() == 'mssql') { 
    778              
    779             $sql = $this->translateLimitOffsetToRowNumber($sql); 
    780              
    781             static $regex_mssql = array( 
    782                 // These wrap multiple mssql functions to accomplish another function 
    783                 '#\blog\(\s*((?>[^()\',]+|\'[^\']*\'|\((?1)(?:,(?1))?\)|\(\))+)\s*,\s*((?>[^()\',]+|\'[^\']*\'|\((?2)(?:,(?2))?\)|\(\))+)\s*\)#i' => '(LOG(\1)/LOG(\2))', 
    784                 '#\btrim\(\s*((?>[^()\',]+|\'(?:\'\'|\\\\\'|\\\\[^\']|[^\'\\\\]+)*\'|\((?1)\)|\(\))+)\s*\)#i' => 'RTRIM(LTRIM(\1))' 
    785             ); 
    786          
    787             $sql = preg_replace(array_keys($regex_mssql), array_values($regex_mssql), $sql); 
    788              
    789             if (preg_match('#select(\s*(?:[^()\']+|\'(?>\'\'|\\\\\'|\\\\[^\']|[^\'\\\\]+)*\'|\((?1)\)|\(\))+\s*)\s+limit\s+(\d+)#i', $sql, $match)) { 
    790                 $sql = str_replace($match[0], 'SELECT TOP ' . $match[2] . $match[1], $sql); 
    791             } 
    792         } 
    793          
    794         return $sql; 
    795     } 
    796      
    797      
    798     /** 
    799      * Translates the structure of `CREATE TABLE` statements to the database specific syntax 
    800      *  
    801      * @param  string $sql  The SQL to translate 
    802      * @return string  The translated SQL 
    803      */ 
    804     private function translateCreateTableStatements($sql) 
    805     { 
     884    private function translateCreateTableStatements($sql, &$extra_statements) 
     885    { 
     886        if (stripos($sql, 'CREATE TABLE') === FALSE) { 
     887            return $sql;     
     888        } 
     889         
    806890        // Make sure MySQL uses InnoDB tables 
    807         if ($this->database->getType() == 'mysql' && stripos($sql, 'CREATE TABLE') !== FALSE) { 
    808             preg_match_all('#(?<=,|\()\s*(\w+)\s+(?:[a-z]+)(?:\(\d+\))?(?:(\s+NOT\s+NULL)|(\s+DEFAULT\s+(?:[^, \']*|\'(?:\'\'|[^\']+)*\'))|(\s+UNIQUE)|(\s+PRIMARY\s+KEY)|(\s+CHECK\s*\(\w+\s+IN\s+(\(\s*(?:(?:[^, \']+|\'(?:\'\'|[^\']+)*\')\s*,\s*)*\s*(?:[^, \']+|\'(?:\'\'|[^\']+)*\')\))\)))*(\s+REFERENCES\s+\w+\s*\(\s*\w+\s*\)\s*(?:\s+(?:ON\s+DELETE|ON\s+UPDATE)\s+(?:CASCADE|NO\s+ACTION|RESTRICT|SET\s+NULL|SET\s+DEFAULT))*(?:\s+(?:DEFERRABLE|NOT\s+DEFERRABLE))?)?\s*(?:,|\s*(?=\)))#mi', $sql, $matches, PREG_SET_ORDER); 
     891        if ($this->database->getType() == 'mysql') { 
     892            preg_match_all('#(?<=,|\()\s*(\w+)\s+(?:[a-z]+)(?:\(\d+\))?(?:\s+unsigned|\s+zerofill|\s+character\s+set\s+[^ ]+|\s+collate\s+[^ ]+|\s+NULL|\s+NOT\s+NULL|(\s+DEFAULT\s+(?:[^, \']*|\'(?:\'\'|[^\']+)*\'))|\s+UNIQUE|\s+PRIMARY\s+KEY|(\s+CHECK\s*\(\w+\s+IN\s+(\(\s*(?:(?:[^, \']+|\'(?:\'\'|[^\']+)*\')\s*,\s*)*\s*(?:[^, \']+|\'(?:\'\'|[^\']+)*\')\))\)))*(\s+REFERENCES\s+\w+\s*\(\s*\w+\s*\)\s*(?:\s+(?:ON\s+DELETE|ON\s+UPDATE)\s+(?:CASCADE|NO\s+ACTION|RESTRICT|SET\s+NULL|SET\s+DEFAULT))*(?:\s+(?:DEFERRABLE|NOT\s+DEFERRABLE))?)?\s*(,|\s*(?=\)))#mi', $sql, $matches, PREG_SET_ORDER); 
    809893             
    810894            foreach ($matches as $match) { 
    811                 if (!empty($match[6])) { 
    812                     $sql = str_replace($match[0], "\n " . $match[1] . ' enum' . $match[7] . $match[2] . $match[3] . $match[4] . $match[5] . $match[8] . ', ', $sql); 
     895                // MySQL has the enum data type, so we switch check constraints to that 
     896                if (!empty($match[3])) { 
     897                    $replacement = "\n " . $match[1] . ' enum' . $match[4] . $match[2] . $match[5] . $match[6]; 
     898                    $sql = str_replace($match[0], $replacement, $sql); 
     899                    // This allows us to do a str_replace below for converting foreign key syntax 
     900                    $match[0] = $replacement; 
     901                } 
     902                 
     903                // Even InnoDB table types don't allow specify foreign key constraints in the column 
     904                // definition, so we move it to its own definition on the next line 
     905                if (!empty($match[5])) { 
     906                    $updated_match_0 = str_replace($match[5], ",\nFOREIGN KEY (" . $match[1] . ') ' . $match[5], $match[0]); 
     907                    $sql = str_replace($match[0], $updated_match_0, $sql);   
    813908                } 
    814909            } 
     
    823918             
    824919            preg_match_all('#(?:(?<=,|\()\s*(\w+)\s+(?:[a-z]+)(?:\((?:\d+)\))?(?:(\s+NOT\s+NULL)|(?:\s+DEFAULT\s+(?:[^, \']*|\'(?:\'\'|[^\']+)*\'))|(?:\s+UNIQUE)|(?:\s+PRIMARY\s+KEY(?:\s+AUTOINCREMENT)?)|(?:\s+CHECK\s*\(\w+\s+IN\s+\(\s*(?:(?:[^, \']+|\'(?:\'\'|[^\']+)*\')\s*,\s*)*\s*(?:[^, \']+|\'(?:\'\'|[^\']+)*\')\)\)))*(\s+REFERENCES\s+(\w+)\s*\(\s*(\w+)\s*\)\s*(?:\s+(?:ON\s+DELETE\s+(CASCADE|NO\s+ACTION|RESTRICT|SET\s+NULL)))?(?:\s+(?:ON\s+UPDATE\s+(CASCADE|NO\s+ACTION|RESTRICT|SET\s+NULL)))?)?\s*(?:,|\s*(?=\)))|(?<=,|\()\s*FOREIGN\s+KEY\s*(?:(\w+)|\((\w+)\))\s+REFERENCES\s+(\w+)\s*\(\s*(\w+)\s*\)\s*(?:\s+(?:ON\s+DELETE\s+(CASCADE|NO\s+ACTION|RESTRICT|SET\s+NULL)))?(?:\s+(?:ON\s+UPDATE\s+(CASCADE|NO\s+ACTION|RESTRICT|SET\s+NULL)))?\s*(?:,|\s*(?=\))))#mi', $sql, $matches, PREG_SET_ORDER); 
    825              
    826             // Make sure we have a semicolon so we can add triggers 
    827             $sql = trim($sql); 
    828             if (substr($sql, -1) != ';') { 
    829                 $sql .= ';'; 
    830             } 
    831920             
    832921            $not_null_columns = array(); 
     
    847936                // Handle column level foreign key inserts/updates 
    848937                if ($match[1]) { 
    849                     $sql .= $this->createSQLiteForeignKeyTriggerValidInsertUpdate($referencing_table, $match[1], $match[4], $match[5], in_array($match[1], $not_null_columns)); 
     938                    $this->createSQLiteForeignKeyTriggerValidInsertUpdate($extra_statements, $referencing_table, $match[1], $match[4], $match[5], in_array($match[1], $not_null_columns)); 
    850939                 
    851940                // Handle table level foreign key inserts/update 
    852941                } elseif ($match[9]) { 
    853                     $sql .= $this->createSQLiteForeignKeyTriggerValidInsertUpdate($referencing_table, $match[9], $match[10], $match[11], in_array($match[9], $not_null_columns)); 
     942                    $this->createSQLiteForeignKeyTriggerValidInsertUpdate($extra_statements, $referencing_table, $match[9], $match[10], $match[11], in_array($match[9], $not_null_columns)); 
    854943                } 
    855944                 
     
    862951                if (!empty($match[3])) { 
    863952                    if ($match[6]) { 
    864                         $sql .= $this->createSQLiteForeignKeyTriggerOnDelete($referencing_table, $match[1], $match[4], $match[5], $match[6]); 
     953                        $this->createSQLiteForeignKeyTriggerOnDelete($extra_statements, $referencing_table, $match[1], $match[4], $match[5], $match[6]); 
    865954                    } 
    866                     if ($match[7]) { 
    867                         $sql .= $this->createSQLiteForeignKeyTriggerOnUpdate($referencing_table, $match[1], $match[4], $match[5], $match[7]); 
     955                    if (!empty($match[7])) { 
     956                        $this->createSQLiteForeignKeyTriggerOnUpdate($extra_statements, $referencing_table, $match[1], $match[4], $match[5], $match[7]); 
    868957                    } 
    869958                    continue; 
     
    872961                // Handle table level foreign key delete/update clauses 
    873962                if ($match[12]) { 
    874                     $sql .= $this->createSQLiteForeignKeyTriggerOnDelete($referencing_table, $match[9], $match[10], $match[11], $match[12]); 
     963                    $this->createSQLiteForeignKeyTriggerOnDelete($extra_statements, $referencing_table, $match[9], $match[10], $match[11], $match[12]); 
    875964                } 
    876965                if ($match[13]) { 
    877                     $sql .= $this->createSQLiteForeignKeyTriggerOnUpdate($referencing_table, $match[9], $match[10], $match[11], $match[13]); 
    878                 } 
    879             } 
    880         } 
     966                    $this->createSQLiteForeignKeyTriggerOnUpdate($extra_statements, $referencing_table, $match[9], $match[10], $match[11], $match[13]); 
     967                } 
     968            } 
     969         
     970         
     971        // Create sequences and triggers for Oracle 
     972        } elseif ($this->database->getType() == 'oracle' && preg_match('#CREATE\s+TABLE\s+(\w+)#i', $sql, $table_matches) !== FALSE && stripos($sql, 'autoincrement') !== FALSE) { 
     973             
     974            if (preg_match('#(?<=,|\()\s*(\w+)\s+(?:[a-z]+)(?:\((?:\d+)\))?.*?\bAUTOINCREMENT\b[^,\)]*(?:,|\s*(?=\)))#mi', $sql, $matches)) { 
     975                $table_name    = $table_matches[1]; 
     976                $column        = $matches[1]; 
     977                 
     978                $table_column  = substr($table_name . '_' . $column, 0, 26); 
     979                 
     980                $sequence_name = $table_column . '_seq'; 
     981                $trigger_name  = $table_column . '_trg'; 
     982                 
     983                $sequence = 'CREATE SEQUENCE ' . $sequence_name; 
     984                 
     985                $trigger  = 'CREATE OR REPLACE TRIGGER '. $trigger_name . "\n"; 
     986                $trigger .= "BEFORE INSERT ON " . $table_name . "\n"; 
     987                $trigger .= "FOR EACH ROW\n"; 
     988                $trigger .= "BEGIN\n"; 
     989                $trigger .= "  IF :new." . $column . " IS NULL THEN\n"; 
     990                $trigger .= "   SELECT " . $sequence_name . ".nextval INTO :new." . $column . " FROM dual;\n"; 
     991                $trigger .= "  END IF;\n"; 
     992                $trigger .= "END;"; 
     993                 
     994                $extra_statements[] = $sequence; 
     995                $extra_statements[] = $trigger;  
     996                 
     997                $sql = preg_replace('#\s+autoincrement\b#i', '', $sql); 
     998            } 
     999                     
     1000        } 
     1001         
    8811002         
    8821003        return $sql; 
     
    8921013    private function translateDateFunctions($sql) 
    8931014    { 
    894         // fix diff_seconds() 
    895         preg_match_all("#diff_seconds\\(((?>(?:[^()',]+|'[^']+')|\\((?1)(?:,(?1))?\\)|\\(\\))+)\\s*,\\s*((?>(?:[^()',]+|'[^']+')|\\((?2)(?:,(?2))?\\)|\\(\\))+)\\)#ims", $sql, $diff_matches, PREG_SET_ORDER); 
     1015        // Handle diff_seconds() 
     1016        // diff_seconds() accepts two parameters, the two dates to get the difference between 
     1017        // Regex matches: 
     1018        // 1 - The first date 
     1019        // 2 - The second date 
     1020        preg_match_all("#diff_seconds\(((?>(?:[^()',]+|'[^']+')|\((?1)(?:,(?1))?\)|\(\))+)\s*,\s*((?>(?:[^()',]+|'[^']+')|\((?2)(?:,(?2))?\)|\(\))+)\)#ims", $sql, $diff_matches, PREG_SET_ORDER); 
    8961021        foreach ($diff_matches as $match) { 
    8971022             
     
    9041029                $sql = str_replace($match[0], "extract(EPOCH FROM age(" . $match[2] . ", " . $match[1] . "))", $sql); 
    9051030             
     1031            // Oracle 
     1032            } elseif ($this->database->getType() == 'oracle') { 
     1033                if (substr($match[1], 0, 1) == "'") { 
     1034                    $match[1] = 'CAST(' . $match[1] . " AS TIMESTAMP)";  
     1035                } 
     1036                if (substr($match[2], 0, 1) == "'") { 
     1037                    $match[2] = 'CAST(' . $match[2] . " AS TIMESTAMP)"; 
     1038                } 
     1039                $sql = str_replace($match[0], "((TO_NUMBER(TO_CHAR(" . $match[2] . ", 'J')) - TO_NUMBER(TO_CHAR(" . $match[1] . ", 'J'))) * 86400) + (TO_NUMBER(TO_CHAR(" . $match[2] . ", 'SSSSS')) - TO_NUMBER(TO_CHAR(" . $match[1] . ", 'SSSSS')))", $sql); 
     1040             
    9061041            // MySQL 
    9071042            } elseif ($this->database->getType() == 'mysql') { 
     
    9141049        } 
    9151050         
    916         // fix add_interval() 
    917         preg_match_all("#add_interval\\(((?>(?:[^()',]+|'[^']+')|\\((?1)(?:,(?1))?\\)|\\(\\))+)\\s*,\\s*'([^']+)'\\s*\\)#i", $sql, $add_matches, PREG_SET_ORDER); 
     1051        // Handle add_interval() 
     1052        // add_interval() accepts two parameters, the date to modify and the interval to add 
     1053        // Regex matches: 
     1054        // 1 - The first parameter 
     1055        // 2 - The second parameter 
     1056        preg_match_all("#add_interval\(((?>(?:[^()',]+|'[^']+')|\((?1)(?:,(?1))?\)|\(\))+)\s*,\s*'([^']+)'\s*\)#i", $sql, $add_matches, PREG_SET_ORDER); 
    9181057        foreach ($add_matches as $match) { 
    9191058             
    9201059            // SQLite 
    9211060            if ($this->database->getType() == 'sqlite') { 
     1061                 
     1062                // Regex matches: 
     1063                // 0 - The adjustment in the form: +/- number units 
    9221064                preg_match_all("#(?:\\+|\\-)\\d+\\s+(?:year|month|day|hour|minute|second)(?:s)?#i", $match[2], $individual_matches); 
    9231065                $strings = "'" . join("', '", $individual_matches[0]) . "'"; 
     
    9261068            // PostgreSQL 
    9271069            } elseif ($this->database->getType() == 'postgresql') { 
     1070                if (substr($match[1], 0, 1) == "'") { 
     1071                    if (preg_match('#^\'\d{4}-\d{2}-\d{2}\'$#', $match[1])) { 
     1072                        $match[1] = 'DATE ' . $match[1]; 
     1073                    } elseif (preg_match('#^\'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\'$#', $match[1])) { 
     1074                        $match[1] = 'TIMESTAMP ' . $match[1]; 
     1075                    } elseif (preg_match('#^\'\d{2}:\d{2}:\d{2}\'$#', $match[1])) { 
     1076                        $match[1] = 'TIME ' . $match[1]; 
     1077                    }    
     1078                } 
    9281079                $sql = str_replace($match[0], "(" . $match[1] . " + INTERVAL '" . $match[2] . "')", $sql); 
    9291080             
    930             // MySQL 
    931             } elseif ($this->database->getType() == 'mysql') { 
     1081            // MySQL and Oracle 
     1082            } elseif ($this->database->getType() == 'mysql' || $this->database->getType() == 'oracle') { 
     1083                 
     1084                // Regex matches: 
     1085                // 1 - The sign, +/- 
     1086                // 2 - The number 
     1087                // 3 - The units, hour, minute, etc 
    9321088                preg_match_all("#(\\+|\\-)(\\d+)\\s+(year|month|day|hour|minute|second)(?:s)?#i", $match[2], $individual_matches, PREG_SET_ORDER); 
    9331089                $intervals_string = ''; 
    9341090                foreach ($individual_matches as $individual_match) { 
    935                     $intervals_string .= ' ' . $individual_match[1] . ' INTERVAL ' . $individual_match[2] . ' ' . strtoupper($individual_match[3]); 
    936                 } 
     1091                    $intervals_string .= ' ' . $individual_match[1] . " INTERVAL '" . $individual_match[2] . "' " . strtoupper($individual_match[3]); 
     1092                } 
     1093                 
     1094                if ($this->database->getType() == 'oracle' && substr($match[1], 0, 1) == "'") { 
     1095                    if (preg_match('#^\'\d{4}-\d{2}-\d{2}\'$#', $match[1])) { 
     1096                        $match[1] = 'CAST(' . $match[1] . ' AS DATE)'; 
     1097                    } elseif (preg_match('#^\'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\'$#', $match[1])) { 
     1098                        $match[1] = 'CAST(' . $match[1] . ' AS TIMESTAMP)'; 
     1099                    }    
     1100                } 
     1101                 
    9371102                $sql = str_replace($match[0], "(" . $match[1] . $intervals_string . ")", $sql); 
    9381103             
    9391104            // MSSQL 
    9401105            } elseif ($this->database->getType() == 'mssql') { 
     1106                 
     1107                // Regex matches: 
     1108                // 1 - The sign, +/- 
     1109                // 2 - The number 
     1110                // 3 - The units, hour, minute, etc 
    9411111                preg_match_all("#(\\+|\\-)(\\d+)\\s+(year|month|day|hour|minute|second)(?:s)?#i", $match[2], $individual_matches, PREG_SET_ORDER); 
    9421112                $date_add_string = ''; 
     
    9471117                } 
    9481118                $sql = str_replace($match[0], $date_add_string . $match[1] . str_pad('', $stack, ')'), $sql); 
     1119                 
    9491120            } 
    9501121        } 
     
    9621133    private function translateLimitOffsetToRowNumber($sql) 
    9631134    { 
    964         preg_match_all('#((select(?:\s*(?:[^()\']+|\'(?:\'\'|\\\\\'|\\\\[^\']|[^\'\\\\]+)*\'|\((?1)\)|\(\))+\s*))\s+limit\s+(\d+)\s+offset\s+(\d+))#i', $sql, $matches, PREG_SET_ORDER); 
     1135        // Regex details: 
     1136        // 1 - The SELECT clause 
     1137        // 2 - () recursion handler 
     1138        // 3 - FROM clause 
     1139        // 4 - () recursion handler 
     1140        // 5 - ORDER BY clause 
     1141        // 6 - () recursion handler 
     1142        // 7 - LIMIT number 
     1143        // 8 - OFFSET number 
     1144        preg_match_all('#select((?:(?:(?!\sfrom\s)[^()])+|\(((?:[^()]+|\((?2)\))*)\))*\s)(from(?:(?:(?!\slimit\s|\sorder by\s)[^()])+|\(((?:[^()]+|\((?4)\))*)\))*\s)(order by(?:(?:(?!\slimit\s)[^()])+|\(((?:[^()]+|\((?6)\))*)\))*\s)?limit\s+(\d+)(?:\s+offset\s+(\d+))?#i', $sql, $matches, PREG_SET_ORDER); 
    9651145         
    9661146        foreach ($matches as $match) { 
    967             $clauses = self::parseSelectSQL($match[1]); 
    968              
    969             if ($clauses['ORDER BY'] == NULL) { 
    970                 $clauses['ORDER BY'] = '1 ASC'; 
    971             } 
    972              
    973             $replacement = ''; 
    974             foreach ($clauses as $key => $value) { 
    975                 if (empty($value)) { 
    976                     continue; 
    977                 } 
    978                  
    979                 if ($key == 'SELECT') { 
    980                     $replacement .= 'SELECT ' . $value . ', ROW_NUMBER() OVER ('; 
    981                     $replacement .= 'ORDER BY ' . $clauses['ORDER BY']; 
    982                     $replacement .= ') AS __flourish_limit_offset_row_num '; 
    983                 } elseif ($key == 'LIMIT' || $key == 'ORDER BY') { 
    984                     // Skip this clause 
     1147            if ($this->database->getType() == 'mssql') { 
     1148                 
     1149                // This means we have an offset clause 
     1150                if (!empty($match[8])) { 
     1151                     
     1152                    if ($match[5] === '') { 
     1153                        $match[5] = "ORDER BY rand(1) ASC";  
     1154                    } 
     1155                     
     1156                    $select  = $match[1] . ', ROW_NUMBER() OVER ('; 
     1157                    $select .= $match[5]; 
     1158                    $select .= ') AS flourish__row__num '; 
     1159                    $select .= $match[3]; 
     1160                     
     1161                    $replacement = 'SELECT * FROM (SELECT ' . trim($match[1]) . ', ROW_NUMBER() OVER (' . $match[5] . ') AS flourish__row__num ' . $match[3] . ') AS original_query WHERE flourish__row__num > ' . $match[8] . ' AND flourish__row__num <= ' . ($match[7] + $match[8]) . ' ORDER BY flourish__row__num'; 
     1162                 
     1163                // Otherwise we just have a limit 
    9851164                } else { 
    986                     $replacement .= $key . ' ' . $value . ' '; 
    987                 } 
    988             } 
    989              
    990             $replacement = 'SELECT * FROM (' . trim($replacement) . ') AS original_query WHERE __flourish_limit_offset_row_num > ' . $match[4] . ' AND __flourish_limit_offset_row_num <= ' . ($match[3] + $match[4]) . ' ORDER BY __flourish_limit_offset_row_num'; 
    991              
    992             $sql = str_replace($match[1], $replacement, $sql); 
     1165                    $replacement = 'SELECT TOP ' . $match[7] . ' ' . trim($match[1] . $match[3] . $match[5]); 
     1166                } 
     1167                 
     1168            // While Oracle has the row_number() construct, the rownum pseudo-column is better 
     1169            } elseif ($this->database->getType() == 'oracle') { 
     1170                 
     1171                 // This means we have an offset clause 
     1172                if (!empty($match[8])) { 
     1173                     
     1174                    $replacement = 'SELECT * FROM (SELECT flourish__sq.*, rownum flourish__row__num FROM (SELECT' . $match[1] . $match[3] . $match[5] . ') flourish__sq WHERE rownum <= ' . ($match[7] + $match[8]) . ') WHERE flourish__row__num > ' . $match[8]; 
     1175                 
     1176                // Otherwise we just have a limit 
     1177                } else { 
     1178                    $replacement = 'SELECT * FROM (SELECT' . $match[1] . $match[3] . $match[5] . ') WHERE rownum <= ' . $match[7]; 
     1179                } 
     1180                     
     1181            } 
     1182             
     1183            $sql = str_replace($match[0], $replacement, $sql); 
    9931184        } 
    9941185         
  • fSchema.php

    r547 r562 Hide Line Numbers
    1010 * @link       http://flourishlib.com/fSchema 
    1111 *  
    12  * @version    1.0.0b15 
     12 * @version    1.0.0b16 
     13 * @changes    1.0.0b16  Backwards Compatibility Break - ::setCacheFile() changed to ::enableCaching() and now requires an fCache object, ::flushInfo() renamed to ::clearCache(), added Oracle support [wb, 2009-05-04] 
    1314 * @changes    1.0.0b15  Added support for the three different types of identifier quoting in SQLite [wb, 2009-03-28] 
    1415 * @changes    1.0.0b14  Added support for MySQL column definitions containing the COLLATE keyword [wb, 2009-03-28] 
     
    3031{ 
    3132    /** 
    32      * The file to cache the info to 
    33      *  
    34      * @var string 
    35      */ 
    36     private $cache_file = NULL; 
     33     * The place to cache to 
     34     *  
     35     * @var fCache 
     36     */ 
     37    private $cache = NULL; 
    3738     
    3839    /** 
     
    6566     
    6667    /** 
    67      * If the info has changed (and should be written to cache) 
    68      *  
    69      * @var boolean 
    70      */ 
    71     private $info_changed = FALSE; 
    72      
    73     /** 
    7468     * The cached key info 
    7569     *  
     
    10599     */ 
    106100    private $relationships = array(); 
    107      
    108     /** 
    109      * The state of the info 
    110      *  
    111      * @var string 
    112      */ 
    113     private $state = 'current'; 
    114101     
    115102    /** 
     
    130117    { 
    131118        $this->database = $database; 
    132     } 
    133      
    134      
    135     /** 
    136      * Stores the info in the cache file if set 
    137      *  
    138      * @return void 
    139      */ 
    140     public function __destruct() 
    141     { 
    142         if ($this->cache_file && $this->info_changed) { 
    143             $contents = serialize(array('column_info'   => $this->column_info, 
    144                                         'keys'          => $this->keys)); 
    145             file_put_contents($this->cache_file, $contents); 
    146         } 
    147119    } 
    148120     
     
    179151     
    180152    /** 
     153     * Clears all of the schema info out of the object and, if set, the fCache object 
     154     *  
     155     * @internal 
     156     *  
     157     * @return void 
     158     */ 
     159    public function clearCache() 
     160    { 
     161        $this->column_info        = array(); 
     162        $this->databases          = array(); 
     163        $this->keys               = array(); 
     164        $this->merged_column_info = array(); 
     165        $this->merged_keys        = array(); 
     166        $this->relationships      = array(); 
     167        $this->tables             = array(); 
     168        if ($this->cache) { 
     169            $prefix = $this->makeCachePrefix(); 
     170            $this->cache->delete($prefix . 'column_info'); 
     171            $this->cache->delete($prefix . 'databases'); 
     172            $this->cache->delete($prefix . 'keys'); 
     173            $this->cache->delete($prefix . 'tables'); 
     174        } 
     175    } 
     176     
     177     
     178    /** 
     179     * Sets the schema to be cached to the fCache object specified 
     180     *  
     181     * @param  fCache $cache  The cache to cache to 
     182     * @return void 
     183     */ 
     184    public function enableCaching($cache) 
     185    { 
     186        $this->cache = $cache; 
     187         
     188        $prefix = $this->makeCachePrefix(); 
     189        $this->column_info = $this->cache->get($prefix . 'column_info', array()); 
     190        $this->databases   = $this->cache->get($prefix . 'databases',   NULL); 
     191        $this->keys        = $this->cache->get($prefix . 'keys',        array()); 
     192        $this->tables      = $this->cache->get($prefix . 'tables',      NULL); 
     193    } 
     194     
     195     
     196    /** 
    181197     * Gets the column info from the database for later access 
    182198     *  
     
    186202    private function fetchColumnInfo($table) 
    187203    { 
     204        if (isset($this->column_info[$table])) { 
     205            return;  
     206        } 
     207         
    188208        switch ($this->database->getType()) { 
    189209            case 'mssql': 
     
    195215                break; 
    196216                 
     217            case 'oracle': 
     218                $column_info = $this->fetchOracleColumnInfo($table); 
     219                break; 
     220             
    197221            case 'postgresql': 
    198222                $column_info = $this->fetchPostgreSQLColumnInfo($table); 
     
    209233             
    210234        $this->column_info[$table] = $column_info; 
    211         $this->info_changed = TRUE; 
     235        if ($this->cache) { 
     236            $this->cache->set($this->makeCachePrefix() . 'column_info', $this->column_info);     
     237        } 
    212238    } 
    213239     
     
    220246    private function fetchKeys() 
    221247    { 
     248        if ($this->keys) { 
     249            return;  
     250        } 
     251         
    222252        switch ($this->database->getType()) { 
    223253            case 'mssql': 
     
    229259                break; 
    230260                 
     261            case 'oracle': 
     262                $keys = $this->fetchOracleKeys(); 
     263                break; 
     264             
    231265            case 'postgresql': 
    232266                $keys = $this->fetchPostgreSQLKeys(); 
     
    239273             
    240274        $this->keys = $keys; 
    241         $this->info_changed = TRUE; 
     275        if ($this->cache) { 
     276            $this->cache->set($this->makeCachePrefix() . 'keys', $this->keys);   
     277        } 
    242278    } 
    243279     
     
    277313            'datetime'          => 'timestamp', 
    278314            'smalldatetime'     => 'timestamp', 
     315            'datetime2'         => 'timestamp', 
     316            'date'              => 'date', 
     317            'time'              => 'time', 
    279318            'varchar'           => 'varchar', 
    280319            'nvarchar'          => 'varchar', 
     
    298337                        c.column_name              AS 'column', 
    299338                        c.data_type                AS 'type', 
    300                         c.is_nullable              AS not_null
     339                        c.is_nullable              AS nullable
    301340                        c.column_default           AS 'default', 
    302341                        c.character_maximum_length AS max_length, 
    303342                        c.numeric_scale            AS decimal_places, 
    304                         CASE WHEN COLUMNPROPERTY(OBJECT_ID(QUOTENAME(c.table_schema) + '.' + QUOTENAME(c.table_name)), c.column_name, 'IsIdentity') = 1 AND 
    305                                   OBJECTPROPERTY(OBJECT_ID(QUOTENAME(c.table_schema) + '.' + QUOTENAME(c.table_name)), 'IsMSShipped') = 0 
    306                              THEN '1' 
    307                              ELSE '0' 
     343                        CASE 
     344                            WHEN 
     345                              COLUMNPROPERTY(OBJECT_ID(QUOTENAME(c.table_schema) + '.' + QUOTENAME(c.table_name)), c.column_name, 'IsIdentity') = 1 AND 
     346                              OBJECTPROPERTY(OBJECT_ID(QUOTENAME(c.table_schema) + '.' + QUOTENAME(c.table_name)), 'IsMSShipped') = 0 
     347                            THEN '1' 
     348                            ELSE '0' 
    308349                          END AS auto_increment, 
    309                         cc.check_clause            AS 'constraint' 
     350                        cc.check_clause AS 'constraint' 
    310351                    FROM 
    311352                        INFORMATION_SCHEMA.COLUMNS AS c LEFT JOIN 
     
    349390            // If the column has a constraint, look for valid values 
    350391            if (in_array($info['type'], array('char', 'varchar')) && !empty($row['constraint'])) { 
    351                 if (preg_match('#^\(((?:(?: OR )?\[[^\]]+\]=\'(?:\'\'|[^\']+)+\')+)\)$#D', $row['constraint'], $matches)) { 
     392                if (preg_match('#^\(((?:(?: OR )?\[[^\]]+\]\s*=\s*\'(?:\'\'|[^\']+)+\')+)\)$#D', $row['constraint'], $matches)) { 
    352393                    $valid_values = explode(' OR ', $matches[1]); 
    353394                    foreach ($valid_values as $key => $value) { 
    354                         $valid_values[$key] = substr($value, 4 + strlen($row['column']), -1); 
     395                        $value = preg_replace('#^\s*\[' . preg_quote($row['column'], '#') . '\]\s*=\s*\'(.*)\'\s*$#', '\1', $value); 
     396                        $valid_values[$key] = str_replace("''", "'", $value); 
    355397                    } 
    356                     $info['valid_values'] = $valid_values; 
     398                    // SQL Server turns CHECK constraint values into a reversed list, so we fix it here 
     399                    $info['valid_values'] = array_reverse($valid_values); 
    357400                } 
    358401            } 
     
    369412                } elseif (in_array($info['type'], array('char', 'varchar', 'text', 'timestamp')) ) { 
    370413                    $info['default'] = substr($row['default'], 2, -2); 
    371                 } elseif (in_array($info['type'], array('integer', 'float', 'boolean')) ) { 
     414                } elseif ($info['type'] == 'boolean') { 
     415                    $info['default'] = (boolean) substr($row['default'], 2, -2); 
     416                } elseif (in_array($info['type'], array('integer', 'float')) ) { 
    372417                    $info['default'] = str_replace(array('(', ')'), '', $row['default']); 
    373418                } else { 
     
    377422             
    378423            // Handle not null 
    379             $info['not_null'] = ($row['not_null'] == 'NO') ? TRUE : FALSE; 
     424            $info['not_null'] = ($row['nullable'] == 'NO') ? TRUE : FALSE; 
    380425             
    381426            $column_info[$row['column']] = $info; 
     
    446491                        INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE AS ccu ON ccu.constraint_name = rc.unique_constraint_name 
    447492                    WHERE 
    448                         c.constraint_catalog = '" . $this->database->getDatabase() . "' 
     493                        c.constraint_catalog = '" . $this->database->getDatabase() . "' AND 
     494                        c.table_name != 'sysdiagrams' 
    449495                    ORDER BY 
    450496                        LOWER(c.table_name), 
    451497                        c.constraint_type, 
    452498                        LOWER(kcu.constraint_name), 
     499                        kcu.ordinal_position, 
    453500                        LOWER(kcu.column_name)"; 
    454501         
     
    464511                if ($last_name) { 
    465512                    if ($last_type == 'foreign' || $last_type == 'unique') { 
     513                        if (!isset($keys[$last_table][$last_type])) { 
     514                            $keys[$last_table][$last_type] = array();        
     515                        } 
    466516                        $keys[$last_table][$last_type][] = $temp; 
    467517                    } else { 
     
    500550        if (isset($temp)) { 
    501551            if ($last_type == 'foreign') { 
     552                if (!isset($keys[$last_table][$last_type])) { 
     553                    $keys[$last_table][$last_type] = array();        
     554                } 
    502555                $keys[$last_table][$last_type][] = $temp; 
    503556            } else { 
     
    535588    { 
    536589        $data_type_mapping = array( 
    537             'tinyint(1)'        => 'boolean', 
    538590            'tinyint'           => 'integer', 
    539591            'smallint'          => 'integer', 
     
    582634            foreach ($data_type_mapping as $data_type => $mapped_data_type) { 
    583635                if (stripos($match[2], $data_type) === 0) { 
     636                    if ($match[2] == 'tinyint' && $match[3] == 1) { 
     637                        $mapped_data_type = 'boolean';   
     638                    } 
     639                 
    584640                    $info['type'] = $mapped_data_type; 
    585641                    break; 
     
    714770                } 
    715771                $keys[$table]['foreign'][] = $temp; 
     772            } 
     773        } 
     774         
     775        return $keys; 
     776    } 
     777     
     778     
     779    /** 
     780     * Gets the column info from an Oracle database 
     781     *  
     782     * The returned array is in the format: 
     783     *  
     784     * {{{ 
     785     * array( 
     786     *     (string) {column name} => array( 
     787     *         'type'           => (string)  {data type}, 
     788     *         'not_null'       => (boolean) {if value can't be null}, 
     789     *         'default'        => (mixed)   {the default value-may contain special strings CURRENT_TIMESTAMP, CURRENT_TIME or CURRENT_DATE}, 
     790     *         'valid_values'   => (array)   {the valid values for a char/varchar field}, 
     791     *         'max_length'     => (integer) {the maximum length in a char/varchar field}, 
     792     *         'decimal_places' => (integer) {the number of decimal places for a decimal field}, 
     793     *         'auto_increment' => (boolean) {if the integer primary key column is auto_increment} 
     794     *     ), ... 
     795     * ) 
     796     * }}} 
     797     *  
     798     * @param  string $table  The table to fetch the column info for 
     799     * @return array  The column info for the table specified - see method description for details 
     800     */ 
     801    private function fetchOracleColumnInfo($table) 
     802    { 
     803        $table = strtoupper($table); 
     804         
     805        $column_info = array(); 
     806         
     807        $data_type_mapping = array( 
     808            'boolean'           => 'boolean', 
     809            'integer'           => 'integer', 
     810            'timestamp'         => 'timestamp', 
     811            'date'              => 'date', 
     812            'varchar2'          => 'varchar', 
     813            'nvarchar2'         => 'varchar', 
     814            'char'              => 'char', 
     815            'nchar'             => 'char', 
     816            'float'             => 'float', 
     817            'binary_float'      => 'float', 
     818            'binary_double'     => 'float', 
     819            'blob'              => 'blob', 
     820            'bfile'             => 'varchar', 
     821            'clob'              => 'text', 
     822            'nclob'             => 'text' 
     823        ); 
     824         
     825        $sql = "SELECT 
     826                        LOWER(UTC.COLUMN_NAME) COLUMN_NAME, 
     827                        CASE 
     828                            WHEN 
     829                                UTC.DATA_TYPE = 'NUMBER' AND 
     830                                UTC.DATA_PRECISION IS NULL AND 
     831                                UTC.DATA_SCALE = 0 
     832                            THEN 
     833                                'integer' 
     834                            WHEN 
     835                                UTC.DATA_TYPE = 'NUMBER' AND 
     836                                UTC.DATA_PRECISION = 1 AND 
     837                                UTC.DATA_SCALE = 0 
     838                            THEN 
     839                                'boolean' 
     840                            WHEN 
     841                                UTC.DATA_TYPE = 'NUMBER' AND 
     842                                UTC.DATA_PRECISION IS NOT NULL AND 
     843                                UTC.DATA_SCALE != 0 AND 
     844                                UTC.DATA_SCALE IS NOT NULL 
     845                            THEN 
     846                                'float' 
     847                            ELSE 
     848                                LOWER(UTC.DATA_TYPE) 
     849                            END DATA_TYPE, 
     850                        CASE 
     851                            WHEN 
     852                                UTC.CHAR_LENGTH <> 0 
     853                            THEN 
     854                                UTC.CHAR_LENGTH 
     855                            WHEN 
     856                                UTC.DATA_TYPE = 'NUMBER' AND 
     857                                UTC.DATA_PRECISION != 1 AND 
     858                                UTC.DATA_SCALE != 0 AND 
     859                                UTC.DATA_PRECISION IS NOT NULL 
     860                            THEN 
     861                                UTC.DATA_SCALE   
     862                            ELSE 
     863                                NULL 
     864                            END LENGTH, 
     865                        UTC.NULLABLE, 
     866                        UTC.DATA_DEFAULT, 
     867                        UC.SEARCH_CONDITION CHECK_CONSTRAINT 
     868                    FROM 
     869                        USER_TAB_COLUMNS UTC LEFT JOIN 
     870                        USER_CONS_COLUMNS UCC ON 
     871                            UTC.COLUMN_NAME = UCC.COLUMN_NAME AND 
     872                            UTC.TABLE_NAME = UCC.TABLE_NAME AND 
     873                            UCC.POSITION IS NULL LEFT JOIN 
     874                        USER_CONSTRAINTS UC ON 
     875                            UC.CONSTRAINT_NAME = UCC.CONSTRAINT_NAME AND 
     876                            UC.CONSTRAINT_TYPE = 'C' AND 
     877                            UC.STATUS = 'ENABLED' 
     878                    WHERE 
     879                        UTC.TABLE_NAME = %s 
     880                    ORDER BY 
     881                        UTC.TABLE_NAME ASC, 
     882                        UTC.COLUMN_ID ASC"; 
     883        $result = $this->database->query($sql, $table); 
     884         
     885        foreach ($result as $row) { 
     886             
     887            $column = $row['column_name']; 
     888             
     889            // Since Oracle stores check constraints in LONG columns, it is 
     890            // not possible to check or modify the constraints in SQL which 
     891            // ends up causing multiple rows with duplicate data except for 
     892            // the check constraint 
     893            $duplicate = FALSE; 
     894             
     895            if (isset($column_info[$column])) { 
     896                $info = $column_info[$column]; 
     897                $duplicate = TRUE;   
     898            } else { 
     899                $info = array();     
     900            } 
     901             
     902            if (!$duplicate) { 
     903                // Get the column type 
     904                foreach ($data_type_mapping as $data_type => $mapped_data_type) { 
     905                    if (stripos($row['data_type'], $data_type) === 0) { 
     906                        $info['type'] = $mapped_data_type; 
     907                        break; 
     908                    } 
     909                } 
     910                 
     911                if (!isset($info['type'])) { 
     912                    $info['type'] = $row['data_type']; 
     913                } 
     914                 
     915                // Handle the length of decimal/numeric fields 
     916                if ($info['type'] == 'float' && $row['length']) { 
     917                    $info['decimal_places'] = (int) $row['length']; 
     918                } 
     919                 
     920                // Handle the special data for varchar fields 
     921                if (in_array($info['type'], array('char', 'varchar'))) { 
     922                    $info['max_length'] = (int) $row['length']; 
     923                } 
     924            } 
     925             
     926            // Handle check constraints that are just simple lists 
     927            if (in_array($info['type'], array('varchar', 'char')) && $row['check_constraint']) { 
     928                if (preg_match('/^\s*' . preg_quote($column, '/') . '\s+in\s+\((.*?)\)\s*$/i', $row['check_constraint'], $match)) { 
     929                    if (preg_match_all("/(?<!')'((''|[^']+)*)'/", $match[1], $matches, PREG_PATTERN_ORDER)) { 
     930                        $info['valid_values'] = str_replace("''", "'", $matches[1]); 
     931                    }            
     932                } 
     933            } 
     934             
     935            if (!$duplicate) { 
     936                // Handle default values 
     937                if ($row['data_default'] !== NULL) { 
     938                    if (in_array($info['type'], array('char', 'varchar', 'text'))) { 
     939                        $info['default'] = str_replace("''", "'", substr(trim($row['data_default']), 1, -1)); 
     940                         
     941                    } elseif ($info['type'] == 'boolean') { 
     942                        $info['default'] = (boolean) trim($row['data_default']); 
     943                         
     944                    } elseif (in_array($info['type'], array('integer', 'float'))) { 
     945                        $info['default'] = trim($row['data_default']); 
     946                         
     947                    } else { 
     948                        $info['default'] = $row['data_default']; 
     949                    } 
     950                } 
     951             
     952                // Not null values 
     953                $info['not_null'] = ($row['nullable'] == 'N') ? TRUE : FALSE; 
     954            } 
     955             
     956            $column_info[$column] = $info; 
     957        } 
     958         
     959        $sql = "SELECT 
     960                        TRIGGER_BODY 
     961                    FROM 
     962                        USER_TRIGGERS 
     963                    WHERE 
     964                        TRIGGERING_EVENT = 'INSERT' AND 
     965                        STATUS = 'ENABLED' AND 
     966                        TRIGGER_NAME NOT LIKE 'BIN\$%' AND 
     967                        TABLE_NAME = %s"; 
     968                         
     969        foreach ($this->database->query($sql, $table) as $row) { 
     970            if (preg_match('#SELECT\s+(\w+).nextval\s+INTO\s+:new\.(\w+)\s+FROM\s+dual#i', $row['trigger_body'], $matches)) { 
     971                $column = strtolower($matches[2]); 
     972                $column_info[$column]['auto_increment'] = TRUE; 
     973            } 
     974        } 
     975         
     976        return $column_info; 
     977    } 
     978     
     979     
     980    /** 
     981     * Fetches the key info for an Oracle database 
     982     *  
     983     * The structure of the returned array is: 
     984     *  
     985     * {{{ 
     986     * array( 
     987     *      'primary' => array( 
     988     *          {column name}, ... 
     989     *      ), 
     990     *      'unique'  => array( 
     991     *          array( 
     992     *              {column name}, ... 
     993     *          ), ... 
     994     *      ), 
     995     *      'foreign' => array( 
     996     *          array( 
     997     *              'column'         => {column name}, 
     998     *              'foreign_table'  => {foreign table name}, 
     999     *              'foreign_column' => {foreign column name}, 
     1000     *              'on_delete'      => {the ON DELETE action: 'no_action', 'restrict', 'cascade', 'set_null', or 'set_default'}, 
     1001     *              'on_update'      => {the ON UPDATE action: 'no_action', 'restrict', 'cascade', 'set_null', or 'set_default'} 
     1002     *          ), ... 
     1003     *      ) 
     1004     * ) 
     1005     * }}} 
     1006     *  
     1007     * @return array  The keys arrays for every table in the database - see method description for details 
     1008     */ 
     1009    private function fetchOracleKeys() 
     1010    { 
     1011        $keys = array(); 
     1012         
     1013        $tables = $this->getTables(); 
     1014        foreach ($tables as $table) { 
     1015            $keys[$table] = array(); 
     1016            $keys[$table]['primary'] = array(); 
     1017            $keys[$table]['unique']  = array(); 
     1018            $keys[$table]['foreign'] = array(); 
     1019        } 
     1020         
     1021        $sql  = "SELECT 
     1022                         LOWER(UC.TABLE_NAME) \"TABLE\", 
     1023                         UC.CONSTRAINT_NAME CONSTRAINT_NAME, 
     1024                         CASE UC.CONSTRAINT_TYPE 
     1025                             WHEN 'P' THEN 'primary' 
     1026                             WHEN 'R' THEN 'foreign' 
     1027                             WHEN 'U' THEN 'unique' 
     1028                             END TYPE, 
     1029                         LOWER(UCC.COLUMN_NAME) \"COLUMN\", 
     1030                         LOWER(FKC.TABLE_NAME) FOREIGN_TABLE, 
     1031                         LOWER(FKC.COLUMN_NAME) FOREIGN_COLUMN, 
     1032                         CASE WHEN FKC.TABLE_NAME IS NOT NULL THEN REPLACE(LOWER(UC.DELETE_RULE), ' ', '_') ELSE NULL END ON_DELETE 
     1033                     FROM 
     1034                         USER_CONSTRAINTS UC INNER JOIN 
     1035                         USER_CONS_COLUMNS UCC ON UC.CONSTRAINT_NAME = UCC.CONSTRAINT_NAME LEFT JOIN 
     1036                         USER_CONSTRAINTS FK ON UC.R_CONSTRAINT_NAME = FK.CONSTRAINT_NAME LEFT JOIN 
     1037                         USER_CONS_COLUMNS FKC ON FK.CONSTRAINT_NAME = FKC.CONSTRAINT_NAME 
     1038                     WHERE 
     1039                         UC.CONSTRAINT_TYPE IN ('U', 'P', 'R') AND 
     1040                         UC.STATUS = 'ENABLED' AND 
     1041                         SUBSTR(UC.TABLE_NAME, 1, 4) <> 'BIN\$' 
     1042                     ORDER BY 
     1043                         UC.TABLE_NAME ASC, 
     1044                         UC.CONSTRAINT_TYPE ASC, 
     1045                         UC.CONSTRAINT_NAME ASC, 
     1046                         UCC.POSITION ASC"; 
     1047         
     1048        $result = $this->database->query($sql); 
     1049         
     1050        $last_name  = ''; 
     1051        $last_table = ''; 
     1052        $last_type  = ''; 
     1053        foreach ($result as $row) { 
     1054             
     1055            if ($row['constraint_name'] != $last_name) { 
     1056                 
     1057                if ($last_name) { 
     1058                    if ($last_type == 'foreign' || $last_type == 'unique') { 
     1059                        $keys[$last_table][$last_type][] = $temp; 
     1060                    } else { 
     1061                        $keys[$last_table][$last_type] = $temp; 
     1062                    } 
     1063                } 
     1064                 
     1065                $temp = array(); 
     1066                if ($row['type'] == 'foreign') { 
     1067                     
     1068                    $temp['column']         = $row['column']; 
     1069                    $temp['foreign_table']  = $row['foreign_table']; 
     1070                    $temp['foreign_column'] = $row['foreign_column']; 
     1071                    $temp['on_delete']      = 'no_action'; 
     1072                    $temp['on_update']      = 'no_action'; 
     1073                     
     1074                    if (!empty($row['on_delete'])) { 
     1075                        $temp['on_delete'] = $row['on_delete']; 
     1076                    } 
     1077                     
     1078                } else { 
     1079                    $temp[] = $row['column']; 
     1080                } 
     1081                 
     1082                $last_table = $row['table']; 
     1083                $last_name  = $row['constraint_name']; 
     1084                $last_type  = $row['type']; 
     1085                 
     1086            } else { 
     1087                $temp[] = $row['column']; 
     1088            } 
     1089        } 
     1090         
     1091        if (isset($temp)) { 
     1092            if ($last_type == 'foreign' || $last_type == 'unique') { 
     1093                $keys[$last_table][$last_type][] = $temp; 
     1094            } else { 
     1095                $keys[$last_table][$last_type] = $temp; 
    7161096            } 
    7171097        } 
     
    8171197             
    8181198            // Handle the special data for varchar fields 
    819             if (in_array($info['type'], array('char', 'varchar'))) { 
     1199            if (in_array($info['type'], array('char', 'varchar')) && !empty($column_data_type[2])) { 
    8201200                $info['max_length'] = $column_data_type[2]; 
    8211201            } 
     
    9421322                         con.contype, 
    9431323                         con.conname, 
     1324                         CASE WHEN con.conkey IS NOT NULL THEN position('-'||col.attnum||'-' in '-'||array_to_string(con.conkey, '-')||'-') ELSE 0 END, 
    9441325                         col.attname"; 
    9451326         
     
    10591440        } 
    10601441         
    1061         preg_match_all('#(?<=,|\()\s*(?:`|"|\[)?(\w+)(?:`|"|\])?\s+([a-z]+)(?:\(\s*(\d+)(?:\s*,\s*(\d+))?\s*\))?(?:(\s+NOT\s+NULL)|(?:\s+DEFAULT\s+([^, \']*|\'(?:\'\'|[^\']+)*\'))|(\s+UNIQUE)|(\s+PRIMARY\s+KEY(?:\s+AUTOINCREMENT)?)|(\s+CHECK\s*\(\w+\s+IN\s+\(\s*(?:(?:[^, \']+|\'(?:\'\'|[^\']+)*\')\s*,\s*)*\s*(?:[^, \']+|\'(?:\'\'|[^\']+)*\')\)\)))*(\s+REFERENCES\s+\w+\s*\(\s*\w+\s*\)\s*(?:\s+(?:ON\s+DELETE|ON\s+UPDATE)\s+(?:CASCADE|NO\s+ACTION|RESTRICT|SET\s+NULL|SET\s+DEFAULT))*(?:\s+(?:DEFERRABLE|NOT\s+DEFERRABLE))?)?\s*(?:,|\s*(?=\)))#mi', $create_sql, $matches, PREG_SET_ORDER); 
     1442        preg_match_all('#(?<=,|\()\s*(?:`|"|\[)?(\w+)(?:`|"|\])?\s+([a-z]+)(?:\(\s*(\d+)(?:\s*,\s*(\d+))?\s*\))?(?:(\s+NOT\s+NULL)|(?:\s+NULL)|(?:\s+DEFAULT\s+([^, \']*|\'(?:\'\'|[^\']+)*\'))|(\s+UNIQUE)|(\s+PRIMARY\s+KEY(?:\s+AUTOINCREMENT)?)|(\s+CHECK\s*\(\w+\s+IN\s+\(\s*(?:(?:[^, \']+|\'(?:\'\'|[^\']+)*\')\s*,\s*)*\s*(?:[^, \']+|\'(?:\'\'|[^\']+)*\')\)\)))*(\s+REFERENCES\s+\w+\s*\(\s*\w+\s*\)\s*(?:\s+(?:ON\s+DELETE|ON\s+UPDATE)\s+(?:CASCADE|NO\s+ACTION|RESTRICT|SET\s+NULL|SET\s+DEFAULT))*(?:\s+(?:DEFERRABLE|NOT\s+DEFERRABLE))?)?\s*(?:,|\s*(?=\)))#mi', $create_sql, $matches, PREG_SET_ORDER); 
    10621443         
    10631444        foreach ($matches as $match) { 
     
    10721453         
    10731454            // Type specific information 
    1074             if (in_array($info['type'], array('char', 'varchar'))) { 
     1455            if (in_array($info['type'], array('char', 'varchar')) && !empty($match[3])) { 
    10751456                $info['max_length'] = $match[3]; 
    10761457            } 
     
    11401521    private function fetchSQLiteKeys() 
    11411522    { 
    1142         $tables   = $this->getTables(); 
    1143         $keys = array(); 
     1523        $tables = $this->getTables(); 
     1524        $keys   = array(); 
    11441525         
    11451526        foreach ($tables as $table) { 
     
    13311712     
    13321713    /** 
    1333      * Makes sure the info is current, if not it deletes it so that all info is current 
    1334      *  
    1335      * @internal 
    1336      *  
    1337      * @return void 
    1338      */ 
    1339     public function flushInfo() 
    1340     { 
    1341         if ($this->state != 'current') { 
    1342             $this->tables             = NULL; 
    1343             $this->column_info        = array(); 
    1344             $this->keys               = array(); 
    1345             $this->merged_column_info = array(); 
    1346             $this->merged_keys        = array(); 
    1347             $this->relationships      = array(); 
    1348             $this->state              = 'current'; 
    1349             $this->info_changed       = TRUE; 
    1350         } 
    1351     } 
    1352      
    1353      
    1354     /** 
    13551714     * Returns column information for the table specified 
    13561715     *  
     
    15031862            $keys = array_keys($row); 
    15041863            $this->databases[] = $row[$keys[0]]; 
     1864        } 
     1865         
     1866        if ($this->cache) { 
     1867            $this->cache->set($this->makeCachePrefix() . 'databases', $this->databases);         
    15051868        } 
    15061869         
     
    16822045        switch ($this->database->getType()) { 
    16832046            case 'mssql': 
    1684                 $sql = 'SELECT 
     2047                $sql = "SELECT 
    16852048                                TABLE_NAME 
    16862049                            FROM 
    16872050                                INFORMATION_SCHEMA.TABLES 
     2051                            WHERE 
     2052                                TABLE_NAME != 'sysdiagrams' 
    16882053                            ORDER BY 
    1689                                 LOWER(TABLE_NAME)'
     2054                                LOWER(TABLE_NAME)"
    16902055                break; 
    16912056             
    16922057            case 'mysql': 
    16932058                $sql = 'SHOW TABLES'; 
     2059                break; 
     2060             
     2061            case 'oracle': 
     2062                $sql = "SELECT 
     2063                                LOWER(TABLE_NAME) 
     2064                            FROM 
     2065                                USER_TABLES 
     2066                            WHERE 
     2067                                SUBSTR(TABLE_NAME, 1, 4) <> 'BIN\$' 
     2068                            ORDER BY 
     2069                                TABLE_NAME ASC"; 
    16942070                break; 
    16952071             
     
    17272103        } 
    17282104         
     2105        if ($this->cache) { 
     2106            $this->cache->set($this->makeCachePrefix() . 'tables', $this->tables); 
     2107        } 
     2108         
    17292109        return $this->tables; 
    17302110    } 
     
    17582138         
    17592139        return sizeof($foreign_key_columns) == 2 && !array_diff($foreign_key_columns, $primary_key_columns); 
     2140    } 
     2141     
     2142     
     2143    /** 
     2144     * Creates a unique cache prefix to help prevent cache conflicts 
     2145     *  
     2146     * @return void 
     2147     */ 
     2148    private function makeCachePrefix() 
     2149    { 
     2150        $prefix  = 'fSchema::' . $this->database->getType() . '::'; 
     2151        if ($this->database->getHost()) { 
     2152            $prefix .= $this->database->getHost() . '::';    
     2153        } 
     2154        if ($this->database->getPort()) { 
     2155            $prefix .= $this->database->getPort() . '::';    
     2156        } 
     2157        $prefix .= $this->database->getDatabase() . '::'; 
     2158        if ($this->database->getUsername()) { 
     2159            $prefix .= $this->database->getUsername() . '::';    
     2160        } 
     2161        return $prefix;  
    17602162    } 
    17612163     
     
    17962198        } 
    17972199         
    1798         $optional_elements = array('default', 'not_null', 'valid_values', 'max_length', 'decimal_places', 'auto_increment'); 
     2200        $optional_elements = array('not_null', 'default', 'valid_values', 'max_length', 'decimal_places', 'auto_increment'); 
    17992201         
    18002202        foreach ($this->merged_column_info as $table => $column_array) { 
     
    18312233         
    18322234        $this->findRelationships(); 
    1833     } 
    1834      
    1835      
    1836     /** 
    1837      * Sets a file to cache the schema info to 
    1838      *  
    1839      * @param  string $file  The cache file 
    1840      * @return void 
    1841      */ 
    1842     public function setCacheFile($file) 
    1843     { 
    1844         $file = realpath($file); 
    1845          
    1846         if (file_exists($file) && !is_writable($file)) { 
    1847             throw new fEnvironmentException( 
    1848                 'The cache file specified, %s, is not writable', 
    1849                 $file 
    1850             ); 
    1851         } 
    1852          
    1853         if (!file_exists($file) && !is_writable(dirname($file))) { 
    1854             throw new fEnvironmentException( 
    1855                 'The cache file specified, %1$s, does not exist and the cache file directory, %2$s, is not writable', 
    1856                 $file, 
    1857                 dirname($file) . DIRECTORY_SEPARATOR 
    1858             ); 
    1859         } 
    1860          
    1861         $this->cache_file = $file; 
    1862          
    1863         $contents = file_get_contents($this->cache_file); 
    1864         if ($contents) { 
    1865             $info = unserialize($contents); 
    1866             $this->tables        = $info['tables']; 
    1867             $this->column_info   = $info['column_info']; 
    1868             $this->keys          = $info['keys']; 
    1869         } 
    1870          
    1871         if (!empty($this->column_info) || !empty($this->keys)) { 
    1872             $this->state = 'cached'; 
    1873         } 
    18742235    } 
    18752236     
  • fUnbufferedResult.php

    r514 r562 Hide Line Numbers
    1010 * @link       http://flourishlib.com/fUnbufferedResult 
    1111 *  
    12  * @version    1.0.0b2 
     12 * @version    1.0.0b3 
     13 * @changes    1.0.0b3  Added support for Oracle, various bug fixes [wb, 2009-05-04] 
    1314 * @changes    1.0.0b2  Updated for new fCore API [wb, 2009-02-16] 
    1415 * @changes    1.0.0b   The initial implementation [wb, 2008-05-07] 
     
    101102     * @internal 
    102103     *  
    103      * @param  string $type           The type of database: `'mssql'`, `'mysql'`, `'postgresql'`, `'sqlite'` 
    104      * @param  string $extension      The database extension used: `'mssql'`, `'mysql'`, `'mysqli'`, `'odbc'`, `'pdo'`, `'pgsql'`, `'sqlite', 'sqlsrv'` 
     104     * @param  string $type           The type of database: `'mssql'`, `'mysql'`, `'oracle'`, `'postgresql'`, `'sqlite'` 
     105     * @param  string $extension      The database extension used: `'mssql'`, `'mysql'`, `'mysqli'`, `'oci8'` `'odbc'`, `'pdo'`, `'pgsql'`, `'sqlite', 'sqlsrv'` 
    105106     * @param  string $character_set  MSSQL only: the character set to transcode from since MSSQL doesn't do UTF-8 
    106107     * @return fUnbufferedResult 
     
    108109    public function __construct($type, $extension, $character_set=NULL) 
    109110    { 
    110         $valid_types = array('mssql', 'mysql', 'postgresql', 'sqlite'); 
     111        $valid_types = array('mssql', 'mysql', 'oracle', 'postgresql', 'sqlite'); 
    111112        if (!in_array($type, $valid_types)) { 
    112113            throw new fProgrammerException( 
     
    117118        } 
    118119         
    119         $valid_extensions = array('mssql', 'mysql', 'mysqli', 'odbc', 'pdo', 'pgsql', 'sqlite', 'sqlsrv'); 
     120        $valid_extensions = array('mssql', 'mysql', 'mysqli', 'oci8', 'odbc', 'pdo', 'pgsql', 'sqlite', 'sqlsrv'); 
    120121        if (!in_array($extension, $valid_extensions)) { 
    121122            throw new fProgrammerException( 
     
    151152        } elseif ($this->extension == 'mysqli') { 
    152153            mysqli_free_result($this->result); 
     154        } elseif ($this->extension == 'oci8') { 
     155            oci_free_statement($this->result); 
    153156        } elseif ($this->extension == 'odbc') { 
    154157            odbc_free_result($this->result); 
     
    156159            pg_free_result($this->result); 
    157160        } elseif ($this->extension == 'sqlite') { 
    158             sqlite_fetch_all($this->result); 
     161            unset($this->result); 
    159162        } elseif ($this->extension == 'sqlsrv') { 
    160163            sqlsrv_free_stmt($this->result); 
     
    162165            $this->result->closeCursor(); 
    163166        } 
     167         
     168        $this->result = NULL; 
    164169    } 
    165170     
     
    185190    { 
    186191        if ($this->extension == 'mssql') { 
    187             $row = mssql_fetch_assoc($this->result); 
    188             if (empty($row)) { 
    189                 mssql_fetch_batch($this->result); 
     192            // For some reason the mssql extension will return an empty row even 
     193            // when now rows were returned, so we have to explicitly check for this 
     194            if ($this->pointer == 0 && !mssql_num_rows($this->result)) { 
     195                $row = FALSE;    
     196             
     197            } else { 
    190198                $row = mssql_fetch_assoc($this->result); 
    191             } 
    192             if (!empty($row)) { 
    193                 $row = $this->fixDblibMSSQLDriver($row); 
    194                  
    195                 // This is an unfortunate fix that required for databases that don't support limit 
    196                 // clauses with an offset. It prevents unrequested columns from being returned. 
    197                 if (!empty($row) && $this->untranslated_sql !== NULL && isset($row['__flourish_limit_offset_row_num'])) { 
    198                     unset($row['__flourish_limit_offset_row_num']); 
     199                if (empty($row)) { 
     200                    mssql_fetch_batch($this->result); 
     201                    $row = mssql_fetch_assoc($this->result); 
     202                } 
     203                if (!empty($row)) { 
     204                    $row = $this->fixDblibMSSQLDriver($row); 
    199205                } 
    200206            } 
     
    204210        } elseif ($this->extension == 'mysqli') { 
    205211            $row = mysqli_fetch_assoc($this->result); 
     212        } elseif ($this->extension == 'oci8') { 
     213            $row = oci_fetch_assoc($this->result); 
    206214        } elseif ($this->extension == 'odbc') { 
    207215            $row = odbc_fetch_array($this->result); 
     
    214222        } elseif ($this->extension == 'pdo') { 
    215223            $row = $this->result->fetch(PDO::FETCH_ASSOC); 
     224        }    
     225         
     226        // Fix uppercase column names to lowercase 
     227        if ($row && $this->type == 'oracle') { 
     228            $new_row = array(); 
     229            foreach ($row as $column => $value) { 
     230                $new_row[strtolower($column)] = $value; 
     231            }    
     232            $row = $new_row; 
     233        } 
     234         
     235        // This is an unfortunate fix that required for databases that don't support limit 
     236        // clauses with an offset. It prevents unrequested columns from being returned. 
     237        if ($row && ($this->type == 'mssql' || $this->type == 'oracle')) { 
     238            if ($this->untranslated_sql !== NULL && isset($row['flourish__row__num'])) { 
     239                unset($row['flourish__row__num']); 
     240            }    
    216241        } 
    217242         
     
    220245            if ($this->character_set) { 
    221246                foreach ($row as $key => $value) { 
    222                     if (!is_string($value) || strpos($key, '__flourish_mssqln_') === 0) { 
     247                    if (!is_string($value) || strpos($key, '__flourish_mssqln_') === 0 || isset($row['fmssqln__' . $key]) || preg_match('#[\x0-\x8\xB\xC\xD-\x1F]#', $value)) { 
    223248                        continue; 
    224249                    }        
     
    244269    public function current() 
    245270    { 
     271        $this->validateState(); 
     272         
    246273        // Primes the result set 
    247274        if ($this->pointer === NULL) { 
     
    269296    private function decodeMSSQLNationalColumns($row) 
    270297    { 
    271         if (strpos($this->sql, '__flourish_mssqln_') === FALSE) { 
     298        if (strpos($this->sql, 'fmssqln__') === FALSE) { 
    272299            return $row; 
    273300        } 
     
    276303         
    277304        foreach ($columns as $column) { 
    278             if (substr($column, 0, 18) != '__flourish_mssqln_') { 
     305            if (substr($column, 0, 9) != 'fmssqln__') { 
    279306                continue; 
    280307            }    
    281308             
    282             $real_column = substr($column, 18); 
     309            $real_column = substr($column, 9); 
    283310             
    284311            $row[$real_column] = iconv('ucs-2le', 'utf-8', $row[$column]); 
     
    300327    public function fetchRow() 
    301328    { 
     329        $this->validateState(); 
     330         
    302331        $row = $this->current(); 
    303332        $this->next(); 
     
    329358                ob_end_clean(); 
    330359                 
    331                 $using_dblib = preg_match('#FreeTDS#ims', $module_info, $match); 
     360                $using_dblib = !preg_match('#FreeTDS#ims', $module_info, $match); 
    332361            } 
    333362        } 
     
    381410    public function getResult() 
    382411    { 
     412        $this->validateState(); 
     413         
    383414        return $this->result; 
    384415    } 
     
    417448    public function key() 
    418449    { 
     450        $this->validateState(); 
     451         
    419452        if ($this->pointer === NULL) { 
    420453            $this->current(); 
     
    435468    public function next() 
    436469    { 
     470        $this->validateState(); 
     471         
    437472        if ($this->pointer === NULL) { 
    438473            $this->current(); 
     
    453488    public function rewind() 
    454489    { 
     490        $this->validateState(); 
     491         
    455492        if (!empty($this->pointer)) { 
    456493            throw new fProgrammerException( 
     
    531568    public function valid() 
    532569    { 
     570        $this->validateState(); 
     571         
    533572        if ($this->pointer === NULL) { 
    534573            $this->advanceCurrentRow(); 
     
    537576         
    538577        return !empty($this->current_row); 
     578    } 
     579     
     580     
     581    /** 
     582     * Throws an exception if this object has been deconstructed already 
     583     *  
     584     * @return void 
     585     */ 
     586    private function validateState() 
     587    { 
     588        if ($this->result === NULL) { 
     589            throw new fProgrammerException('This unbuffered result has been fully fetched, or replaced by a newer result');  
     590        }    
    539591    } 
    540592}