You are here

schema.module in Schema 7

Same filename and directory in other branches
  1. 8 schema.module
  2. 5 schema.module
  3. 6 schema.module

The Schema module provides functionality built on the Schema API.

File

schema.module
View source
<?php

/**
 * @file
 * The Schema module provides functionality built on the Schema API.
 */

/**
 * Implements hooK_help().
 */
function schema_help($path, $arg) {
  switch ($path) {
    case 'admin/structure/schema':
      return '<p>' . t('This page compares the live database as it currently exists against the combination of all schema information provided by all enabled modules.') . '<p>';
    case 'admin/structure/schema/describe':
      return '<p>' . t("This page describes the Drupal database schema. Click on a table name to see that table's description and fields. Table names within a table or field description are hyperlinks to that table's description.") . '</p>';
    case 'admin/structure/schema/inspect':
      $output = '<p>' . t("This page shows the live database schema as it currently exists on this system. Known tables are grouped by the module that defines them; unknown tables are all grouped together.") . '</p>';
      $output .= '<p>' . t("To implement hook_schema() for a module that has existing tables, copy the schema structure for those tables directly into the module's hook_schema() and return \$schema.") . '</p>';
      return $output;
    case 'admin/structure/schema/sql':
      return '<p>' . t('This page shows the CREATE TABLE statements that the Schema API generates for the selected database engine for each table defined by a module. It is for debugging purposes.') . '</p>';
    case 'admin/structure/schema/show':
      return '<p>' . t('This page displays the Drupal database schema data structure. It is for debugging purposes.') . '</p>';
  }
}

/**
 * Implements hook_permission().
 */
function schema_permission() {
  return array(
    'administer schema' => array(
      'title' => t('Administer schema module'),
    ),
  );
}

/**
 * Implements hook_menu().
 */
function schema_menu() {
  $items['admin/structure/schema'] = array(
    'title' => 'Schema',
    'description' => 'Manage the database schema for this system.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'schema_compare',
    ),
    'access arguments' => array(
      'administer schema',
    ),
    'file' => 'schema.pages.inc',
  );
  $items['admin/structure/schema/compare'] = array(
    'title' => 'Compare',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );
  $items['admin/structure/schema/describe'] = array(
    'title' => 'Describe',
    'type' => MENU_LOCAL_TASK,
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'schema_describe',
    ),
    'weight' => -8,
    'access arguments' => array(
      'administer schema',
    ),
    'file' => 'schema.pages.inc',
  );
  $items['admin/structure/schema/inspect'] = array(
    'title' => 'Inspect',
    'type' => MENU_LOCAL_TASK,
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'schema_inspect',
    ),
    'weight' => -6,
    'access arguments' => array(
      'administer schema',
    ),
    'file' => 'schema.pages.inc',
  );
  $items['admin/structure/schema/sql'] = array(
    'title' => 'SQL',
    'type' => MENU_LOCAL_TASK,
    'page callback' => 'schema_sql',
    'weight' => -4,
    'access arguments' => array(
      'administer schema',
    ),
    'file' => 'schema.pages.inc',
  );

  // This can't work unless we rename the functions in database.*.inc.
  // global $_schema_engines;
  // if (FALSE && isset($_schema_engines) && is_array($_schema_engines)) {
  //   foreach ($_schema_engines as $engine) {
  //     $items['admin/structure/schema/sql/' . $engine] = array(
  //       'title' => $engine,
  //       'type' => ($engine == db_driver() ? MENU_DEFAULT_LOCAL_TASK :
  //         MENU_LOCAL_TASK),
  //       'page callback' => 'schema_sql',
  //       'callback arguments' => $engine,
  //       'access arguments' => array('administer schema'),
  //       );
  //   }
  // }
  $items['admin/structure/schema/show'] = array(
    'title' => 'Show',
    'type' => MENU_LOCAL_TASK,
    'page callback' => 'schema_show',
    'weight' => -2,
    'access arguments' => array(
      'administer schema',
    ),
    'file' => 'schema.pages.inc',
  );
  $items['admin/structure/schema/settings'] = array(
    'title' => 'Settings',
    'type' => MENU_LOCAL_TASK,
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'schema_settings_form',
    ),
    'weight' => 50,
    'access arguments' => array(
      'administer schema',
    ),
    'file' => 'schema.admin.inc',
  );
  return $items;
}

