diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..c86aeb5 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,48 @@ +language: php + +php: + - '5.5' + - '5.6' + - '7.0' + - hhvm + - nightly + +install: + - composer install + +env: + - DB=mysql + - DB=pgsql POSTGRESQL_VERSION=9.1 + - DB=pgsql POSTGRESQL_VERSION=9.2 + - DB=pgsql POSTGRESQL_VERSION=9.3 + - DB=pgsql POSTGRESQL_VERSION=9.4 + +before_script: + - sh -c "if [ '$DB' = 'mysql' ]; then mysql -e 'create database qcubed;'; mysql -u root qcubed < test/db/mysql_innodb.sql; fi" + - sh -c "if [ '$DB' = 'pgsql' ]; then createdb qcubed -U postgres; psql -d qcubed -f test/db/pgsql.sql -U postgres; fi" + +script: + - ./vendor/bin/phpunit -c ./test/phpunit.xml --coverage-clover ./build/logs/clover.xml + +# code coverage +addons: + code_climate: + repo_token: 814dfe2ee0ae6198e43e566e32ab85f40379b5abe06cd52b1f6a24e92b5de883 + +# code coverage +after_script: + - vendor/bin/test-reporter + +sudo: false + +matrix: + exclude: + - php: hhvm + env: DB=pgsql POSTGRESQL_VERSION=9.1 # driver currently unsupported by HHVM + - php: hhvm + env: DB=pgsql POSTGRESQL_VERSION=9.2 # driver currently unsupported by HHVM + - php: hhvm + env: DB=pgsql POSTGRESQL_VERSION=9.3 # driver currently unsupported by HHVM + - php: hhvm + env: DB=pgsql POSTGRESQL_VERSION=9.4 # driver currently unsupported by HHVM + diff --git a/LICENSE b/LICENSE index 06996eb..1c1961a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017 Shannon +Copyright (c) 2017 Shannon Pekary spekary@gmail.com Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 0d0118a..4839cc3 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ -# qcubed-orm +# QCubed Orm QCubed Standalone ORM diff --git a/composer.json b/composer.json index 26eec7d..35aeb83 100644 --- a/composer.json +++ b/composer.json @@ -1,4 +1,11 @@ { + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/qcubed/common" + } + ], + "name": "qcubed/orm", "description": "Database drivers, query support, and code generator for the object relational model for QCubed", "keywords": ["php", "database", "orm"], @@ -12,14 +19,14 @@ }, "require": { "php": ">=5.5.0", - "qcubed/common": "^4.0" + "qcubed/common": "dev-master" + }, + "require-dev" : { + "phpunit/phpunit": "~4.8|~5.0" }, "autoload": { "psr-4": { - "QCubed\\Database": "src/Database", - "QCubed\\Query": "src/Query", - "QCubed\\Codegen": "src/Codegen" - + "QCubed\\": "src/" } } } diff --git a/install/project/includes/Codegen.php b/install/project/includes/Codegen.php index ad33d0c..295ef22 100644 --- a/install/project/includes/Codegen.php +++ b/install/project/includes/Codegen.php @@ -1,6 +1,6 @@ Format parameter after -creating the control, or specify this in an override in a Comment option. - - - - - - diff --git a/install/project/includes/configuration/_LICENSE.txt b/install/project/includes/configuration/_LICENSE.txt deleted file mode 100644 index 1932c41..0000000 --- a/install/project/includes/configuration/_LICENSE.txt +++ /dev/null @@ -1,27 +0,0 @@ -Unless otherwise specified, all files in the QCubed Development Framework -are under the following copyright and licensing policies: - -The QCubed Development Framework is distributed by the QCubed Project -under the terms of the MIT License. More information can be found at -http://www.opensource.org/licenses/mit-license.php - -Copyright (c) 2001 - 2009, Quasidea Development, LLC, QCubed Project - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - diff --git a/install/project/includes/configuration/_README.txt b/install/project/includes/configuration/_README.txt deleted file mode 100644 index c05e9c1..0000000 --- a/install/project/includes/configuration/_README.txt +++ /dev/null @@ -1,102 +0,0 @@ -This is the central location for all included configuration files. Feel free to include -any new classes or include files in this directory. - - - -**** configuration.inc.php **** - -This contains server-level configuration information (e.g. database connection -information, docroot paths (including subfoldering and virtual directories), -etc. A sample file is provided for you, and is used by the QCubed startup wizard -to create an initial version. Feel free to make modifications to this file to have it reflect the -configuration of your application, including any global defines that are particular to your application. - -See the inline documentation in configuration.sample.inc.php for more information. - - - -**** prepend.inc.php **** - -This is the top-level include file for any and all PHP scripts which use -Qcubed. Global, application-wide loaders, settings, etc. are in this file. - -Feel free to make modifications/changes/additions to this file as you wish. -Note that the QApplication class is defined in prepend.inc as well. Feel free -to make changes, override methods, or add functionality to QApplication as -needed. - -See the inline documentation in prepend.inc.php for more information. - - -**** codegen_settings.xml **** - -This file controls overall settings for parts of the code generation. Feel free -to change these as needed. - - -**** codegen_options.json **** - -This file is created and maintained by the ModelConnectorEditor. It has options for -the individual controls that correspond to fields in your database. There may be times -that you need to directly edit this file, and you should feel free to do so. - -**** Codegen Notes **** - -QCubed is set up to generate a default set of objects and forms to get you started with your application. -This is called “codegen”. The notes below will help you understand the process and how to customize it to your needs. -Ideally, you should customize the codegen process first before starting to write you application code, -but we know that development does not go always as planned, and the whole QCubed system is set up so that you can -separate out your hand written code from the generated code, and continue to tweak the codegen process and re-generate code at any time. - -The codegen process starts at the QCubed start screen by clicking on the codegen link. -PHP is executed to generate the files. Therefore, the target directories for codegen will need to be writable by the web server process. - -The codegen process works by instantiating a QCodeGen object. This object then looks in the template directories and begins -to include the php files there that start with an underscore (_). These templates then include other files, which in turn -may include other template files. This combination will eventually generate the forms, model connectors, and data table -interface classes that you will base your application on. - - -Model Connectors -Model Connectors are helper classes that have methods which connect form controls to columns in SQLn data tables. Each column -in the data table corresponds to a control that is generated in a model connector class. Your form object calls methods -in the model connector to get copies of the controls and then to place them in the form. - -To customize the generated controls, you have the following choices: -- Use the ModelConnectorEditor (see the example on this), to set specific options on each control. -- Create your own code generating templates and place them in your project/includes/codegen/templates directory. Its best - to do this by copying the corresponding file in the qcubed/framework/includes/codegen/templates directory and then - editing the file and placing it in the corresponding location in the above project directory. The project directory - files will override the files in the vendor directory. -- Override the generated code by editing the model connectors in your project/includes/connector directory. - - -Version 3 - -QCubed Version 3 introduces the concept of having the controls themselves create the code to interact with the database -for the ModelConnector, rather than the templates. Coupled with this is the ModelConnectorEditDlg dialog, which lets you -right click on a control and edit many of the controls options. These changes get embedded into the generated ModelConnector. -You can see a description of each option by hovering over the item in the dialog. - -These new features give the developer the ability to do the following: -- Override the default control type to specify a particular control type -- Allow custom controls and plugins to generate their own model connector code and have that code automatically be used - instead of the default code just by specifying that control in the comments of a column. -- Allow subclasses of standard controls to override the code generation methods to generate different code. -- Specify additional overrides to control many aspects of control creation in the generated model connector. - -Notes for Upgrading from version 2 - -Many of the problems that caused programmers to create their own templates are now solvable through the new Options -feature available through Comments. However, you are still free to override the templates as needed. In fact, this new -feature is implemented entirely through the templates, so if you want to keep your old templates, simply replace the new -templates with the old ones from version 2. - -QLabel no longer accepts a strFormat parameter at create time. You can always set it using the ->Format parameter after -creating the control, or specify this in an override in a Comment option. - - - - - - diff --git a/install/project/includes/configuration/active/assets.cfg.php b/install/project/includes/configuration/active/assets.cfg.php deleted file mode 100644 index 30b9f24..0000000 --- a/install/project/includes/configuration/active/assets.cfg.php +++ /dev/null @@ -1,78 +0,0 @@ - 'MySqli5', + 'adapter' => 'Mysqli5', 'server' => 'db', 'port' => null, 'database' => 'qcubed', diff --git a/install/project/includes/configuration/active/directories.cfg.php b/install/project/includes/configuration/active/directories.cfg.php deleted file mode 100644 index 31d9cea..0000000 --- a/install/project/includes/configuration/active/directories.cfg.php +++ /dev/null @@ -1,44 +0,0 @@ - diff --git a/install/project/includes/configuration/configuration.inc.sample.php b/install/project/includes/configuration/configuration.inc.sample.php deleted file mode 100644 index 8295682..0000000 --- a/install/project/includes/configuration/configuration.inc.sample.php +++ /dev/null @@ -1,435 +0,0 @@ - '{db1_adapter}', - 'server' => '{db1_serverAddress}', - 'port' => '{db1_serverport}', - 'database' => '{db1_dbname}', - 'username' => '{db1_username}', - 'password' => '{db1_password}', - 'caching' => false, - 'profiling' => false))); - -->*/ - - // Additional Database Connection Strings can be defined here (e.g. for connection #2, #3, #4, #5, etc.) - // define('DB_CONNECTION_2', serialize(array('adapter'=>'SqlServer', 'server'=>'localhost', 'port'=>null, 'database'=>'qcubed', 'username'=>'root', 'password'=>'', 'profiling'=>false))); - // define('DB_CONNECTION_3', serialize(array('adapter'=>'MySqli', 'server'=>'localhost', 'port'=>null, 'database'=>'qcubed', 'username'=>'root', 'password'=>'', 'profiling'=>false))); - // define('DB_CONNECTION_4', serialize(array('adapter'=>'MySql', 'server'=>'localhost', 'port'=>null, 'database'=>'qcubed', 'username'=>'root', 'password'=>'', 'profiling'=>false))); - // define('DB_CONNECTION_5', serialize(array('adapter'=>'PostgreSql', 'server'=>'localhost', 'port'=>null, 'database'=>'qcubed', 'username'=>'root', 'password'=>'', 'profiling'=>false))); - // define('DB_CONNECTION_6', serialize(array('adapter' => 'InformixPdo', 'host' => 'maxdata', 'server' => 'maxdata', 'service' => 9088, 'protocol' => 'onsoctcp', 'database' => 'qcubed', 'username' => 'root', 'password' => '', 'profiling' => false))); - - // Maximum index of the DB connections defined by DB_CONNECTION_# constants above - // When reading the DB_CONNECTION_# constants, it will only go up to (and including) the index defined here - // See ApplicationBase::InitializeDatabaseConnections() - define ('MAX_DB_CONNECTION_INDEX', 1); - - /** The value for QApplication::$EncodingType constant */ - define('__APPLICATION_ENCODING_TYPE__', 'UTF-8'); - - // (For PHP > v5.1) Setup the default timezone (if not already specified in php.ini) - if ((function_exists('date_default_timezone_set')) && (!ini_get('date.timezone'))) - date_default_timezone_set('America/Los_Angeles'); - - - /* - * Caching support for QCubed (Vaibhav Kaushal Jan 21, 2012) - * Determines which class as a Cache Provider. It should be a subclass of QAbstractCacheProvider. - * Setting it to null will disable caching. Current implentations are - * - * "QCacheProviderMemcache": this will use Memcache as the caching provider. - * You must have the 'php5-memcache' package installed for this provider to work. - * - * "QCacheProviderLocalMemory": a local memory cache provider with a lifespan of the request - * or session (if KeepInSession is configured). - * - * "QCacheProviderAPC": supports the APC interface. To use it, use PECL to install either - * APC or APCu. - * - * "QCacheProviderNoCahce": provider which does no caching at all - * - * "QMultiLevelCacheProvider": a provider that can combine multiple providers into one. - * This can be used for example to combine the LocalMemory cache provider with the Memcache based provider. - */ - define("CACHE_PROVIDER_CLASS", null); - - /* - * Options passed to the constructor of the Caching Provider class above. - * For QCacheProviderMemcache, it's an array, where each item is an associative array of - * server configuration options. - * Please see the documentation for the constructor for each provider for a description of the accepted - * options. - */ - define ('CACHE_PROVIDER_OPTIONS' , serialize( - array( - array('host' => '127.0.0.1', 'port' => 11211, ), - //array('host' => '10.0.2.2', 'port' => 11211, ), // adds a second server - ) - ) ); - - /* - * Support for Watchers and automated updating of objects that display the results of table queries. - * - * The preferred way to setup your watcher is to make changes to the - * public/includes/controls/QWatcher.class.php file. However, you can also set up your watcher here - * using the following define. The main purpose of the define is to help with automated testing - * and the examples site. - */ - //define ('WATCHER_CLASS', 'QWatcherDB'); - - /* Form State Handler. Determines which class is used to serialize the form in-between Ajax callbacks. - * - * Possible values are: - * "QFormStateHandler": This is the "standard" FormState handler, storing the base64 encoded session data - * (and if requested by QForm, encrypted) as a hidden form variable on the page, itself. - * - * "QSessionFormStateHandler": Simple Session-based FormState handler. Uses PHP Sessions so it's very straightforward - * and simple, utilizing the session handling and cleanup functionality in PHP, itself. - * The downside is that for long running sessions, each individual session file can get - * very, very large, storing all hte various formstate data. Eventually (if individual - * session files are larger than 10MB), you can theoretically observe a geometrical - * degradation of performance. - * - * "QFileFormStateHandler": This will store the formstate in a pre-specified directory (__FILE_FORM_STATE_HANDLER_PATH__) - * on the file system. This offers significant speed advantage over PHP SESSION because EACH - * form state is saved in its own file, and only the form state that is needed for loading will - * be accessed (as opposed to with session, ALL the form states are loaded into memory - * every time). - * The downside is that because it doesn't utilize PHP's session management subsystem, - * this class must take care of its own garbage collection/deleting of old/outdated - * formstate files. - * - * "QDbBackedFormStateHandler": This will store the formstate in a predefined table in one of the DBs in the array above. - * It provides a way to maintain the FormStates without creating too many files on the server. - * It also makes sure that the application remains fast and provides all the features of QFileFormStateHandler. - * The algorithm to periodically clean up the DB is also provided (just like QFileFormStateHandler) . - * - * To use the QDbBackedFormStateHandler, the following two constants must be defined: - * 1. __DB_BACKED_FORM_STATE_HANDLER_DB_INDEX__ : It is the numerical index of the DB from the list of DBs defined - * above where the table to store the FormStates is present. Note, it is the numerical Index, not the DB name. - * e.g. If it is present in the DB_CONNECTION_1, then the value must be defined as '1'. - * 2. __DB_BACKED_FORM_STATE_HANDLER_TABLE_NAME__ : It is the name of the table where the FormStates must be stored - * It must have following 4 columns: - * i) page_id: varchar(80) - It must be the primary key. - * ii) save_time: integer - This column should be indexed for performance reasons - * iii) session_id: varchar(32) - This column should be indexed for performance reasons - * iv) state_data: text - This column must NOT be indexed otherwise it will degrade the performance. - * - * NOTE: Formstates can be large, depending on the complexity of your forms. - * For MySQL, you might have to increase the max_allowed_packet variable in your my.cnf file to the maximum size of a formstate. - * Also for MySQL, you should choose a MEDIUMTEXT type of column, rather than TEXT. TEXT is limited to 64KB, - * which will not be big enough for moderately complex forms, and will result in data errors. - */ - define('__FORM_STATE_HANDLER__', 'QSessionFormStateHandler'); - - // If using the QFileFormStateHandler, specify the path where QCubed will save the session state files (has to be writeable!) - define('__FILE_FORM_STATE_HANDLER_PATH__', __PROJECT__ . '/tmp'); - - // If using the QDbBackedSessionHandler, define the DB index where the table to store the formstates is present - define('__DB_BACKED_FORM_STATE_HANDLER_DB_INDEX__', 1); - // If using QDbBackedSessionHandler, specify the table name which would hold the formstates (must meet the requirements laid out above) - define('__DB_BACKED_FORM_STATE_HANDLER_TABLE_NAME__', 'qc_formstate'); - - - /* - * QCubed allows you to save / read / write your user PHP sessions in a database. - * This is immensely helpful when you want to develop your QCubed based application - * to support running on two different web servers with same data backends or with load balancing. - * If you are using QSessionFormStateHandler, it also automatically centralizes your formstates. - * - * To avail this feature, you must have a dedicated table in one of your databases above. - * The table must have 3 columns with follwing names and datatypes (note that column names should match exactly): - * - * [Column 1] - * Name = id - * Data Type = varchar / character with variable length. Must hold session number + session name. Typically this is 34 characters, but - * you should make it big enough to handle any situation you may encounter. - * - * [Column 2] - * Name = last_access_time - * Data type = integer - * - * [Column 3] - * Name = data - * Data type = text - * - * For this to work, we need to know two things: - * 1. The DB_CONNECTION index (repeat: the numerical index) of the database from the list of databases above - * where this table is located. - * 2. The name of the table in the database. - * - * Notes: - * 1. if you do not want to use this feature, set the value of DB_BACKED_SESSION_HANDLER_DB_INDEX to 0. - * 2. It is recommended that you create a primary key on the 'id' field and an index on the 'last_access_time' field - * to speed up the database queries. - * 3. This feature does not make use of the codegen feature. So you may exclude this table from being codegened. - */ - // The database index where the Session storage tables are present. Remember, define it as an integer. - define("DB_BACKED_SESSION_HANDLER_DB_INDEX", 0); - - // The table name to be used for session data storage (must meet the requirements laid out above) - define("DB_BACKED_SESSION_HANDLER_TABLE_NAME", "qc_session"); - - // Define the Filepath for the error page (path MUST be relative from the DOCROOT) - define('ERROR_PAGE_PATH', __PHP_ASSETS__ . '/error_page.php'); - - // Define the Filepath for any logged errors - define('ERROR_LOG_PATH', __TMP__ . '/error_log'); - - // Name of session variable used to save the state of form objects. - define('__SESSION_SAVED_STATE__', 'QSavedState'); - - // To Log ALL errors that have occurred, set flag to true - // define('ERROR_LOG_FLAG', true); - - // To enable the display of "Friendly" error pages and messages, define them here (path MUST be relative from the DOCROOT) - // define('ERROR_FRIENDLY_PAGE_PATH', __PHP_ASSETS__ . '/friendly_error_page.php'); - // define('ERROR_FRIENDLY_AJAX_MESSAGE', 'Oops! An error has occurred.\r\n\r\nThe error was logged, and we will take a look into this right away.'); - - // If using HTML Purifier, the location of the writeable cache directory. - define ('__PURIFIER_CACHE__', __CACHE__ . '/purifier'); - - /** Uncomment if you are using QTimer to do performance testing. Will automatically output the results of your timers to the file. */ - //define ('__TIMER_OUT_FILE__', __TMP__ . '/timers.txt'); - break; - } -} -?> diff --git a/install/project/includes/configuration/footer.inc.php b/install/project/includes/configuration/footer.inc.php deleted file mode 100644 index 0b2cee4..0000000 --- a/install/project/includes/configuration/footer.inc.php +++ /dev/null @@ -1,9 +0,0 @@ - - - - - \ No newline at end of file diff --git a/install/project/includes/configuration/header.inc.php b/install/project/includes/configuration/header.inc.php deleted file mode 100644 index d662ee4..0000000 --- a/install/project/includes/configuration/header.inc.php +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - <?php _p($strPageTitle); ?> - - - RenderStyles(); ?> - - -
\ No newline at end of file diff --git a/install/project/includes/configuration/prepend.inc.php b/install/project/includes/configuration/prepend.inc.php deleted file mode 100644 index 835d24c..0000000 --- a/install/project/includes/configuration/prepend.inc.php +++ /dev/null @@ -1,162 +0,0 @@ - \ No newline at end of file diff --git a/src/Database/AbstractBase.php b/src/Database/AbstractBase.php new file mode 100644 index 0000000..be87842 --- /dev/null +++ b/src/Database/AbstractBase.php @@ -0,0 +1,778 @@ +intTransactionDepth) { + $this->ExecuteTransactionBegin(); + } + $this->intTransactionDepth++; + } + + /** + * This function commits the database transaction. + * + * @throws Caller + * @return void Nothing + */ + public final function TransactionCommit() { + if (1 == $this->intTransactionDepth) { + $this->ExecuteTransactionCommit(); + } + if ($this->intTransactionDepth <= 0) { + throw new Caller("The transaction commit call is called before the transaction begin was called."); + } + $this->intTransactionDepth--; + } + + /** + * This function rolls back the database transaction. + * + * @return void Nothing + */ + public final function TransactionRollBack() { + $this->ExecuteTransactionRollBack(); + $this->intTransactionDepth = 0; + } + + abstract public function SqlLimitVariablePrefix($strLimitInfo); + abstract public function SqlLimitVariableSuffix($strLimitInfo); + abstract public function SqlSortByVariable($strSortByInfo); + + /** + * Closes the database connection + * + * @return mixed + */ + abstract public function Close(); + + /** + * Given an identifier for a SQL query, this method returns the escaped identifier + * + * @param string $strIdentifier Identifier to be escaped + * + * @return string Escaped identifier string + */ + public function EscapeIdentifier($strIdentifier) { + return $this->strEscapeIdentifierBegin . $strIdentifier . $this->strEscapeIdentifierEnd; + } + + /** + * Given an array of identifiers, this method returns array of escaped identifiers + * For corner case handling, if a single identifier is supplied, a single escaped identifier is returned + * + * @param array|string $mixIdentifiers Array of escaped identifiers (array) or one unescaped identifier (string) + * + * @return array|string Array of escaped identifiers (array) or one escaped identifier (string) + */ + public function EscapeIdentifiers($mixIdentifiers) { + if (is_array($mixIdentifiers)) { + return array_map(array($this, 'EscapeIdentifier'), $mixIdentifiers); + } else { + return $this->EscapeIdentifier($mixIdentifiers); + } + } + + /** + * Escapes values (or single value) which we can then send to the database + * + * @param array|mixed $mixValues Array of values (or a single value) to be escaped + * + * @return array|string Array of (or a single) escaped value(s) + */ + public function EscapeValues($mixValues) { + if (is_array($mixValues)) { + return array_map(array($this, 'SqlVariable'), $mixValues); + } else { + return $this->SqlVariable($mixValues); + } + } + + /** + * Escapes both column and values when supplied as an array + * + * @param array $mixColumnsAndValuesArray Array with column=>value format with both (column and value) sides unescaped + * + * @return array Array with column=>value format data with both column and value escaped + */ + public function EscapeIdentifiersAndValues($mixColumnsAndValuesArray) { + $result = array(); + foreach ($mixColumnsAndValuesArray as $strColumn => $mixValue) { + $result[$this->EscapeIdentifier($strColumn)] = $this->SqlVariable($mixValue); + } + return $result; + } + + /** + * INSERTs or UPDATEs a table + * + * @param string $strTable Table name + * @param array $mixColumnsAndValuesArray column=>value array + * (they are given to 'EscapeIdentifiersAndValues' method) + * @param null|string|array $strPKNames Name(s) of primary key column(s) (expressed as string or array) + */ + public function InsertOrUpdate($strTable, $mixColumnsAndValuesArray, $strPKNames = null) { + $strEscapedArray = $this->EscapeIdentifiersAndValues($mixColumnsAndValuesArray); + $strColumns = array_keys($strEscapedArray); + $strUpdateStatement = ''; + foreach ($strEscapedArray as $strColumn => $strValue) { + if ($strUpdateStatement) $strUpdateStatement .= ', '; + $strUpdateStatement .= $strColumn . ' = ' . $strValue; + } + if (is_null($strPKNames)) { + $strMatchCondition = 'target_.'.$strColumns[0].' = source_.'.$strColumns[0]; + } else if (is_array($strPKNames)) { + $strMatchCondition = ''; + foreach ($strPKNames as $strPKName) { + if ($strMatchCondition) $strMatchCondition .= ' AND '; + $strMatchCondition .= 'target_.'.$this->EscapeIdentifier($strPKName).' = source_.'.$this->EscapeIdentifier($strPKName); + } + } else { + $strMatchCondition = 'target_.'.$this->EscapeIdentifier($strPKNames).' = source_.'.$this->EscapeIdentifier($strPKNames); + } + $strTable = $this->EscapeIdentifierBegin . $strTable . $this->EscapeIdentifierEnd; + $strSql = sprintf('MERGE INTO %s AS target_ USING %s AS source_ ON %s WHEN MATCHED THEN UPDATE SET %s WHEN NOT MATCHED THEN INSERT (%s) VALUES (%s)', + $strTable, $strTable, + $strMatchCondition, $strUpdateStatement, + implode(', ', $strColumns), + implode(', ', array_values($strEscapedArray)) + ); + $this->ExecuteNonQuery($strSql); + } + + /** + * Sends the 'SELECT' query to the database and returns the result + * + * @param string $strQuery query string + * + * @return AbstractResult + */ + public final function Query($strQuery) { + $timerName = null; + if (!$this->blnConnectedFlag) { + $this->Connect(); + } + + + if ($this->blnEnableProfiling) { + $timerName = 'queryExec' . mt_rand() ; + Timer::Start($timerName); + } + + $result = $this->ExecuteQuery($strQuery); + + if ($this->blnEnableProfiling) { + $dblQueryTime = Timer::Stop($timerName); + Timer::Reset($timerName); + + // Log Query (for Profiling, if applicable) + $this->LogQuery($strQuery, $dblQueryTime); + } + + return $result; + } + + /** + * This is basically the same as 'Query' but is used when SQL statements other than 'SELECT' + * @param string $strNonQuery The SQL to be sent + * + * @return mixed + * @throws Caller + */ + public final function NonQuery($strNonQuery) { + if (!$this->blnConnectedFlag) { + $this->Connect(); + } + $timerName = ''; + if ($this->blnEnableProfiling) { + $timerName = 'queryExec' . mt_rand() ; + Timer::Start($timerName); + } + + $result = $this->ExecuteNonQuery($strNonQuery); + + if ($this->blnEnableProfiling) { + $dblQueryTime = Timer::Stop($timerName); + Timer::Reset($timerName); + + // Log Query (for Profiling, if applicable) + $this->LogQuery($strNonQuery, $dblQueryTime); + } + + return $result; + } + + /** + * PHP magic method + * @param string $strName Property name + * + * @return mixed + * @throws \Exception|Caller + */ + public function __get($strName) { + switch ($strName) { + case 'EscapeIdentifierBegin': + return $this->strEscapeIdentifierBegin; + case 'EscapeIdentifierEnd': + return $this->strEscapeIdentifierEnd; + case 'EnableProfiling': + return $this->blnEnableProfiling; + case 'AffectedRows': + return -1; + case 'Profile': + return $this->strProfileArray; + case 'DatabaseIndex': + return $this->intDatabaseIndex; + case 'Adapter': + $strConstantName = get_class($this) . '::Adapter'; + return constant($strConstantName) . ' (' . $this->objConfigArray['adapter'] . ')'; + case 'Server': + case 'Port': + case 'Database': + // Informix naming + case 'Service': + case 'Protocol': + case 'Host': + + case 'Username': + case 'Password': + case 'Caching': + return $this->objConfigArray[strtolower($strName)]; + case 'DateFormat': + return (is_null($this->objConfigArray[strtolower($strName)])) ? (\QDateTime::FormatIso) : ($this->objConfigArray[strtolower($strName)]); + case 'OnlyFullGroupBy': + return (!isset($this->objConfigArray[strtolower($strName)])) ? $this->blnOnlyFullGroupBy : $this->objConfigArray[strtolower($strName)]; + + default: + try { + return parent::__get($strName); + } catch (Caller $objExc) { + $objExc->IncrementOffset(); + throw $objExc; + } + } + } + + /** + * PHP magic method to set class properties + * @param string $strName Property name + * @param string $mixValue Property value + * + * @return mixed|void + * @throws \Exception|Caller + */ + public function __set($strName, $mixValue) { + switch ($strName) { + case 'Caching': + $this->objConfigArray[strtolower($strName)] = $mixValue; + break; + + default: + try { + parent::__set($strName, $mixValue); + } catch (Caller $objExc) { + $objExc->IncrementOffset(); + throw $objExc; + } + } + } + + /** + * Constructs a Database Adapter based on the database index and the configuration array of properties + * for this particular adapter. Sets up the base-level configuration properties for this database, + * namely DB Profiling and Database Index + * + * @param integer $intDatabaseIndex + * @param string[] $objConfigArray configuration array as passed in to the constructor + * by QApplicationBase::InitializeDatabaseConnections(); + * + * @throws \Exception|Caller|InvalidCast + */ + public function __construct($intDatabaseIndex, $objConfigArray) { + // Setup DatabaseIndex + $this->intDatabaseIndex = $intDatabaseIndex; + + // Save the ConfigArray + $this->objConfigArray = $objConfigArray; + + // Setup Profiling Array (if applicable) + $this->blnEnableProfiling = Type::Cast($objConfigArray['profiling'], Type::Boolean); + if ($this->blnEnableProfiling) + $this->strProfileArray = array(); + } + + /** + * Allows for the enabling of DB profiling while in middle of the script + * + * @return void + */ + public function EnableProfiling() { + // Only perform profiling initialization if profiling is not yet enabled + if (!$this->blnEnableProfiling) { + $this->blnEnableProfiling = true; + $this->strProfileArray = array(); + } + } + + /** + * If EnableProfiling is on, then log the query to the profile array + * + * @param string $strQuery + * @param double $dblQueryTime query execution time in milliseconds + * @return void + */ + private function LogQuery($strQuery, $dblQueryTime) { + if ($this->blnEnableProfiling) { + // Dereference-ize Backtrace Information + $objDebugBacktrace = debug_backtrace(); + + // get rid of unnecessary backtrace info in case of: + // query + if ((count($objDebugBacktrace) > 3) && + (array_key_exists('function', $objDebugBacktrace[2])) && + (($objDebugBacktrace[2]['function'] == 'QueryArray') || + ($objDebugBacktrace[2]['function'] == 'QuerySingle') || + ($objDebugBacktrace[2]['function'] == 'QueryCount'))) + $objBacktrace = $objDebugBacktrace[3]; + else + if (isset($objDebugBacktrace[2])) + // non query + $objBacktrace = $objDebugBacktrace[2]; + else + // ad hoc query + $objBacktrace = $objDebugBacktrace[1]; + + // get rid of reference to current object in backtrace array + if( isset($objBacktrace['object'])) + $objBacktrace['object'] = null; + + for ($intIndex = 0, $intMax = count($objBacktrace['args']); $intIndex < $intMax; $intIndex++) { + $obj = $objBacktrace['args'][$intIndex]; + + if (is_null($obj)) + $obj = 'null'; + else if (gettype($obj) == 'integer') {} + else if (gettype($obj) == 'object') { + $obj = 'Object: ' . get_class($obj); + if (method_exists($obj, '__toString')) { + $obj .= '- ' . $obj; + } + } + else if (is_array($obj)) + $obj = 'Array'; + else + $obj = sprintf("'%s'", $obj); + $objBacktrace['args'][$intIndex] = $obj; + } + + // Push it onto the profiling information array + $arrProfile = array( + 'objBacktrace' => $objBacktrace, + 'strQuery' => $strQuery, + 'dblTimeInfo' => $dblQueryTime); + + array_push( $this->strProfileArray, $arrProfile); + } + } + + /** + * Properly escapes $mixData to be used as a SQL query parameter. + * If IncludeEquality is set (usually not), then include an equality operator. + * So for most data, it would just be "=". But, for example, + * if $mixData is NULL, then most RDBMS's require the use of "IS". + * + * @param mixed $mixData + * @param boolean $blnIncludeEquality whether or not to include an equality operator + * @param boolean $blnReverseEquality whether the included equality operator should be a "NOT EQUAL", e.g. "!=" + * @return string the properly formatted SQL variable + */ + public function SqlVariable($mixData, $blnIncludeEquality = false, $blnReverseEquality = false) { + // Are we SqlVariabling a BOOLEAN value? + if (is_bool($mixData)) { + // Yes + if ($blnIncludeEquality) { + // We must include the inequality + + if ($blnReverseEquality) { + // Do a "Reverse Equality" + + // Check against NULL, True then False + if (is_null($mixData)) + return 'IS NOT NULL'; + else if ($mixData) + return '= 0'; + else + return '!= 0'; + } else { + // Check against NULL, True then False + if (is_null($mixData)) + return 'IS NULL'; + else if ($mixData) + return '!= 0'; + else + return '= 0'; + } + } else { + // Check against NULL, True then False + if (is_null($mixData)) + return 'NULL'; + else if ($mixData) + return '1'; + else + return '0'; + } + } + + // Check for Equality Inclusion + if ($blnIncludeEquality) { + if ($blnReverseEquality) { + if (is_null($mixData)) + $strToReturn = 'IS NOT '; + else + $strToReturn = '!= '; + } else { + if (is_null($mixData)) + $strToReturn = 'IS '; + else + $strToReturn = '= '; + } + } else + $strToReturn = ''; + + // Check for NULL Value + if (is_null($mixData)) + return $strToReturn . 'NULL'; + + // Check for NUMERIC Value + if (is_integer($mixData) || is_float($mixData)) + return $strToReturn . sprintf('%s', $mixData); + + // Check for DATE Value + if ($mixData instanceof QDateTime) { + /** @var QDateTime $mixData */ + if ($mixData->IsTimeNull()) { + if ($mixData->IsDateNull()) { + return $strToReturn . 'NULL'; // null date and time is a null value + } + return $strToReturn . sprintf("'%s'", $mixData->qFormat('YYYY-MM-DD')); + } + elseif ($mixData->IsDateNull()) { + return $strToReturn . sprintf("'%s'", $mixData->qFormat('hhhh:mm:ss')); + } + return $strToReturn . sprintf("'%s'", $mixData->qFormat(QDateTime::FormatIso)); + } + + // an array. Assume we are using it in an array context, like an IN clause + if (is_array($mixData)) { + $items = []; + foreach ($mixData as $item) { + $items[] = $this->SqlVariable($item); // recurse + } + return '(' . implode(',', $items) . ')'; + } + + // Assume it's some kind of string value + return $strToReturn . sprintf("'%s'", addslashes($mixData)); + } + + public function PrepareStatement($strQuery, $mixParameterArray) { + foreach ($mixParameterArray as $strKey => $mixValue) { + if (is_array($mixValue)) { + $strParameters = array(); + foreach ($mixValue as $mixParameter) + array_push($strParameters, $this->SqlVariable($mixParameter)); + $strQuery = str_replace(chr(QCubed::NAMED_VALUE_DELIMITER) . '{' . $strKey . '}', implode(',', $strParameters) . ')', $strQuery); + } else { + $strQuery = str_replace(chr(QCubed::NAMED_VALUE_DELIMITER) . '{=' . $strKey . '=}', $this->SqlVariable($mixValue, true, false), $strQuery); + $strQuery = str_replace(chr(QCubed::NAMED_VALUE_DELIMITER) . '{!' . $strKey . '!}', $this->SqlVariable($mixValue, true, true), $strQuery); + $strQuery = str_replace(chr(QCubed::NAMED_VALUE_DELIMITER) . '{' . $strKey . '}', $this->SqlVariable($mixValue), $strQuery); + } + } + + return $strQuery; + } + + /** + * Displays the OutputProfiling results, plus a link which will popup the details of the profiling. + * + * @param bool $blnPrintOutput + * @return null|string + */ + public function OutputProfiling($blnPrintOutput = true) { + + $strOut = '
'; + if ($this->blnEnableProfiling) { + $strOut .= sprintf('
', + $this->intDatabaseIndex, __VIRTUAL_DIRECTORY__ . __PHP_ASSETS__); + $strOut.= sprintf('', + base64_encode(serialize($this->strProfileArray))); + $strOut .= sprintf('', $this->intDatabaseIndex); + $strOut .= sprintf('
', \QApplication::HtmlEntities(\QApplication::$RequestUri)); + + $intCount = round(count($this->strProfileArray)); + if ($intCount == 0) + $strQueryString = 'No queries'; + else if ($intCount == 1) + $strQueryString = '1 query'; + else + $strQueryString = $intCount . ' queries'; + + $strOut .= sprintf('PROFILING INFORMATION FOR DATABASE CONNECTION #%s: %s performed. Please click here to view profiling detail
', + $this->intDatabaseIndex, $strQueryString, $this->intDatabaseIndex); + } else { + $strOut .= '
Profiling was not enabled for this database connection (#' . $this->intDatabaseIndex . '). To enable, ensure that ENABLE_PROFILING is set to TRUE.'; + } + $strOut .= '
'; + + $strOut .= ''; // make it draggable so you can move it out of the way if needed. + + if ($blnPrintOutput) { + print ($strOut); + return null; + } + else { + return $strOut; + } + } + + /** + * Executes the explain statement for a given query and returns the output without any transformation. + * If the database adapter does not support EXPLAIN statements, returns null. + * + * @param $strSql + * + * @return null + */ + public function ExplainStatement($strSql) { + return null; + } + + + /** + * Utility function to extract the json embedded options structure from the comments. + * + * Usage: + * + * list($strComment, $options) = AbstractBase::ExtractCommentOptions($strComment); + * + * + * @param string $strComment The comment to analyze + * @return array A two item array, with first item the comment with the options removed, and 2nd item the options array. + * + */ + public static function ExtractCommentOptions($strComment) { + $ret[0] = null; // comment string without options + $ret[1] = null; // the options array + if (($strComment) && + ($pos1 = strpos ($strComment, '{')) !== false && + ($pos2 = strrpos ($strComment, '}', $pos1))) { + + $strJson = substr ($strComment, $pos1, $pos2 - $pos1 + 1); + $a = json_decode($strJson, true); + + if ($a) { + $ret[0] = substr ($strComment, 0, $pos1) . substr ($strComment, $pos2 + 1); // return comment without options + $ret[1] = $a; + } else { + $ret[0] = $strComment; + } + } + + return $ret; + } + +} + diff --git a/src/Database/AbstractException.php b/src/Database/AbstractException.php new file mode 100644 index 0000000..7862d61 --- /dev/null +++ b/src/Database/AbstractException.php @@ -0,0 +1,45 @@ +intErrorNumber; + case "Query"; + return $this->strQuery; + default: + return parent::__get($strName); + } + } +} + diff --git a/src/Database/AbstractField.php b/src/Database/AbstractField.php new file mode 100644 index 0000000..47ba7ee --- /dev/null +++ b/src/Database/AbstractField.php @@ -0,0 +1,98 @@ +strName; + case "OriginalName": + return $this->strOriginalName; + case "Table": + return $this->strTable; + case "OriginalTable": + return $this->strOriginalTable; + case "Default": + return $this->strDefault; + case "MaxLength": + return $this->intMaxLength; + case "Identity": + return $this->blnIdentity; + case "NotNull": + return $this->blnNotNull; + case "PrimaryKey": + return $this->blnPrimaryKey; + case "Unique": + return $this->blnUnique; + case "Timestamp": + return $this->blnTimestamp; + case "Type": + return $this->strType; + case "Comment": + return $this->strComment; + default: + try { + return parent::__get($strName); + } catch (Caller $objExc) { + $objExc->IncrementOffset(); + throw $objExc; + } + } + } +} + diff --git a/src/Database/AbstractResult.php b/src/Database/AbstractResult.php new file mode 100644 index 0000000..ff9672e --- /dev/null +++ b/src/Database/AbstractResult.php @@ -0,0 +1,99 @@ +value style) array from the result set + * @abstract + * @return mixed + */ + abstract public function FetchArray(); + + /** + * Fetches one row as enumerated (with numerical indexes) array from the result set + * @abstract + * @return mixed + */ + abstract public function FetchRow(); + + abstract public function FetchField(); + abstract public function FetchFields(); + abstract public function CountRows(); + abstract public function CountFields(); + + /** + * @return AbstractRow + */ + abstract public function GetNextRow(); + abstract public function GetRows(); + + abstract public function Close(); + + /** + * PHP magic method + * + * @param string $strName Property name + * + * @return mixed + * @throws \Exception|Caller + */ + public function __get($strName) { + switch ($strName) { + case 'ColumnAliasArray': + return $this->strColumnAliasArray; + default: + try { + return parent::__get($strName); + } catch (Caller $objExc) { + $objExc->IncrementOffset(); + throw $objExc; + } + } + } + + public function __set($strName, $mixValue) { + switch ($strName) { + case 'ColumnAliasArray': + try { + return ($this->strColumnAliasArray = Type::Cast($mixValue, Type::ArrayType)); + } catch (InvalidCast $objExc) { + $objExc->IncrementOffset(); + throw $objExc; + } + default: + try { + return parent::__set($strName, $mixValue); + } catch (Caller $objExc) { + $objExc->IncrementOffset(); + throw $objExc; + } + } + } +} + diff --git a/src/Database/AbstractRow.php b/src/Database/AbstractRow.php new file mode 100644 index 0000000..fca41f9 --- /dev/null +++ b/src/Database/AbstractRow.php @@ -0,0 +1,51 @@ +Save() with $blnForceUpdate set to true'), $strClass, 2)); + } +} diff --git a/src/Database/Exception/UndefinedPrimaryKey.php b/src/Database/Exception/UndefinedPrimaryKey.php new file mode 100644 index 0000000..3e1a5d1 --- /dev/null +++ b/src/Database/Exception/UndefinedPrimaryKey.php @@ -0,0 +1,24 @@ +strKeyName = $strKeyName; + $this->strColumnNameArray = $strColumnNameArray; + $this->strReferenceTableName = $strReferenceTableName; + $this->strReferenceColumnNameArray = $strReferenceColumnNameArray; + } + + /** + * PHP magic method + * + * @param string $strName Property name + * + * @return mixed + * @throws \Exception|Caller + */ + public function __get($strName) { + switch ($strName) { + case "KeyName": + return $this->strKeyName; + case "ColumnNameArray": + return $this->strColumnNameArray; + case "ReferenceTableName": + return $this->strReferenceTableName; + case "ReferenceColumnNameArray": + return $this->strReferenceColumnNameArray; + default: + try { + return parent::__get($strName); + } catch (Caller $objExc) { + $objExc->IncrementOffset(); + throw $objExc; + } + } + } +} + diff --git a/src/Database/Index.php b/src/Database/Index.php new file mode 100644 index 0000000..6f38c81 --- /dev/null +++ b/src/Database/Index.php @@ -0,0 +1,68 @@ +strKeyName = $strKeyName; + $this->blnPrimaryKey = $blnPrimaryKey; + $this->blnUnique = $blnUnique; + $this->strColumnNameArray = $strColumnNameArray; + } + + /** + * PHP magic function + * @param string $strName + * + * @return mixed + * @throws \Exception|Caller + */ + public function __get($strName) { + switch ($strName) { + case "KeyName": + return $this->strKeyName; + case "PrimaryKey": + return $this->blnPrimaryKey; + case "Unique": + return $this->blnUnique; + case "ColumnNameArray": + return $this->strColumnNameArray; + default: + try { + return parent::__get($strName); + } catch (Caller $objExc) { + $objExc->IncrementOffset(); + throw $objExc; + } + } + } +} + diff --git a/src/Database/LICENSE b/src/Database/LICENSE new file mode 100644 index 0000000..fbc7e8c --- /dev/null +++ b/src/Database/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 QCubed + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/Database/Mysqli5/Database.php b/src/Database/Mysqli5/Database.php new file mode 100644 index 0000000..8b22178 --- /dev/null +++ b/src/Database/Mysqli5/Database.php @@ -0,0 +1,123 @@ +blnConnectedFlag) $this->Connect(); + + // Use the MySQL5 Information Schema to get a list of all the tables in this database + // (excluding views, etc.) + $strDatabaseName = $this->Database; + + $objResult = $this->Query(" + SELECT + table_name + FROM + information_schema.tables + WHERE + table_type <> 'VIEW' AND + table_schema = '$strDatabaseName'; + "); + + $strToReturn = array(); + while ($strRowArray = $objResult->FetchRow()) + array_push($strToReturn, $strRowArray[0]); + return $strToReturn; + } + + /** + * @param string $strQuery + * @return Result + * @throws Caller + * @throws MysqliException + */ + protected function ExecuteQuery($strQuery) { + // Perform the Query + $objResult = $this->objMySqli->query($strQuery); + if ($this->objMySqli->error) + throw new MysqliException($this->objMySqli->error, $this->objMySqli->errno, $strQuery); + + if (is_bool($objResult)) { + throw new Caller ("Use ExecuteNonQuery when no results are expected from a query."); + } + + // Return the Result + $objMySqliDatabaseResult = new Result($objResult, $this); + return $objMySqliDatabaseResult; + } + + /** + * @param string $strQuery + * @return Result[] array of results + * @throws MysqliException + */ + public function MultiQuery($strQuery) { + // Connect if Applicable + if (!$this->blnConnectedFlag) $this->Connect(); + + // Perform the Query + $this->objMySqli->multi_query($strQuery); + if ($this->objMySqli->error) + throw new MysqliException($this->objMySqli->error, $this->objMySqli->errno, $strQuery); + + $objResultSets = array(); + do { + if ($objResult = $this->objMySqli->store_result()) { + array_push($objResultSets,new Result($objResult, $this)); + } + } while ($this->objMySqli->more_results() && $this->objMySqli->next_result()); + + return $objResultSets; + } + + /** + * Generic stored procedure executor. For Mysql 5, you can have your stored procedure return results by + * "SELECT"ing the results. The results will be returned as an array. + * + * @param string $strProcName Name of stored procedure + * @param array|null $params + * @return Result[] + * @throws MysqliException + */ + public function ExecuteProcedure($strProcName, $params = null) { + $strParams = ''; + if ($params) { + $a = array_map(function ($val) { + return $this->SqlVariable($val); + }, $params); + $strParams = implode(',', $a); + } + $strSql = "call {$strProcName}({$strParams})"; + return $this->MultiQuery($strSql); + } + +} + + diff --git a/src/Database/Mysqli5/Field.php b/src/Database/Mysqli5/Field.php new file mode 100644 index 0000000..dedf5e7 --- /dev/null +++ b/src/Database/Mysqli5/Field.php @@ -0,0 +1,34 @@ +strType = FieldType::VarChar; + break; + + case MYSQLI_TYPE_BIT: + $this->strType = FieldType::Bit; + break; + + default: + parent::SetFieldType($intMySqlFieldType, $intFlags); + } + } +} \ No newline at end of file diff --git a/src/Database/Mysqli5/MysqliDatabase.php b/src/Database/Mysqli5/MysqliDatabase.php new file mode 100644 index 0000000..268f5d5 --- /dev/null +++ b/src/Database/Mysqli5/MysqliDatabase.php @@ -0,0 +1,501 @@ +objConfigArray) && $this->objConfigArray['usefoundrows']) + return 'SQL_CALC_FOUND_ROWS'; + + return null; + } + + public function SqlLimitVariableSuffix($strLimitInfo) { + // Setup limit suffix (if applicable) via a LIMIT clause + if (strlen($strLimitInfo)) { + if (strpos($strLimitInfo, ';') !== false) + throw new \Exception('Invalid Semicolon in LIMIT Info'); + if (strpos($strLimitInfo, '`') !== false) + throw new \Exception('Invalid Backtick in LIMIT Info'); + return "LIMIT $strLimitInfo"; + } + + return null; + } + + public function SqlSortByVariable($strSortByInfo) { + // Setup sorting information (if applicable) via a ORDER BY clause + if (strlen($strSortByInfo)) { + if (strpos($strSortByInfo, ';') !== false) + throw new \Exception('Invalid Semicolon in ORDER BY Info'); + if (strpos($strSortByInfo, '`') !== false) + throw new \Exception('Invalid Backtick in ORDER BY Info'); + + return "ORDER BY $strSortByInfo"; + } + + return null; + } + + public function InsertOrUpdate($strTable, $mixColumnsAndValuesArray, $strPKNames = null) { + $strEscapedArray = $this->EscapeIdentifiersAndValues($mixColumnsAndValuesArray); + $strUpdateStatement = ''; + foreach ($strEscapedArray as $strColumn => $strValue) { + if ($strUpdateStatement) $strUpdateStatement .= ', '; + $strUpdateStatement .= $strColumn . ' = ' . $strValue; + } + $strSql = sprintf('INSERT INTO %s%s%s (%s) VALUES (%s) ON DUPLICATE KEY UPDATE %s', + $this->EscapeIdentifierBegin, $strTable, $this->EscapeIdentifierEnd, + implode(', ', array_keys($strEscapedArray)), + implode(', ', array_values($strEscapedArray)), + $strUpdateStatement + ); + $this->ExecuteNonQuery($strSql); + } + + public function Connect() { + // Connect to the Database Server + $this->objMySqli = new \MySqli($this->Server, $this->Username, $this->Password, $this->Database, $this->Port); + + if (!$this->objMySqli) + throw new MysqliException("Unable to connect to Database", -1, null); + + if ($this->objMySqli->error) + throw new MysqliException($this->objMySqli->error, $this->objMySqli->errno, null); + + // Update "Connected" Flag + $this->blnConnectedFlag = true; + + // Set to AutoCommit + $this->NonQuery('SET AUTOCOMMIT=1;'); + + // Set NAMES (if applicable) + if (array_key_exists('encoding', $this->objConfigArray)) + $this->NonQuery('SET NAMES ' . $this->objConfigArray['encoding'] . ';'); + } + + public function __get($strName) { + switch ($strName) { + case 'AffectedRows': + return $this->objMySqli->affected_rows; + default: + try { + return parent::__get($strName); + } catch (Caller $objExc) { + $objExc->IncrementOffset(); + throw $objExc; + } + } + } + + /** + * @param string $strQuery + * @return MysqliResult + * @throws MysqliException + */ + protected function ExecuteQuery($strQuery) { + // Perform the Query + $objResult = $this->objMySqli->query($strQuery); + if ($this->objMySqli->error) + throw new MysqliException($this->objMySqli->error, $this->objMySqli->errno, $strQuery); + + // Return the Result + $objMySqliDatabaseResult = new MysqliResult($objResult, $this); + return $objMySqliDatabaseResult; + } + + protected function ExecuteNonQuery($strNonQuery) { + // Perform the Query + $this->objMySqli->query($strNonQuery); + if ($this->objMySqli->error) + throw new MysqliException($this->objMySqli->error, $this->objMySqli->errno, $strNonQuery); + } + + public function GetTables() { + // Use the MySQL "SHOW TABLES" functionality to get a list of all the tables in this database + $objResult = $this->Query("SHOW TABLES"); + $strToReturn = array(); + while ($strRowArray = $objResult->FetchRow()) + array_push($strToReturn, $strRowArray[0]); + return $strToReturn; + } + + public function GetFieldsForTable($strTableName) { + $objResult = $this->Query(sprintf('SELECT * FROM %s%s%s LIMIT 1', $this->strEscapeIdentifierBegin, $strTableName, $this->strEscapeIdentifierEnd)); + return $objResult->FetchFields(); + } + + public function InsertId($strTableName = null, $strColumnName = null) { + return $this->objMySqli->insert_id; + } + + public function Close() { + $this->objMySqli->close(); + + // Update Connected Flag + $this->blnConnectedFlag = false; + } + + protected function ExecuteTransactionBegin() { + // Set to AutoCommit + $this->NonQuery('SET AUTOCOMMIT=0;'); + } + + protected function ExecuteTransactionCommit() { + $this->NonQuery('COMMIT;'); + // Set to AutoCommit + $this->NonQuery('SET AUTOCOMMIT=1;'); + } + + protected function ExecuteTransactionRollBack() { + $this->NonQuery('ROLLBACK;'); + // Set to AutoCommit + $this->NonQuery('SET AUTOCOMMIT=1;'); + } + + public function GetFoundRows() { + if (array_key_exists('usefoundrows', $this->objConfigArray) && $this->objConfigArray['usefoundrows']) { + $objResult = $this->Query('SELECT FOUND_ROWS();'); + $strRow = $objResult->FetchArray(); + return $strRow[0]; + } else + throw new Caller('Cannot call GetFoundRows() on the database when "usefoundrows" configuration was not set to true.'); + } + + public function GetIndexesForTable($strTableName) { + // Figure out the Table Type (InnoDB, MyISAM, etc.) by parsing the Create Table description + $strCreateStatement = $this->GetCreateStatementForTable($strTableName); + $strTableType = $this->GetTableTypeForCreateStatement($strCreateStatement); + + switch (true) { + case substr($strTableType, 0, 6) == 'MYISAM': + return $this->ParseForIndexes($strCreateStatement); + + case substr($strTableType, 0, 6) == 'INNODB': + return $this->ParseForIndexes($strCreateStatement); + + case substr($strTableType, 0, 6) == 'MEMORY': + case substr($strTableType, 0, 4) == 'HEAP': + return $this->ParseForIndexes($strCreateStatement); + + default: + throw new \Exception("Table Type is not supported: $strTableType"); + } + } + + public function GetForeignKeysForTable($strTableName) { + // Figure out the Table Type (InnoDB, MyISAM, etc.) by parsing the Create Table description + $strCreateStatement = $this->GetCreateStatementForTable($strTableName); + $strTableType = $this->GetTableTypeForCreateStatement($strCreateStatement); + + switch (true) { + case substr($strTableType, 0, 6) == 'MYISAM': + $objForeignKeyArray = array(); + break; + + case substr($strTableType, 0, 6) == 'MEMORY': + case substr($strTableType, 0, 4) == 'HEAP': + $objForeignKeyArray = array(); + break; + + case substr($strTableType, 0, 6) == 'INNODB': + $objForeignKeyArray = $this->ParseForInnoDbForeignKeys($strCreateStatement); + break; + + default: + throw new \Exception("Table Type is not supported: $strTableType"); + } + + return $objForeignKeyArray; + } + + // MySql defines KeyDefinition to be [OPTIONAL_NAME] ([COL], ...) + // If the key name exists, this will parse it out and return it + private function ParseNameFromKeyDefinition($strKeyDefinition) { + $strKeyDefinition = trim($strKeyDefinition); + + $intPosition = strpos($strKeyDefinition, '('); + + if ($intPosition === false) + throw new \Exception("Invalid Key Definition: $strKeyDefinition"); + else if ($intPosition == 0) + // No Key Name Defined + return null; + + // If we're here, then we have a key name defined + $strName = trim(substr($strKeyDefinition, 0, $intPosition)); + + // Rip Out leading and trailing "`" character (if applicable) + if (substr($strName, 0, 1) == '`') + return substr($strName, 1, strlen($strName) - 2); + else + return $strName; + } + + // MySql defines KeyDefinition to be [OPTIONAL_NAME] ([COL], ...) + // This will return an array of strings that are the names [COL], etc. + private function ParseColumnNameArrayFromKeyDefinition($strKeyDefinition) { + $strKeyDefinition = trim($strKeyDefinition); + + // Get rid of the opening "(" and the closing ")" + $intPosition = strpos($strKeyDefinition, '('); + if ($intPosition === false) + throw new \Exception("Invalid Key Definition: $strKeyDefinition"); + $strKeyDefinition = trim(substr($strKeyDefinition, $intPosition + 1)); + + $intPosition = strpos($strKeyDefinition, ')'); + if ($intPosition === false) + throw new \Exception("Invalid Key Definition: $strKeyDefinition"); + $strKeyDefinition = trim(substr($strKeyDefinition, 0, $intPosition)); + + // Create the Array + // TODO: Current method doesn't support key names with commas or parenthesis in them! + $strToReturn = explode(',', $strKeyDefinition); + + // Take out trailing and leading "`" character in each name (if applicable) + for ($intIndex = 0; $intIndex < count($strToReturn); $intIndex++) { + $strColumn = $strToReturn[$intIndex]; + + if (substr($strColumn, 0, 1) == '`') + $strColumn = substr($strColumn, 1, strpos($strColumn, '`', 1) - 1); + + $strToReturn[$intIndex] = $strColumn; + } + + return $strToReturn; + } + + private function ParseForIndexes($strCreateStatement) { + // MySql nicely splits each object in a table into it's own line + // Split the create statement into lines, and then pull out anything + // that says "PRIMARY KEY", "UNIQUE KEY", or just plain ol' "KEY" + $strLineArray = explode("\n", $strCreateStatement); + + $objIndexArray = array(); + + // We don't care about the first line or the last line + for ($intIndex = 1; $intIndex < (count($strLineArray) - 1); $intIndex++) { + $strLine = $strLineArray[$intIndex]; + + // Each object has a two-space indent + // So this is a key object if any of those key-related words exist at position 2 + switch (2) { + case (strpos($strLine, 'PRIMARY KEY')): + $strKeyDefinition = substr($strLine, strlen(' PRIMARY KEY ')); + + $strKeyName = $this->ParseNameFromKeyDefinition($strKeyDefinition); + $strColumnNameArray = $this->ParseColumnNameArrayFromKeyDefinition($strKeyDefinition); + + $objIndex = new Index($strKeyName, $blnPrimaryKey = true, $blnUnique = true, $strColumnNameArray); + array_push($objIndexArray, $objIndex); + break; + + case (strpos($strLine, 'UNIQUE KEY')): + $strKeyDefinition = substr($strLine, strlen(' UNIQUE KEY ')); + + $strKeyName = $this->ParseNameFromKeyDefinition($strKeyDefinition); + $strColumnNameArray = $this->ParseColumnNameArrayFromKeyDefinition($strKeyDefinition); + + $objIndex = new Index($strKeyName, $blnPrimaryKey = false, $blnUnique = true, $strColumnNameArray); + array_push($objIndexArray, $objIndex); + break; + + case (strpos($strLine, 'KEY')): + $strKeyDefinition = substr($strLine, strlen(' KEY ')); + + $strKeyName = $this->ParseNameFromKeyDefinition($strKeyDefinition); + $strColumnNameArray = $this->ParseColumnNameArrayFromKeyDefinition($strKeyDefinition); + + $objIndex = new Index($strKeyName, $blnPrimaryKey = false, $blnUnique = false, $strColumnNameArray); + array_push($objIndexArray, $objIndex); + break; + } + } + + return $objIndexArray; + } + + private function ParseForInnoDbForeignKeys($strCreateStatement) { + // MySql nicely splits each object in a table into it's own line + // Split the create statement into lines, and then pull out anything + // that starts with "CONSTRAINT" and contains "FOREIGN KEY" + $strLineArray = explode("\n", $strCreateStatement); + + $objForeignKeyArray = array(); + + // We don't care about the first line or the last line + for ($intIndex = 1; $intIndex < (count($strLineArray) - 1); $intIndex++) { + $strLine = $strLineArray[$intIndex]; + + // Check to see if the line: + // * Starts with "CONSTRAINT" at position 2 AND + // * contains "FOREIGN KEY" + if ((strpos($strLine, "CONSTRAINT") == 2) && + (strpos($strLine, "FOREIGN KEY") !== false)) { + $strLine = substr($strLine, strlen(' CONSTRAINT ')); + + // By the end of the following lines, we will end up with a strTokenArray + // Index 0: the FK name + // Index 1: the list of columns that are the foreign key + // Index 2: the table which this FK references + // Index 3: the list of columns which this FK references + $strTokenArray = explode(' FOREIGN KEY ', $strLine); + $strTokenArray[1] = explode(' REFERENCES ', $strTokenArray[1]); + $strTokenArray[2] = $strTokenArray[1][1]; + $strTokenArray[1] = $strTokenArray[1][0]; + $strTokenArray[2] = explode(' ', $strTokenArray[2]); + $strTokenArray[3] = $strTokenArray[2][1]; + $strTokenArray[2] = $strTokenArray[2][0]; + + // Cleanup, and change Index 1 and Index 3 to be an array based on the + // parsed column name list + if (substr($strTokenArray[0], 0, 1) == '`') + $strTokenArray[0] = substr($strTokenArray[0], 1, strlen($strTokenArray[0]) - 2); + $strTokenArray[1] = $this->ParseColumnNameArrayFromKeyDefinition($strTokenArray[1]); + if (substr($strTokenArray[2], 0, 1) == '`') + $strTokenArray[2] = substr($strTokenArray[2], 1, strlen($strTokenArray[2]) - 2); + $strTokenArray[3] = $this->ParseColumnNameArrayFromKeyDefinition($strTokenArray[3]); + + // Create the FK object and add it to the return array + $objForeignKey = new ForeignKey($strTokenArray[0], $strTokenArray[1], $strTokenArray[2], $strTokenArray[3]); + array_push($objForeignKeyArray, $objForeignKey); + + // Ensure the FK object has matching column numbers (or else, throw) + if ((count($objForeignKey->ColumnNameArray) == 0) || + (count($objForeignKey->ColumnNameArray) != count($objForeignKey->ReferenceColumnNameArray))) + throw new \Exception("Invalid Foreign Key definition: $strLine"); + } + } + return $objForeignKeyArray; + } + + private function GetCreateStatementForTable($strTableName) { + // Use the MySQL "SHOW CREATE TABLE" functionality to get the table's Create statement + $objResult = $this->Query(sprintf('SHOW CREATE TABLE `%s`', $strTableName)); + $objRow = $objResult->FetchRow(); + $strCreateTable = $objRow[1]; + $strCreateTable = str_replace("\r", "", $strCreateTable); + return $strCreateTable; + } + + private function GetTableTypeForCreateStatement($strCreateStatement) { + // Table Type is in the last line of the Create Statement, "TYPE=DbTableType" + $strLineArray = explode("\n", $strCreateStatement); + $strFinalLine = strtoupper($strLineArray[count($strLineArray) - 1]); + + if (substr($strFinalLine, 0, 7) == ') TYPE=') { + return trim(substr($strFinalLine, 7)); + } else if (substr($strFinalLine, 0, 9) == ') ENGINE=') { + return trim(substr($strFinalLine, 9)); + } else + throw new \Exception("Invalid Table Description"); + } + + /** + * + * @param string $sql + * @return MysqliResult + */ + public function ExplainStatement($sql) { + // As of MySQL 5.6.3, EXPLAIN provides information about + // SELECT, DELETE, INSERT, REPLACE, and UPDATE statements. + // Before MySQL 5.6.3, EXPLAIN provides information only about SELECT statements. + + $objDbResult = $this->Query("select version()"); + $strDbRow = $objDbResult->FetchRow(); + $strVersion = Type::Cast($strDbRow[0], Type::String); + $strVersionArray = explode('.', $strVersion); + $strMajorVersion = null; + if (count($strVersionArray) > 0) { + $strMajorVersion = $strVersionArray[0]; + } + if (null === $strMajorVersion) { + return null; + } + if (intval($strMajorVersion) > 5) { + return $this->Query("EXPLAIN " . $sql); + } else if (5 == intval($strMajorVersion)) { + $strMinorVersion = null; + if (count($strVersionArray) > 1) { + $strMinorVersion = $strVersionArray[1]; + } + if (null === $strMinorVersion) { + return null; + } + if (intval($strMinorVersion) > 6) { + return $this->Query("EXPLAIN " . $sql); + } else if (6 == intval($strMinorVersion)) { + $strSubMinorVersion = null; + if (count($strVersionArray) > 2) { + $strSubMinorVersion = $strVersionArray[2]; + } + if (null === $strSubMinorVersion) { + return null; + } + if (!is_integer($strSubMinorVersion)) { + $strSubMinorVersionArray = explode("-", $strSubMinorVersion); + if (count($strSubMinorVersionArray) > 1) { + $strSubMinorVersion = $strSubMinorVersionArray[0]; + if (!is_integer($strSubMinorVersion)) { + // Failed to determine the sub-minor version. + return null; + } + } else { + // Failed to determine the sub-minor version. + return null; + } + } + if (intval($strSubMinorVersion) > 2) { + return $this->Query("EXPLAIN " . $sql); + } else { + // We have the version before 5.6.3 + // let's check if it is SELECT-only request + if (0 == substr_count($sql, "DELETE") && + 0 == substr_count($sql, "INSERT") && + 0 == substr_count($sql, "REPLACE") && + 0 == substr_count($sql, "UPDATE") + ) { + return $this->Query("EXPLAIN " . $sql); + } + } + } + } + // Return null by default + return null; + } +} + + + diff --git a/src/Database/Mysqli5/MysqliException.php b/src/Database/Mysqli5/MysqliException.php new file mode 100644 index 0000000..e8e22a0 --- /dev/null +++ b/src/Database/Mysqli5/MysqliException.php @@ -0,0 +1,25 @@ +intErrorNumber = $intNumber; + $this->strQuery = $strQuery; + } +} \ No newline at end of file diff --git a/src/Database/Mysqli5/MysqliField.php b/src/Database/Mysqli5/MysqliField.php new file mode 100644 index 0000000..fb6beba --- /dev/null +++ b/src/Database/Mysqli5/MysqliField.php @@ -0,0 +1,176 @@ +strName = $mixFieldData->name; + $this->strOriginalName = $mixFieldData->orgname; + $this->strTable = $mixFieldData->table; + $this->strOriginalTable = $mixFieldData->orgtable; + $this->strDefault = $mixFieldData->def; + $this->intMaxLength = null; + $this->strComment = null; + + // Set strOriginalName to Name if it isn't set + if (!$this->strOriginalName) + $this->strOriginalName = $this->strName; + + if($this->strOriginalTable) + { + $objDescriptionResult = $objDb->Query(sprintf("SHOW FULL FIELDS FROM `%s`", $this->strOriginalTable)); + while (($objRow = $objDescriptionResult->FetchArray())) { + if ($objRow["Field"] == $this->strOriginalName) { + + $this->strDefault = $objRow["Default"]; + // Calculate MaxLength of this column (e.g. if it's a varchar, calculate length of varchar + // NOTE: $mixFieldData->max_length in the MySQL spec is **DIFFERENT** + $strLengthArray = explode("(", $objRow["Type"]); + if ((count($strLengthArray) > 1) && + (strtolower($strLengthArray[0]) != 'enum') && + (strtolower($strLengthArray[0]) != 'set')) { + $strLengthArray = explode(")", $strLengthArray[1]); + $this->intMaxLength = $strLengthArray[0]; + + // If the length is something like (7,2), then let's pull out just the "7" + $intCommaPosition = strpos($this->intMaxLength, ','); + if ($intCommaPosition !== false) { + $this->intMaxLength = substr($this->intMaxLength, 0, $intCommaPosition); + $this->intMaxLength++; // this is a decimal, so max length should include the decimal point too. + } + + if (!is_numeric($this->intMaxLength)) { + throw new \Exception("Not a valid Column Length: " . $objRow["Type"]); + } + } + + // Get the field comment + $this->strComment = $objRow["Comment"]; + } + } + } + + $this->blnIdentity = ($mixFieldData->flags & MYSQLI_AUTO_INCREMENT_FLAG) ? true: false; + $this->blnNotNull = ($mixFieldData->flags & MYSQLI_NOT_NULL_FLAG) ? true : false; + $this->blnPrimaryKey = ($mixFieldData->flags & MYSQLI_PRI_KEY_FLAG) ? true : false; + $this->blnUnique = ($mixFieldData->flags & MYSQLI_UNIQUE_KEY_FLAG) ? true : false; + + $this->SetFieldType($mixFieldData->type, $mixFieldData->flags); + } + + protected function SetFieldType($intMySqlFieldType, $intFlags) { + + if (version_compare(PHP_VERSION, '5.6.15') >= 0) { + if (defined("MYSQLI_TYPE_JSON") && $intMySqlFieldType == MYSQLI_TYPE_JSON) { + $this->strType = FieldType::Json; + return; + } + } + switch ($intMySqlFieldType) { + case MYSQLI_TYPE_TINY: + if ($this->intMaxLength == 1) + $this->strType = FieldType::Bit; + else + $this->strType = FieldType::Integer; + break; + case MYSQLI_TYPE_SHORT: + case MYSQLI_TYPE_LONG: + case MYSQLI_TYPE_LONGLONG: + case MYSQLI_TYPE_INT24: + $this->strType = FieldType::Integer; + break; + case MYSQLI_TYPE_NEWDECIMAL: + case MYSQLI_TYPE_DECIMAL: + // NOTE: PHP's best response to fixed point exact precision numbers is to use the bcmath library. + // bcmath requires string inputs. If you try to do math directly on these, PHP will convert to float, + // so for those who care, they will need to be careful. For those who do not care, then PHP will do + // the conversion anyway. + $this->strType = FieldType::VarChar; + break; + + case MYSQLI_TYPE_FLOAT: + $this->strType = FieldType::Float; + break; + case MYSQLI_TYPE_DOUBLE: + // NOTE: PHP does not offer full support of double-precision floats. + // Value will be set as a VarChar which will guarantee that the precision will be maintained. + // However, you will not be able to support full typing control (e.g. you would + // not be able to use a QFloatTextBox -- only a regular QTextBox) + $this->strType = FieldType::VarChar; + break; + case MYSQLI_TYPE_DATE: + $this->strType = FieldType::Date; + break; + case MYSQLI_TYPE_TIME: + $this->strType = FieldType::Time; + break; + case MYSQLI_TYPE_TIMESTAMP: + // Special situation that we take advantage of to automatically implement optimistic locking + if ($intFlags & MYSQLI_ON_UPDATE_NOW_FLAG) { + $this->strType = FieldType::VarChar; + $this->blnTimestamp = true; + } else { + $this->strType = FieldType::DateTime; + } + break; + case MYSQLI_TYPE_DATETIME: + $this->strType = FieldType::DateTime; + break; + case MYSQLI_TYPE_TINY_BLOB: + case MYSQLI_TYPE_MEDIUM_BLOB: + case MYSQLI_TYPE_LONG_BLOB: + case MYSQLI_TYPE_BLOB: + case MYSQLI_TYPE_STRING: + case MYSQLI_TYPE_VAR_STRING: + if ($intFlags & MYSQLI_BINARY_FLAG) { + $this->strType = FieldType::Blob; + } else { + $this->strType = FieldType::VarChar; + } + break; + case MYSQLI_TYPE_CHAR: + $this->strType = FieldType::Char; + break; + case MYSQLI_TYPE_INTERVAL: + throw new \Exception("QCubed MySqliDatabase library: MYSQLI_TYPE_INTERVAL is not supported"); + break; + case MYSQLI_TYPE_NULL: + throw new \Exception("QCubed MySqliDatabase library: MYSQLI_TYPE_NULL is not supported"); + break; + case MYSQLI_TYPE_YEAR: + $this->strType = FieldType::Integer; + break; + case MYSQLI_TYPE_NEWDATE: + throw new \Exception("QCubed MySqliDatabase library: MYSQLI_TYPE_NEWDATE is not supported"); + break; + case MYSQLI_TYPE_ENUM: + throw new \Exception("QCubed MySqliDatabase library: MYSQLI_TYPE_ENUM is not supported. Use TypeTables instead."); + break; + case MYSQLI_TYPE_SET: + throw new \Exception("QCubed MySqliDatabase library: MYSQLI_TYPE_SET is not supported. Use TypeTables instead."); + break; + case MYSQLI_TYPE_GEOMETRY: + throw new \Exception("QCubed MySqliDatabase library: MYSQLI_TYPE_GEOMETRY is not supported"); + break; + default: + throw new \Exception("Unable to determine MySqli Database Field Type: " . $intMySqlFieldType); + break; + } + } +} diff --git a/src/Database/Mysqli5/MysqliResult.php b/src/Database/Mysqli5/MysqliResult.php new file mode 100644 index 0000000..384a964 --- /dev/null +++ b/src/Database/Mysqli5/MysqliResult.php @@ -0,0 +1,81 @@ +objMySqliResult = $objResult; + $this->objDb = $objDb; + } + + public function FetchArray() { + return $this->objMySqliResult->fetch_array(); + } + + public function FetchFields() { + $objArrayToReturn = array(); + while ($objField = $this->objMySqliResult->fetch_field()) + array_push($objArrayToReturn, new MysqliField($objField, $this->objDb)); + return $objArrayToReturn; + } + + public function FetchField() { + if ($objField = $this->objMySqliResult->fetch_field()) { + return new MysqliField($objField, $this->objDb); + } + return null; + } + + public function FetchRow() { + return $this->objMySqliResult->fetch_row(); + } + + public function MySqlFetchField() { + return $this->objMySqliResult->fetch_field(); + } + + public function CountRows() { + return $this->objMySqliResult->num_rows; + } + + public function CountFields() { + return $this->objMySqliResult->field_count; + } + + public function Close() { + $this->objMySqliResult->free(); + } + + public function GetNextRow() { + $strColumnArray = $this->FetchArray(); + + if ($strColumnArray) + return new MysqliRow($strColumnArray); + else + return null; + } + + public function GetRows() { + $objDbRowArray = array(); + while ($objDbRow = $this->GetNextRow()) + array_push($objDbRowArray, $objDbRow); + return $objDbRowArray; + } +} + diff --git a/src/Database/Mysqli5/MysqliRow.php b/src/Database/Mysqli5/MysqliRow.php new file mode 100644 index 0000000..19508a5 --- /dev/null +++ b/src/Database/Mysqli5/MysqliRow.php @@ -0,0 +1,91 @@ +strColumnArray = $strColumnArray; + } + + /** + * Gets the value of a column from a result row returned by the database + * + * @param string $strColumnName Name of the column + * @param null|string $strColumnType A FieldType string + * + * @return mixed + */ + public function GetColumn($strColumnName, $strColumnType = null) { + if (!isset($this->strColumnArray[$strColumnName])) { + return null; + } + $strColumnValue = $this->strColumnArray[$strColumnName]; + + switch ($strColumnType) { + case FieldType::Bit: + // Account for single bit value + $chrBit = $strColumnValue; + if ((strlen($chrBit) == 1) && (ord($chrBit) == 0)) + return false; + + // Otherwise, use PHP conditional to determine true or false + return ($strColumnValue) ? true : false; + + case FieldType::Blob: + case FieldType::Char: + case FieldType::VarChar: + return Type::Cast($strColumnValue, Type::String); + + case FieldType::Date: + return new QDateTime($strColumnValue, null, QDateTime::DateOnlyType); + case FieldType::DateTime: + return new QDateTime($strColumnValue, null, QDateTime::DateAndTimeType); + case FieldType::Time: + return new QDateTime($strColumnValue, null, QDateTime::TimeOnlyType); + + case FieldType::Float: + return Type::Cast($strColumnValue, Type::Float); + + case FieldType::Integer: + return Type::Cast($strColumnValue, Type::Integer); + + default: + return $strColumnValue; + } + } + + /** + * Tells whether a particular column exists in a returned database row + * + * @param string $strColumnName Name of te column + * + * @return bool + */ + public function ColumnExists($strColumnName) { + return array_key_exists($strColumnName, $this->strColumnArray); + } + + public function GetColumnNameArray() { + return $this->strColumnArray; + } +} + diff --git a/src/Database/Mysqli5/Result.php b/src/Database/Mysqli5/Result.php new file mode 100644 index 0000000..c063ea0 --- /dev/null +++ b/src/Database/Mysqli5/Result.php @@ -0,0 +1,31 @@ +objMySqliResult->fetch_field()) + array_push($objArrayToReturn, new Field($objField, $this->objDb)); + return $objArrayToReturn; + } + + public function FetchField() { + if ($objField = $this->objMySqliResult->fetch_field()) { + return new Field($objField, $this->objDb); + } + return null; + } +} diff --git a/src/Database/PostgreSql/Database.php b/src/Database/PostgreSql/Database.php new file mode 100644 index 0000000..7e6539e --- /dev/null +++ b/src/Database/PostgreSql/Database.php @@ -0,0 +1,522 @@ +IsTimeNull()) { + if ($mixData->IsDateNull()) { + return $strToReturn . 'NULL'; // null date and time is a null value + } + return $strToReturn . sprintf("'%s'", $mixData->qFormat('YYYY-MM-DD')); + } elseif ($mixData->IsDateNull()) { + return $strToReturn . sprintf("'%s'", $mixData->qFormat('hhhh:mm:ss')); + } else { + return $strToReturn . sprintf("'%s'", $mixData->qFormat(QDateTime::FormatIso)); + } + } + + // Assume it's some kind of string value + return $strToReturn . sprintf("'%s'", pg_escape_string($mixData)); + } + + public function SqlLimitVariablePrefix($strLimitInfo) { + // PostgreSQL uses Limit by Suffixes (via a LIMIT clause) + // Prefix is not used, therefore, return null + return null; + } + + public function SqlLimitVariableSuffix($strLimitInfo) { + // Setup limit suffix (if applicable) via a LIMIT clause + if (strlen($strLimitInfo)) { + if (strpos($strLimitInfo, ';') !== false) + throw new \Exception('Invalid Semicolon in LIMIT Info'); + if (strpos($strLimitInfo, '`') !== false) + throw new \Exception('Invalid Backtick in LIMIT Info'); + + // First figure out if we HAVE an offset + $strArray = explode(',', $strLimitInfo); + + if (count($strArray) == 2) { + // Yep -- there's an offset + return sprintf('LIMIT %s OFFSET %s', $strArray[1], $strArray[0]); + } else if (count($strArray) == 1) { + return sprintf('LIMIT %s', $strArray[0]); + } else { + throw new Exception('Invalid Limit Info: ' . $strLimitInfo, 0, null); + } + } + + return null; + } + + public function SqlSortByVariable($strSortByInfo) { + // Setup sorting information (if applicable) via a ORDER BY clause + if (strlen($strSortByInfo)) { + if (strpos($strSortByInfo, ';') !== false) + throw new \Exception('Invalid Semicolon in ORDER BY Info'); + if (strpos($strSortByInfo, '`') !== false) + throw new \Exception('Invalid Backtick in ORDER BY Info'); + + return "ORDER BY $strSortByInfo"; + } + + return null; + } + + public function InsertOrUpdate($strTable, $mixColumnsAndValuesArray, $strPKNames = null) { + $strEscapedArray = $this->EscapeIdentifiersAndValues($mixColumnsAndValuesArray); + $strColumns = array_keys($strEscapedArray); + $strUpdateStatement = ''; + foreach ($strEscapedArray as $strColumn => $strValue) { + if ($strUpdateStatement) $strUpdateStatement .= ', '; + $strUpdateStatement .= $strColumn . ' = ' . $strValue; + } + if (is_null($strPKNames)) { + $strPKNames = array($strColumns[0]); + } else if (is_array($strPKNames)) { + $strPKNames = $this->EscapeIdentifiers($strPKNames); + } else { + $strPKNames = array($this->EscapeIdentifier($strPKNames)); + } + $strMatchCondition = ''; + foreach ($strPKNames as $strPKName) { + if ($strMatchCondition) $strMatchCondition .= ' AND '; + $strMatchCondition .= $strPKName.' = '.$strEscapedArray[$strPKName]; + } + $strTable = $this->EscapeIdentifierBegin . $strTable . $this->EscapeIdentifierEnd; + $strUpdateSql = sprintf('UPDATE %s SET %s WHERE %s', + $strTable, $strUpdateStatement, $strMatchCondition); + $strInsertSql = sprintf('INSERT INTO %s (%s) SELECT %s WHERE NOT EXISTS (SELECT 1 FROM %s WHERE %s)', + $strTable, + implode(', ', $strColumns), + implode(', ', array_values($strEscapedArray)), + $strTable, $strMatchCondition); + $this->TransactionBegin(); + try { + $this->ExecuteNonQuery($strUpdateSql); + $this->ExecuteNonQuery($strInsertSql); + $this->TransactionCommit(); + } catch (Exception $ex) { + $this->TransactionRollback(); + throw $ex; + } + } + + /** + * Connects to the database + * + * @throws Exception + */ + public function Connect() { + // Lookup Adapter-Specific Connection Properties + $strServer = $this->Server; + $strName = $this->Database; + $strUsername = $this->Username; + $strPassword = $this->Password; + $strPort = $this->Port; + + // Connect to the Database Server + $this->objPgSql = pg_connect(sprintf('host=%s dbname=%s user=%s password=%s port=%s',$strServer, $strName, $strUsername, $strPassword, $strPort)); + + if (!$this->objPgSql) + throw new Exception("Unable to connect to Database", -1, null); + + // Update Connected Flag + $this->blnConnectedFlag = true; + } + + public function __get($strName) { + switch ($strName) { + case 'AffectedRows': + return pg_affected_rows($this->objMostRecentResult); + default: + try { + return parent::__get($strName); + } catch (Caller $objExc) { + $objExc->IncrementOffset(); + throw $objExc; + } + } + } + + /** + * @param string $strQuery + * @return Result + * @throws Exception + */ + protected function ExecuteQuery($strQuery) { + // Perform the Query + $objResult = pg_query($this->objPgSql, $strQuery); + if (!$objResult) + throw new Exception(pg_last_error(), 0, $strQuery); + + // Return the Result + $this->objMostRecentResult = $objResult; + $objPgSqlDatabaseResult = new Result($objResult, $this); + return $objPgSqlDatabaseResult; + } + + /** + * @param string $strNonQuery + * @throws Exception + */ + protected function ExecuteNonQuery($strNonQuery) { + // Perform the Query + $objResult = pg_query($this->objPgSql, $strNonQuery); + if (!$objResult) + throw new Exception(pg_last_error(), 0, $strNonQuery); + $this->objMostRecentResult = $objResult; + } + + /** + * Returns the list of tables in the database as string + * + * @return array List of tables in the database as string + */ + public function GetTables() { + $objResult = $this->Query("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = current_schema() AND TABLE_TYPE = 'BASE TABLE' ORDER BY TABLE_NAME ASC"); + $strToReturn = array(); + while ($strRowArray = $objResult->FetchRow()) + array_push($strToReturn, $strRowArray[0]); + return $strToReturn; + } + + public function GetFieldsForTable($strTableName) { + $strTableName = $this->SqlVariable($strTableName); + $strQuery = sprintf(' + SELECT + columns.table_name, + columns.column_name, + columns.ordinal_position, + columns.column_default, + columns.is_nullable, + columns.data_type, + columns.character_maximum_length, + descr.description AS comment, + (pg_get_serial_sequence(columns.table_name,columns.column_name) IS NOT NULL) AS is_serial + FROM + INFORMATION_SCHEMA.COLUMNS columns + JOIN pg_catalog.pg_class klass ON (columns.table_name = klass.relname AND klass.relkind = \'r\') + LEFT JOIN pg_catalog.pg_description descr ON (descr.objoid = klass.oid AND descr.objsubid = columns.ordinal_position) + WHERE + columns.table_schema = current_schema() + AND + columns.table_name = %s + ORDER BY + ordinal_position + ', $strTableName); + + $objResult = $this->Query($strQuery); + + $objFields = array(); + + while ($objRow = $objResult->GetNextRow()) { + array_push($objFields, new Field($objRow, $this)); + } + + return $objFields; + } + + public function InsertId($strTableName = null, $strColumnName = null) { + $strQuery = sprintf(' + SELECT currval(pg_get_serial_sequence(%s, %s)) + ', $this->SqlVariable($strTableName), $this->SqlVariable($strColumnName)); + + $objResult = $this->Query($strQuery); + $objRow = $objResult->FetchRow(); + return $objRow[0]; + } + + public function Close() { + pg_close($this->objPgSql); + + // Update Connected Flag + $this->blnConnectedFlag = false; + } + + /** + * Sends the 'BEGIN' command to the PostgreSQL server to start a transaction + */ + protected function ExecuteTransactionBegin() { + $this->NonQuery('BEGIN;'); + } + + /** + * Sends the 'COMMIT' command to the PostgreSQL server to commit/end a transaction + */ + protected function ExecuteTransactionCommit() { + $this->NonQuery('COMMIT;'); + } + + /** + * Sends the 'ROLLBACK' command to the PostgreSQL server to revert a transaction + */ + protected function ExecuteTransactionRollBack() { + $this->NonQuery('ROLLBACK;'); + } + + private function ParseColumnNameArrayFromKeyDefinition($strKeyDefinition) { + $strKeyDefinition = trim($strKeyDefinition); + + // Get rid of the opening "(" and the closing ")" + $intPosition = strpos($strKeyDefinition, '('); + if ($intPosition === false) + throw new \Exception("Invalid Key Definition: $strKeyDefinition"); + $strKeyDefinition = trim(substr($strKeyDefinition, $intPosition + 1)); + + $intPosition = strpos($strKeyDefinition, ')'); + if ($intPosition === false) + throw new \Exception("Invalid Key Definition: $strKeyDefinition"); + $strKeyDefinition = trim(substr($strKeyDefinition, 0, $intPosition)); + $strKeyDefinition = str_replace(" ","",$strKeyDefinition); + + // Create the Array + // TODO: Current method doesn't support key names with commas or parenthesis in them! + $strToReturn = explode(',', $strKeyDefinition); + + // Take out trailing and leading '"' character in each name (if applicable) + for ($intIndex = 0; $intIndex < count($strToReturn); $intIndex++) { + $strColumn = $strToReturn[$intIndex]; + + if (substr($strColumn, 0, 1) == '"') + $strColumn = substr($strColumn, 1, strpos($strColumn, '"', 1) - 1); + + $strToReturn[$intIndex] = $strColumn; + } + + return $strToReturn; + } + + public function GetIndexesForTable($strTableName) { + $objIndexArray = array(); + + $objResult = $this->Query(sprintf(' + SELECT + c2.relname AS indname, + i.indisprimary, + i.indisunique, + pg_catalog.pg_get_indexdef(i.indexrelid) AS inddef + FROM + pg_catalog.pg_class c, + pg_catalog.pg_class c2, + pg_catalog.pg_index i + WHERE + c.relname = %s + AND + pg_catalog.pg_table_is_visible(c.oid) + AND + c.oid = i.indrelid + AND + i.indexrelid = c2.oid + ORDER BY + c2.relname + ', $this->SqlVariable($strTableName))); + + while ($objRow = $objResult->GetNextRow()) { + $strIndexDefinition = $objRow->GetColumn('inddef'); + $strKeyName = $objRow->GetColumn('indname'); + $blnPrimaryKey = ($objRow->GetColumn('indisprimary') === "t"); + $blnUnique = ($objRow->GetColumn('indisunique') === "t"); + $strColumnNameArray = $this->ParseColumnNameArrayFromKeyDefinition($strIndexDefinition); + + $objIndex = new Index($strKeyName, $blnPrimaryKey, $blnUnique, $strColumnNameArray); + array_push($objIndexArray, $objIndex); + } + + return $objIndexArray; + } + + /** + * @param string $strTableName + * @return ForeignKey[] + */ + public function GetForeignKeysForTable($strTableName) { + $objForeignKeyArray = array(); + + // Use Query to pull the FKs + $strQuery = sprintf(' + SELECT + pc.conname, + pg_catalog.pg_get_constraintdef(pc.oid, true) AS consrc + FROM + pg_catalog.pg_constraint pc + WHERE + pc.conrelid = + ( + SELECT + oid + FROM + pg_catalog.pg_class + WHERE + relname=%s + AND + relnamespace = + ( + SELECT + oid + FROM + pg_catalog.pg_namespace + WHERE + nspname=current_schema() + ) + ) + AND + pc.contype = \'f\' + ', $this->SqlVariable($strTableName)); + + $objResult = $this->Query($strQuery); + + while ($objRow = $objResult->GetNextRow()) { + $strKeyName = $objRow->GetColumn('conname'); + + // Remove leading and trailing '"' characters (if applicable) + if (substr($strKeyName, 0, 1) == '"') + $strKeyName = substr($strKeyName, 1, strlen($strKeyName) - 2); + + // By the end of the following lines, we will end up with a strTokenArray + // Index 1: the list of columns that are the foreign key + // Index 2: the table which this FK references + // Index 3: the list of columns which this FK references + $strTokenArray = explode('FOREIGN KEY ', $objRow->GetColumn('consrc')); + $strTokenArray[1] = explode(' REFERENCES ', $strTokenArray[1]); + $strTokenArray[2] = $strTokenArray[1][1]; + $strTokenArray[1] = $strTokenArray[1][0]; + $strTokenArray[2] = explode("(", $strTokenArray[2]); + $strTokenArray[3] = "(".$strTokenArray[2][1]; + $strTokenArray[2] = $strTokenArray[2][0]; + + // Remove leading and trailing '"' characters (if applicable) + if (substr($strTokenArray[2], 0, 1) == '"') + $strTokenArray[2] = substr($strTokenArray[2], 1, strlen($strTokenArray[2]) - 2); + + $strColumnNameArray = $this->ParseColumnNameArrayFromKeyDefinition($strTokenArray[1]); + $strReferenceTableName = $strTokenArray[2]; + $strReferenceColumnNameArray = $this->ParseColumnNameArrayFromKeyDefinition($strTokenArray[3]); + + $objForeignKey = new ForeignKey( + $strKeyName, + $strColumnNameArray, + $strReferenceTableName, + $strReferenceColumnNameArray); + array_push($objForeignKeyArray, $objForeignKey); + } + + // Return the Array of Foreign Keys + return $objForeignKeyArray; + } + + /** + * @param $sql + * @return mixed + */ + public function ExplainStatement($sql) { + return $this->Query("EXPLAIN " . $sql); + } +} + diff --git a/src/Database/PostgreSql/Exception.php b/src/Database/PostgreSql/Exception.php new file mode 100644 index 0000000..5973f7a --- /dev/null +++ b/src/Database/PostgreSql/Exception.php @@ -0,0 +1,34 @@ +intErrorNumber = $intNumber; + $this->strQuery = $strQuery; + } +} + diff --git a/src/Database/PostgreSql/Field.php b/src/Database/PostgreSql/Field.php new file mode 100644 index 0000000..0ef9596 --- /dev/null +++ b/src/Database/PostgreSql/Field.php @@ -0,0 +1,199 @@ +strName = $mixFieldData->GetColumn('column_name'); + $this->strOriginalName = $this->strName; + $this->strTable = $mixFieldData->GetColumn('table_name'); + $this->strOriginalTable = $this->strTable; + $this->strDefault = $mixFieldData->GetColumn('column_default'); + $this->intMaxLength = $mixFieldData->GetColumn('character_maximum_length', FieldType::Integer); + $this->blnNotNull = ($mixFieldData->GetColumn('is_nullable') == "NO") ? true : false; + + // If this column was created as SERIAL and is a simple (non-composite) primary key + // then we assume it's the identity field. + // Otherwise, no identity field will be set for this table. + $this->blnIdentity = false; + if ($mixFieldData->GetColumn('is_serial') == 't') { + $objIndexes = $objDb->GetIndexesForTable($this->strTable); + foreach ($objIndexes as $objIndex) { + if ($objIndex->PrimaryKey) { + $columns = $objIndex->ColumnNameArray; + $this->blnIdentity = (count($columns) == 1 && $columns[0] == $this->strName); + break; + } + } + } + + // Determine Primary Key + $objResult = $objDb->Query(sprintf(' + SELECT + kcu.column_name + FROM + information_schema.table_constraints tc, + information_schema.key_column_usage kcu + WHERE + tc.table_name = %s + AND + tc.table_schema = current_schema() + AND + tc.constraint_type = \'PRIMARY KEY\' + AND + kcu.table_name = tc.table_name + AND + kcu.table_schema = tc.table_schema + AND + kcu.constraint_name = tc.constraint_name + ', $objDb->SqlVariable($this->strTable))); + + while ($objRow = $objResult->GetNextRow()) { + if ($objRow->GetColumn('column_name') == $this->strName) + $this->blnPrimaryKey = true; + } + + if (!$this->blnPrimaryKey) + $this->blnPrimaryKey = false; + + // UNIQUE + $objResult = $objDb->Query(sprintf(' + SELECT + kcu.column_name, (SELECT COUNT(*) FROM information_schema.key_column_usage kcu2 WHERE kcu2.constraint_name=kcu.constraint_name ) as unique_fields + FROM + information_schema.table_constraints tc, + information_schema.key_column_usage kcu + WHERE + tc.table_name = %s + AND + tc.table_schema = current_schema() + AND + tc.constraint_type = \'UNIQUE\' + AND + kcu.table_name = tc.table_name + AND + kcu.table_schema = tc.table_schema + AND + kcu.constraint_name = tc.constraint_name + GROUP BY + kcu.constraint_name, kcu.column_name + ', $objDb->SqlVariable($this->strTable))); + while ($objRow = $objResult->GetNextRow()) { + if ($objRow->GetColumn('column_name') == $this->strName && $objRow->GetColumn('unique_fields') == '1' ) + $this->blnUnique = true; + } + if (!$this->blnUnique) + $this->blnUnique = false; + + // Determine Type + $this->strType = $mixFieldData->GetColumn('data_type'); + + switch ($this->strType) { + case 'integer': + case 'smallint': + case 'bigint': // 8-byte. PHP int sizes are platform dependent. On 64-bit machines, + // this is fine. On 32-bit, PHP will convert to float for numbers too big. + // However, we do NOT want to return a float, as we lose the ability to + // compare against real integers. (float(0) != int(0))! Assume the developer knows what he + // is doing if he uses these. + // http://php.net/manual/en/language.types.integer.php + $this->strType = FieldType::Integer; + + break; + case 'money': + // NOTE: The money type is deprecated in PostgreSQL. + throw new QPostgreSqlDatabaseException('Unsupported Field Type: money. Use numeric or decimal instead.', 0,null); + break; + case 'decimal': + case 'numeric': + // NOTE: PHP's best response to fixed point exact precision numbers is to use the bcmath library. + // bcmath requires string inputs. If you try to do math directly on these, PHP will convert to float, + // so for those who care, they will need to be careful. For those who do not care, then PHP will do + // the conversion automatically. + $this->strType = FieldType::VarChar; + break; + + case 'real': + $this->strType = FieldType::Float; + break; + case 'bit': + if ($this->intMaxLength == 1) + $this->strType = FieldType::Bit; + else + throw new QPostgreSqlDatabaseException('Unsupported Field Type: bit with MaxLength > 1', 0, null); + break; + case 'boolean': + $this->strType = FieldType::Bit; + break; + case 'character': + $this->strType = FieldType::Char; + break; + case 'character varying': + case 'double precision': + // NOTE: PHP does not offer full support of double-precision floats. + // Value will be set as a VarChar which will guarantee that the precision will be maintained. + // However, you will not be able to support full typing control (e.g. you would + // not be able to use a QFloatTextBox -- only a regular QTextBox) + $this->strType = FieldType::VarChar; + break; + case 'json': + case 'jsonb': + $this->strType = FieldType::Json; + break; + case 'tsvector': + // this is the TSVector data type in PostgreSQL used for full text search systems. + // It can safely be used as a text type for displaying the data. + // NOTE: It must be handled via custom queries. + // NOTE: It is added here to avoid code generator halting after error because of unrecognized type + $this->strType = FieldType::VarChar; + break; + case 'text': + $this->strType = FieldType::VarChar; + break; + case 'bytea': + $this->strType = FieldType::Blob; + break; + case 'timestamp': + case 'timestamp with time zone': + // this data type is not heavily used but is important to be included to avoid errors when code generating. + case 'timestamp without time zone': + // System-generated Timestamp values need to be treated as plain text + $this->strType = FieldType::DateTime; // PostgreSql treats timestamp as a datetime + //$this->blnTimestamp = true; + break; + case 'date': + $this->strType = FieldType::Date; + break; + case 'time': + case 'time without time zone': + $this->strType = FieldType::Time; + break; + default: + throw new QPostgreSqlDatabaseException('Unsupported Field Type: ' . $this->strType, 0, null); + } + + // Retrieve comment + $this->strComment = $mixFieldData->GetColumn('comment'); + } +} \ No newline at end of file diff --git a/src/Database/PostgreSql/Result.php b/src/Database/PostgreSql/Result.php new file mode 100644 index 0000000..4d009d6 --- /dev/null +++ b/src/Database/PostgreSql/Result.php @@ -0,0 +1,119 @@ +objPgSqlResult = $objResult; + $this->objDb = $objDb; + } + + /** + * Fetch result (single result) as array + * + * @return array + */ + public function FetchArray() { + return pg_fetch_array($this->objPgSqlResult); + } + + /** + * Fetch fields (currently just null) + * + * @return null + */ + public function FetchFields() { + return null; // Not implemented + } + + /** + * Fetch field (currently just null) + * + * @return null + */ + public function FetchField() { + return null; // Not implemented + } + + /** + * Fetch row + * + * @return array + */ + public function FetchRow() { + return pg_fetch_row($this->objPgSqlResult); + } + + /** + * Return number of rows in result + * + * @return int + */ + public function CountRows() { + return pg_num_rows($this->objPgSqlResult); + } + + /** + * Return number of fields in a result + * + * @return int + */ + public function CountFields() { + return pg_num_fields($this->objPgSqlResult); + } + + /** + * Free the memory. Connection closes when script ends + */ + public function Close() { + pg_free_result($this->objPgSqlResult); + } + + /** + * @return null|Row + */ + public function GetNextRow() { + $strColumnArray = $this->FetchArray(); + + if ($strColumnArray) + return new Row($strColumnArray); + else + return null; + } + + /** + * Returns all results in the result set as array + * + * @return array + */ + public function GetRows() { + $objDbRowArray = array(); + while ($objDbRow = $this->GetNextRow()) + array_push($objDbRowArray, $objDbRow); + return $objDbRowArray; + } +} + diff --git a/src/Database/PostgreSql/Row.php b/src/Database/PostgreSql/Row.php new file mode 100644 index 0000000..54f4e38 --- /dev/null +++ b/src/Database/PostgreSql/Row.php @@ -0,0 +1,113 @@ +strColumnArray = $strColumnArray; + } + + /** + * Gets the value of a column from a result row returned by the database + * + * @param string $strColumnName Name of the column + * @param null|string $strColumnType Data type + * + * @return mixed + */ + public function GetColumn($strColumnName, $strColumnType = null) { + if (!isset($this->strColumnArray[$strColumnName])) { + return null; + } + $strColumnValue = $this->strColumnArray[$strColumnName]; + switch ($strColumnType) { + case FieldType::Bit: + // PostgreSQL returns 't' or 'f' for boolean fields + if ($strColumnValue == 'f') { + return false; + } else { + return ($strColumnValue) ? true : false; + } + + case FieldType::Blob: + case FieldType::Char: + case FieldType::VarChar: + case FieldType::Json: // JSON is basically String + return Type::Cast($strColumnValue, Type::String); + case FieldType::Date: + case FieldType::DateTime: + case FieldType::Time: + return new QDateTime($strColumnValue); + + case FieldType::Float: + return Type::Cast($strColumnValue, Type::Float); + + case FieldType::Integer: + return Type::Cast($strColumnValue, Type::Integer); + + default: + return $strColumnValue; + } + } + + /** + * Tells whether a particular column exists in a returned database row + * + * @param string $strColumnName Name of te column + * + * @return bool + */ + public function ColumnExists($strColumnName) { + return array_key_exists($strColumnName, $this->strColumnArray); + } + + /** + * @return string|string[] + */ + public function GetColumnNameArray() { + return $this->strColumnArray; + } + + /** + * Returns the boolean value corresponding to whatever a bit column returns. Postgres + * returns a 't' or 'f' (or null). + * @param bool|null $mixValue Value of the BIT column + * @return bool + */ + public function ResolveBooleanValue ($mixValue) { + if ($mixValue == 'f') { + return false; + } elseif ($mixValue == 't') { + return true; + } + else + return null; + } +} + + diff --git a/src/Database/README.md b/src/Database/README.md new file mode 100644 index 0000000..17c829e --- /dev/null +++ b/src/Database/README.md @@ -0,0 +1,2 @@ +# database +Database adapters for QCubed - v4 diff --git a/src/Database/Service.php b/src/Database/Service.php new file mode 100644 index 0000000..e15d881 --- /dev/null +++ b/src/Database/Service.php @@ -0,0 +1,106 @@ + // ClassPaths for the ClassName ?> class - + $a['ClassName) ?>'] = __MODEL__ . '/ClassName ?>.class.php'; $a['nodeClassName) ?>'] = __MODEL__ . '/ClassName ?>.class.php'; $a['reversereferencenodeClassName) ?>'] = __MODEL__ . '/ClassName ?>.class.php'; - + $a['ClassName) ?>connector'] = __MODEL_CONNECTOR__ . '/ClassName ?>Connector.class.php'; $a['ClassName) ?>list'] = __MODEL_CONNECTOR__ . '/ClassName ?>List.class.php'; diff --git a/templates/db_orm/class_gen/_main.tpl.php b/templates/db_orm/class_gen/_main.tpl.php index 1e026e2..3b81e78 100644 --- a/templates/db_orm/class_gen/_main.tpl.php +++ b/templates/db_orm/class_gen/_main.tpl.php @@ -81,11 +81,7 @@ abstract class ClassName ?>Gen extends \QCubed\AbstractBase imple ////////////////////////// // SAVE, DELETE AND RELOAD ////////////////////////// -PrivateColumnVars) { ?> - - - diff --git a/templates/db_orm/class_gen/object_save.30.tpl.php b/templates/db_orm/class_gen/object_save.30.tpl.php deleted file mode 100644 index 52ad8fe..0000000 --- a/templates/db_orm/class_gen/object_save.30.tpl.php +++ /dev/null @@ -1,202 +0,0 @@ - -/** - * Save this ClassName ?> - - * @param bool $blnForceInsert - * @param bool $blnForceUpdate -ColumnArray as $objColumn) - if ($objColumn->Identity) { - $returnType = 'int'; - break; - } - print ' * @return '.$returnType; - - $strCols = ''; - $strValues = ''; - $strColUpdates = ''; - foreach ($objTable->ColumnArray as $objColumn) { - if ((!$objColumn->Identity) && (!$objColumn->Timestamp)) { - if ($strCols) $strCols .= ",\n"; - if ($strValues) $strValues .= ",\n"; - if ($strColUpdates) $strColUpdates .= ",\n"; - $strCol = ' ' . $strEscapeIdentifierBegin.$objColumn->Name.$strEscapeIdentifierEnd; - $strCols .= $strCol; - $strValue = '\' . $objDatabase->SqlVariable($this->'.$objColumn->VariableName.') . \''; - $strValues .= ' ' . $strValue; - $strColUpdates .= $strCol .' = '.$strValue; - } elseif ($objColumn->Timestamp && $objColumn->AutoUpdate) { - if ($strCols) $strCols .= ",\n"; - if ($strValues) $strValues .= ",\n"; - if ($strColUpdates) $strColUpdates .= ",\n"; - $strCol = ' ' . $strEscapeIdentifierBegin.$objColumn->Name.$strEscapeIdentifierEnd; - $strCols .= $strCol; - $strValue = '\' . $objDatabase->SqlVariable(QDateTime::NowToString(QDateTime::FormatIso)) . \''; - $strValues .= ' ' . $strValue; - $strColUpdates .= $strCol .' = '.$strValue; - - } - } - if ($strValues) { - $strCols = " (\n".$strCols."\n )"; - $strValues = " VALUES (\n".$strValues."\n )\n"; - } else { - $strValues = " DEFAULT VALUES"; - } - - $strIds = ''; - foreach ($objTable->PrimaryKeyColumnArray as $objPkColumn) { - if ($strIds) $strIds .= " AND \n"; - $strIds .= ' ' . $strEscapeIdentifierBegin.$objPkColumn->Name.$strEscapeIdentifierEnd . - ' = \' . $objDatabase->SqlVariable($this->' . ($objPkColumn->Identity ? '' : '__') . $objPkColumn->VariableName . ') . \''; - } - -?> - - */ - public function Save($blnForceInsert = false, $blnForceUpdate = false) { - // Get the Database Object for this Class - $objDatabase = ClassName ?>::GetDatabase(); - - $mixToReturn = null; - - try { - if ((!$this->__blnRestored && !$blnForceUpdate) || ($blnForceInsert)) { - // Perform an INSERT query - $objDatabase->NonQuery(' - INSERT INTO Name ?> - '); - -PrimaryKeyColumnArray as $objColumn) { - if ($objColumn->Identity) { - print sprintf(' // Update Identity column and return its value - $mixToReturn = $this->%s = $objDatabase->InsertId(\'%s\', \'%s\');', - $objColumn->VariableName, $objTable->Name, $objColumn->Name); - } - } -?> - - } else { - // Perform an UPDATE query - - // First checking for Optimistic Locking constraints (if applicable) -ColumnArray as $objColumn) { ?> -Timestamp) { ?> - if (!$blnForceUpdate) { - // Perform the Optimistic Locking check - $objResult = $objDatabase->Query(' - SELECT - Name ?> - - FROM - Name ?> - - WHERE - - - '); - - $objRow = $objResult->FetchArray(); - if ($objRow[0] != $this->VariableName ?>) - throw new \QCubed\Database\Exception\OptimisticLocking('ClassName ?>'); - } - - - - // Perform the UPDATE query - - $objDatabase->NonQuery(' - UPDATE - Name ?> - - SET - - - WHERE - - - '); - - // Nothing to update - - } - -ReverseReferenceArray as $objReverseReference) { ?> -Unique) { ?> -TableArray[strtolower($objReverseReference->Table)]; ?> -ColumnArray[strtolower($objReverseReference->Column)]; ?> - - - // Update the adjoined ObjectDescription ?> object (if applicable) - // TODO: Make this into hard-coded SQL queries - if ($this->blnDirtyObjectPropertyName ?>) { - // Unassociate the old one (if applicable) - if ($objAssociated = VariableType ?>::LoadByPropertyName ?>(ImplodeObjectArray(', ', '$this->', '', 'VariableName', $objTable->PrimaryKeyColumnArray) ?>)) { - $objAssociated->PropertyName ?> = null; - $objAssociated->Save(); - } - - // Associate the new one (if applicable) - if ($this->ObjectMemberVariable ?>) { - $this->ObjectMemberVariable ?>->PropertyName ?> = $this->PrimaryKeyColumnArray[0]->VariableName ?>; - $this->ObjectMemberVariable ?>->Save(); - } - - // Reset the "Dirty" flag - $this->blnDirtyObjectPropertyName ?> = false; - } - - - } catch (Caller $objExc) { - $objExc->IncrementOffset(); - throw $objExc; - } - - // Update __blnRestored and any Non-Identity PK Columns (if applicable) - $this->__blnRestored = true; -PrimaryKeyColumnArray as $objColumn) { ?> -Identity) && ($objColumn->PrimaryKey)) { ?> - $this->__VariableName ?> = $this->VariableName ?>; - - - -ColumnArray as $objColumn) { ?> -Timestamp) { ?> - // Update Local Timestamp - $objResult = $objDatabase->Query(' - SELECT - Name ?> - - FROM - Name ?> - - WHERE - - - '); - - $objRow = $objResult->FetchArray(); - $this->VariableName ?> = $objRow[0]; - - - - $this->DeleteFromCache(); - - if (static::$blnWatchChanges) { - QWatcher::MarkTableModified (static::GetDatabase()->Database, 'Name ?>'); - } - - // Return - return $mixToReturn; - } \ No newline at end of file diff --git a/test/localbootstrap.php b/test/localbootstrap.php index a48ba48..8e2b4a7 100644 --- a/test/localbootstrap.php +++ b/test/localbootstrap.php @@ -15,7 +15,7 @@ include (__APP_INCLUDES__ . '/model_includes.php'); \QCubed\AutoloaderService::instance() - ->initialize(QCUBED_BASE_DIR . '/../autload.php') + ->initialize(QCUBED_BASE_DIR . '/../autoload.php') ->addClassmapFile(__MODEL_GEN__ . '/_class_paths.inc.php') ->addClassmapFile(__MODEL_GEN__ . '/_type_class_paths.inc.php'); diff --git a/test/travis-config.inc.php b/test/travis-config.inc.php index 02de58d..e0a2a09 100644 --- a/test/travis-config.inc.php +++ b/test/travis-config.inc.php @@ -1,8 +1,26 @@ initialize('./vendor/autoload.php') + ->addPsr4('QCubed\\', __WORKING_DIR__ . '/src'); + +// Codegen +require(__CONFIGURATION__ . '/Codegen.php'); +require( __DOCROOT__ . __SUBDIRECTORY__ . '/tools/codegen.cli.php'); + +// Load up generated classes +include (__APP_INCLUDES__ . '/model_includes.php'); + +\QCubed\AutoloaderService::instance() + ->addClassmapFile(__MODEL_GEN__ . '/_class_paths.inc.php') + ->addClassmapFile(__MODEL_GEN__ . '/_type_class_paths.inc.php'); diff --git a/test/travis/Codegen.php b/test/travis/Codegen.php new file mode 100644 index 0000000..18bd27c --- /dev/null +++ b/test/travis/Codegen.php @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/travis/configuration.inc.php b/test/travis/configuration.inc.php new file mode 100644 index 0000000..fecccba --- /dev/null +++ b/test/travis/configuration.inc.php @@ -0,0 +1,40 @@ + 'Mysqli5', + 'server' => 'localhost', + 'port' => null, + 'database' => 'qcubed', + 'username' => 'root', + 'password' => '', + 'caching' => false, + 'profiling' => false))); diff --git a/test/travis/pgsql.inc.php b/test/travis/pgsql.inc.php new file mode 100644 index 0000000..1d66d7b --- /dev/null +++ b/test/travis/pgsql.inc.php @@ -0,0 +1,14 @@ + 'PostgreSql', + 'server' => 'localhost', + 'port' => null, + 'database' => 'qcubed', + 'username' => 'postgres', + 'password' => '', + 'caching' => false, + 'profiling' => false))); diff --git a/test/unit/BasicOrmTest.php b/test/unit/BasicOrmTest.php index 1affe0b..cd2e28b 100644 --- a/test/unit/BasicOrmTest.php +++ b/test/unit/BasicOrmTest.php @@ -213,10 +213,12 @@ public function testQuerySingleEmpty() { public function testQuerySelectSubset() { $objPersonArray = Person::LoadAll(QQ::Select(QQN::Person()->FirstName)); foreach ($objPersonArray as $objPerson) { - $this->expectException('\\QCubed\\Exception\\Caller'); - $this->expectExceptionMessageRegExp('/LastName .* is not valid./'); - $objPerson->LastName; - $this->expectException(null); + if (PHP_VERSION_ID > 50600) { // PHP unit keeps making backwards incompatible changes + $this->expectException('\\QCubed\\Exception\\Caller'); + $this->expectExceptionMessageRegExp('/LastName .* is not valid./'); + $objPerson->LastName; + $this->expectException(null); + } // If we now set the last name, we should be able to get it $objPerson->LastName = "Test"; @@ -237,12 +239,14 @@ public function testQuerySelectSubsetSkipPK() { $objSelect = QQ::Select(QQN::Person()->FirstName); $objSelect->SetSkipPrimaryKey(true); $objPersonArray = Person::LoadAll($objSelect); - foreach ($objPersonArray as $objPerson) { - $this->expectException('\\QCubed\\Exception\\Caller'); - $this->expectExceptionMessageRegExp('/LastName .* is not valid./'); - $objPerson->LastName; - $this->expectException(null); - $this->assertNull($objPerson->Id, "Id should be null since SkipPrimaryKey is set on the Select object"); + if (PHP_VERSION_ID > 50600) { // PHP unit keeps making backwards incompatible changes + foreach ($objPersonArray as $objPerson) { + $this->expectException('\\QCubed\\Exception\\Caller'); + $this->expectExceptionMessageRegExp('/LastName .* is not valid./'); + $objPerson->LastName; + $this->expectException(null); + $this->assertNull($objPerson->Id, "Id should be null since SkipPrimaryKey is set on the Select object"); + } } } diff --git a/test/unit/ExpandAsArrayTest.php b/test/unit/ExpandAsArrayTest.php index ba0bb40..f0a3fe8 100644 --- a/test/unit/ExpandAsArrayTest.php +++ b/test/unit/ExpandAsArrayTest.php @@ -199,17 +199,19 @@ public function testSelectSubsetInExpand() { ) ); - foreach ($objPersonArray as $objPerson) { - $this->expectException('\\QCubed\\Exception\\Caller'); - $objPerson->FirstName; // FirstName should throw exception, since it was not selected - $this->expectException(null); - - $this->assertNotNull($objPerson->Id, "Id should not be null since it's always added to the select list"); - $this->assertNotNull($objPerson->_ProjectAsManager->Id, "ProjectAsManager->Id should not be null since id's are always added to the select list"); - - $this->expectException('\\QCubed\\Exception\\Caller'); - $objPerson->_ProjectAsManager->Name; // not selected - $this->expectException(null); + if (PHP_VERSION_ID > 50600) { // PHP unit keeps making backwards incompatible changes + foreach ($objPersonArray as $objPerson) { + $this->expectException('\\QCubed\\Exception\\Caller'); + $objPerson->FirstName; // FirstName should throw exception, since it was not selected + $this->expectException(null); + + $this->assertNotNull($objPerson->Id, "Id should not be null since it's always added to the select list"); + $this->assertNotNull($objPerson->_ProjectAsManager->Id, "ProjectAsManager->Id should not be null since id's are always added to the select list"); + + $this->expectException('\\QCubed\\Exception\\Caller'); + $objPerson->_ProjectAsManager->Name; // not selected + $this->expectException(null); + } } } @@ -223,36 +225,38 @@ public function testSelectSubsetInExpandAsArray() { ) ); - foreach ($objPersonArray as $objPerson) { - $this->expectException('\\QCubed\\Exception\\Caller'); - $objPerson->LastName; // Should throw exception, since it was not selected - $this->expectException(null); + if (PHP_VERSION_ID > 50600) { // PHP unit keeps making backwards incompatible changes + foreach ($objPersonArray as $objPerson) { + $this->expectException('\\QCubed\\Exception\\Caller'); + $objPerson->LastName; // Should throw exception, since it was not selected + $this->expectException(null); - $this->assertNotNull($objPerson->Id, "Id should not be null since it's always added to the select list"); - if (sizeof($objPerson->_AddressArray) > 0) { - foreach ($objPerson->_AddressArray as $objAddress) { - $this->assertNotNull($objAddress->Id, "Address->Id should not be null since it's always added to the select list"); + $this->assertNotNull($objPerson->Id, "Id should not be null since it's always added to the select list"); + if (sizeof($objPerson->_AddressArray) > 0) { + foreach ($objPerson->_AddressArray as $objAddress) { + $this->assertNotNull($objAddress->Id, "Address->Id should not be null since it's always added to the select list"); - $this->expectException('\\QCubed\\Exception\\Caller'); - $objAddress->PersonId; // Should throw exception, since it was not selected - $this->expectException(null); + $this->expectException('\\QCubed\\Exception\\Caller'); + $objAddress->PersonId; // Should throw exception, since it was not selected + $this->expectException(null); + } } - } - if (sizeof($objPerson->_ProjectAsManagerArray) > 0) { - foreach($objPerson->_ProjectAsManagerArray as $objProject) { - $this->assertNotNull($objProject->Id, "Project->Id should not be null since it's always added to the select list"); - - $this->expectException('\\QCubed\\Exception\\Caller'); - $objProject->Name; // Should throw exception, since it was not selected - $this->expectException(null); - - if (sizeof($objProject->_MilestoneArray) > 0) { - foreach ($objProject->_MilestoneArray as $objMilestone) { - $this->assertNotNull($objMilestone->Id, "Milestone->Id should not be null since it's always added to the select list"); - - $this->expectException('\\QCubed\\Exception\\Caller'); - $objMilestone->ProjectId; // Should throw exception, since it was not selected - $this->expectException(null); + if (sizeof($objPerson->_ProjectAsManagerArray) > 0) { + foreach ($objPerson->_ProjectAsManagerArray as $objProject) { + $this->assertNotNull($objProject->Id, "Project->Id should not be null since it's always added to the select list"); + + $this->expectException('\\QCubed\\Exception\\Caller'); + $objProject->Name; // Should throw exception, since it was not selected + $this->expectException(null); + + if (sizeof($objProject->_MilestoneArray) > 0) { + foreach ($objProject->_MilestoneArray as $objMilestone) { + $this->assertNotNull($objMilestone->Id, "Milestone->Id should not be null since it's always added to the select list"); + + $this->expectException('\\QCubed\\Exception\\Caller'); + $objMilestone->ProjectId; // Should throw exception, since it was not selected + $this->expectException(null); + } } } } diff --git a/tools/README.md b/tools/README.md index f73f680..d606ec7 100644 --- a/tools/README.md +++ b/tools/README.md @@ -7,8 +7,8 @@ and plugins. You should not include this directory in your deployment. * `codegen.cli` - for Unix/Linux/Mac OS X command lines * `codegen.phpexe` - for Windows command line -Both use the QCodeGen and related QCubed codegen libraries to do the bulk - of the work. They simply instantiate a QCodeGen object, execute various +Both use the CodeGen and related QCubed codegen libraries to do the bulk + of the work. They simply instantiate a CodeGen object, execute various public methods on it to do the code generation, and create a text-based report of its activities, outputting it to STDOUT. diff --git a/tools/cli_prepend.inc.php b/tools/cli_prepend.inc.php index e99f11b..aa23389 100644 --- a/tools/cli_prepend.inc.php +++ b/tools/cli_prepend.inc.php @@ -1,4 +1,8 @@ -GetTitle()); + printf("%s\r\n", $objCodeGen->GetReportLabel()); + printf("%s\r\n", $objCodeGen->GenerateAll()); + if ($strErrors = $objCodeGen->Errors) + printf("The following errors were reported:\r\n%s\r\n", $strErrors); + print("\r\n"); +} + +foreach (CodeGen::GenerateAggregate() as $strMessage) { + printf("%s\r\n\r\n", $strMessage); +} \ No newline at end of file diff --git a/tools/codegen.inc.php b/tools/codegen.inc.php deleted file mode 100644 index 10e4973..0000000 --- a/tools/codegen.inc.php +++ /dev/null @@ -1,65 +0,0 @@ -= 2) - $settingsFile = $_SERVER['argv'][1]; - - if (!is_file($settingsFile)) { - PrintInstructions(); - } - - ///////////////////// - // Run Code Gen - QCodeGen::Run($settingsFile); - ///////////////////// - - - if ($strErrors = \CodeGen::$RootErrors) { - printf("The following ROOT ERRORS were reported:\r\n%s\r\n\r\n", $strErrors); - } else { - printf("CodeGen settings (as evaluted from %s):\r\n%s\r\n\r\n", $settingsFile, QCodeGen::GetSettingsXml()); - } - - foreach (QCodeGen::$CodeGenArray as $objCodeGen) { - printf("%s\r\n---------------------------------------------------------------------\r\n", $objCodeGen->GetTitle()); - printf("%s\r\n", $objCodeGen->GetReportLabel()); - printf("%s\r\n", $objCodeGen->GenerateAll()); - if ($strErrors = $objCodeGen->Errors) - printf("The following errors were reported:\r\n%s\r\n", $strErrors); - print("\r\n"); - } - - foreach (QCodeGen::GenerateAggregate() as $strMessage) { - printf("%s\r\n\r\n", $strMessage); - } \ No newline at end of file diff --git a/tools/codegen.phpexe b/tools/codegen.phpexe index 855e4be..374be0e 100755 --- a/tools/codegen.phpexe +++ b/tools/codegen.phpexe @@ -13,5 +13,8 @@ // Running as a Windows Command Name $strCommandName = 'c:\\php\\php.exe codegen.phpexe'; +// Call the CLI prepend.inc.php +require('cli_prepend.inc.php'); + // Include the rest of the OS-agnostic script - require('codegen.inc.php'); \ No newline at end of file + require('codegen.cli.php'); \ No newline at end of file