<?php

/**
 * Abstract database connectivity class.
 * Sub-classes of this implement the actual database connection libraries
 *
 * @package framework
 * @subpackage model
 */
abstract class SS_Database {

	/**
	 * Database connector object
	 *
	 * @var DBConnector
	 */
	protected $connector = null;

	/**
	 * Get the current connector
	 *
	 * @return DBConnector
	 */
	public function getConnector() {
		return $this->connector;
	}

	/**
	 * Injector injection point for connector dependency
	 *
	 * @param DBConnector $connector
	 */
	public function setConnector(DBConnector $connector) {
		$this->connector = $connector;
	}

	/**
	 * Database schema manager object
	 *
	 * @var DBSchemaManager
	 */
	protected $schemaManager = null;

	/**
	 * Returns the current schema manager
	 *
	 * @return DBSchemaManager
	 */
	public function getSchemaManager() {
		return $this->schemaManager;
	}

	/**
	 * Injector injection point for schema manager
	 *
	 * @param DBSchemaManager $schemaManager
	 */
	public function setSchemaManager(DBSchemaManager $schemaManager) {
		$this->schemaManager = $schemaManager;

		if ($this->schemaManager) {
			$this->schemaManager->setDatabase($this);
		}
	}

	/**
	 * Query builder object
	 *
	 * @var DBQueryBuilder
	 */
	protected $queryBuilder = null;

	/**
	 * Returns the current query builder
	 *
	 * @return DBQueryBuilder
	 */
	public function getQueryBuilder() {
		return $this->queryBuilder;
	}

	/**
	 * Injector injection point for schema manager
	 *
	 * @param DBQueryBuilder $queryBuilder
	 */
	public function setQueryBuilder(DBQueryBuilder $queryBuilder) {
		$this->queryBuilder = $queryBuilder;
	}

	/**
	 * Execute the given SQL query.
	 *
	 * @param string $sql The SQL query to execute
	 * @param int $errorLevel The level of error reporting to enable for the query
	 * @return SS_Query
	 */
	public function query($sql, $errorLevel = E_USER_ERROR) {
		// Check if we should only preview this query
		if ($this->previewWrite($sql)) {
			return;
		}

		// Benchmark query
		$connector = $this->connector;
		return $this->benchmarkQuery(
			$sql,
			function($sql) use($connector, $errorLevel) {
				return $connector->query($sql, $errorLevel);
			}
		);
	}