/**
 * Fetch the schema engine class name for a given database connection.
 *
 * @param string $connection
 *   A database connection key, defaults to 'default'.
 *
 * @return
 *   The schema engine class name if available, otherwise FALSE.
 */
function schema_get_connection_engine_class($connection = 'default') {
  if ($info = Database::getConnectionInfo($connection)) {
    $driver = $info['default']['driver'];
    $class_name = 'SchemaDatabaseSchema_' . $driver;
    if (class_exists($class_name)) {
      return $class_name;
    }
  }
  return FALSE;
}

/**
 * Fetch a schema engine class instance for a given database connection.
 *
 * @param string $connection
 *   A database connection key, defaults to the schema_database_connection
 *   variable, which itself defaults to 'default'.
 *
 * @return object
 *   A schema engine class set to the given connection.
 */
function schema_dbobject($connection = NULL) {
  if (!isset($connection)) {
    $connection = variable_get('schema_database_connection', 'default');
  }
  if ($class = schema_get_connection_engine_class($connection)) {
    return new $class(Database::getConnection('default', $connection));
  }
}

/**
 * Get an array of connection options that are supported by schema inspection.
 */
function schema_get_connection_options() {
  $options =& drupal_static(__FUNCTION__);
  if (!isset($options)) {
    foreach ($GLOBALS['databases'] as $connection => $targets) {

      // Only support connections that can be inspected by schema module.
      if (!schema_get_connection_engine_class($connection)) {
        continue;
      }
      $options[$connection] = $connection;
    }
  }
  return $options;
}

//////////////////////////////////////////////////////////////////////

// Schema print functions

//////////////////////////////////////////////////////////////////////

/**
 * Builds a pretty ASCII-formatted version of a $schema array.
 *
 * This is nothing more than a specialized variation of var_dump and
 * similar functions and is used only as a convenience to generate the
 * PHP for existing database tables (to bootstrap support for modules
 * that previously used CREATE TABLE explicitly) and for debugging.
 */
