? Doxyfile ? Drupal6pdo.kdevelop ? Drupal6pdo.kdevses ? Drupal7PDO.kpf ? Makefile ? database.sqlite-schema.inc ? database.sqlite.inc ? pdo-3.patch ? pdo-4.patch ? pdo.patch ? test.php ? tests.txt ? includes/database-old.pgsql.inc ? sites/all/modules ? sites/default/files ? sites/default/settings.php Index: install.php =================================================================== RCS file: /cvs/drupal/drupal/install.php,v retrieving revision 1.116 diff -u -F^f -r1.116 install.php --- install.php 10 Feb 2008 19:03:47 -0000 1.116 +++ install.php 10 Mar 2008 22:38:36 -0000 @@ -55,7 +55,7 @@ function install_main() { // Establish a connection to the database. require_once './includes/database.inc'; - db_set_active(); + //db_set_active(); // Check if Drupal is installed. $task = install_verify_drupal(); @@ -136,9 +136,12 @@ function install_main() { */ function install_verify_drupal() { // Read the variable manually using the @ so we don't trigger an error if it fails. - $result = @db_query("SELECT value FROM {variable} WHERE name = '%s'", 'install_task'); - if ($result) { - return unserialize(db_result($result)); + try { + if ($result = db_query("SELECT value FROM {variable} WHERE name = '%s'", 'install_task')) { + return unserialize(db_result($result)); + } + } + catch (Exception $e) { } } @@ -146,23 +149,18 @@ function install_verify_drupal() { * Verify existing settings.php */ function install_verify_settings() { - global $db_prefix, $db_type, $db_url; + global $db_prefix, $databases; // Verify existing settings (if any). - if (!empty($db_url)) { + if (!empty($databases)) { // We need this because we want to run form_get_errors. include_once './includes/form.inc'; - $url = parse_url(is_array($db_url) ? $db_url['default'] : $db_url); - $db_user = urldecode($url['user']); - $db_pass = isset($url['pass']) ? urldecode($url['pass']) : NULL; - $db_host = urldecode($url['host']); - $db_port = isset($url['port']) ? urldecode($url['port']) : ''; - $db_path = ltrim(urldecode($url['path']), '/'); + $database = $databases['default']['default']; $settings_file = './'. conf_path(FALSE, TRUE) .'/settings.php'; $form_state = array(); - _install_settings_form_validate($db_prefix, $db_type, $db_user, $db_pass, $db_host, $db_port, $db_path, $settings_file, $form_state); + _install_settings_form_validate($database, $settings_file, $form_state); if (!form_get_errors()) { return TRUE; } @@ -174,30 +172,23 @@ function install_verify_settings() { * Configure and rewrite settings.php. */ function install_change_settings($profile = 'default', $install_locale = '') { - global $db_url, $db_type, $db_prefix; + global $databases, $db_prefix; - $url = parse_url(is_array($db_url) ? $db_url['default'] : $db_url); - $db_user = isset($url['user']) ? urldecode($url['user']) : ''; - $db_pass = isset($url['pass']) ? urldecode($url['pass']) : ''; - $db_host = isset($url['host']) ? urldecode($url['host']) : ''; - $db_port = isset($url['port']) ? urldecode($url['port']) : ''; - $db_path = ltrim(urldecode($url['path']), '/'); $conf_path = './'. conf_path(FALSE, TRUE); $settings_file = $conf_path .'/settings.php'; - + $database = $databases['default']['default']; // We always need this because we want to run form_get_errors. include_once './includes/form.inc'; install_task_list('database'); - if ($db_url == 'mysql://username:password@localhost/databasename') { - $db_user = $db_pass = $db_path = ''; - } - elseif (!empty($db_url)) { +/* + if (isset($database['username'])) { // Do not install over a configured settings.php. install_already_done_error(); } +*/ - $output = drupal_get_form('install_settings_form', $profile, $install_locale, $settings_file, $db_url, $db_type, $db_prefix, $db_user, $db_pass, $db_host, $db_port, $db_path); + $output = drupal_get_form('install_settings_form', $profile, $install_locale, $settings_file, $database); drupal_set_title(st('Database configuration')); print theme('install_page', $output); exit; @@ -207,19 +198,11 @@ function install_change_settings($profil /** * Form API array definition for install_settings. */ -function install_settings_form(&$form_state, $profile, $install_locale, $settings_file, $db_url, $db_type, $db_prefix, $db_user, $db_pass, $db_host, $db_port, $db_path) { - if (empty($db_host)) { - $db_host = 'localhost'; - } - $db_types = drupal_detect_database_types(); +function install_settings_form(&$form_state, $profile, $install_locale, $settings_file, $database) { + $drivers = drupal_detect_database_types(); - // If both 'mysql' and 'mysqli' are available, we disable 'mysql': - if (isset($db_types['mysqli'])) { - unset($db_types['mysql']); - } - - if (count($db_types) == 0) { - $form['no_db_types'] = array( + if (!$drivers) { + $form['no_drivers'] = array( '#value' => st('Your web server does not appear to support any common database types. Check with your hosting provider to see if they offer any databases that Drupal supports.', array('@drupal-databases' => 'http://drupal.org/node/270#database')), ); } @@ -230,54 +213,51 @@ function install_settings_form(&$form_st '#description' => '
'. st('To set up your @drupal database, enter the following information.', array('@drupal' => drupal_install_profile_name())) .'
', ); - if (count($db_types) > 1) { - $form['basic_options']['db_type'] = array( + if (count($drivers) == 1) { + $form['basic_options']['driver'] = array( + '#type' => 'hidden', + '#value' => current(array_keys($drivers)), + ); + $database_description = st('The name of the %driver database your @drupal data will be stored in. It must exist on your server before @drupal can be installed.', array('%driver' => current($drivers), '@drupal' => drupal_install_profile_name())); + } + else { + $form['basic_options']['driver'] = array( '#type' => 'radios', - '#title' => st('Database type'), + '#title' => st('Database driver'), '#required' => TRUE, - '#options' => $db_types, - '#default_value' => ($db_type ? $db_type : current($db_types)), + '#options' => $drivers, + '#default_value' => !empty($database['driver']) ? $database['driver'] : current($drivers), '#description' => st('The type of database your @drupal data will be stored in.', array('@drupal' => drupal_install_profile_name())), ); - $db_path_description = st('The name of the database your @drupal data will be stored in. It must exist on your server before @drupal can be installed.', array('@drupal' => drupal_install_profile_name())); - } - else { - if (count($db_types) == 1) { - $db_types = array_values($db_types); - $form['basic_options']['db_type'] = array( - '#type' => 'hidden', - '#value' => $db_types[0], - ); - $db_path_description = st('The name of the %db_type database your @drupal data will be stored in. It must exist on your server before @drupal can be installed.', array('%db_type' => $db_types[0], '@drupal' => drupal_install_profile_name())); - } + $database_description = st('The name of the database your @drupal data will be stored in. It must exist on your server before @drupal can be installed.', array('@drupal' => drupal_install_profile_name())); } // Database name - $form['basic_options']['db_path'] = array( + $form['basic_options']['database'] = array( '#type' => 'textfield', '#title' => st('Database name'), - '#default_value' => $db_path, + '#default_value' => empty($database['database']) ? '' : $database['database'], '#size' => 45, '#maxlength' => 45, '#required' => TRUE, - '#description' => $db_path_description + '#description' => $database_description, ); // Database username - $form['basic_options']['db_user'] = array( + $form['basic_options']['username'] = array( '#type' => 'textfield', '#title' => st('Database username'), - '#default_value' => $db_user, + '#default_value' => empty($database['username']) ? '' : $database['username'], '#size' => 45, '#maxlength' => 45, '#required' => TRUE, ); // Database username - $form['basic_options']['db_pass'] = array( + $form['basic_options']['password'] = array( '#type' => 'password', '#title' => st('Database password'), - '#default_value' => $db_pass, + '#default_value' => empty($database['password']) ? '' : $database['password'], '#size' => 45, '#maxlength' => 45, ); @@ -291,10 +271,10 @@ function install_settings_form(&$form_st ); // Database host - $form['advanced_options']['db_host'] = array( + $form['advanced_options']['host'] = array( '#type' => 'textfield', '#title' => st('Database host'), - '#default_value' => $db_host, + '#default_value' => empty($database['host']) ? 'localhost' : $database['host'], '#size' => 45, '#maxlength' => 45, '#required' => TRUE, @@ -302,10 +282,10 @@ function install_settings_form(&$form_st ); // Database port - $form['advanced_options']['db_port'] = array( + $form['advanced_options']['port'] = array( '#type' => 'textfield', '#title' => st('Database port'), - '#default_value' => $db_port, + '#default_value' => empty($database['port']) ? '' : $database['port'], '#size' => 45, '#maxlength' => 45, '#description' => st('If your database server is listening to a non-standard port, enter its number.'), @@ -329,7 +309,7 @@ function install_settings_form(&$form_st $form['errors'] = array(); $form['settings_file'] = array('#type' => 'value', '#value' => $settings_file); - $form['_db_url'] = array('#type' => 'value'); + $form['_database'] = array('#type' => 'value'); $form['#action'] = "install.php?profile=$profile". ($install_locale ? "&locale=$install_locale" : ''); $form['#redirect'] = FALSE; } @@ -341,48 +321,43 @@ function install_settings_form(&$form_st */ function install_settings_form_validate($form, &$form_state) { global $db_url; - _install_settings_form_validate($form_state['values']['db_prefix'], $form_state['values']['db_type'], $form_state['values']['db_user'], $form_state['values']['db_pass'], $form_state['values']['db_host'], $form_state['values']['db_port'], $form_state['values']['db_path'], $form_state['values']['settings_file'], $form_state, $form); + _install_settings_form_validate($form_state['values'], $form_state['values']['settings_file'], $form_state, $form); } /** * Helper function for install_settings_validate. */ -function _install_settings_form_validate($db_prefix, $db_type, $db_user, $db_pass, $db_host, $db_port, $db_path, $settings_file, &$form_state, $form = NULL) { - global $db_url; - +function _install_settings_form_validate($database, $settings_file, &$form_state, $form = NULL) { + global $databases; // Verify the table prefix - if (!empty($db_prefix) && is_string($db_prefix) && !preg_match('/^[A-Za-z0-9_.]+$/', $db_prefix)) { + if (!empty($database['prefix']) && is_string($database['prefix']) && !preg_match('/^[A-Za-z0-9_.]+$/', $database['dprefix'])) { form_set_error('db_prefix', st('The database table prefix you have entered, %db_prefix, is invalid. The table prefix can only contain alphanumeric characters, periods, or underscores.', array('%db_prefix' => $db_prefix)), 'error'); } - if (!empty($db_port) && !is_numeric($db_port)) { + if (!empty($database['port']) && !is_numeric($database['port'])) { form_set_error('db_port', st('Database port must be a number.')); } // Check database type - if (!isset($form)) { - $_db_url = is_array($db_url) ? $db_url['default'] : $db_url; - $db_type = substr($_db_url, 0, strpos($_db_url, '://')); - } - $databases = drupal_detect_database_types(); - if (!in_array($db_type, $databases)) { - form_set_error('db_type', st("In your %settings_file file you have configured @drupal to use a %db_type server, however your PHP installation currently does not support this database type.", array('%settings_file' => $settings_file, '@drupal' => drupal_install_profile_name(), '%db_type' => $db_type))); + $database_types = drupal_detect_database_types(); + $driver = $database['driver']; + if (!isset($database_types[$driver])) { + form_set_error('driver', st("In your %settings_file file you have configured @drupal to use a %driver server, however your PHP installation currently does not support this database type.", array('%settings_file' => $settings_file, '@drupal' => drupal_install_profile_name(), '%driver' => $database['driver']))); } else { - // Verify - $db_url = $db_type .'://'. urlencode($db_user) . ($db_pass ? ':'. urlencode($db_pass) : '') .'@'. ($db_host ? urlencode($db_host) : 'localhost') . ($db_port ? ":$db_port" : '') .'/'. urlencode($db_path); if (isset($form)) { - form_set_value($form['_db_url'], $db_url, $form_state); + form_set_value($form['_database'], $database, $form_state); } - $success = array(); - - $function = 'drupal_test_'. $db_type; - if (!$function($db_url, $success)) { - if (isset($success['CONNECT'])) { - form_set_error('db_type', st('In order for Drupal to work, and to continue with the installation process, you must resolve all permission issues reported above. We were able to verify that we have permission for the following commands: %commands. For more help with configuring your database server, see the Installation and upgrading handbook. If you are unsure what any of this means you should probably contact your hosting provider.', array('%commands' => implode($success, ', ')))); + $class = "DatabaseInstaller_$driver"; + $test = new $class; + $databases = array('default' => array('default' => $database)); + $return = $test->test(); + if (!$return || $test->error) { + if (!empty($test->success)) { + form_set_error('db_type', st('In order for Drupal to work, and to continue with the installation process, you must resolve all permission issues reported above. We were able to verify that we have permission for the following commands: %commands. For more help with configuring your database server, see the Installation and upgrading handbook. If you are unsure what any of this means you should probably contact your hosting provider.', array('%commands' => implode($test->success, ', ')))); } else { - form_set_error('db_type', ''); + form_set_error('driver', ''); } } } @@ -394,9 +369,10 @@ function _install_settings_form_validate function install_settings_form_submit($form, &$form_state) { global $profile, $install_locale; + $database = array_intersect_key($form_state['values']['_database'], array_flip(array('driver', 'database', 'username', 'password', 'host', 'port'))); // Update global settings array and save - $settings['db_url'] = array( - 'value' => $form_state['values']['_db_url'], + $settings['databases'] = array( + 'value' => array('default' => array('default' => $database)), 'required' => TRUE, ); $settings['db_prefix'] = array( @@ -722,6 +698,17 @@ function install_tasks($profile, $task) }', 'inline'); // Build menu to allow clean URL check. menu_rebuild(); + + // Cache a fully-built schema. This is necessary for any + // invocation of index.php because: (1) setting cache table + // entries requires schema information, (2) that occurs during + // bootstrap before any module are loaded, so (3) if there is no + // cached schema, drupal_get_schema() will try to generate one + // but with no loaded modules will return nothing. + // + // This logically could be done during task 'done' but the clean + // URL check requires it now. + drupal_get_schema(NULL, TRUE); } else { @@ -797,6 +784,9 @@ function install_tasks($profile, $task) _drupal_flush_css_js(); variable_set('install_profile', $profile); + + // Cache a fully-built schema. + drupal_get_schema(NULL, TRUE); } // Set task for user, and remember the task in the database. @@ -1150,3 +1140,4 @@ function install_configure_form_submit($ // Start the installer. install_main(); + Index: includes/bootstrap.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/bootstrap.inc,v retrieving revision 1.206 diff -u -F^f -r1.206 bootstrap.inc --- includes/bootstrap.inc 10 Jan 2008 22:47:17 -0000 1.206 +++ includes/bootstrap.inc 10 Mar 2008 22:38:37 -0000 @@ -274,7 +274,7 @@ function conf_init() { global $base_url, $base_path, $base_root; // Export the following settings.php variables to the global namespace - global $db_url, $db_prefix, $cookie_domain, $conf, $installed_profile, $update_free_access; + global $databases, $db_prefix, $cookie_domain, $conf, $installed_profile, $update_free_access; $conf = array(); if (file_exists('./'. conf_path() .'/settings.php')) { @@ -462,11 +462,9 @@ function variable_get($name, $default) { function variable_set($name, $value) { global $conf; - $serialized_value = serialize($value); - db_query("UPDATE {variable} SET value = '%s' WHERE name = '%s'", $serialized_value, $name); - if (!db_affected_rows()) { - @db_query("INSERT INTO {variable} (name, value) VALUES ('%s', '%s')", $name, $serialized_value); - } + $fields['name'] = $name; + $update['value'] = $fields['value'] = serialize($value); + db_insert('variable_set', 'variable')->fields($fields)->duplicateUpdate($update)->execute(); cache_clear_all('variables', 'cache'); @@ -756,24 +754,33 @@ function request_uri() { function watchdog($type, $message, $variables = array(), $severity = WATCHDOG_NOTICE, $link = NULL) { global $user, $base_root; - // Prepare the fields to be logged - $log_message = array( - 'type' => $type, - 'message' => $message, - 'variables' => $variables, - 'severity' => $severity, - 'link' => $link, - 'user' => $user, - 'request_uri' => $base_root . request_uri(), - 'referer' => referer_uri(), - 'ip' => ip_address(), - 'timestamp' => time(), - ); - - // Call the logging hooks to log/process the message - foreach (module_implements('watchdog', TRUE) as $module) { - module_invoke($module, 'watchdog', $log_message); + static $in_error_state = FALSE; + + // It is possible that the error handling will itself trigger an error. In that case, we could + // end up in an infinite loop. To avoid that, we implement a simple static semaphore. + if (!$in_error_state) { + $in_error_state = TRUE; + + // Prepare the fields to be logged + $log_message = array( + 'type' => $type, + 'message' => $message, + 'variables' => $variables, + 'severity' => $severity, + 'link' => $link, + 'user' => $user, + 'request_uri' => $base_root . request_uri(), + 'referer' => referer_uri(), + 'ip' => ip_address(), + 'timestamp' => time(), + ); + + // Call the logging hooks to log/process the message + foreach (module_implements('watchdog', TRUE) as $module) { + module_invoke($module, 'watchdog', $log_message); + } } + $in_error_state = FALSE; } /** @@ -917,9 +924,24 @@ function drupal_bootstrap($phase) { $current_phase = $phases[$phase_index]; unset($phases[$phase_index++]); _drupal_bootstrap($current_phase); + + global $_drupal_current_bootstrap_phase; + $_drupal_current_bootstrap_phase = $current_phase; } } +/** + * Return the current bootstrap phase for this Drupal process. The + * current phase is the one most recently completed by + * drupal_bootstrap(). + * + * @see drupal_bootstrap + */ +function drupal_get_bootstrap_phase() { + global $_drupal_current_bootstrap_phase; + return $_drupal_current_bootstrap_phase; +} + function _drupal_bootstrap($phase) { global $conf; @@ -947,9 +969,11 @@ function _drupal_bootstrap($phase) { break; case DRUPAL_BOOTSTRAP_DATABASE: - // Initialize the default database. + // Initialize the database system. Note that the connection + // won't be initialized until it is actually requested. require_once './includes/database.inc'; - db_set_active(); + // Load module handling. + require_once './includes/module.inc'; break; case DRUPAL_BOOTSTRAP_ACCESS: @@ -1135,3 +1159,112 @@ function ip_address() { return $ip_address; } + +/** + * @ingroup schemaapi + * @{ + */ + +/** + * Get the schema definition of a table, or the whole database schema. + * + * The returned schema will include any modifications made by any + * module that implements hook_schema_alter(). + * + * @param $table + * The name of the table. If not given, the schema of all tables is returned. + * @param $rebuild + * If true, the schema will be rebuilt instead of retrieved from the cache. + */ +function drupal_get_schema($table = NULL, $rebuild = FALSE) { + static $schema = array(); + + if (empty($schema) || $rebuild) { + // Try to load the schema from cache. + if (!$rebuild && $cached = cache_get('schema')) { + $schema = $cached->data; + } + // Otherwise, rebuild the schema cache. + else { + $schema = array(); + // Load the .install files to get hook_schema. + module_load_all_includes('install'); + + // Invoke hook_schema for all modules. + foreach (module_implements('schema') as $module) { + $current = module_invoke($module, 'schema'); + _drupal_initialize_schema($module, $current); + $schema = array_merge($schema, $current); + } + + drupal_alter('schema', $schema); + + if (drupal_get_bootstrap_phase() == DRUPAL_BOOTSTRAP_FULL) { + cache_set('schema', $schema); + } + } + } + + if (!isset($table)) { + return $schema; + } + elseif (isset($schema[$table])) { + return $schema[$table]; + } + else { + return FALSE; + } +} + +/** + * @} End of "ingroup schemaapi". + */ + +/** + * This dispatch function hands off structured Drupal arrays to type-specific + * *_alter implementations. It ensures a consistent interface for all altering + * operations. + * + * @param $type + * The data type of the structured array. 'form', 'links', + * 'node_content', and so on are several examples. + * @param $data + * The structured array to be altered. + * @param ... + * Any additional params will be passed on to the called + * hook_$type_alter functions. + */ +function drupal_alter($type, &$data) { + // PHP's func_get_args() always returns copies of params, not references, so + // drupal_alter() can only manipulate data that comes in via the required first + // param. For the edge case functions that must pass in an arbitrary number of + // alterable parameters (hook_form_alter() being the best example), an array of + // those params can be placed in the __drupal_alter_by_ref key of the $data + // array. This is somewhat ugly, but is an unavoidable consequence of a flexible + // drupal_alter() function, and the limitations of func_get_args(). + // @todo: Remove this in Drupal 7. + if (is_array($data) && isset($data['__drupal_alter_by_ref'])) { + $by_ref_parameters = $data['__drupal_alter_by_ref']; + unset($data['__drupal_alter_by_ref']); + } + + // Hang onto a reference to the data array so that it isn't blown away later. + // Also, merge in any parameters that need to be passed by reference. + $args = array(&$data); + if (isset($by_ref_parameters)) { + $args = array_merge($args, $by_ref_parameters); + } + + // Now, use func_get_args() to pull in any additional parameters passed into + // the drupal_alter() call. + $additional_args = func_get_args(); + array_shift($additional_args); + array_shift($additional_args); + $args = array_merge($args, $additional_args); + + foreach (module_implements($type .'_alter') as $module) { + $function = $module .'_'. $type .'_alter'; + call_user_func_array($function, $args); + } +} + Index: includes/cache.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/cache.inc,v retrieving revision 1.17 diff -u -F^f -r1.17 cache.inc --- includes/cache.inc 29 Jan 2008 11:36:06 -0000 1.17 +++ includes/cache.inc 10 Mar 2008 22:38:37 -0000 @@ -28,7 +28,6 @@ function cache_get($cid, $table = 'cache // If the data is permanent or we're not enforcing a minimum cache lifetime // always return the cached data. if ($cache->expire == CACHE_PERMANENT || !variable_get('cache_lifetime', 0)) { - $cache->data = db_decode_blob($cache->data); if ($cache->serialized) { $cache->data = unserialize($cache->data); } @@ -100,16 +99,24 @@ function cache_get($cid, $table = 'cache * A string containing HTTP header information for cached pages. */ function cache_set($cid, $data, $table = 'cache', $expire = CACHE_PERMANENT, $headers = NULL) { - $serialized = 0; + $fields = array( + 'serialized' => 0, + 'created' => time(), + 'expire' => $expire, + 'headers' => $headers, + ); if (is_object($data) || is_array($data)) { - $data = serialize($data); - $serialized = 1; + $fields['data'] = serialize($data); + $fields['serialized'] = 1; } - $created = time(); - db_query("UPDATE {". $table ."} SET data = %b, created = %d, expire = %d, headers = '%s', serialized = %d WHERE cid = '%s'", $data, $created, $expire, $headers, $serialized, $cid); - if (!db_affected_rows()) { - @db_query("INSERT INTO {". $table ."} (cid, data, created, expire, headers, serialized) VALUES ('%s', %b, %d, %d, '%s', %d)", $cid, $data, $created, $expire, $headers, $serialized); + else { + $fields['data'] = $data; } + + $update = $fields; + $fields['cid'] = $cid; + + db_insert('cache_set', $table)->fields($fields)->duplicateUpdate($update)->execute(); } /** @@ -169,14 +176,14 @@ function cache_clear_all($cid = NULL, $t else { if ($wildcard) { if ($cid == '*') { - db_query("DELETE FROM {". $table ."}"); + db_delete('cache_clear_all_complete', $table)->execute(); } else { - db_query("DELETE FROM {". $table ."} WHERE cid LIKE '%s%%'", $cid); + db_delete('cache_clear_all_cid_like', $table)->condition('cid', 'LIKE', $cid .'%')->execute(); } } else { - db_query("DELETE FROM {". $table ."} WHERE cid = '%s'", $cid); + db_delete('cache_clear_all_cid_equals', $table)->condition('cid', $cid)->execute(); } } } Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.759 diff -u -F^f -r1.759 common.inc --- includes/common.inc 20 Feb 2008 13:38:32 -0000 1.759 +++ includes/common.inc 10 Mar 2008 22:38:38 -0000 @@ -2435,6 +2435,8 @@ function _drupal_bootstrap_full() { fix_gpc_magic(); // Load all enabled modules module_load_all(); + // Rebuild the module hook cache + module_implements('', NULL, TRUE); // Let all modules take action before menu system handles the request // We do not want this while running update.php. if (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update') { @@ -2610,56 +2612,6 @@ function drupal_system_listing($mask, $d return $files; } - -/** - * This dispatch function hands off structured Drupal arrays to type-specific - * *_alter implementations. It ensures a consistent interface for all altering - * operations. - * - * @param $type - * The data type of the structured array. 'form', 'links', - * 'node_content', and so on are several examples. - * @param $data - * The structured array to be altered. - * @param ... - * Any additional params will be passed on to the called - * hook_$type_alter functions. - */ -function drupal_alter($type, &$data) { - // PHP's func_get_args() always returns copies of params, not references, so - // drupal_alter() can only manipulate data that comes in via the required first - // param. For the edge case functions that must pass in an arbitrary number of - // alterable parameters (hook_form_alter() being the best example), an array of - // those params can be placed in the __drupal_alter_by_ref key of the $data - // array. This is somewhat ugly, but is an unavoidable consequence of a flexible - // drupal_alter() function, and the limitations of func_get_args(). - // @todo: Remove this in Drupal 7. - if (is_array($data) && isset($data['__drupal_alter_by_ref'])) { - $by_ref_parameters = $data['__drupal_alter_by_ref']; - unset($data['__drupal_alter_by_ref']); - } - - // Hang onto a reference to the data array so that it isn't blown away later. - // Also, merge in any parameters that need to be passed by reference. - $args = array(&$data); - if (isset($by_ref_parameters)) { - $args = array_merge($args, $by_ref_parameters); - } - - // Now, use func_get_args() to pull in any additional parameters passed into - // the drupal_alter() call. - $additional_args = func_get_args(); - array_shift($additional_args); - array_shift($additional_args); - $args = array_merge($args, $additional_args); - - foreach (module_implements($type .'_alter') as $module) { - $function = $module .'_'. $type .'_alter'; - call_user_func_array($function, $args); - } -} - - /** * Renders HTML given a structured array tree. * @@ -3009,54 +2961,6 @@ function drupal_common_theme() { */ /** - * Get the schema definition of a table, or the whole database schema. - * - * The returned schema will include any modifications made by any - * module that implements hook_schema_alter(). - * - * @param $table - * The name of the table. If not given, the schema of all tables is returned. - * @param $rebuild - * If true, the schema will be rebuilt instead of retrieved from the cache. - */ -function drupal_get_schema($table = NULL, $rebuild = FALSE) { - static $schema = array(); - - if (empty($schema) || $rebuild) { - // Try to load the schema from cache. - if (!$rebuild && $cached = cache_get('schema')) { - $schema = $cached->data; - } - // Otherwise, rebuild the schema cache. - else { - $schema = array(); - // Load the .install files to get hook_schema. - module_load_all_includes('install'); - - // Invoke hook_schema for all modules. - foreach (module_implements('schema') as $module) { - $current = module_invoke($module, 'schema'); - _drupal_initialize_schema($module, $current); - $schema = array_merge($schema, $current); - } - - drupal_alter('schema', $schema); - cache_set('schema', $schema); - } - } - - if (!isset($table)) { - return $schema; - } - elseif (isset($schema[$table])) { - return $schema[$table]; - } - else { - return FALSE; - } -} - -/** * Create all tables that a module defines in its hook_schema(). * * Note: This function does not pass the module's schema through Index: includes/database.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/database.inc,v retrieving revision 1.92 diff -u -F^f -r1.92 database.inc --- includes/database.inc 8 Jan 2008 16:03:31 -0000 1.92 +++ includes/database.inc 10 Mar 2008 22:38:39 -0000 @@ -3,7 +3,7 @@ /** * @file - * Wrapper for database interface code. + * Base classes for the database layer. */ /** @@ -18,13 +18,18 @@ * @{ * Allow the use of different database servers using the same code base. * - * Drupal provides a slim database abstraction layer to provide developers with - * the ability to support multiple database servers easily. The intent of this - * layer is to preserve the syntax and power of SQL as much as possible, while - * letting Drupal control the pieces of queries that need to be written - * differently for different servers and provide basic security checks. + * Drupal provides a database abstraction layer to provide developers with + * the ability to support multiple database servers easily. The intent of + * this layer is to preserve the syntax and power of SQL as much as possible, + * but also allow developers a way to leverage more complex functionality in + * a unified way. It also provides a structured interface for dynamically + * constructing queries when appropriate, and enforcing security checks and + * similar good practices. * - * Most Drupal database queries are performed by a call to db_query() or + * The system is built atop PHP's PDO (PHP Data Objects) database API and + * inherits much of its syntax and semantics. + * + * Most Drupal database SELECT queries are performed by a call to db_query() or * db_query_range(). Module authors should also consider using pager_query() for * queries that return results that need to be presented on multiple pages, and * tablesort_sql() for generating appropriate queries for sortable tables. @@ -37,186 +42,2099 @@ * one would instead call the Drupal functions: * @code * $result = db_query_range('SELECT n.title, n.body, n.created - * FROM {node} n WHERE n.uid = %d', $uid, 0, 10); - * while ($node = db_fetch_object($result)) { + * FROM {node} n WHERE n.uid = :uid', array(':uid' => $uid), 0, 10); + * foreach($result as $record) { * // Perform operations on $node->body, etc. here. * } * @endcode * Curly braces are used around "node" to provide table prefixing via - * db_prefix_tables(). The explicit use of a user ID is pulled out into an - * argument passed to db_query() so that SQL injection attacks from user input - * can be caught and nullified. The LIMIT syntax varies between database servers, - * so that is abstracted into db_query_range() arguments. Finally, note the - * common pattern of iterating over the result set using db_fetch_object(). + * DatabaseConnection::prefixTables(). The explicit use of a user ID is pulled + * out into an argument passed to db_query() so that SQL injection attacks + * from user input can be caught and nullified. The LIMIT syntax varies between + * database servers, so that is abstracted into db_query_range() arguments. + * Finally, note the PDO-based ability to foreach() over the result set. + * + * + * INSERT, UPDATE, and DELETE queries need special care in order to behave + * consistently across all different databases. Therefore, they use a special + * object-oriented API for defining a query structurally. For example, rather than + * @code + * INSERT INTO node (nid, title, body) VALUES (1, 'my title', 'my body') + * @endcode + * one would instead write: + * @code + * $fields = array('nid' => 1, 'title' => 'my title', 'body' => 'my body'); + * db_insert('my_query', 'node')->fields($fields)->execute(); + * @endcode + * This method allows databases that need special data type handling to do so, + * while also allowing optimizations such as multi-insert queries. UPDATE and DELETE + * queries have a similar pattern. */ + /** - * Perform an SQL query and return success or failure. + * Base Database API class. * - * @param $sql - * A string containing a complete SQL query. %-substitution - * parameters are not supported. + * This class provides a Drupal-specific extension of the PDO database abstraction class in PHP. + * Every database driver implementation must provide a concrete implementation of it to support + * special handling required by that database. + * + * @link http://us.php.net/manual/en/ref.pdo.php + */ +abstract class DatabaseConnection extends PDO { + + /** + * Reference to the last statement that was executed. + * + * We only need this for the legacy db_affected_rows() call, which will be removed. + * + * @var DatabaseStatement + * @todo Remove this variable. + */ + public $lastStatement; + + function __construct($dsn, $username, $password, $driver_options = array()) { + $driver_options[PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION; // Because the other methods don't seem to work right. + parent::__construct($dsn, $username, $password, $driver_options); + $this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('DatabaseStatement', array($this))); + } + + /** + * Return the default query options for any given query. + * + * A given query can be customized with a number of option flags in an associative array. + * + * return_affected - If true, this method will return the number of rows + * affected by the previous query. If false, this function will return + * the executed statement. It should be set to TRUE for INSERT, UPDATE, + * and DELETE queries and FALSE otherwise. In most cases, this will be set + * by the database system correctly and a module author should not set it. + * + * fetch - This element controls how rows from a result set will be returned. + * legal values include PDO::FETCH_ASSOC, PDO::FETCH_BOTH, PDO::FETCH_OBJ, + * PDO::FETCH_NUM, or a string representing the name of a class. If a string + * is specified, each record will be fetched into a new object of that class. + * The behavior of all other values is defined by PDO. See + * http://www.php.net/PDOStatement-fetch + * + * target - The database "target" against which to execute a query. Valid values + * are "default" or "slave". The system will first try to open a connection to + * a database specified with the user-supplied key. If one is not available, it + * will silently fall back to the "default" target. If multiple databases connections + * are specified with the same target, one will be selected at random for the duration + * of the request. + * + * throw_exception - By default, the database system will catch any errors on a query as + * an Exception, log it, and then rethrow it so that code further up the call chain can + * take an appropriate action. To supress that behavior and simply return NULL on failure, + * set this option to FALSE. + * + * @return + * An array of default query options. + */ + protected function defaultOptions() { + return array( + 'target' => 'default', + 'fetch' => PDO::FETCH_OBJ, + 'return_affected' => FALSE, + 'throw_exception' => TRUE, + ); + } + + /** + * Returns whether or not this database connection supports transactions. + * + * @return + * TRUE if this connection supports transactions, FALSE if not. + */ + public function transactions() { + return $this->transactionSupport; + } + + /** + * Append a database prefix to all tables in a query. + * + * Queries sent to Drupal should wrap all table names in curly brackets. This + * function searches for this syntax and adds Drupal's table prefix to all + * tables, allowing Drupal to coexist with other systems in the same database if + * necessary. + * + * @param $sql + * A string containing a partial or entire SQL query. + * @return + * The properly-prefixed string. + */ + protected function prefixTables($sql) { + global $db_prefix; + + if (is_array($db_prefix)) { + if (array_key_exists('default', $db_prefix)) { + $tmp = $db_prefix; + unset($tmp['default']); + foreach ($tmp as $key => $val) { + $sql = strtr($sql, array('{'. $key .'}' => $val . $key)); + } + return strtr($sql, array('{' => $db_prefix['default'] , '}' => '')); + } + else { + foreach ($db_prefix as $key => $val) { + $sql = strtr($sql, array('{'. $key .'}' => $val . $key)); + } + return strtr($sql, array('{' => '' , '}' => '')); + } + } + else { + return strtr($sql, array('{' => $db_prefix , '}' => '')); + } + } + + /** + * Executes a query string against the database. + * + * This method provides a central handler for the actual execution + * of every query. All queries executed by Drupal are executed as + * PDO prepared statements. This method statically caches those + * prepared statements, reusing them when possible. + * + * @param $query + * The query string to execute, as a prepared statement. + * @param $args + * An array of arguments for the prepared statement. If the prepared + * statement uses ? placeholders, this array must be an indexed array. + * If it contains named placeholders, it must be an associative array. + * @param $options + * An associative array of options to control how the query is run. See + * the documentation for DatabaseConnection::defaultOptions() for details. + * @return + * If $options['affected_rows'] is TRUE, the query is assumed to be + * a modifier query (INSERT, UPDATE, DELETE) and the number of affected + * rows is returned. Otherwise, the executed prepared statement object + * is returned. If there is an error, this method will return NULL. + */ + protected function runQuery($query, Array $args, $options = array()) { + + static $statements = array(); + + try { + $query = self::prefixTables($query); + + // Cache each prepared statement, keyed by the query itself. This way, + // we get the benefit of prepared statement caching without any extra + // work by the module author. + if (!isset($statements[$query])) { + $statements[$query] = $this->prepare($query); + } + + $options += $this->defaultOptions(); + $statements[$query]->execute($args, $options); + + $this->lastStatement = $statements[$query]; + + return $options['return_affected'] ? $statements[$query]->rowCount() : $statements[$query]; + } + catch (PDOException $e) { + if (!function_exists('module_implements')) { + _db_need_install(); + } + watchdog('database', var_export($e, TRUE) . $e->getMessage(), NULL, WATCHDOG_ERROR); + if ($options['throw_exception']) { + throw new PDOException($query .':'. print_r($args,1) . $e->getMessage()); + } + return NULL; + } + } + + /** + * Execute an arbitrary query string against this database. + * + * @param $query + * A string containing an SQL query. + * @param $args + * An array of values to substitute into the query at placeholder markers. + * @param $options + * An array of options on the query. + * @return + * A database query result resource, or NULL if the query was not executed + * correctly. + */ + public function query($query, Array $args, Array $options = array()) { + return $this->runQuery($query, $args, $options); + } + + /** + * Prepare and return a SELECT query object with the specified ID. + * + * @see SelectQuery + * @param $query_id + * A string containing the unique ID of this query. It will be used for + * query_alter hooks. + * @param $options + * An array of options on the query. + * @return + * A new SelectQuery object. + */ + public function select($query_id, Array $options = array()) { + $class_type = 'SelectQuery_'. $this->driver(); + return new $class_type($query_id, $this, $options); + } + + /** + * Prepare and return an INSERT query object with the specified ID. + * + * @see InsertQuery + * @param $query_id + * A string containing the unique ID of this query. It will be used for + * query_alter hooks. + * @param $options + * An array of options on the query. + * @return + * A new InsertQuery object. + */ + public function insert($query_id, $table, Array $options = array()) { + $class_type = 'InsertQuery_'. $this->driver(); + return new $class_type($query_id, $this, $table, $options); + } + + /** + * Prepare and return an UPDATE query object with the specified ID. + * + * @see UpdateQuery + * @param $query_id + * A string containing the unique ID of this query. It will be used for + * query_alter hooks. + * @param $options + * An array of options on the query. + * @return + * A new UpdateQuery object. + */ + public function update($query_id, $table, Array $options = array()) { + $class_type = 'UpdateQuery_'. $this->driver(); + return new $class_type($query_id, $this, $table, $options); + } + + /** + * Prepare and return a DELETE query object with the specified ID. + * + * @see DeleteQuery + * @param $query_id + * A string containing the unique ID of this query. It will be used for + * query_alter hooks. + * @param $options + * An array of options on the query. + * @return + * A new DeleteQuery object. + */ + public function delete($query_id, $table, Array $options = array()) { + $class_type = 'DeleteQuery_'. $this->driver(); + return new $class_type($query_id, $this, $table, $options); + } + + /** + * Returns a DatabaseSchema object for manipulating the schema of this database. + * + * This method will lazy-load the appropriate schema library file. + * + * @return + * The DatabaseSchema object for this connection. + */ + public function schema() { + static $schema; + if (empty($schema)) { + require_once('./includes/schema.inc'); + require_once('./includes/schema.'. $this->driver() .'.inc'); + $class_type = 'DatabaseSchema_'. $this->driver(); + $schema = new $class_type($this); + } + return $schema; + } + + /** + * Returns a new DatabaseTransaction object on this connection. + * + * @see DatabaseTransaction + */ + public function startTransaction() { + $class_type = 'DatabaseTransaction_'. $this->driver(); + return new $class_type($this); + } + + /** + * Escapes a table name string. + * + * Force all table names to be strictly alphanumeric-plus-underscore. + * For some database drivers, it may also wrap the table name in + * database-specific escape characters. + * + * @return + * The sanitized table name string. + */ + public function escapeTable($table) { + return preg_replace('/[^A-Za-z0-9_]+/', '', $string); + } + + /** + * Runs a limited-range query on this database object. + * + * Use this as a substitute for ->query() when a subset of the query is to be + * returned. + * User-supplied arguments to the query should be passed in as separate parameters + * so that they can be properly escaped to avoid SQL injection attacks. + * + * @param $query + * A string containing an SQL query. + * @param $args + * An array of values to substitute into the query at placeholder markers. + * @param $from + * The first result row to return. + * @param $count + * The maximum number of result rows to return. + * @param $options + * An array of options on the query. + * @return + * A database query result resource, or NULL if the query was not executed + * correctly. + */ + abstract public function queryRange($query, Array $args, $from, $count, Array $options); + + /** + * Runs a SELECT query and stores its results in a temporary table. + * + * Use this as a substitute for ->query() when the results need to stored + * in a temporary table. Temporary tables exist for the duration of the page + * request. + * User-supplied arguments to the query should be passed in as separate parameters + * so that they can be properly escaped to avoid SQL injection attacks. + * + * Note that if you need to know how many results were returned, you should do + * a SELECT COUNT(*) on the temporary table afterwards. + * + * @param $query + * A string containing a normal SELECT SQL query. + * @param $args + * An array of values to substitute into the query at placeholder markers. + * @param $tablename + * The name of the temporary table to select into. This name will not be + * prefixed as there is no risk of collision. + * @return + * A database query result resource, or FALSE if the query was not executed + * correctly. + */ + abstract function queryTemporary($query, Array $args, $tablename); + + /** + * Returns the type of database driver. + * + * This is not necessarily the same as the type of the database itself. + * For instance, there could be two MySQL drivers, mysql and mysql_mock. + * This function would return different values for each, but both would + * return "mysql" for databaseType(). + */ + abstract public function driver(); + + /** + * Determine if this driver supports transactions. + */ + abstract public function supportsTransactions(); + + /** + * Returns the type of the database being accessed. + */ + abstract public function databaseType(); + + /** + * Declare the table and serial column affected by the previous + * INSERT query so that lastInsertId() can work on drivers that + * require this information. This is an internal function but is + * declared public because db_last_insert_id() needs to use it (PHP + * does not support "friend" functions like C++). + */ + public function setLastInsertInfo($table, $field) { + } +} + +/** + * Primary front-controller for the database system. + * + * This class is uninstantiatable and un-extendable. It acts to encapsulate + * all control and shepherding of database connections into a single location + * without the use of globals. + * + */ +abstract class Database { + + /** + * An nested array of all active connections. It is keyed by database name and target. + * + * @var array + */ + static protected $connections = array(); + + /** + * A processed copy of the database connection information from settings.php + * + * @var array + */ + static protected $databaseInfo = NULL; + + /** + * The key of the currently active database connection. + * + * @var string + */ + static protected $activeKey = 'default'; + + /** + * Gets the active connection object for the specified target. + * + * @return + * The active connection object. + */ + final public static function getActiveConnection($target = 'default') { + return self::getConnection(self::$activeKey, $target); + } + + /** + * Gets the connection object for the specified database key and target. + * + * @return + * The corresponding connection object. + */ + final public static function getConnection($key = 'default', $target = 'default') { + if (!isset(self::$connections[$key][$target])) { + self::openConnection($key, $target); + } + + return isset(self::$connections[$key][$target]) ? self::$connections[$key][$target] : NULL; + } + + /** + * Determine if there is an active connection. + * + * Note that this method will return FALSE if no connection has been established + * yet, even if one could be. + * + * @return + * TRUE if there is at least one database connection established, FALSE otherwise. + */ + final public static function isActiveConnection() { + return !empty(self::$connections); + } + + /** + * Set the active connection to the specified key. + * + * @return + * The previous database connection key. + */ + final public static function setActiveConnection($key = 'default') { + if (empty(self::$databaseInfo)) { + self::parseConnectionInfo(); + } + + if (!empty(self::$databaseInfo[$key])) { + $old_key = self::$activeKey; + self::$activeKey = $key; + return $old_key; + } + } + + /** + * Parse out the database connection information specified in the config + * file and specify defaults where necessary. + */ + final protected static function parseConnectionInfo() { + global $databases; + + if (empty($databases)) { + _db_need_install(); + } + $databaseInfo = $databases; + + // If no database key is specified, default to default. + if (!is_array($databaseInfo)) { + $databaseInfo = array('default' => $databaseInfo); + } + + foreach ($databaseInfo as $index => $info) { + // If no targets are specified, default to one default. + if (!is_array($databaseInfo[$index])) { + $databaseInfo[$index] = array('default' => $info); + } + + foreach ($databaseInfo[$index] as $target => $value) { + // If there is no "driver" property, then we assume it's an array of possible connections for + // this target. Pick one at random. That allows us to have, for example, multiple slave servers. + if (empty($value['driver'])) { + $databaseInfo[$index][$target] = $databaseInfo[$index][$target][mt_rand(0, count($databaseInfo[$index][$target]) - 1)]; + } + } + } + + self::$databaseInfo = $databaseInfo; + } + + /** + * Open a connection to the server specified by the given + * key and target. + * + * @param $key + * @param $target + */ + final protected static function openConnection($key, $target) { + if (empty(self::$connectionInfo)) { + self::parseConnectionInfo(); + } + try { + // If the requested database does not exist then it is an unrecoverable error. + // If the requested target does not exist, however, we fall back to the default + // target. The target is typically either "default" or "slave", indicating to + // use a slave SQL server if one is available. If it's not available, then the + // default/master server is the correct server to use. + if (!isset(self::$databaseInfo[$key])) { + throw new Exception('DB does not exist'); + } + if (!isset(self::$databaseInfo[$key][$target])) { + $target = 'default'; + } + + if (!$driver = self::$databaseInfo[$key][$target]['driver']) { + throw new Exception('Drupal is not set up'); + } + $driver_class = 'DatabaseConnection_'. $driver; + $driver_file ='./includes/database.'. $driver .'.inc'; + require_once($driver_file); + self::$connections[$key][$target] = new $driver_class(self::$databaseInfo[$key][$target]); + } + catch (Exception $e) { + _db_need_install(); + throw $e; + // TODO. error handling. + } + } +} + +/** + * A wrapper class for creating and managing database transactions. + * + * Not all databases or database configurations support transactions. For + * example, MySQL MyISAM tables do not. It is also easy to begin a transaction + * and then forget to commit it, which can lead to connection errors when + * another transaction is started. + * + * This class acts as a wrapper for transactions. To begin a transaction, + * simply instantiate it. When the object goes out of scope and is destroyed + * it will automatically commit. It also will check to see if the specified + * connection supports transactions. If not, it will simply skip any transaction + * commands, allowing user-space code to proceed normally. The only difference + * is that rollbacks won't actually do anything. + * + * In the vast majority of cases, you should not instantiate this class directly. + * Instead, call ->startTransaction() from the appropriate connection object. + */ +class DatabaseTransaction { + + /** + * The connection object for this transaction. + * + * @var DatabaseConnection + */ + protected $connection; + + /** + * Whether or not this connection supports transactions. + * + * This can be derived from the connection itself with a method call, + * but is cached here for performance. + * + * @var boolean + */ + protected $supportsTransactions; + + /** + * Whether or not this transaction has been rolled back. + * + * @var boolean + */ + protected $hasRolledBack = FALSE; + + /** + * Whether or not this transaction has been committed. + * + * @var boolean + */ + protected $hasCommitted = FALSE; + + public function __construct(DatabaseConnection $connection) { + $this->connection = $connection; + $this->supportsTransactions = $connection->supportsTransactions(); + + if ($this->supportsTransactions) { + $connection->beginTransaction(); + } + } + + /** + * Commit this transaction. + */ + public function commit() { + if ($this->supportsTransactions) { + $this->connection->commit(); + $this->hasCommitted = TRUE; + } + } + + /** + * Roll back this transaction. + */ + public function rollBack() { + if ($this->supportsTransactions) { + $this->connection->rollBack(); + $this->hasRolledBack = TRUE; + } + } + + /** + * Determine if this transaction has already been rolled back. + * + * @return + * TRUE if the transaction has been rolled back, FALSE otherwise. + */ + public function hasRolledBack() { + return $this->hasRolledBack; + } + + public function __destruct() { + if ($this->supportsTransactions && !$this->hasRolledBack && !$this->hasCommitted) { + $this->connection->commit(); + } + } + +} + +/** + * Prepared statement class. + * + * PDO allows us to extend the PDOStatement class to provide additional functionality beyond + * that offered by default. We do need extra functionality. By default, this class is not + * driver-specific. If a given driver needs to set a custom statement class, it may do so + * in its constructor. + * + * @link http://us.php.net/manual/en/ref.pdo.php + */ +class DatabaseStatement extends PDOStatement { + + public $dbh; + + protected function __construct($dbh) { + $this->dbh = $dbh; + $this->setFetchMode(PDO::FETCH_OBJ); + } + + /** + * Executes a prepared statement + * + * @param $args + * An array of values with as many elements as there are bound parameters in the SQL statement being executed. + * @param $options + * An array of options for this query. + * @return + * TRUE on success, or FALSE on failure. + */ + public function execute($args, $options) { + if (is_string($options['fetch'])) { + $this->setFetchMode(PDO::FETCH_CLASS, $options['fetch']); + } + else { + $this->setFetchMode($options['fetch']); + } + return parent::execute($args); + } + + /** + * Returns an entire single column of a result set as an indexed array. + * + * Note that this method will run the result set to the end. + * + * @param $index + * The index of the column number to fetch. + * @return + * An indexed array. + */ + public function fetchCol($index = 0) { + return $this->fetchAll(PDO::FETCH_COLUMN, $index); + } + + /** + * Returns an entire result set as an associative array of stdClass objects, keyed by the named field. + * + * If the given key appears multiple times, later records will overwrite earlier ones. + * + * Note that this method will run the result set to the end. + * + * @param $key + * The name of the field on which to index the array. + * @return + * An associative array. + */ + public function fetchAllAssoc($key) { + $return = array(); + $this->setFetchMode(PDO::FETCH_OBJ); + foreach ($this as $record) { + $return[$record->$key] = $record; + } + return $return; + } + + /** + * Returns the entire result set as a single associative array. + * + * This method is only useful for two-column result sets. It will return + * an associative array where the key is one column from the result set + * and the value is another field. In most cases, the default of the first two + * columns is appropriate. + * + * Note that this method will run the result set to the end. + * + * @param $key_index + * The numeric index of the field to use as the array key. + * @param $value_index + * The numeric index of the field to use as the array value. + * @return + * An associative array. + */ + public function fetchAllKeyed($key_index = 0, $value_index = 1) { + $return = array(); + $this->setFetchMode(PDO::FETCH_NUM); + foreach ($this as $record) { + $return[$record[$key_index]] = $record[$value_index]; + } + return $return; + } + + /** + * Return a single field out of the current + * + * @param $index + * The numeric index of the field to return. Defaults to the first field. + * @return + * A single field from the next record. + */ + public function fetchOne($index = 0) { + return $this->fetchColumn($index); + } + + /** + * Fetches the next row and returns it as an associative array. + * + * This method corresponds to PDOStatement::fetchObject(), + * but for associative arrays. For some reason PDOStatement does + * not have a corresponding array helper method, so one is added. + * + * @return + * An associative array. + */ + public function fetchAssoc() { + return $this->fetch(PDO::FETCH_ASSOC); + } +} + +/** + * Interface for a conditional clause in a query. + */ +interface QueryConditionInterface { + + /** + * Helper function to build most common conditional clauses. + * + * This method can take a variable number of parameters. If called with two + * parameters, they are taken as $field and $value with $operator having a value + * of =. + * + * @param $field + * The name of the field to check. + * @param $operator + * The comparison operator, such as =, <, or >=. It also accepts more complex + * options such as IN, LIKE, or BETWEEN. + * @param $value + * The value to test the field against. In most cases, this is a scalar. For more + * complex options, it is an array. The meaning of each element in the array is + * dependent on the $operator. + * @param $num_args + * For internal use only. This argument is used to track the recursive calls when + * processing complex conditions. + * @return + * The called object. + */ + public function condition($field, $operator = NULL, $value = NULL, $num_args = NULL); + + /** + * Add an arbitrary WHERE clause to the query. + * + * @param $snippet + * A portion of a WHERE clause as a prepared statement. It must use named placeholders, + * not ? placeholders. + * @param $args + * An associative array of arguments. + * @return + * The called object. + */ + public function where($snippet, $args = array()); + + /** + * Gets a complete list of all conditions in this conditional clause. + * + * If there is at least one condition, there will also be an array key #conjunction + * that represents the conjunction to use on the conditions. That is, if the + * conjunction is AND, the different clauses will be ANDed together. If there are no + * conditions, an empty string is returned. + */ + public function conditions(); + + /** + * Gets a complete list of all values to insert into the prepared statement. + * + * @returns + * An associative array of placeholders and values. + */ + public function arguments(); +} + +/** + * Base class for the query builders. + * + * All query builders inherit from a common base class. Any built query has a unique + * queryId, which is used to uniquely identify a query to hook_query_alter(). + * + */ +abstract class Query implements QueryConditionInterface { + + /** + * The ID of the query, which will be used for query_alter() hooks. + * + * @var string + */ + protected $queryId; + + /** + * The connection object on which to run this query. + * + * @var DatabaseConnection + */ + protected $connection; + + /** + * The query options to pass on to the connection object. + * + * @var array + */ + protected $queryOptions; + + /** + * The condition object for this query. Condition handling is handled via + * composition. + * + * @var DatabaseCondition + */ + protected $condition; + + public function __construct($query_id, DatabaseConnection $connection, $options) { + $this->queryId = $query_id; + $this->connection = $connection; + $this->queryOptions = $options; + + $this->condition = new DatabaseCondition('AND'); + } + + public function condition($field, $operator = NULL, $value = NULL, $num_args = NULL) { + if (!isset($num_args)) { + $num_args = func_num_args(); + } + $this->condition->condition($field, $operator, $value, $num_args); + return $this; + } + + public function conditions() { + return $this->condition->conditions(); + } + + public function arguments() { + return $this->condition->arguments(); + } + + public function where($snippet, $args = array()) { + $this->condition->where($snippet, $args); + return $this; + } + + /** + * Parse an array of conditionals into a WHERE clause. + */ + protected function parseWhere($array) { + foreach ($array as $key => $value) { + if (is_array($value)) { + $array[$key] = $this->parseWhere($value); + } + } + $conjunction = ' '. $array['#conjunction'] .' '; + unset($array['#conjunction']); + return '('. implode($conjunction, $array) .')'; + } + + /** + * Run the query against the database. + */ + abstract protected function execute(); + + /** + * Returns the query as a prepared statement string. + */ + abstract protected function __toString(); +} + +/** + * General class for an abstracted INSERT operation. + */ +abstract class InsertQuery extends Query { + + /** + * The table on which to insert. + * + * @var string + */ + protected $table; + + /** + * Whether or not this query is "delay-safe". Different database drivers + * may or may not implement this feature in their own ways. + * + * @var boolean + */ + protected $delay; + + /** + * An array of fields on which to insert. + * + * @var array + */ + protected $insertFields = array(); + + /** + * A nested array of values to insert. + * + * $insertValues itself is an array of arrays. Each sub-array is an array of + * field names to values to insert. Whether multiple insert sets + * will be run in a single query or multiple queries is left to individual drivers + * to implement in whatever manner is most efficient. The order of values in each + * sub-array must match the order of fields in $insertFields. + * + * @var string + */ + protected $insertValues = array(); + + /** + * An array of fields to update in case the initial insert fails. + * + * The implementation details of this operation are database-dependant. + * + * @var array + */ + protected $updateFields = array(); + + /** + * An array of values to update in case the initial insert fails. + * + * The implementation details of this operation are database-dependant. + * + * @var array + */ + protected $updateValues = array(); + + public function __construct($query_id, $connection, $table, Array $options = array()) { + $options['return_affected'] = TRUE; + $options += array('delay' => FALSE); + parent::__construct($query_id, $connection, $options); + $this->table = $table; + } + + /** + * Add a set of field->value pairs to be inserted. + * + * This method may be called multiple times. If it is, multiple sets of values + * will be inserted. The order of fields must be the same each time. + * + * @param $fields + * An associative array of fields to insert into the database. The array keys + * are the field names while the values are the values to insert. + * @return + * The called object. + */ + public function fields(Array $fields, Array $values = array()) { + if (empty($this->insertFields)) { + if (!$values) { + $values = array_values($fields); + $fields = array_keys($fields); + } + $this->insertFields = $fields; + $this->insertValues[] = $values; + } + + return $this; + } + + /** + * Add another set of values to the query to be inserted. + * + * If $values is a numeric array, it will be assumed to be in the same + * order as the original fields() call. If it is associative, it may be + * in any order as long as the keys of the array match the names of the + * fields. + * + * @param $values + * An array of values to add to the query. + * + */ + public function values(Array $values) { + if (is_numeric(key($values))) { + $this->insertValues[] = $values; + } + else { + foreach ($this->insertFields as $key) { + $insert_values[$key] = $values[$key]; + } + $this->insertValues[] = $insert_values; + } + return $this; + } + + /** + * Add a set of field->value pairs to be updated in case the key of the table + * already exists. + * + * The implementation of this functionality is driver-specific. On MySQL, it + * corresponds directly to "INSERT ... ON DUPLICATE KEY UPDATE..." The implementation + * for other databases is left up to each driver to determine. + * + * @param $fields + * An associative array of fields to update in the database. The array keys + * are the field names while the values are the values to which to set those fields. + * @return + * The called object. + */ + public function duplicateUpdate(Array $fields) { + $max_placeholder = 0; + $this->updateFields = array(); + $this->updateValues = array(); + foreach ($fields as $field => $value) { + // If $field is numeric, the $value is the entire clause. + if (is_numeric($field)) { + $this->updateFields[] = $value; + } + else { + $placeholder = ':db_update_placeholder_'. ($max_placeholder++); + $this->updateFields[] = $field .'='. $placeholder; + $this->updateValues[$placeholder] = $value; + } + } + return $this; + } + + public function execute() { + drupal_alter('query', $this->queryId, $this); + + // Each insert happens in its own query in the degenerate case. However, + // we wrap it in a transaction so that it is atomic where possible. On many + // databases, such as SQLite, this is also a notable performance boost. + $transaction = $this->connection->startTransaction(); + foreach ($this->insertValues as $insert_values) { + + // There is no good DB-agnostic equivalent of MySQL's ON DUPLICATE KEY UPDATE + // functionality. Implementing drivers will need to provide their own + // database-specific implementation in their child class. + + $num_affected = $this->connection->runQuery((string)$this, $insert_values, $this->queryOptions); + } + $transaction->commit(); + + return $this->connection->lastInsertId(); + } + + public function __toString() { + $placeholders = array_fill(0, count($this->insertFields), '?'); + + return 'INSERT INTO {'. $this->table .'} ('. implode(', ', $this->insertFields) .') VALUES ('. implode(', ', $placeholders) .')'; + } +} + +/** + * General class for an abstracted DELETE operation. + * + * The conditional WHERE handling of this class is all inherited from Query. + */ +abstract class DeleteQuery extends Query { + + /** + * The table from which to delete. + * + * @var string + */ + protected $table; + + public function __construct($query_id, DatabaseConnection $connection, $table, Array $options = array()) { + $options['return_affected'] = TRUE; + $options += array('delay' => FALSE); + parent::__construct($query_id, $connection, $options); + $this->table = $table; + } + + public function execute() { + drupal_alter('query', $this->queryId, $this); + $values = array(); + if ($this->condition->conditions()) { + $values = $this->condition->arguments(); + } + + return $this->connection->runQuery((string)$this, $values, $this->queryOptions); + } + + public function __toString() { + $query = 'DELETE FROM {'. $this->connection->escapeTable($this->table) .'} '; + + $conditions = $this->condition->conditions(); + if ($conditions) { + $query .= "\nWHERE ". $this->parseWhere($conditions); + } + + return $query; + } +} + +/** + * General class for an abstracted UPDATE operation. + * + * The conditional WHERE handling of this class is all inherited from Query. + */ +abstract class UpdateQuery extends Query { + + /** + * The table to update. + * + * @var string + */ + protected $table; + + /** + * An array of fields that will be updated. + * + * @var array + */ + protected $fields; + + /** + * An array of values to update to. + * + * @var array + */ + protected $arguments; + + public function __construct($query_id, DatabaseConnection $connection, $table, Array $options = array()) { + $options['return_affected'] = TRUE; + $options += array('delay' => FALSE); + parent::__construct($query_id, $connection, $options); + $this->table = $table; + } + + /** + * Add a set of field->value pairs to be updated. + * + * @param $fields + * An associative array of fields to write into the database. The array keys + * are the field names while the values are the values to which to set them. + * @return + * The called object. + */ + public function fields(Array $fields) { + // Parse out the fields into placeholders and values. We need to do this + // on assignment to avoid having to double-iterate, once to get + // the query string and once to get the values array. + $max_placeholder = 0; + $this->fields = array(); + foreach ($fields as $field => $value) { + $placeholder = ':db_update_placeholder_'. ($max_placeholder++); + $this->fields[] = $field .'='. $placeholder; + $this->arguments[$placeholder] = $value; + } + + return $this; + } + + public function execute() { + drupal_alter('query', $this->queryId, $this); + $update_values = $this->arguments; + + $conditions = $this->condition->conditions(); + if ($conditions) { + $update_values = array_merge($update_values, $this->condition->arguments()); + } + return $this->connection->runQuery((string)$this, $update_values, $this->queryOptions); + } + + public function __toString() { + $query = 'UPDATE {'. $this->connection->escapeTable($this->table) .'} SET '. implode(', ', $this->fields); + + $conditions = $this->condition->conditions(); + if ($conditions) { + $query .= "\nWHERE ". $this->parseWhere($conditions); + } + + return $query; + } + +} + +/** + * Abstract query builder for SELECT statements. + */ +abstract class SelectQuery extends Query { + + /** + * The fields to SELECT. + * + * @var array + */ + protected $fields = array(); + + /** + * The tables against which to JOIN. + * + * This property is a nested array. Each entry is an array representing + * a single table against which to join. The structure of each entry is: + * + * array( + * 'type' => $join_type (one of INNER JOIN, LEFT OUTER JOIN, RIGHT OUTER JOIN), + * 'table' => $name_of_table, + * 'alias' => $alias_of_the_table, + * 'condition' => $condition_clause_on_which_to_join, + * ) + * + * @var array + */ + protected $tables = array(); + + /** + * The values to insert into the prepared statement of this query. + * + * @var array + */ + protected $arguments = array(); + + /** + * The fields by which to order this query. + * + * This is an associative array. The keys are the fields to order, and the value + * is the direction to order, either ASC or DESC. + * + * @var array + */ + protected $order = array(); + + /** + * The fields by which to group. + * + * @var array + */ + protected $group = array(); + + /** + * The conditional object for the HAVING clause. + * + * @var DatabaseCondition + */ + protected $having; + + /** + * Whether or not this query should be DISTINCT + * + * @var boolean + */ + protected $distinct = FALSE; + + /** + * The range limiters for this query. + * + * @var array + */ + protected $limits; + + public function __construct($query_id, DatabaseConnection $connection, $options = array()) { + parent::__construct($query_id, $connection, $options); + $this->having = new DatabaseCondition('AND'); + } + + public function execute() { + drupal_alter('query', $this->queryId, $this); + + if (!empty($this->limit)) { + return $this->connection->queryRange((string)$this, $this->arguments(), $this->limit['start'], $this->limit['length'], $this->queryOptions); + } + + return $this->connection->runQuery((string)$this, $this->arguments(), $this->queryOptions); + } + + /** + * Retuns a duplicate of this query object, but with fields set to "COUNT(*)". + * + * @return + * A new SelectQuery object. + */ + public function countQuery() { + $count_query = clone($this); + + $count_query->setFields('COUNT(*)'); + + return $count_query; + } + + /** + * Sets this query to be DISTINCT. + * + * @param $distinct + * TRUE to flag this query DISTINCT, FALSE to disable it. + * @return + * The called object. + */ + public function distinct($distinct = TRUE) { + $this->distinct = $distinct; + return $this; + } + + /** + * Adds more fields to be SELECTed. + * + * If there are already fields to be selcted, this method will concatenate to the list. + * + * @param $field + * This method takes a variable number of parameters. Each parameter + * is treated as a field to be placed into the SELECT clause. A compound + * field or aliased field will be accepted literally. + * @return + * The called object. + */ + public function fields() { + $args = func_get_args(); + $this->fields = array_merge($this->fields, $args); + return $this; + } + + /** + * Sets the fields to be SELECTed. + * + * If there are already fields to be selected, this method will overwrite the list. + * + * @param $fields + * This method takes a variable number of parameters. Each parameter + * is treated as a field to be placed into the SELECT clause. A compound + * field or aliased field will be accepted literally. + * @return + * The called object. + */ + public function setFields() { + $this->fields = func_get_args(); + return $this; + } + + /** + * Default Join against another table in the database. + * + * This method is a convenience method for innerJoin(). + * + * @param $table + * The table against which to join. + * @param $alias + * The alias for the table. In most cases this should be the first letter + * of the table, or the first letter of each "word" in the table. + * @param $condition + * The condition on which to join this table. If the join requires values, + * this clause should use a named placeholder and the value or values to + * insert should be passed in the 4th parameter. For the first table joined + * on a query, this value is ignored as the first table is taken as the base + * table. + * @param $values + * An array of values to replace into the $condition of this join. + * @return + * The called object. + */ + public function join($table, $alias, $condition = NULL, $values = array()) { + return $this->addJoin('INNER', $table, $alias, $condition, $values); + } + + /** + * Inner Join against another table in the database. + * + * @param $table + * The table against which to join. + * @param $alias + * The alias for the table. In most cases this should be the first letter + * of the table, or the first letter of each "word" in the table. + * @param $condition + * The condition on which to join this table. If the join requires values, + * this clause should use a named placeholder and the value or values to + * insert should be passed in the 4th parameter. For the first table joined + * on a query, this value is ignored as the first table is taken as the base + * table. + * @param $values + * An array of values to replace into the $condition of this join. + * @return + * The called object. + */ + public function innerJoin($table, $alias, $condition = NULL, $values = array()) { + return $this->addJoin('INNER', $table, $alias, $condition, $values); + } + + /** + * Left Outer Join against another table in the database. + * + * @param $table + * The table against which to join. + * @param $alias + * The alias for the table. In most cases this should be the first letter + * of the table, or the first letter of each "word" in the table. + * @param $condition + * The condition on which to join this table. If the join requires values, + * this clause should use a named placeholder and the value or values to + * insert should be passed in the 4th parameter. For the first table joined + * on a query, this value is ignored as the first table is taken as the base + * table. + * @param $values + * An array of values to replace into the $condition of this join. + * @return + * The called object. + */ + public function leftJoin($table, $alias, $condition = NULL, $values = array()) { + return $this->addJoin('LEFT OUTER', $table, $alias, $condition, $values); + } + + /** + * Right Outer Join against another table in the database. + * + * @param $table + * The table against which to join. + * @param $alias + * The alias for the table. In most cases this should be the first letter + * of the table, or the first letter of each "word" in the table. + * @param $condition + * The condition on which to join this table. If the join requires values, + * this clause should use a named placeholder and the value or values to + * insert should be passed in the 4th parameter. For the first table joined + * on a query, this value is ignored as the first table is taken as the base + * table. + * @param $values + * An array of values to replace into the $condition of this join. + * @return + * The called object. + */ + public function rightJoin($table, $alias, $condition = NULL, $values = array()) { + return $this->addJoin('RIGHT OUTER', $table, $alias, $condition, $values); + } + + /** + * Join against another table in the database. + * + * This method does the "hard" work of queuing up a table to be joined against. + * In some cases, that may include dipping into the Schema API to find the necessary + * fields on which to join. + * + * @param $table + * The table against which to join. + * @param $alias + * The alias for the table. In most cases this should be the first letter + * of the table, or the first letter of each "word" in the table. + * @param $condition + * The condition on which to join this table. If the join requires values, + * this clause should use a named placeholder and the value or values to + * insert should be passed in the 4th parameter. For the first table joined + * on a query, this value is ignored as the first table is taken as the base + * table. + * @param $values + * An array of values to replace into the $condition of this join. + * @return + * The called object. + */ + protected function addJoin($type, $table, $alias, $condition = NULL, $values = array()) { + if (!isset($conditions)) { + // @todo: Check the Schema API and build a direct equals on the appropriate foreign keys. + } + + $this->tables[] = array( + 'type' => "$type JOIN ", + 'table' => $table, + 'alias' => $alias, + 'condition' => $condition, + ); + + return $this; + } + + /** + * Orders the result set by a given field. + * + * If called multiple times, the query will order by each specified field in the + * order this method is called. + * + * @param $field + * The field on which to order. + * @param $direction + * The direction to sort. Legal values are "ASC" and "DESC". + * @return + * The called object. + */ + public function orderBy($field, $direction = 'ASC') { + $this->order[$field] = $direction; + return $this; + } + + /** + * Restricts a query to a given range in the result set. + * + * @param $start + * The first record from the result set to return. + * @param $limit + * The number of records to return from the result set. + * @return + * The called object. + */ + public function limit($start, $length) { + $this->limit = array('start' => $start, 'length' => $length); + return $this; + } + + /** + * Groups the result set by the specified field. + * + * @param $field + * The field on which to group. + * @return + * The called object. + */ + public function groupBy($field) { + $this->group[] = $field; + } + + /** + * Helper function to build most common HAVING conditional clauses. + * + * This method is equivalent to condition(), but operates on the HAVING clause + * rather than the WHERE clause. See the note in SelectQuery::having(). + * + * @see SelectQuery::having() + * @see DatabaseCondition::condition() + * @param $field + * The name of the field to check. + * @param $operator + * The comparison operator, such as =, <, or >=. It also accepts more complex + * options such as IN, LIKE, or BETWEEN. + * @param $value + * The value to test the field against. In most cases, this is a scalar. For more + * complex options, it is an array. The meaning of each element in the array is + * dependent on the $operator. + * @param $num_args + * For internal use only. This argument is used to track the recursive calls when + * processing complex conditions. + * @return + * The called object. + */ + public function havingCondition($field, $operator = NULL, $value = NULL, $num_args = NULL) { + if (!isset($num_args)) { + $num_args = func_num_args(); + } + $this->having->condition($field, $operator, $value, $num_args); + return $this; + } + + /** + * Adds a conditional clause to the HAVING portion of the query. + * + * HAVING works in essentially the same way as WHERE, and this method operates the same + * way as where(). HAVING applies after a GROUP BY statement has taken effect, however, + * while WHERE applies before. + * + * @see DatabaseCondition::where() + * @param $snippet + * A portion of a HAVING clause as a prepared statement. It must use named placeholders, + * not ? placeholders. + * @param $args + * An associative array of arguments. + * @return + * The called object. + */ + public function having($snippet, $args = array()) { + $this->having->where($snippet, $args); + return $this; + } + + public function __toString() { + + // SELECT + $query = 'SELECT '; + if ($this->distinct) { + $query .= 'DISTINCT '; + } + + // FIELDS + $query .= implode(', ', $this->fields); + + // FROM + $tables = $this->tables; + $query .= "\nFROM ". $this->connection->escapeTable($tables[0]['table']) .' AS '. $tables[0]['alias']; + unset($tables[0]); + foreach ($tables as $params) { + $query .= "\n". $params['type'] .' JOIN '. $this->connection->escapeTable($params['table']) .' AS '. $params['alias'] .' ON '. $params['condition']; + } + + // WHERE + $conditions = $this->condition->conditions(); + if ($conditions) { + $query .= "\nWHERE ". $this->parseWhere($conditions); + } + + // GROUP BY + if ($this->group) { + $query .= "\nGROUP BY " . implode(', ', $this->group); + } + + // HAVING + $conditions = $this->having->conditions(); + if ($conditions) { + $query .= "\nHAVING ". $this->parseWhere($conditions); + } + + // ORDER BY + if ($this->order) { + $query .= "\nORDER BY "; + foreach ($this->order as $field => $direction) { + $query .= $field .' '. $direction .' '; + } + } + + // LIMIT is database specific, so we can't do it here. + + return $query; + } +} + +/** + * Generic class for a series of conditions in a query. + */ +class DatabaseCondition implements QueryConditionInterface { + + protected $conditions = array(); + protected $arguments = array(); + + function __construct($conjunction) { + $this->conditions['#conjunction'] = $conjunction; + } + + function condition($field, $operator = NULL, $value = NULL, $num_args = NULL) { + if (!isset($num_args)) { + $num_args = func_num_args(); + } + list($condition, $value) = $this->_DatabaseCondition($field, $operator, $value, $num_args); + $this->conditions[] = $condition; + $this->arguments += $value; + return $this; + } + + public function where($snippet, $args = array()) { + $this->conditions[] = $snippet; + $this->arguments += $args; + return $this; + } + + public function conditions() { + // If there's only one entry, it's just the default #type, which means + // we don't really have any conditions to speak of. + return (count($this->conditions) > 1) ? $this->conditions : array(); + } + + public function arguments() { + return $this->arguments; + } + + protected function placeholderMultiple($count, $delimiter, $values) { + static $max_placeholder = 0; + $new_placeholder = $max_placeholder + $count; + for ($i = $max_placeholder; $i < $new_placeholder; ++$i) { + $placeholder = ':db_placeholder_multiple_'. $i; + list(, $arguments[$placeholder]) = each($values); + $placeholders[] = $placeholder; + } + $max_placeholder = $new_placeholder; + return array(implode($delimiter, $placeholders), $arguments); + } + + protected function _DatabaseCondition($field, $operator, $value, $process_type) { + static $max_placeholder = 0; + static $specials = array( + 'BETWEEN' => array('delimiter' => ' AND '), + 'IN' => array('delimiter' => ', ', 'prefix' => ' (', 'postfix' => ')'), + 'NOT IN' => array('delimiter' => ', ', 'prefix' => ' (', 'postfix' => ')'), + ); + switch ($process_type) { + case 1: + return array($field->conditions(), $field->arguments()); + case 2: + $value = $operator; + $operator = '='; + case 3: + $return = "$field $operator"; + if (isset($specials[$operator]['prefix'])) { + $return .= $specials[$operator]['prefix']; + } + if ($value instanceOf SelectQuery) { + $values = $value->arguments(); + // There is an implicit string cast on $value, since SelectQuery implements __toString(). + $placeholder = '('. $value .')'; + } + else { + $count = count($value); + if ($count <= 1) { // count(NULL) = 0 + $placeholder = ':db_placeholder_'. ($max_placeholder++); + $values = array($placeholder => $value); + } + elseif (isset($specials[$operator])) { + list($placeholder, $values) = $this->placeholderMultiple($count, $specials[$operator]['delimiter'], $value); + } + else { + // What are you doing here? + } + } + $return .= " $placeholder"; + if (isset($specials[$operator]['postfix'])) { + $return .= $specials[$operator]['postfix']; + } + return array($return, $values); + } + } +} + +/** + * Returns a new DatabaseCondition, set to "AND" all conditions together. + */ +function db_or() { + return new DatabaseCondition('OR'); +} + +/** + * Returns a new DatabaseCondition, set to "OR" all conditions together. + */ +function db_and() { + return new DatabaseCondition('AND'); +} + +/** + * Returns a new DatabaseCondition, set to "XOR" all conditions together. + */ +function db_xor() { + return new DatabaseCondition('XOR'); +} + +/** + * The following utility functions are simply convenience wrappers. + * They should never, ever have any database-specific code in them. + */ + +function _db_query_process_args($query, $args, $options) { + // Temporary backward-compatibliity hacks. Remove later. + if (!is_array($options)) { + $options = array(); + } + + $old_query = $query; + $query = str_replace(array('%d', '%f', '%b', "'%s'", '%s'), '?', $old_query); + if ($old_query !== $query) { + $args = array_values($args); // The old system allowed named arrays, but PDO doesn't if you use ?. + } + + // A large number of queries pass FALSE or empty-string for + // int/float fields because the previous version of db_query() + // casted them to int/float, resulting in 0. MySQL PDO happily + // accepts these values as zero but PostgreSQL PDO does not, and I + // do not feel like tracking down and fixing every such query at + // this time. + if (preg_match_all('/%([dsfb])/', $old_query, $m) > 0) { + foreach ($m[1] as $idx => $char) { + switch ($char) { + case 'd': + $args[$idx] = (int) $args[$idx]; + break; + case 'f': + $args[$idx] = (float) $args[$idx]; + break; + } + } + } + + if (empty($options['target'])) { + $options['target'] = 'default'; + } + + return array($query, $args, $options); +} + +/** + * Execute an arbitrary query string against the active database. + * + * Do not use this function for INSERT, UPDATE, or DELETE queries. Those should + * be handled via the appropriate query builder factory. Use this function for + * SELECT queries that do not require a query builder. + * + * @see DatabaseConnection::defaultOptions() + * @param $query + * The prepared statement query to run. Although it will accept both + * named and unnamed placeholders, named placeholders are strongly preferred + * as they are more self-documenting. + * @param $args + * An array of values to substitute into the query. If the query uses named + * placeholders, this is an associative array in any order. If the query uses + * unnamed placeholders (?), this is an indexed array and the order must match + * the order of placeholders in the query string. + * @param $options + * An array of options to control how the query operates. + * @return + * A prepared statement object, already executed. + */ +function db_query($query, $args = array(), $options = array()) { + if (!is_array($args)) { + $args = func_get_args(); + array_shift($args); + } + + list($query, $args, $options) = _db_query_process_args($query, $args, $options); + return Database::getActiveConnection($options['target'])->query($query, $args, $options); +} + +/** + * Execute an arbitrary query string against the active database, restricted to a specified range. + * + * @see DatabaseConnection::defaultOptions() + * @param $query + * The prepared statement query to run. Although it will accept both + * named and unnamed placeholders, named placeholders are strongly preferred + * as they are more self-documenting. + * @param $args + * An array of values to substitute into the query. If the query uses named + * placeholders, this is an associative array in any order. If the query uses + * unnamed placeholders (?), this is an indexed array and the order must match + * the order of placeholders in the query string. + * @param $from + * The first record from the result set to return. + * @param $limit + * The number of records to return from the result set. + * @param $options + * An array of options to control how the query operates. + * @return + * A prepared statement object, already executed. + */ +function db_query_range($query, $args, $from = 0, $count = 0, $options = array()) { + if (!is_array($args)) { + $args = func_get_args(); + array_shift($args); + $count = array_pop($args); + $from = array_pop($args); + } + + list($query, $args, $options) = _db_query_process_args($query, $args, $options); + return Database::getActiveConnection($options['target'])->queryRange($query, $args, $from, $count, $options); +} + +/** + * Execute a query string against the active database and save the result set to a temp table. + * + * @see DatabaseConnection::defaultOptions() + * @param $query + * The prepared statement query to run. Although it will accept both + * named and unnamed placeholders, named placeholders are strongly preferred + * as they are more self-documenting. + * @param $args + * An array of values to substitute into the query. If the query uses named + * placeholders, this is an associative array in any order. If the query uses + * unnamed placeholders (?), this is an indexed array and the order must match + * the order of placeholders in the query string. + * @param $from + * The first record from the result set to return. + * @param $limit + * The number of records to return from the result set. + * @param $options + * An array of options to control how the query operates. + */ +function db_query_temporary($query, $args, $tablename, $options = array()) { + if (!is_array($args)) { + $args = func_get_args(); + array_shift($args); + } + list($query, $args, $options) = _db_query_process_args($query, $args, $options); + return Database::getActiveConnection($options['target'])->queryTemporary($query, $args, $tablename, $options); +} + +/** + * Returns a new InsertQuery object for the active database. + * + * @param $query_id + * A string containing the unique ID of this query. It will be used for + * query_alter hooks. + * @param $table + * The table into which to insert. + * @param $options + * An array of options to control how the query operates. + * @return + * A new InsertQuery object for this connection. + */ +function db_insert($query_id, $table, Array $options = array()) { + if (empty($options['target']) || $options['target'] == 'slave') { + $options['target'] = 'default'; + } + return Database::getActiveConnection($options['target'])->insert($query_id, $table, $options); +} + +/** + * Returns a new UpdateQuery object for the active database. + * + * @param $query_id + * A string containing the unique ID of this query. It will be used for + * query_alter hooks. + * @param $table + * The table to update. + * @param $options + * An array of options to control how the query operates. + * @return + * A new UpdateQuery object for this connection. + */ +function db_update($query_id, $table, Array $options = array()) { + if (empty($options['target']) || $options['target'] == 'slave') { + $options['target'] = 'default'; + } + return Database::getActiveConnection($options['target'])->update($query_id, $table, $options); +} + +/** + * Returns a new DeleteQuery object for the active database. + * + * @param $query_id + * A string containing the unique ID of this query. It will be used for + * query_alter hooks. + * @param $table + * The table from which to delete. + * @param $options + * An array of options to control how the query operates. * @return - * An array containing the keys: - * success: a boolean indicating whether the query succeeded - * query: the SQL query executed, passed through check_plain() + * A new DeleteQuery object for this connection. */ -function update_sql($sql) { - $result = db_query($sql, true); - return array('success' => $result !== FALSE, 'query' => check_plain($sql)); +function db_delete($query_id, $table, Array $options = array()) { + if (empty($options['target']) || $options['target'] == 'slave') { + $options['target'] = 'default'; + } + return Database::getActiveConnection($options['target'])->delete($query_id, $table, $options); } /** - * Append a database prefix to all tables in a query. + * Returns a new SelectQuery object for the active database. * - * Queries sent to Drupal should wrap all table names in curly brackets. This - * function searches for this syntax and adds Drupal's table prefix to all - * tables, allowing Drupal to coexist with other systems in the same database if - * necessary. - * - * @param $sql - * A string containing a partial or entire SQL query. + * @param $query_id + * A string containing the unique ID of this query. It will be used for + * query_alter hooks. + * @param $options + * An array of options to control how the query operates. * @return - * The properly-prefixed string. + * A new SelectQuery object for this connection. */ -function db_prefix_tables($sql) { - global $db_prefix; - - if (is_array($db_prefix)) { - if (array_key_exists('default', $db_prefix)) { - $tmp = $db_prefix; - unset($tmp['default']); - foreach ($tmp as $key => $val) { - $sql = strtr($sql, array('{'. $key .'}' => $val . $key)); - } - return strtr($sql, array('{' => $db_prefix['default'], '}' => '')); - } - else { - foreach ($db_prefix as $key => $val) { - $sql = strtr($sql, array('{'. $key .'}' => $val . $key)); - } - return strtr($sql, array('{' => '', '}' => '')); - } - } - else { - return strtr($sql, array('{' => $db_prefix, '}' => '')); +function db_select($query_id, Array $options = array()) { + if (empty($options['target'])) { + $options['target'] = 'default'; } + return Database::getActiveConnection($options['target'])->select($query_id, $options); } /** - * Activate a database for future queries. - * - * If it is necessary to use external databases in a project, this function can - * be used to change where database queries are sent. If the database has not - * yet been used, it is initialized using the URL specified for that name in - * Drupal's configuration file. If this name is not defined, a duplicate of the - * default connection is made instead. + * Sets a new active database. * - * Be sure to change the connection back to the default when done with custom - * code. + * @param $key + * The key in the $databases array to set as the default database. + * @returns + * The key of the formerly active database. + */ +function db_set_active($key = 'default') { + return Database::setActiveConnection($key); +} + +/** + * Determine if there is an active connection. * - * @param $name - * The name assigned to the newly active database connection. If omitted, the - * default connection will be made active. + * Note that this method will return FALSE if no connection has been established + * yet, even if one could be. * - * @return the name of the previously active database or FALSE if non was found. + * @return + * TRUE if there is at least one database connection established, FALSE otherwise. */ -function db_set_active($name = 'default') { - global $db_url, $db_type, $active_db; - static $db_conns, $active_name = FALSE; - - if (empty($db_url)) { - include_once 'includes/install.inc'; - install_goto('install.php'); - } - - if (!isset($db_conns[$name])) { - // Initiate a new connection, using the named DB URL specified. - if (is_array($db_url)) { - $connect_url = array_key_exists($name, $db_url) ? $db_url[$name] : $db_url['default']; - } - else { - $connect_url = $db_url; - } - - $db_type = substr($connect_url, 0, strpos($connect_url, '://')); - $handler = "./includes/database.$db_type.inc"; - - if (is_file($handler)) { - include_once $handler; - } - else { - _db_error_page("The database type '". $db_type ."' is unsupported. Please use either 'mysql' or 'mysqli' for MySQL, or 'pgsql' for PostgreSQL databases."); - } - - $db_conns[$name] = db_connect($connect_url); - } - - $previous_name = $active_name; - // Set the active connection. - $active_name = $name; - $active_db = $db_conns[$name]; - - return $previous_name; +function db_is_active() { + return Database::isActiveConnection(); } /** - * Helper function to show fatal database errors. + * Restrict a dynamic table, column or constraint name to safe characters. * - * Prints a themed maintenance page with the 'Site off-line' text, - * adding the provided error message in the case of 'display_errors' - * set to on. Ends the page request; no return. + * Only keeps alphanumeric and underscores. * - * @param $error - * The error message to be appended if 'display_errors' is on. + * @param $string + * The table name to escape. + * @return + * The escaped table name as a string. */ -function _db_error_page($error = '') { - global $db_type; - drupal_maintenance_theme(); - drupal_set_header('HTTP/1.1 503 Service Unavailable'); - drupal_set_title('Site off-line'); - - $message = 'The site is currently not available due to technical problems. Please try again later. Thank you for your understanding.
'; - $message .= 'If you are the maintainer of this site, please check your database settings in the settings.php
file and ensure that your hosting provider\'s database server is running. For more help, see the handbook, or contact your hosting provider.
The '. theme('placeholder', $db_type) .' error was: '. theme('placeholder', $error) .'.
'; - } +function db_escape_table($string) { + return Database::getActiveConnection()->escapeTable($table); +} - print theme('maintenance_page', $message); - exit; +/** + * Perform an SQL query and return success or failure. + * + * @param $sql + * A string containing a complete SQL query. %-substitution + * parameters are not supported. + * @return + * An array containing the keys: + * success: a boolean indicating whether the query succeeded + * query: the SQL query executed, passed through check_plain() + */ +function update_sql($sql) { + $result = Database::getActiveConnection()->query($sql, array(true)); + return array('success' => $result !== FALSE, 'query' => check_plain($sql)); } /** - * Returns a boolean depending on the availability of the database. + * Returns the last insert id. + * + * @todo Remove this function when all queries have been ported to db_insert(). + * @param $table + * The name of the table you inserted into. + * @param $field + * The name of the autoincrement field. */ -function db_is_active() { - global $active_db; - return !empty($active_db); +function db_last_insert_id($table, $field) { + Database::getActiveConnection()->setLastInsertInfo($table, $field); + return Database::getActiveConnection()->lastInsertId(); } /** - * Helper function for db_query(). + * Determine the number of rows changed by the preceding query. + * + * This may not work, actually, without some tricky temp code. + * + * @todo Remove this function when all queries have been ported to db_update(). */ -function _db_query_callback($match, $init = FALSE) { - static $args = NULL; - if ($init) { - $args = $match; - return; - } - - switch ($match[1]) { - case '%d': // We must use type casting to int to convert FALSE/NULL/(TRUE?) - return (int) array_shift($args); // We don't need db_escape_string as numbers are db-safe - case '%s': - return db_escape_string(array_shift($args)); - case '%%': - return '%'; - case '%f': - return (float) array_shift($args); - case '%b': // binary data - return db_encode_blob(array_shift($args)); +function db_affected_rows() { + $statement = Database::getActiveConnection()->lastStatement; + if (!$statement) { + return 0; } + return $statement->rowCount(); } /** @@ -225,6 +2143,7 @@ function _db_query_callback($match, $ini * Given a Schema API field type, return correct %-placeholders to * embed in a query * + * @todo This may be possible to remove in favor of db_select(). * @param $arguments * An array with at least one element. * @param $type @@ -236,16 +2155,12 @@ function db_placeholders($arguments, $ty } /** - * Indicates the place holders that should be replaced in _db_query_callback(). - */ -define('DB_QUERY_REGEXP', '/(%d|%s|%%|%f|%b)/'); - -/** * Helper function for db_rewrite_sql. * * Collects JOIN and WHERE statements via hook_db_rewrite_sql() * Decides whether to select primary_key or DISTINCT(primary_key) * + * @todo Remove this function when all code has been converted to query_alter. * @param $query * Query to be rewritten. * @param $primary_table @@ -292,6 +2207,7 @@ function _db_rewrite_sql($query = '', $p * Rewrites node, taxonomy and comment queries. Use it for listing queries. Do not * use FROM table1, table2 syntax, use JOIN instead. * + * @todo Remove this function when all code has been converted to query_alter. * @param $query * Query to be rewritten. * @param $primary_table @@ -360,129 +2276,43 @@ function db_rewrite_sql($query, $primary } /** - * Restrict a dynamic table, column or constraint name to safe characters. + * Wraps the given table.field entry with a DISTINCT(). The wrapper is added to + * the SELECT list entry of the given query and the resulting query is returned. + * This function only applies the wrapper if a DISTINCT doesn't already exist in + * the query. + * + * @todo Remove this. + * @param $table Table containing the field to set as DISTINCT + * @param $field Field to set as DISTINCT + * @param $query Query to apply the wrapper to + * @return SQL query with the DISTINCT wrapper surrounding the given table.field. + */ +function db_distinct_field($table, $field, $query) { + return Database::getActiveConnection()->distinctField($table, $field, $query); +} + +/** + * Retrieve the name of the currently active database driver, such as + * "mysql" or "pgsql". * - * Only keeps alphanumeric and underscores. + * @return The name of the currently active database driver. */ -function db_escape_table($string) { - return preg_replace('/[^A-Za-z0-9_]+/', '', $string); +function db_driver() { + return Database::getActiveConnection()->driver(); } /** * @} End of "defgroup database". */ + /** - * @defgroup schemaapi Schema API + * @ingroup schemaapi * @{ - * - * A Drupal schema definition is an array structure representing one or - * more tables and their related keys and indexes. A schema is defined by - * hook_schema(), which usually lives in a modulename.install file. - * - * By implementing hook_schema() and specifying the tables your module - * declares, you can easily create and drop these tables on all - * supported database engines. You don't have to deal with the - * different SQL dialects for table creation and alteration of the - * supported database engines. - * - * hook_schema() should return an array with a key for each table that - * the module defines. - * - * The following keys are defined: - * - * - 'description': A string describing this table and its purpose. - * References to other tables should be enclosed in - * curly-brackets. For example, the node_revisions table - * description field might contain "Stores per-revision title and - * body data for each {node}." - * - 'fields': An associative array ('fieldname' => specification) - * that describes the table's database columns. The specification - * is also an array. The following specification parameters are defined: - * - * - 'description': A string describing this field and its purpose. - * References to other tables should be enclosed in - * curly-brackets. For example, the node table vid field - * description might contain "Always holds the largest (most - * recent) {node_revisions}.vid value for this nid." - * - 'type': The generic datatype: 'varchar', 'int', 'serial' - * 'float', 'numeric', 'text', 'blob' or 'datetime'. Most types - * just map to the according database engine specific - * datatypes. Use 'serial' for auto incrementing fields. This - * will expand to 'int auto_increment' on mysql. - * - 'size': The data size: 'tiny', 'small', 'medium', 'normal', - * 'big'. This is a hint about the largest value the field will - * store and determines which of the database engine specific - * datatypes will be used (e.g. on MySQL, TINYINT vs. INT vs. BIGINT). - * 'normal', the default, selects the base type (e.g. on MySQL, - * INT, VARCHAR, BLOB, etc.). - * - * Not all sizes are available for all data types. See - * db_type_map() for possible combinations. - * - 'not null': If true, no NULL values will be allowed in this - * database column. Defaults to false. - * - 'default': The field's default value. The PHP type of the - * value matters: '', '0', and 0 are all different. If you - * specify '0' as the default value for a type 'int' field it - * will not work because '0' is a string containing the - * character "zero", not an integer. - * - 'length': The maximal length of a type 'varchar' or 'text' - * field. Ignored for other field types. - * - 'unsigned': A boolean indicating whether a type 'int', 'float' - * and 'numeric' only is signed or unsigned. Defaults to - * FALSE. Ignored for other field types. - * - 'precision', 'scale': For type 'numeric' fields, indicates - * the precision (total number of significant digits) and scale - * (decimal digits right of the decimal point). Both values are - * mandatory. Ignored for other field types. - * - * All parameters apart from 'type' are optional except that type - * 'numeric' columns must specify 'precision' and 'scale'. - * - * - 'primary key': An array of one or more key column specifiers (see below) - * that form the primary key. - * - 'unique key': An associative array of unique keys ('keyname' => - * specification). Each specification is an array of one or more - * key column specifiers (see below) that form a unique key on the table. - * - 'indexes': An associative array of indexes ('indexame' => - * specification). Each specification is an array of one or more - * key column specifiers (see below) that form an index on the - * table. - * - * A key column specifier is either a string naming a column or an - * array of two elements, column name and length, specifying a prefix - * of the named column. - * - * As an example, here is a SUBSET of the schema definition for - * Drupal's 'node' table. It show four fields (nid, vid, type, and - * title), the primary key on field 'nid', a unique key named 'vid' on - * field 'vid', and two indexes, one named 'nid' on field 'nid' and - * one named 'node_title_type' on the field 'title' and the first four - * bytes of the field 'type': - * - * @code - * $schema['node'] = array( - * 'fields' => array( - * 'nid' => array('type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE), - * 'vid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0), - * 'type' => array('type' => 'varchar', 'length' => 32, 'not null' => TRUE, 'default' => ''), - * 'title' => array('type' => 'varchar', 'length' => 128, 'not null' => TRUE, 'default' => ''), - * ), - * 'primary key' => array('nid'), - * 'unique keys' => array( - * 'vid' => array('vid') - * ), - * 'indexes' => array( - * 'nid' => array('nid'), - * 'node_title_type' => array('title', array('type', 4)), - * ), - * ); - * @endcode - * - * @see drupal_install_schema() */ - /** + +/** * Create a new table from a Drupal table definition. * * @param $ret @@ -493,10 +2323,7 @@ function db_escape_table($string) { * A Schema API table definition array. */ function db_create_table(&$ret, $name, $table) { - $statements = db_create_table_sql($name, $table); - foreach ($statements as $statement) { - $ret[] = update_sql($statement); - } + return Database::getActiveConnection()->schema()->createTable($ret, $name, $table); } /** @@ -511,24 +2338,31 @@ function db_create_table(&$ret, $name, $ * An array of field names. */ function db_field_names($fields) { - $ret = array(); - foreach ($fields as $field) { - if (is_array($field)) { - $ret[] = $field[0]; - } - else { - $ret[] = $field; - } - } - return $ret; + return Database::getActiveConnection()->schema()->fieldNames($fields); +} + +/** + * Check if a table exists. + */ +function db_table_exists($table) { + return Database::getActiveConnection()->schema()->tableExists($table); +} + +/** + * Check if a column exists in the given table. + */ +function db_column_exists($table, $column) { + return Database::getActiveConnection()->schema()->columnExists($table, $column); } + /** * Given a Schema API field type, return the correct %-placeholder. * * Embed the placeholder in a query to be passed to db_query and and pass as an * argument to db_query a value of the specified type. * + * @todo Remove this after all queries are converted to type-agnostic form. * @param $type * The Schema API type of a field. * @return @@ -567,6 +2401,302 @@ function db_type_placeholder($type) { return 'unsupported type '. $type .'for db_type_placeholder'; } + +function _db_create_keys_sql($spec) { + return Database::getActiveConnection()->schema()->createKeysSql($spec); +} + +/** + * This maps a generic data type in combination with its data size + * to the engine-specific data type. + */ +function db_type_map() { + return Database::getActiveConnection()->schema()->getFieldTypeMap(); +} + +/** + * Rename a table. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be renamed. + * @param $new_name + * The new name for the table. + */ +function db_rename_table(&$ret, $table, $new_name) { + return Database::getActiveConnection()->schema()->renameTable($ret, $table, $new_name); +} + +/** + * Drop a table. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be dropped. + */ +function db_drop_table(&$ret, $table) { + return Database::getActiveConnection()->schema()->dropTable($ret, $table); +} + +/** + * Add a new field to a table. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * Name of the table to be altered. + * @param $field + * Name of the field to be added. + * @param $spec + * The field specification array, as taken from a schema definition. + * The specification may also contain the key 'initial', the newly + * created field will be set to the value of the key in all rows. + * This is most useful for creating NOT NULL columns with no default + * value in existing tables. + * @param $keys_new + * Optional keys and indexes specification to be created on the + * table along with adding the field. The format is the same as a + * table specification but without the 'fields' element. If you are + * adding a type 'serial' field, you MUST specify at least one key + * or index including it in this array. @see db_change_field for more + * explanation why. + */ +function db_add_field(&$ret, $table, $field, $spec, $keys_new = array()) { + return Database::getActiveConnection()->schema()->addField($ret, $table, $field, $spec, $keys_new); +} + +/** + * Drop a field. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be altered. + * @param $field + * The field to be dropped. + */ +function db_drop_field(&$ret, $table, $field) { + return Database::getActiveConnection()->schema()->dropField($ret, $table, $field); +} + +/** + * Set the default value for a field. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be altered. + * @param $field + * The field to be altered. + * @param $default + * Default value to be set. NULL for 'default NULL'. + */ +function db_field_set_default(&$ret, $table, $field, $default) { + return Database::getActiveConnection()->schema()->dropField($ret, $table, $field, $default); +} + +/** + * Set a field to have no default value. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be altered. + * @param $field + * The field to be altered. + */ +function db_field_set_no_default(&$ret, $table, $field) { + return Database::getActiveConnection()->schema()->fieldSetNoDefault($ret, $table, $field); +} + +/** + * Add a primary key. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be altered. + * @param $fields + * Fields for the primary key. + */ +function db_add_primary_key(&$ret, $table, $fields) { + return Database::getActiveConnection()->schema()->addPrimaryKey($ret, $table, $field); +} + +/** + * Drop the primary key. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be altered. + */ +function db_drop_primary_key(&$ret, $table) { + return Database::getActiveConnection()->schema()->dropPrimaryKey($ret, $table); +} + +/** + * Add a unique key. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be altered. + * @param $name + * The name of the key. + * @param $fields + * An array of field names. + */ +function db_add_unique_key(&$ret, $table, $name, $fields) { + return Database::getActiveConnection()->schema()->addUniqueKey($ret, $table, $name, $fields); +} + +/** + * Drop a unique key. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be altered. + * @param $name + * The name of the key. + */ +function db_drop_unique_key(&$ret, $table, $name) { + return Database::getActiveConnection()->schema()->dropUniqueKey($ret, $table, $name); +} + +/** + * Add an index. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be altered. + * @param $name + * The name of the index. + * @param $fields + * An array of field names. + */ +function db_add_index(&$ret, $table, $name, $fields) { + return Database::getActiveConnection()->schema()->addIndex($ret, $table, $name, $fields); +} + +/** + * Drop an index. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be altered. + * @param $name + * The name of the index. + */ +function db_drop_index(&$ret, $table, $name) { + return Database::getActiveConnection()->schema()->addIndex($ret, $table, $name); +} + +/** + * Change a field definition. + * + * IMPORTANT NOTE: To maintain database portability, you have to explicitly + * recreate all indices and primary keys that are using the changed field. + * + * That means that you have to drop all affected keys and indexes with + * db_drop_{primary_key,unique_key,index}() before calling db_change_field(). + * To recreate the keys and indices, pass the key definitions as the + * optional $keys_new argument directly to db_change_field(). + * + * For example, suppose you have: + * @code + * $schema['foo'] = array( + * 'fields' => array( + * 'bar' => array('type' => 'int', 'not null' => TRUE) + * ), + * 'primary key' => array('bar') + * ); + * @endcode + * and you want to change foo.bar to be type serial, leaving it as the + * primary key. The correct sequence is: + * @code + * db_drop_primary_key($ret, 'foo'); + * db_change_field($ret, 'foo', 'bar', 'bar', + * array('type' => 'serial', 'not null' => TRUE), + * array('primary key' => array('bar'))); + * @endcode + * + * The reasons for this are due to the different database engines: + * + * On PostgreSQL, changing a field definition involves adding a new field + * and dropping an old one which* causes any indices, primary keys and + * sequences (from serial-type fields) that use the changed field to be dropped. + * + * On MySQL, all type 'serial' fields must be part of at least one key + * or index as soon as they are created. You cannot use + * db_add_{primary_key,unique_key,index}() for this purpose because + * the ALTER TABLE command will fail to add the column without a key + * or index specification. The solution is to use the optional + * $keys_new argument to create the key or index at the same time as + * field. + * + * You could use db_add_{primary_key,unique_key,index}() in all cases + * unless you are converting a field to be type serial. You can use + * the $keys_new argument in all cases. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * Name of the table. + * @param $field + * Name of the field to change. + * @param $field_new + * New name for the field (set to the same as $field if you don't want to change the name). + * @param $spec + * The field specification for the new field. + * @param $keys_new + * Optional keys and indexes specification to be created on the + * table along with changing the field. The format is the same as a + * table specification but without the 'fields' element. + */ + +function db_change_field(&$ret, $table, $field, $field_new, $spec, $keys_new = array()) { + return Database::getActiveConnection()->schema()->changeField($ret, $table, $field, $field_new, $spec, $keys_new); +} + +/** + * @} End of "ingroup schemaapi". + */ + +/** + * @ingroup database-legacy + * + * These functions are no longer necessary, as the DatabaseStatement object + * offers this and much more functionality. They are kept temporarily for backward + * compatibility during conversion and should be removed as soon as possible. + * + * @{ + */ + +function db_fetch_object(DatabaseStatement $statement) { + return $statement->fetch(PDO::FETCH_OBJ); +} + +function db_fetch_array(DatabaseStatement $statement) { + return $statement->fetch(PDO::FETCH_ASSOC); +} + +function db_result(DatabaseStatement $statement) { + return $statement->fetchOne(); +} + +function _db_need_install() { + if (!function_exists('install_goto')) { + include_once 'includes/install.inc'; + install_goto('install.php'); + } +} + /** - * @} End of "defgroup schemaapi". + * @} End of "ingroup database-legacy". */ Index: includes/database.mysql.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/database.mysql.inc,v retrieving revision 1.90 diff -u -F^f -r1.90 database.mysql.inc --- includes/database.mysql.inc 17 Feb 2008 19:39:11 -0000 1.90 +++ includes/database.mysql.inc 10 Mar 2008 22:38:39 -0000 @@ -11,360 +11,135 @@ * @{ */ -// Include functions shared between mysql and mysqli. -require_once './includes/database.mysql-common.inc'; +class DatabaseConnection_mysql extends DatabaseConnection { -/** - * Report database status. - */ -function db_status_report($phase) { - $t = get_t(); + public static $rand = 'RAND()'; + public static $timestamp = 'Y-m-d h:i:s'; + + protected $transactionSupport; - $version = db_version(); + public function __construct(Array $connection_options = array()) { - $form['mysql'] = array( - 'title' => $t('MySQL database'), - 'value' => ($phase == 'runtime') ? l($version, 'admin/reports/status/sql') : $version, - ); + $connection_options += array( + 'transactions' => FALSE, + 'port' => 3306, + ); + $this->transactionSupport = $connection_options['transactions']; - if (version_compare($version, DRUPAL_MINIMUM_MYSQL) < 0) { - $form['mysql']['severity'] = REQUIREMENT_ERROR; - $form['mysql']['description'] = $t('Your MySQL Server is too old. Drupal requires at least MySQL %version.', array('%version' => DRUPAL_MINIMUM_MYSQL)); + $dsn = 'mysql:host='. $connection_options['host'] .';port='. $connection_options['port'] .';dbname='. $connection_options['database']; + parent::__construct($dsn, $connection_options['username'], $connection_options['password'], array( + PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => TRUE , // So we don't have to mess around with cursors. + PDO::ATTR_EMULATE_PREPARES => TRUE,// Because MySQL's prepared statements skip the query cache, because it's dumb. + )); } - return $form; -} + public function queryRange($query, Array $args, $from, $count, Array $options) { + // Backward compatibility hack, temporary. + $query = str_replace(array('%d' , '%f' , '%b' , "'%s'"), '?', $query); -/** - * Returns the version of the database server currently in use. - * - * @return Database server version - */ -function db_version() { - list($version) = explode('-', mysql_get_server_info()); - return $version; -} + return $this->runQuery($query . ' LIMIT ' . $from . ', ' . $count, $args, $options); + } -/** - * Initialize a database connection. - */ -function db_connect($url) { - $url = parse_url($url); + public function queryTemporary($query, Array $args, $tablename) { + $query = preg_replace('/^SELECT/i', 'CREATE TEMPORARY TABLE ' . $tablename . ' Engine=HEAP SELECT', $this->prefixTables($query)); - // Check if MySQL support is present in PHP - if (!function_exists('mysql_connect')) { - _db_error_page('Unable to use the MySQL database because the MySQL extension for PHP is not installed. Check yourphp.ini
to see how you can enable it.');
- }
-
- // Decode url-encoded information in the db connection string
- $url['user'] = urldecode($url['user']);
- // Test if database url has a password.
- $url['pass'] = isset($url['pass']) ? urldecode($url['pass']) : '';
- $url['host'] = urldecode($url['host']);
- $url['path'] = urldecode($url['path']);
-
- // Allow for non-standard MySQL port.
- if (isset($url['port'])) {
- $url['host'] = $url['host'] .':'. $url['port'];
- }
-
- // - TRUE makes mysql_connect() always open a new link, even if
- // mysql_connect() was called before with the same parameters.
- // This is important if you are using two databases on the same
- // server.
- // - 2 means CLIENT_FOUND_ROWS: return the number of found
- // (matched) rows, not the number of affected rows.
- $connection = @mysql_connect($url['host'], $url['user'], $url['pass'], TRUE, 2);
- if (!$connection || !mysql_select_db(substr($url['path'], 1))) {
- // Show error screen otherwise
- _db_error_page(mysql_error());
- }
- // Require ANSI mode to improve SQL portability.
- mysql_query("SET SESSION sql_mode='ANSI'", $connection);
- // Force UTF-8.
- mysql_query('SET NAMES "utf8"', $connection);
- return $connection;
-}
+ return $this->runQuery($query, $args, $options);
+ }
-/**
- * Helper function for db_query().
- */
-function _db_query($query, $debug = 0) {
- global $active_db, $queries, $user;
+ public function driver() {
+ return 'mysql';
+ }
- if (variable_get('dev_query', 0)) {
- list($usec, $sec) = explode(' ', microtime());
- $timer = (float)$usec + (float)$sec;
- // If devel.module query logging is enabled, prepend a comment with the username and calling function
- // to the SQL string. This is useful when running mysql's SHOW PROCESSLIST to learn what exact
- // code is issueing the slow query.
- $bt = debug_backtrace();
- // t() may not be available yet so we don't wrap 'Anonymous'.
- $name = $user->uid ? $user->name : variable_get('anonymous', 'Anonymous');
- // str_replace() to prevent SQL injection via username or anonymous name.
- $name = str_replace(array('*', '/'), '', $name);
- $query = '/* '. $name .' : '. $bt[2]['function'] .' */ '. $query;
- }
-
- $result = mysql_query($query, $active_db);
-
- if (variable_get('dev_query', 0)) {
- $query = $bt[2]['function'] ."\n". $query;
- list($usec, $sec) = explode(' ', microtime());
- $stop = (float)$usec + (float)$sec;
- $diff = $stop - $timer;
- $queries[] = array($query, $diff);
- }
-
- if ($debug) {
- print 'query: '. $query .'
error:'. mysql_error($active_db) .'
php.ini
to see how you can enable it.');
+ parent::__construct($dsn, $connection_options['username'], $connection_options['password'], array(PDO::ATTR_STRINGIFY_FETCHES => TRUE));
}
- $url = parse_url($url);
- $conn_string = '';
+ public function queryRange($query, Array $args, $from, $count, Array $options) {
+ // Backward compatibility hack, temporary.
+ $query = str_replace(array('%d' , '%f' , '%b' , "'%s'"), '?', $query);
- // Decode url-encoded information in the db connection string
- if (isset($url['user'])) {
- $conn_string .= ' user='. urldecode($url['user']);
- }
- if (isset($url['pass'])) {
- $conn_string .= ' password='. urldecode($url['pass']);
- }
- if (isset($url['host'])) {
- $conn_string .= ' host='. urldecode($url['host']);
- }
- if (isset($url['path'])) {
- $conn_string .= ' dbname='. substr(urldecode($url['path']), 1);
- }
- if (isset($url['port'])) {
- $conn_string .= ' port='. urldecode($url['port']);
+ return $this->runQuery($query . ' LIMIT ' . $count . ' OFFSET ' . $from, $args, $options);
}
- // pg_last_error() does not return a useful error message for database
- // connection errors. We must turn on error tracking to get at a good error
- // message, which will be stored in $php_errormsg.
- $track_errors_previous = ini_get('track_errors');
- ini_set('track_errors', 1);
+ public function queryTemporary($query, Array $args, $tablename) {
+ $query = preg_replace('/^SELECT/i', 'CREATE TEMPORARY TABLE ' . $tablename . ' Engine=HEAP SELECT', $this->prefixTables($query));
- $connection = @pg_connect($conn_string);
- if (!$connection) {
- require_once './includes/unicode.inc';
- _db_error_page(decode_entities($php_errormsg));
+ return $this->runQuery($query, $args, $options);
}
- // Restore error tracking setting
- ini_set('track_errors', $track_errors_previous);
-
- return $connection;
-}
-
-/**
- * Runs a basic query in the active database.
- *
- * User-supplied arguments to the query should be passed in as separate
- * parameters so that they can be properly escaped to avoid SQL injection
- * attacks.
- *
- * @param $query
- * A string containing an SQL query.
- * @param ...
- * A variable number of arguments which are substituted into the query
- * using printf() syntax. Instead of a variable number of query arguments,
- * you may also pass a single array containing the query arguments.
- *
- * Valid %-modifiers are: %s, %d, %f, %b (binary data, do not enclose
- * in '') and %%.
- *
- * NOTE: using this syntax will cast NULL and FALSE values to decimal 0,
- * and TRUE values to decimal 1.
- *
- * @return
- * A database query result resource, or FALSE if the query was not
- * executed correctly.
- */
-function db_query($query) {
- $args = func_get_args();
- array_shift($args);
- $query = db_prefix_tables($query);
- if (isset($args[0]) and is_array($args[0])) { // 'All arguments in one array' syntax
- $args = $args[0];
- }
- _db_query_callback($args, TRUE);
- $query = preg_replace_callback(DB_QUERY_REGEXP, '_db_query_callback', $query);
- return _db_query($query);
-}
-
-/**
- * Helper function for db_query().
- */
-function _db_query($query, $debug = 0) {
- global $active_db, $last_result, $queries;
-
- if (variable_get('dev_query', 0)) {
- list($usec, $sec) = explode(' ', microtime());
- $timer = (float)$usec + (float)$sec;
+ public function driver() {
+ return 'pgsql';
}
- $last_result = pg_query($active_db, $query);
-
- if (variable_get('dev_query', 0)) {
- $bt = debug_backtrace();
- $query = $bt[2]['function'] ."\n". $query;
- list($usec, $sec) = explode(' ', microtime());
- $stop = (float)$usec + (float)$sec;
- $diff = $stop - $timer;
- $queries[] = array($query, $diff);
+ public function databaseType() {
+ return 'pgsql';
}
- if ($debug) {
- print 'query: '. $query .'
error:'. pg_last_error($active_db) .'