	/**
	 * Execute the given SQL parameterised query with the specified arguments
	 *
	 * @param string $sql The SQL query to execute. The ? character will denote parameters.
	 * @param array $parameters An ordered list of arguments.
	 * @param int $errorLevel The level of error reporting to enable for the query
	 * @return SS_Query
	 */
	public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR) {
		// Check if we should only preview this query
		if ($this->previewWrite($sql)) {
			return;
		}

		// Benchmark query
		$connector = $this->connector;
		return $this->benchmarkQuery(
			$sql,
			function($sql) use($connector, $parameters, $errorLevel) {
				return $connector->preparedQuery($sql, $parameters, $errorLevel);
			}
		);
	}

	/**
	 * Determines if the query should be previewed, and thus interrupted silently.
	 * If so, this function also displays the query via the debuging system.
	 * Subclasess should respect the results of this call for each query, and not
	 * execute any queries that generate a true response.
	 *
	 * @param string $sql The query to be executed
	 * @return boolean Flag indicating that the query was previewed
	 */
	protected function previewWrite($sql) {
		// Only preview if previewWrite is set, we are in dev mode, and
		// the query is mutable
		if (isset($_REQUEST['previewwrite'])
			&& Director::isDev()
			&& $this->connector->isQueryMutable($sql)
		) {
			// output preview message
			Debug::message("Will execute: $sql");
			return true;
		} else {
			return false;
		}
	}

	/**
	 * Allows the display and benchmarking of queries as they are being run
	 *
	 * @param string $sql Query to run, and single parameter to callback
	 * @param callable $callback Callback to execute code
	 * @return mixed Result of query
	 */
	protected function benchmarkQuery($sql, $callback) {
		if (isset($_REQUEST['showqueries']) && Director::isDev()) {
			$starttime = microtime(true);
			$result = $callback($sql);
			$endtime = round(microtime(true) - $starttime, 4);
			Debug::message("\n$sql\n{$endtime}s\n", false);
			return $result;
		} else {
			return $callback($sql);
		}
	}

	/**
	 * Get the autogenerated ID from the previous INSERT query.
	 *
	 * @param string $table The name of the table to get the generated ID for
	 * @return integer the most recently generated ID for the specified table
	 */
	public function getGeneratedID($table) {
		return $this->connector->getGeneratedID($table);
	}

	/**
	 * Determines if we are connected to a server AND have a valid database
	 * selected.
	 *
	 * @return boolean Flag indicating that a valid database is connected
	 */
	public function isActive() {
		return $this->connector->isActive();
	}

	/**
	 * Returns an escaped string. This string won't be quoted, so would be suitable
	 * for appending to other quoted strings.
	 *
	 * @param mixed $value Value to be prepared for database query
	 * @return string Prepared string
	 */
	public function escapeString($value) {
		return $this->connector->escapeString($value);
	}

	/**
	 * Wrap a string into DB-specific quotes.
	 *
	 * @param mixed $value Value to be prepared for database query
	 * @return string Prepared string
	 */
	public function quoteString($value) {
		return $this->connector->quoteString($value);
	}

	/**
	 * Escapes an identifier (table / database name). Typically the value
	 * is simply double quoted. Don't pass in already escaped identifiers in,
	 * as this will double escape the value!
	 *
	 * @param string $value The identifier to escape
	 * @param string $separator optional identifier splitter
	 */
	public function escapeIdentifier($value, $separator = '.') {
		return $this->connector->escapeIdentifier($value, $separator);
	}

	/**
	 * Escapes unquoted columns keys in an associative array
	 *
	 * @param array $fieldValues
	 * @return array List of field values with the keys as escaped column names
	 */
	protected function escapeColumnKeys($fieldValues) {
		$out = array();
		foreach($fieldValues as $field => $value) {
			$out[$this->escapeIdentifier($field)] = $value;
		}
		return $out;
	}

	/**
	 * Execute a complex manipulation on the database.
	 * A manipulation is an array of insert / or update sequences.  The keys of the array are table names,
	 * and the values are map containing 'command' and 'fields'.  Command should be 'insert' or 'update',
	 * and fields should be a map of field names to field values, NOT including quotes.
	 *
	 * The field values could also be in paramaterised format, such as
	 * array('MAX(?,?)' => array(42, 69)), allowing the use of raw SQL values such as
	 * array('NOW()' => array()).
	 *
	 * @see SQLWriteExpression::addAssignments for syntax examples
	 *
	 * @param array $manipulation
	 */
	public function manipulate($manipulation) {
		if (empty($manipulation)) return;

		foreach ($manipulation as $table => $writeInfo) {
			if(empty($writeInfo['fields'])) continue;
			// Note: keys of $fieldValues are not escaped
			$fieldValues = $writeInfo['fields'];

			// Switch command type
			switch ($writeInfo['command']) {
				case "update":

					// Build update
					$query = new SQLUpdate("\"$table\"", $this->escapeColumnKeys($fieldValues));

					// Set best condition to use
					if(!empty($writeInfo['where'])) {
						$query->addWhere($writeInfo['where']);
					} elseif(!empty($writeInfo['id'])) {
						$query->addWhere(array('"ID"' => $writeInfo['id']));
					}

					// Test to see if this update query shouldn't, in fact, be an insert
					if($query->toSelect()->count()) {
						$query->execute();
						break;
					}
					// ...if not, we'll skip on to the insert code

				case "insert":
					// Ensure that the ID clause is given if possible
					if (!isset($fieldValues['ID']) && isset($writeInfo['id'])) {
						$fieldValues['ID'] = $writeInfo['id'];
					}

					// Build insert
					$query = new SQLInsert("\"$table\"", $this->escapeColumnKeys($fieldValues));

					$query->execute();
					break;

				default:
					user_error("SS_Database::manipulate() Can't recognise command '{$writeInfo['command']}'",
								E_USER_ERROR);
			}
		}
	}

	/**
	 * Enable supression of database messages.
	 */
	public function quiet() {
		$this->schemaManager->quiet();
	}

	/**
	 * Clear all data out of the database
	 */
	public function clearAllData() {
		$tables = $this->getSchemaManager()->tableList();
		foreach ($tables as $table) {
			$this->clearTable($table);
		}
	}

	/**
	 * Clear all data in a given table
	 *
	 * @param string $table Name of table
	 */
	public function clearTable($table) {
		$this->query("TRUNCATE \"$table\"");
	}

	/**
	 * Generate a WHERE clause for text matching.
	 *
	 * @param String $field Quoted field name
	 * @param String $value Escaped search. Can include percentage wildcards.
	 * Ignored if $parameterised is true.
	 * @param boolean $exact Exact matches or wildcard support.
	 * @param boolean $negate Negate the clause.
	 * @param boolean $caseSensitive Enforce case sensitivity if TRUE or FALSE.
	 * Fallback to default collation if set to NULL.
	 * @param boolean $parameterised Insert the ? placeholder rather than the
	 * given value. If this is true then $value is ignored.
	 * @return String SQL
	 */
	abstract public function comparisonClause($field, $value, $exact = false, $negate = false, $caseSensitive = null,
											$parameterised = false);

	/**
	 * function to return an SQL datetime expression that can be used with the adapter in use
	 * used for querying a datetime in a certain format
	 *
	 * @param string $date to be formated, can be either 'now', literal datetime like '1973-10-14 10:30:00' or
	 *                     field name, e.g. '"SiteTree"."Created"'
	 * @param string $format to be used, supported specifiers:
	 * %Y = Year (four digits)
	 * %m = Month (01..12)
	 * %d = Day (01..31)
	 * %H = Hour (00..23)
	 * %i = Minutes (00..59)
	 * %s = Seconds (00..59)
	 * %U = unix timestamp, can only be used on it's own
	 * @return string SQL datetime expression to query for a formatted datetime
	 */
	abstract public function formattedDatetimeClause($date, $format);

	/**
	 * function to return an SQL datetime expression that can be used with the adapter in use
	 * used for querying a datetime addition
	 *
	 * @param string $date, can be either 'now', literal datetime like '1973-10-14 10:30:00' or field name,
	 *                      e.g. '"SiteTree"."Created"'
	 * @param string $interval to be added, use the format [sign][integer] [qualifier], e.g. -1 Day, +15 minutes,
	 *                         +1 YEAR
	 * supported qualifiers:
	 * - years
	 * - months
	 * - days
	 * - hours
	 * - minutes
	 * - seconds
	 * This includes the singular forms as well
	 * @return string SQL datetime expression to query for a datetime (YYYY-MM-DD hh:mm:ss) which is the result of
	 *                the addition
	 */
	abstract public function datetimeIntervalClause($date, $interval);

	/**
	 * function to return an SQL datetime expression that can be used with the adapter in use
	 * used for querying a datetime substraction
	 *
	 * @param string $date1, can be either 'now', literal datetime like '1973-10-14 10:30:00' or field name
	 *                       e.g. '"SiteTree"."Created"'
	 * @param string $date2 to be substracted of $date1, can be either 'now', literal datetime
	 *                      like '1973-10-14 10:30:00' or field name, e.g. '"SiteTree"."Created"'
	 * @return string SQL datetime expression to query for the interval between $date1 and $date2 in seconds which
	 *                is the result of the substraction
	 */
	abstract public function datetimeDifferenceClause($date1, $date2);

	/**
	 * Returns true if this database supports collations
	 *
	 * @return boolean
	 */
	abstract public function supportsCollations();

	/**
	 * Can the database override timezone as a connection setting,
	 * or does it use the system timezone exclusively?
	 *
	 * @return Boolean
	 */
	abstract public function supportsTimezoneOverride();

	/**
	 * Query for the version of the currently connected database
	 * @return string Version of this database
	 */
	public function getVersion() {
		return $this->connector->getVersion();
	}

	/**
	 * Get the database server type (e.g. mysql, postgresql).
	 * This value is passed to the connector as the 'driver' argument when
	 * initiating a database connection
	 *
	 * @return string
	 */
	abstract public function getDatabaseServer();

	/**
	 * Return the number of rows affected by the previous operation.
	 * @return int
	 */
	public function affectedRows() {
		return $this->connector->affectedRows();
	}

	/**
	 * The core search engine, used by this class and its subclasses to do fun stuff.
	 * Searches both SiteTree and File.
	 *
	 * @param array $classesToSearch List of classes to search
	 * @param string $keywords Keywords as a string.
	 * @param integer $start Item to start returning results from
	 * @param integer $pageLength Number of items per page
	 * @param string $sortBy Sort order expression
	 * @param string $extraFilter Additional filter
	 * @param boolean $booleanSearch Flag for boolean search mode
	 * @param string $alternativeFileFilter
	 * @param boolean $invertedMatch
	 * @return PaginatedList Search results
	 */
	abstract public function searchEngine($classesToSearch, $keywords, $start, $pageLength, $sortBy = "Relevance DESC",
		$extraFilter = "", $booleanSearch = false, $alternativeFileFilter = "", $invertedMatch = false);

	/**
	 * Determines if this database supports transactions
	 *
	 * @return boolean Flag indicating support for transactions
	 */
	abstract public function supportsTransactions();

	/*
	 * Determines if the current database connection supports a given list of extensions
	 *
	 * @param array $extensions List of extensions to check for support of. The key of this array
	 * will be an extension name, and the value the configuration for that extension. This
	 * could be one of partitions, tablespaces, or clustering
	 * @return boolean Flag indicating support for all of the above
	 * @todo Write test cases
	 */
	protected function supportsExtensions($extensions) {
		return false;
	}

	/**
	 * Start a prepared transaction
	 * See http://developer.postgresql.org/pgdocs/postgres/sql-set-transaction.html for details on
	 * transaction isolation options
	 *
	 * @param string|boolean $transactionMode Transaction mode, or false to ignore
	 * @param string|boolean $sessionCharacteristics Session characteristics, or false to ignore
	 */
	abstract public function transactionStart($transactionMode = false, $sessionCharacteristics = false);

	/**
	 * Create a savepoint that you can jump back to if you encounter problems
	 *
	 * @param string $savepoint Name of savepoint
	 */
	abstract public function transactionSavepoint($savepoint);

	/**
	 * Rollback or revert to a savepoint if your queries encounter problems
	 * If you encounter a problem at any point during a transaction, you may
	 * need to rollback that particular query, or return to a savepoint
	 *
	 * @param string|boolean $savepoint Name of savepoint, or leave empty to rollback
	 * to last savepoint
	 */
	abstract public function transactionRollback($savepoint = false);

	/**
	 * Commit everything inside this transaction so far
	 *
	 * @param boolean $chain
	 */
	abstract public function transactionEnd($chain = false);

	/**
	 * Determines if the used database supports application-level locks,
	 * which is different from table- or row-level locking.
	 * See {@link getLock()} for details.
	 *
	 * @return boolean Flag indicating that locking is available
	 */
	public function supportsLocks() {
		return false;
	}

	/**
	 * Returns if the lock is available.
	 * See {@link supportsLocks()} to check if locking is generally supported.
	 *
	 * @param string $name Name of the lock
	 * @return boolean
	 */
	public function canLock($name) {
		return false;
	}

	/**
	 * Sets an application-level lock so that no two processes can run at the same time,
	 * also called a "cooperative advisory lock".
	 *
	 * Return FALSE if acquiring the lock fails; otherwise return TRUE, if lock was acquired successfully.
	 * Lock is automatically released if connection to the database is broken (either normally or abnormally),
	 * making it less prone to deadlocks than session- or file-based locks.
	 * Should be accompanied by a {@link releaseLock()} call after the logic requiring the lock has completed.
	 * Can be called multiple times, in which case locks "stack" (PostgreSQL, SQL Server),
	 * or auto-releases the previous lock (MySQL).
	 *
	 * Note that this might trigger the database to wait for the lock to be released, delaying further execution.
	 *
	 * @param string $name Name of lock
	 * @param integer $timeout Timeout in seconds
	 * @return boolean
	 */
	public function getLock($name, $timeout = 5) {
		return false;
	}

	/**
	 * Remove an application-level lock file to allow another process to run
	 * (if the execution aborts (e.g. due to an error) all locks are automatically released).
	 *
	 * @param string $name Name of the lock
	 * @return boolean Flag indicating whether the lock was successfully released
	 */
	public function releaseLock($name) {
		return false;
	}

	/**
	 * Instruct the database to generate a live connection
	 *
	 * @param array $parameters An map of parameters, which should include:
	 *  - server: The server, eg, localhost
	 *  - username: The username to log on with
	 *  - password: The password to log on with
	 *  - database: The database to connect to
	 *  - charset: The character set to use. Defaults to utf8
	 *  - timezone: (optional) The timezone offset. For example: +12:00, "Pacific/Auckland", or "SYSTEM"
	 *  - driver: (optional) Driver name
	 */
	public function connect($parameters) {
		// Ensure that driver is available (required by PDO)
		if(empty($parameters['driver'])) {
			$parameters['driver'] = $this->getDatabaseServer();
		}

		// Notify connector of parameters
		$this->connector->connect($parameters);

		// SS_Database subclass maintains responsibility for selecting database
		// once connected in order to correctly handle schema queries about
		// existence of database, error handling at the correct level, etc
		if (!empty($parameters['database'])) {
			$this->selectDatabase($parameters['database'], false, false);
		}
	}

	/**
	 * Determine if the database with the specified name exists
	 *
	 * @param string $name Name of the database to check for
	 * @return boolean Flag indicating whether this database exists
	 */
	public function databaseExists($name) {
		return $this->schemaManager->databaseExists($name);
	}

	/**
	 * Retrieves the list of all databases the user has access to
	 *
	 * @return array List of database names
	 */
	public function databaseList() {
		return $this->schemaManager->databaseList();
	}

	/**
	 * Change the connection to the specified database, optionally creating the
	 * database if it doesn't exist in the current schema.
	 *
	 * @param string $name Name of the database
	 * @param boolean $create Flag indicating whether the database should be created
	 * if it doesn't exist. If $create is false and the database doesn't exist
	 * then an error will be raised
	 * @param int|boolean $errorLevel The level of error reporting to enable for the query, or false if no error
	 * should be raised
	 * @return boolean Flag indicating success
	 */
	public function selectDatabase($name, $create = false, $errorLevel = E_USER_ERROR) {
		if (!$this->schemaManager->databaseExists($name)) {
			// Check DB creation permisson
			if (!$create) {
				if ($errorLevel !== false) {
					user_error("Attempted to connect to non-existing database \"$name\"", $errorLevel);
				}
				// Unselect database
				$this->connector->unloadDatabase();
				return false;
			}
			$this->schemaManager->createDatabase($name);
		}
		return $this->connector->selectDatabase($name);
	}

	/**
	 * Drop the database that this object is currently connected to.
	 * Use with caution.
	 */
	public function dropSelectedDatabase() {
		$databaseName = $this->connector->getSelectedDatabase();
		if ($databaseName) {
			$this->connector->unloadDatabase();
			$this->schemaManager->dropDatabase($databaseName);
		}
	}

	/**
	 * Returns the name of the currently selected database
	 *
	 * @return string|null Name of the selected database, or null if none selected
	 */
	public function getSelectedDatabase() {
		return $this->connector->getSelectedDatabase();
	}

	/**
	 * Return SQL expression used to represent the current date/time
	 *
	 * @return string Expression for the current date/time
	 */
	abstract public function now();

	/**
	 * Returns the database-specific version of the random() function
	 *
	 * @return string Expression for a random value
	 */
	abstract public function random();

	/**
	 * @deprecated since version 4.0 Use DB::get_schema()->dbDataType($type) instead
	 */
	public function dbDataType($type){
		Deprecation::notice('4.0', 'Use DB::get_schema()->dbDataType($type) instead');
		return $this->getSchemaManager()->dbDataType($type);
	}

	/**
	 * @deprecated since version 4.0 Use selectDatabase('dbname', true) instead
	 */
	public function createDatabase() {
		Deprecation::notice('4.0', 'Use selectDatabase(\'dbname\',true) instead');
		$database = $this->connector->getSelectedDatabase();
		$this->selectDatabase($database, true);
		return $this->isActive();
	}

	/**
	 * @deprecated since version 4.0 SS_Database::getConnect was never implemented and is obsolete
	 */
	public function getConnect($parameters) {
		Deprecation::notice('4.0', 'SS_Database::getConnect was never implemented and is obsolete');
	}

	/**
	 * @deprecated since version 4.0 Use Convert::raw2sql($string, true) instead
	 */
	public function prepStringForDB($string) {
		Deprecation::notice('4.0', 'Use Convert::raw2sql($string, true) instead');
		return $this->quoteString($string);
	}

	/**
	 * @deprecated since version 4.0 Use dropSelectedDatabase instead
	 */
	public function dropDatabase() {
		Deprecation::notice('4.0', 'Use dropSelectedDatabase instead');
		$this->dropSelectedDatabase();
	}

	/**
	 * @deprecated since version 4.0 Use databaseList instead
	 */
	public function allDatabaseNames() {
		Deprecation::notice('4.0', 'Use databaseList instead');
		return $this->databaseList();
	}

	/**
	 * @deprecated since version 4.0 Use DB::create_table instead
	 */
	public function createTable($table, $fields = null, $indexes = null, $options = null, $advancedOptions = null) {
		Deprecation::notice('4.0', 'Use DB::create_table instead');
		return $this->getSchemaManager()->createTable($table, $fields, $indexes, $options, $advancedOptions);
	}

	/**
	 * @deprecated since version 4.0 Use DB::get_schema()->alterTable() instead
	 */
	public function alterTable($table, $newFields = null, $newIndexes = null,
		$alteredFields = null, $alteredIndexes = null, $alteredOptions = null,
		$advancedOptions = null
	) {
		Deprecation::notice('4.0', 'Use DB::get_schema()->alterTable() instead');
		return $this->getSchemaManager()->alterTable(
			$table, $newFields, $newIndexes, $alteredFields,
			$alteredIndexes, $alteredOptions, $advancedOptions
		);
	}

	/**
	 * @deprecated since version 4.0 Use DB::get_schema()->renameTable() instead
	 */
	public function renameTable($oldTableName, $newTableName) {
		Deprecation::notice('4.0', 'Use DB::get_schema()->renameTable() instead');
		$this->getSchemaManager()->renameTable($oldTableName, $newTableName);
	}

	/**
	 * @deprecated since version 4.0 Use DB::create_field() instead
	 */
	public function createField($table, $field, $spec) {
		Deprecation::notice('4.0', 'Use DB::create_field() instead');
		$this->getSchemaManager()->createField($table, $field, $spec);
	}

	/**
	 * @deprecated since version 4.0 Use DB::get_schema()->renameField() instead
	 */
	public function renameField($tableName, $oldName, $newName) {
		Deprecation::notice('4.0', 'Use DB::get_schema()->renameField() instead');
		$this->getSchemaManager()->renameField($tableName, $oldName, $newName);
	}

	/**
	 * @deprecated since version 4.0 Use getSelectedDatabase instead
	 */
	public function currentDatabase() {
		Deprecation::notice('4.0', 'Use getSelectedDatabase instead');
		return $this->getSelectedDatabase();
	}

	/**
	 * @deprecated since version 4.0 Use DB::field_list instead
	 */
	public function fieldList($table) {
		Deprecation::notice('4.0', 'Use DB::field_list instead');
		return $this->getSchemaManager()->fieldList($table);
	}

	/**
	 * @deprecated since version 4.0 Use DB::table_list instead
	 */
	public function tableList() {
		Deprecation::notice('4.0', 'Use DB::table_list instead');
		return $this->getSchemaManager()->tableList();
	}

	/**
	 * @deprecated since version 4.0 Use DB::get_schema()->hasTable() instead
	 */
	public function hasTable($tableName) {
		Deprecation::notice('4.0', 'Use DB::get_schema()->hasTable() instead');
		return $this->getSchemaManager()->hasTable($tableName);
	}

	/**
	 * @deprecated since version 4.0 Use DB::get_schema()->enumValuesForField() instead
	 */
	public function enumValuesForField($tableName, $fieldName) {
		Deprecation::notice('4.0', 'Use DB::get_schema()->enumValuesForField() instead');
		return $this->getSchemaManager()->enumValuesForField($tableName, $fieldName);
	}

	/**
	 * @deprecated since version 4.0 Use Convert::raw2sql instead
	 */
	public function addslashes($value) {
		Deprecation::notice('4.0', 'Use Convert::raw2sql instead');
		return $this->escapeString($value);
	}

	/**
	 * @deprecated since version 3.2 Use DB::get_schema()->schemaUpdate with a callback instead
	 */
	public function beginSchemaUpdate() {
		Deprecation::notice('3.2', 'Use DB::get_schema()->schemaUpdate with a callback instead');
		// Unable to recover so throw appropriate exception
		throw new BadMethodCallException('Use DB::get_schema()->schemaUpdate with a callback instead');
	}

	/**
	 * @deprecated since version 3.2 Use DB::get_schema()->schemaUpdate with a callback instead
	 */
	public function endSchemaUpdate() {
		Deprecation::notice('3.2', 'Use DB::get_schema()->schemaUpdate with a callback instead');
		// Unable to recover so throw appropriate exception
		throw new BadMethodCallException('Use DB::get_schema()->schemaUpdate with a callback instead');
	}

	/**
	 * @deprecated since version 4.0 Use DB::get_schema()->cancelSchemaUpdate instead
	 */
	public function cancelSchemaUpdate() {
		Deprecation::notice('4.0', 'Use DB::get_schema()->cancelSchemaUpdate instead');
		$this->getSchemaManager()->cancelSchemaUpdate();
	}

	/**
	 * @deprecated since version 4.0 Use DB::get_schema()->isSchemaUpdating() instead
	 */
	public function isSchemaUpdating() {
		Deprecation::notice('4.0', 'Use DB::get_schema()->isSchemaUpdating() instead');
		return $this->getSchemaManager()->isSchemaUpdating();
	}

	/**
	 * @deprecated since version 4.0 Use DB::get_schema()->doesSchemaNeedUpdating() instead
	 */
	public function doesSchemaNeedUpdating() {
		Deprecation::notice('4.0', 'Use DB::get_schema()->doesSchemaNeedUpdating() instead');
		return $this->getSchemaManager()->doesSchemaNeedUpdating();
	}

	/**
	 * @deprecated since version 4.0 Use DB::get_schema()->transCreateTable() instead
	 */
	public function transCreateTable($table, $options = null, $advanced_options = null) {
		Deprecation::notice('4.0', 'Use DB::get_schema()->transCreateTable() instead');
		$this->getSchemaManager()->transCreateTable($table, $options, $advanced_options);
	}

	/**
	 * @deprecated since version 4.0 Use DB::get_schema()->transAlterTable() instead
	 */
	public function transAlterTable($table, $options, $advanced_options) {
		Deprecation::notice('4.0', 'Use DB::get_schema()->transAlterTable() instead');
		$this->getSchemaManager()->transAlterTable($table, $index, $schema);
	}

	/**
	 * @deprecated since version 4.0 Use DB::get_schema()->transCreateField() instead
	 */
	public function transCreateField($table, $field, $schema) {
		Deprecation::notice('4.0', 'Use DB::get_schema()->transCreateField() instead');
		$this->getSchemaManager()->transCreateField($table, $index, $schema);
	}

	/**
	 * @deprecated since version 4.0 Use DB::get_schema()->transCreateIndex() instead
	 */
	public function transCreateIndex($table, $index, $schema) {
		Deprecation::notice('4.0', 'Use DB::get_schema()->transCreateIndex() instead');
		$this->getSchemaManager()->transCreateIndex($table, $index, $schema);
	}

	/**
	 * @deprecated since version 4.0 Use DB::get_schema()->transAlterField() instead
	 */
	public function transAlterField($table, $field, $schema) {
		Deprecation::notice('4.0', 'Use DB::get_schema()->transAlterField() instead');
		$this->getSchemaManager()->transAlterField($table, $index, $schema);
	}

	/**
	 * @deprecated since version 4.0 Use DB::get_schema()->transAlterIndex() instead
	 */
	public function transAlterIndex($table, $index, $schema) {
		Deprecation::notice('4.0', 'Use DB::get_schema()->transAlterIndex() instead');
		$this->getSchemaManager()->transAlterIndex($table, $index, $schema);
	}

	/**
	 * @deprecated since version 4.0 Use DB::require_table() instead
	 */
	public function requireTable($table, $fieldSchema = null, $indexSchema = null,
		$hasAutoIncPK = true, $options = array(), $extensions = false
	) {
		Deprecation::notice('4.0', 'Use DB::require_table() instead');
		return $this->getSchemaManager()->requireTable(
			$table, $fieldSchema, $indexSchema, $hasAutoIncPK, $options, $extensions
		);
	}

	/**
	 * @deprecated since version 4.0 Use DB::dont_require_table() instead
	 */
	public function dontRequireTable($table) {
		Deprecation::notice('4.0', 'Use DB::dont_require_table() instead');
		$this->getSchemaManager()->dontRequireTable($table);
	}

	/**
	 * @deprecated since version 4.0 Use DB::require_index() instead
	 */
	public function requireIndex($table, $index, $spec) {
		Deprecation::notice('4.0', 'Use DB::require_index() instead');
		$this->getSchemaManager()->requireIndex($table, $index, $spec);
	}

	/**
	 * @deprecated since version 4.0 Use DB::get_schema()->hasField() instead
	 */
	public function hasField($tableName, $fieldName) {
		Deprecation::notice('4.0', 'Use DB::get_schema()->hasField() instead');
		return $this->getSchemaManager()->hasField($tableName, $fieldName);
	}

	/**
	 * @deprecated since version 4.0 Use DB::require_field() instead
	 */
	public function requireField($table, $field, $spec) {
		Deprecation::notice('4.0', 'Use DB::require_field() instead');
		$this->getSchemaManager()->requireField($table, $field, $spec);
	}

	/**
	 * @deprecated since version 4.0 Use DB::dont_require_field() instead
	 */
	public function dontRequireField($table, $fieldName) {
		Deprecation::notice('4.0', 'Use DB::dont_require_field() instead');
		$this->getSchemaManager()->dontRequireField($table, $fieldName);
	}

	/**
	 * @deprecated since version 4.0 Use DB::build_sql() instead
	 */
	public function sqlQueryToString(SQLExpression $query, &$parameters = array()) {
		Deprecation::notice('4.0', 'Use DB::build_sql() instead');
		return $this->getQueryBuilder()->buildSQL($query, $parameters);
	}
}