function schema_phpprint($schema) {
  $out = '';
  foreach ($schema as $name => $table) {
    $out .= schema_phpprint_table($name, $table);
  }
  return $out;
}
function schema_phpprint_table($name, $table) {
  $cols = array();
  if (isset($table['fields'])) {
    foreach ($table['fields'] as $colname => $col) {
      $cols[] = "'{$colname}' => " . schema_phpprint_column($col, TRUE);
    }
  }
  $unique = $index = array();
  if (isset($table['unique keys'])) {
    foreach ($table['unique keys'] as $keyname => $key) {
      $unique[] = "'{$keyname}' => " . schema_phpprint_key($key);
    }
  }
  if (isset($table['indexes'])) {
    foreach ($table['indexes'] as $keyname => $key) {
      $index[] = "'{$keyname}' => " . schema_phpprint_key($key);
    }
  }
  if ($table['description']) {
    $description = $table['description'];
  }
  else {
    $description = t('TODO: please describe this table!');
  }
  $out = '';
  $out .= "\$schema['" . $name . "'] = array(\n";
  $out .= "  'description' => '{$description}',\n";
  $out .= "  'fields' => array(\n    ";
  $out .= implode(",\n    ", $cols);
  $out .= ",\n  ),\n";
  if (isset($table['primary key'])) {
    $out .= "  'primary key' => array('" . implode("', '", $table['primary key']) . "'),\n";
  }
  if (count($unique) > 0) {
    $out .= "  'unique keys' => array(\n    ";
    $out .= implode(",\n    ", $unique);
    $out .= "\n  ),\n";
  }
  if (count($index) > 0) {
    $out .= "  'indexes' => array(\n    ";
    $out .= implode(",\n    ", $index);
    $out .= ",\n  ),\n";
  }
  $out .= ");\n";
  return $out;
}
function schema_phpprint_column($col, $multiline = FALSE) {
  $attrs = array();
  if (isset($col['description']) && $col['description']) {
    $description = $col['description'];
  }
  else {
    $description = t('TODO: please describe this field!');
  }
  unset($col['description']);
  $attrs[] = "'description' => '{$description}'";
  if ($col['type'] == 'varchar' || $col['size'] == 'normal') {
    unset($col['size']);
  }
  foreach (array(
    'type',
    'unsigned',
    'size',
    'length',
    'not null',
    'default',
  ) as $attr) {
    if (isset($col[$attr])) {
      if (is_string($col[$attr])) {
        $attrs[] = "'{$attr}' => '{$col[$attr]}'";
      }
      elseif (is_bool($col[$attr])) {
        $attrs[] = "'{$attr}' => " . ($col[$attr] ? 'TRUE' : 'FALSE');
      }
      else {
        $attrs[] = "'{$attr}' => {$col[$attr]}";
      }
      unset($col[$attr]);
    }
  }
  foreach (array_keys($col) as $attr) {
    if (is_string($col[$attr])) {
      $attrs[] = "'{$attr}' => '{$col[$attr]}'";
    }
    else {
      $attrs[] = "'{$attr}' => {$col[$attr]}";
    }
  }
  if ($multiline) {
    return "array(\n      " . implode(",\n      ", $attrs) . ",\n    )";
  }
  return "array(" . implode(', ', $attrs) . ")";
}
function schema_phpprint_key($keys) {
  $ret = array();
  foreach ($keys as $key) {
    if (is_array($key)) {
      $ret[] = "array('{$key[0]}', {$key[1]})";
    }
    else {
      $ret[] = "'{$key}'";
    }
  }
  return "array(" . implode(", ", $ret) . ")";
}

//////////////////////////////////////////////////////////////////////

// Schema comparison functions

//////////////////////////////////////////////////////////////////////

/**
 * Unprefix a table name.
 *
 * This is pretty much the converse of DatabaseConnection::prefixTables().
 *
 * @param string $name
 *   The prefixed table name.
 * @param DatabaseConnection $connection
 *   An optional database connection object.
 *
 * @return string
 *   The unprefixed table name.
 */
function schema_unprefix_table($name, $connection = NULL) {
  $prefixes =& drupal_static(__FUNCTION__, array());
  if (!isset($connection)) {
    $connection = Database::getConnection();
  }
  $key = $connection
    ->getKey();
  if (!isset($prefixes[$key])) {
    $prefixes[$key] = array();
    $info = $connection
      ->getConnectionOptions();
    if (isset($info['prefix'])) {
      if (is_array($info['prefix'])) {
        $info['prefix'] = $info['prefix'] + array(
          'default' => '',
        );
      }
      else {
        $info['prefix'] = array(
          'default' => $info['prefix'],
        );
      }
      foreach ($info['prefix'] as $table => $prefix) {
        if ($table != 'default') {
          $prefixes[$key][$prefix . $table] = $table;
        }
        elseif ($prefix !== '') {
          $prefixes[$key][$prefix] = '';
        }
      }
    }
  }
  return !empty($prefixes[$key]) ? strtr($name, $prefixes[$key]) : $name;
}

/**
 * Converts a column's Schema type into an engine-specific data type.
 */
function schema_engine_type($col, $table, $field, $engine = NULL) {
  $map = schema_dbobject()
    ->getFieldTypeMap();
  $size = isset($col['size']) ? $col['size'] : 'normal';
  $type = $col['type'] . ':' . $size;
  if (isset($map[$type])) {
    return $map[$type];
  }
  else {
    trigger_error(t('@table.@field: no @engine type for schema type @type.', array(
      '@engine' => $engine,
      '@type' => $type,
      '@table' => $table,
      '@field' => $field,
    )), E_USER_WARNING);
    return $col['type'];
  }
}

/**
 * Convert an engine-specific data type into a Schema type.
 */
function schema_schema_type($type, $table, $field, $engine = NULL) {
  $map = schema_dbobject()
    ->schema_type_map();
  $type = strtolower($type);
  if (isset($map[$type])) {
    return explode(':', $map[$type]);
  }
  else {
    if (!variable_get('schema_suppress_type_warnings', FALSE)) {
      trigger_error(t('@table.@field: no schema type for @engine type @type.', array(
        '@engine' => $engine,
        '@type' => $type,
        '@table' => $table,
        '@field' => $field,
      )), E_USER_WARNING);
    }
    return array(
      $type,
      'normal',
    );
  }
}

/**
 * Compares two complete schemas.
 * @param $ref is considered the reference copy
 * @param $inspect is compared against it.  If $inspect is NULL, a
 *         schema for the active database is generated and used.
 */
function schema_compare_schemas($ref, $inspect = NULL) {
  if (!isset($inspect)) {
    $inspect = schema_dbobject()
      ->inspect();
  }
  $info = array();

  // Error checks to consider adding:
  // All type serial columns must be in an index or key.
  // All columns in a primary or unique key must be NOT NULL.
  // Error check: column type and default type must match
  foreach ($ref as $t_name => $table) {
    if (!isset($table['fields']) || !is_array($table['fields'])) {
      drupal_set_message(t('Table %table: Missing or invalid \'fields\' array.', array(
        '%table' => $t_name,
      )), 'warning');
      continue;
    }
    foreach ($table['fields'] as $c_name => $col) {
      switch ($col['type']) {
        case 'int':
        case 'float':
        case 'numeric':
          if (isset($col['default']) && (!is_numeric($col['default']) || is_string($col['default']))) {
            $info['warn'][] = t('%table.%column is type %type but its default %default is PHP type %phptype', array(
              '%table' => $t_name,
              '%column' => $c_name,
              '%type' => $col['type'],
              '%default' => $col['default'],
              '%phptype' => gettype($col['default']),
            ));
          }
          break;
        default:
          if (isset($col['default']) && !is_string($col['default'])) {
            $info['warn'][] = t('%table.%column is type %type but its default %default is PHP type %phptype', array(
              '%table' => $t_name,
              '%column' => $c_name,
              '%type' => $col['type'],
              '%default' => $col['default'],
              '%phptype' => gettype($col['default']),
            ));
          }
          break;
      }
    }
  }

  // Error check: 'text' and 'blob' columns cannot have a default value
  foreach ($ref as $t_name => $table) {
    if (!isset($table['fields'])) {
      continue;
    }
    foreach ($table['fields'] as $c_name => $col) {
      switch ($col['type']) {
        case 'text':
        case 'blob':
          if (isset($col['default'])) {
            $info['warn'][] = t('%table.%column is type %type and may not have a default value', array(
              '%table' => $t_name,
              '%column' => $c_name,
              '%type' => $col['type'],
            ));
          }
          break;
      }
    }
  }

  // Error check: primary keys must be 'not null'
  foreach ($ref as $t_name => $table) {
    if (isset($table['primary key'])) {
      $keys = db_field_names($table['primary key']);
      foreach ($keys as $key) {
        if (!isset($table['fields'][$key]['not null']) || $table['fields'][$key]['not null'] != TRUE) {
          $info['warn'][] = t('%table.%column is part of the primary key but is not specified to be \'not null\'.', array(
            '%table' => $t_name,
            '%column' => $key,
          ));
        }
      }
    }
  }
  foreach ($ref as $name => $table) {
    if (isset($table['module'])) {
      $module = $table['module'];
    }
    else {
      $module = '';
    }
    if (!isset($inspect[$name])) {
      $info['missing'][$module][$name] = array(
        'status' => 'missing',
      );
    }
    else {
      $status = schema_compare_table($table, $inspect[$name]);
      $info[$status['status']][$module][$name] = $status;
      unset($inspect[$name]);
    }
  }
  foreach ($inspect as $name => $table) {
    $info['extra'][] = $name;
  }
  return $info;
}

/**
 * Compares a reference specification (such as one returned by a
 * module's hook_schema) to an inspected specification from the
 * database.
 * @param $inspect if not provided, the database is inspected.
 */
function schema_compare_table($ref, $inspect = NULL) {
  $_db_type = db_driver();
  if (!isset($inspect)) {

    // TODO: Handle prefixing the D7 way
    //    $ref_name = db_prefix_tables('{' . $ref['name'] . '}');
    $ref_name = $ref['name'];
    $inspect = schema_dbobject()
      ->inspect(NULL, $ref_name);
    $inspect = $inspect[$ref['name']];
  }
  if (!isset($inspect)) {
    return array(
      'status' => 'missing',
    );
  }
  $reasons = $notes = array();
  $col_keys = array_flip(array(
    'type',
    'size',
    'not null',
    'length',
    'unsigned',
    'default',
    'scale',
    'precision',
  ));
  foreach ($ref['fields'] as $colname => $col) {

    // Many Schema types can map to the same engine type (e.g. in
    // PostgresSQL, text:{small,medium,big} are all just text).  When
    // we inspect the database, we see the common type, but the
    // reference we are comparing against can have a specific type.
    // We therefore run the reference's specific type through the
    // type conversion cycle to get its common type for comparison.
    //
    // Sadly, we need a special-case hack for 'serial'.
    $serial = $col['type'] == 'serial' ? TRUE : FALSE;
    $name = isset($ref['name']) ? $ref['name'] : '';
    $dbtype = schema_engine_type($col, $name, $colname);
    list($col['type'], $col['size']) = schema_schema_type($dbtype, $name, $colname);
    if ($serial) {
      $col['type'] = 'serial';
    }

    // If an engine-specific type is specified, use it.  XXX $inspect
    // will contain the schema type for the engine type, if one
    // exists, whereas dbtype_type contains the engine type.
    if (isset($col[$_db_type . '_type'])) {
      $col['type'] = $col[$_db_type . '_type'];
    }
    $col = array_intersect_key($col, $col_keys);
    if (!isset($inspect['fields'][$colname])) {
      $reasons[] = "{$colname}: not in database";
      continue;
    }

    // Account for schemas that contain unnecessary 'default' => NULL
    if (!isset($col['default']) || is_null($col['default']) && !isset($inspect['fields'][$colname]['default'])) {
      unset($col['default']);
    }
    $kdiffs = array();
    foreach ($col_keys as $key => $val) {
      if (!(isset($col[$key]) && !is_null($col[$key]) && $col[$key] !== FALSE && isset($inspect['fields'][$colname][$key]) && $inspect['fields'][$colname][$key] !== FALSE && $col[$key] == $inspect['fields'][$colname][$key] || (!isset($col[$key]) || is_null($col[$key]) || $col[$key] === FALSE) && (!isset($inspect['fields'][$colname][$key]) || $inspect['fields'][$colname][$key] === FALSE))) {

        // One way or another, difference between the two so note it to explicitly identify it later.
        $kdiffs[] = $key;
      }
    }
    if (count($kdiffs) != 0) {
      $reasons[] = "column {$colname} - difference" . (count($kdiffs) > 1 ? 's' : '') . " on: " . implode(', ', $kdiffs) . "<br/>declared: " . schema_phpprint_column($col) . '<br/>actual: ' . schema_phpprint_column($inspect['fields'][$colname]);
    }
    unset($inspect['fields'][$colname]);
  }
  foreach ($inspect['fields'] as $colname => $col) {
    $reasons[] = "{$colname}: unexpected column in database";
  }
  if (isset($ref['primary key'])) {
    if (!isset($inspect['primary key'])) {
      $reasons[] = "primary key: missing in database";
    }
    elseif ($ref['primary key'] !== $inspect['primary key']) {
      $reasons[] = "primary key:<br />declared: " . schema_phpprint_key($ref['primary key']) . '<br />actual: ' . schema_phpprint_key($inspect['primary key']);
    }
  }
  elseif (isset($inspect['primary key'])) {
    $reasons[] = "primary key: missing in schema";
  }
  foreach (array(
    'unique keys',
    'indexes',
  ) as $type) {
    if (isset($ref[$type])) {
      foreach ($ref[$type] as $keyname => $key) {
        if (!isset($inspect[$type][$keyname])) {
          $reasons[] = "{$type} {$keyname}: missing in database";
          continue;
        }

        // $key is column list
        if ($key !== $inspect[$type][$keyname]) {
          $reasons[] = "{$type} {$keyname}:<br />declared: " . schema_phpprint_key($key) . '<br />actual: ' . schema_phpprint_key($inspect[$type][$keyname]);
        }
        unset($inspect[$type][$keyname]);
      }
    }
    if (isset($inspect[$type])) {
      foreach ($inspect[$type] as $keyname => $col) {

        // this is not an error, the dba might have added it on purpose
        $notes[] = "{$type} {$keyname}: unexpected (not an error)";
      }
    }
  }
  $status = count($reasons) ? 'different' : 'same';
  return array(
    'status' => $status,
    'reasons' => $reasons,
    'notes' => $notes,
  );
}

/**
 * Implements hook_schema_field_type_map_alter() on behalf of field.module.
 */
function field_schema_field_type_map_alter(array &$map, DatabaseSchema $schema, DatabaseConnection $connection) {
  $db_type = $connection
    ->databaseType() . '_type';

  // Load all fields in order to inspect each field's schema.
  $include_additional = array(
    'include_deleted' => TRUE,
    'include_inactive' => TRUE,
  );
  $fields = field_read_fields(array(), $include_additional);
  foreach ($fields as $field) {
    $cols = $field['columns'];

    // Loop through each field column and add any missing mappings.
    foreach ($cols as $col) {
      if (!isset($col['type'])) {
        continue;
      }
      $size = isset($col['size']) ? $col['size'] : 'normal';
      $generic_type = $col['type'] . ':' . $size;
      if (!isset($map[$generic_type])) {

        // Use engine specific type if it exists.
        $map[$generic_type] = isset($col[$db_type]) ? $col[$db_type] : $col['type'];
        $map[$generic_type] = drupal_strtoupper($map[$generic_type]);
      }
    }
  }
}

/**
 * A copy of drupal_get_schema() optimized for schema module use.
 */
function schema_get_schema() {
  $schema =& drupal_static(__FUNCTION__);
  if (!isset($schema)) {
    $schema = array();
    module_load_all_includes('install');

    // Invoke hook_schema for all modules.
    foreach (module_implements('schema') as $module) {

      // Cast the result of hook_schema() to an array, as a NULL return value
      // would cause array_merge() to set the $schema variable to NULL as well.
      // That would break modules which use $schema further down the line.
      $current = (array) module_invoke($module, 'schema');

      // Set 'module' and 'name' keys for each table. Keep descriptions,
      // they are slow but very useful for this module.
      _drupal_schema_initialize($current, $module, FALSE);
      $schema = array_merge($schema, $current);
    }
    drupal_alter('schema', $schema);
  }
  return $schema;
}

Functions

Namesort descending Description
field_schema_field_type_map_alter Implements hook_schema_field_type_map_alter() on behalf of field.module.
schema_compare_schemas Compares two complete schemas.
schema_compare_table Compares a reference specification (such as one returned by a module's hook_schema) to an inspected specification from the database.
schema_dbobject Fetch a schema engine class instance for a given database connection.
schema_engine_type Converts a column's Schema type into an engine-specific data type.
schema_get_connection_engine_class Fetch the schema engine class name for a given database connection.
schema_get_connection_options Get an array of connection options that are supported by schema inspection.
schema_get_schema A copy of drupal_get_schema() optimized for schema module use.
schema_help Implements hooK_help().
schema_menu Implements hook_menu().
schema_permission Implements hook_permission().
schema_phpprint Builds a pretty ASCII-formatted version of a $schema array.
schema_phpprint_column
schema_phpprint_key
schema_phpprint_table
schema_schema_type Convert an engine-specific data type into a Schema type.
schema_unprefix_table Unprefix a table name.