diff --git a/dibi/drivers/firebird.php b/dibi/drivers/firebird.php new file mode 100644 index 00000000..9910b693 --- /dev/null +++ b/dibi/drivers/firebird.php @@ -0,0 +1,835 @@ +connection = $config['resource']; + + } else { + // default values + if (!isset($config['username'])) $config['username'] = ini_get('ibase.default_password'); + if (!isset($config['password'])) $config['password'] = ini_get('ibase.default_user'); + if (!isset($config['database'])) $config['database'] = ini_get('ibase.default_db'); + if (!isset($config['charset'])) $config['charset'] = ini_get('ibase.default_charset'); + if (!isset($config['buffers'])) $config['buffers'] = 0; + + DibiDriverException::tryError(); + if (empty($config['persistent'])) { + $this->connection = ibase_connect($config['database'], $config['username'], $config['password'], $config['charset'], $config['buffers']); // intentionally @ + } else { + $this->connection = ibase_pconnect($config['database'], $config['username'], $config['password'], $config['charset'], $config['buffers']); // intentionally @ + } + if (DibiDriverException::catchError($msg)) { + throw new DibiDriverException($msg, ibase_errcode()); + } + + if (!is_resource($this->connection)) { + throw new DibiDriverException(ibase_errmsg(), ibase_errcode()); + } + } + + } + + + + /** + * Disconnects from a database. + * @return void + */ + public function disconnect() + { + ibase_close($this->connection); + } + + + + /** + * Executes the SQL query. + * @param string SQL statement. + * @return IDibiDriver|NULL + * @throws DibiDriverException|DibiException + */ + public function query($sql) + { + DibiDriverException::tryError(); + $resource = $this->inTransaction ? $this->transaction : $this->connection; + $this->resultSet = ibase_query($resource, $sql); + + if (DibiDriverException::catchError($msg)) { + if (ibase_errcode() == self::ERROR_EXCEPTION_THROWN) { + preg_match('/exception (\d+) (\w+) (.*)/i', ibase_errmsg(), $match); + throw new DibiProcedureException($match[3], $match[1], $match[2], dibi::$sql); + + } else { + throw new DibiDriverException(ibase_errmsg(), ibase_errcode(), dibi::$sql); + } + } + + if ($this->resultSet === FALSE) { + throw new DibiDriverException(ibase_errmsg(), ibase_errcode(), $sql); + } + + return is_resource($this->resultSet) ? clone $this : NULL; + } + + + + /** + * Gets the number of affected rows by the last INSERT, UPDATE or DELETE query. + * @return int|FALSE number of rows or FALSE on error + */ + public function getAffectedRows() + { + return ibase_affected_rows($this->connection); + } + + + + /** + * Retrieves the ID generated for an AUTO_INCREMENT column by the previous INSERT query. + * @param string generator name + * @return int|FALSE int on success or FALSE on failure + */ + public function getInsertId($sequence) + { + return ibase_gen_id($sequence, 0, $this->connection); + //throw new NotSupportedException('Firebird/InterBase does not support autoincrementing.'); + } + + + + /** + * Begins a transaction (if supported). + * @param string optional savepoint name + * @return void + * @throws DibiDriverException + */ + public function begin($savepoint = NULL) + { + if ($savepoint !== NULL) { + throw new DibiDriverException('Savepoints are not supported in Firebird/Interbase.'); + } + $this->transaction = ibase_trans($this->resource); + $this->inTransaction = TRUE; + } + + + + /** + * Commits statements in a transaction. + * @param string optional savepoint name + * @return void + * @throws DibiDriverException + */ + public function commit($savepoint = NULL) + { + if ($savepoint !== NULL) { + throw new DibiDriverException('Savepoints are not supported in Firebird/Interbase.'); + } + + if (!ibase_commit($this->transaction)) { + DibiDriverException('Unable to handle operation - failure when commiting transaction.'); + } + + $this->inTransaction = FALSE; + } + + + + /** + * Rollback changes in a transaction. + * @param string optional savepoint name + * @return void + * @throws DibiDriverException + */ + public function rollback($savepoint = NULL) + { + if ($savepoint !== NULL) { + throw new DibiDriverException('Savepoints are not supported in Firebird/Interbase.'); + } + + if (!ibase_rollback($this->transaction)) { + DibiDriverException('Unable to handle operation - failure when rolbacking transaction.'); + } + + $this->inTransaction = FALSE; + } + + + + /** + * Returns the connection resource. + * @return resource + */ + public function getResource() + { + return $this->connection; + } + + + + /********************* SQL ********************/ + + + + /** + * Encodes data for use in a SQL statement. + * @param string value + * @param string type (dibi::TEXT, dibi::BOOL, ...) + * @return string encoded value + * @throws InvalidArgumentException + */ + public function escape($value, $type) + { + switch ($type) { + case dibi::TEXT: + case dibi::BINARY: + return "'" . str_replace("'", "''", $value) . "'"; + + case dibi::IDENTIFIER: + return $value; + + case dibi::BOOL: + return $value ? -1 : 0; + + case dibi::DATE: + return date("'Y-m-d'", $value); + + case dibi::DATETIME: + return date("'Y-m-d H:i:s'", $value); + + default: + throw new InvalidArgumentException('Unsupported type.'); + } + } + + + + /** + * Decodes data from result set. + * @param string value + * @param string type (dibi::BINARY) + * @return string decoded value + * @throws InvalidArgumentException + */ + public function unescape($value, $type) + { + if ($type === dibi::BINARY) { + return $value; + } + throw new InvalidArgumentException('Unsupported type.'); + } + + + + /** + * Injects LIMIT/OFFSET to the SQL query. + * @param string &$sql The SQL query that will be modified. + * @param int $limit + * @param int $offset + * @return void + */ + public function applyLimit(&$sql, $limit, $offset) + { + if ($limit < 0 && $offset < 1) return; + + // see http://scott.yang.id.au/2004/01/limit-in-select-statements-in-firebird/ + $sql = 'SELECT FIRST ' . (int) $limit . ($offset > 0 ? ' SKIP ' . (int) $offset : '') . ' * FROM (' . $sql . ')'; + } + + + + /********************* result set ********************/ + + + + /** + * Returns the number of rows in a result set. + * @return int + */ + public function getRowCount() + { + return ibase_num_fields($this->resultSet); + } + + + + /** + * Fetches the row at current position and moves the internal cursor to the next position. + * @param bool TRUE for associative array, FALSE for numeric + * @return array array on success, nonarray if no next record + * @internal + */ + public function fetch($assoc) + { + DibiDriverException::tryError(); + $result = $assoc ? ibase_fetch_assoc($this->resultSet) : ibase_fetch_row($this->resultSet); // intentionally @ + + if (DibiDriverException::catchError($msg)) { + if (ibase_errcode() == self::ERROR_EXCEPTION_THROWN) { + preg_match('/exception (\d+) (\w+) (.*)/i', ibase_errmsg(), $match); + throw new DibiProcedureException($match[3], $match[1], $match[2], dibi::$sql); + + } else { + throw new DibiDriverException($msg, ibase_errcode(), dibi::$sql); + } + } + + return $result; + } + + + + /** + * Moves cursor position without fetching row. + * @param int the 0-based cursor pos to seek to + * @return boolean TRUE on success, FALSE if unable to seek to specified record + * @throws DibiException + */ + public function seek($row) + { + throw new DibiDriverException("Firebird/Interbase do not support seek in result set."); + } + + + + /** + * Frees the resources allocated for this result set. + * @return void + */ + public function free() + { + ibase_free_result($this->resultSet); + $this->resultSet = NULL; + } + + + + /** + * Returns the result set resource. + * @return mysqli_result + */ + public function getResultResource() + { + return $this->resultSet; + } + + + + /** + * Returns metadata for all columns in a result set. + * @return array + */ + public function getColumnsMeta() + { + throw new NotImplementedException; + } + + + + /********************* reflection ********************/ + + + + /** + * Returns list of tables. + * @return array + */ + public function getTables() + { + $this->query(" + SELECT TRIM(RDB\$RELATION_NAME), + CASE RDB\$VIEW_BLR WHEN NULL THEN 'TRUE' ELSE 'FALSE' END + FROM RDB\$RELATIONS + WHERE RDB\$SYSTEM_FLAG = 0;" + ); + $res = array(); + while ($row = $this->fetch(FALSE)) { + $res[] = array( + 'name' => $row[0], + 'view' => $row[1] === 'TRUE', + ); + } + $this->free(); + return $res; + } + + + + /** + * Returns metadata for all columns in a table. + * @param string + * @return array + */ + public function getColumns($table) + { + $table = strtoupper($table); + $this->query(" + SELECT TRIM(r.RDB\$FIELD_NAME) AS FIELD_NAME, + CASE f.RDB\$FIELD_TYPE + WHEN 261 THEN 'BLOB' + WHEN 14 THEN 'CHAR' + WHEN 40 THEN 'CSTRING' + WHEN 11 THEN 'D_FLOAT' + WHEN 27 THEN 'DOUBLE' + WHEN 10 THEN 'FLOAT' + WHEN 16 THEN 'INT64' + WHEN 8 THEN 'INTEGER' + WHEN 9 THEN 'QUAD' + WHEN 7 THEN 'SMALLINT' + WHEN 12 THEN 'DATE' + WHEN 13 THEN 'TIME' + WHEN 35 THEN 'TIMESTAMP' + WHEN 37 THEN 'VARCHAR' + ELSE 'UNKNOWN' + END AS FIELD_TYPE, + f.RDB\$FIELD_LENGTH AS FIELD_LENGTH, + r.RDB\$DEFAULT_VALUE AS DEFAULT_VALUE, + CASE r.RDB\$NULL_FLAG + WHEN 1 THEN 'FALSE' ELSE 'TRUE' + END AS NULLABLE + FROM RDB\$RELATION_FIELDS r + LEFT JOIN RDB\$FIELDS f ON r.RDB\$FIELD_SOURCE = f.RDB\$FIELD_NAME + WHERE r.RDB\$RELATION_NAME = '$table' + ORDER BY r.RDB\$FIELD_POSITION;" + + ); + $res = array(); + while ($row = $this->fetch(TRUE)) { + $key = $row['FIELD_NAME']; + $res[$key] = array( + 'name' => $key, + 'table' => $table, + 'nativetype' => trim($row['FIELD_TYPE']), + 'size' => $row['FIELD_LENGTH'], + 'nullable' => $row['NULLABLE'] === 'TRUE', + 'default' => $row['DEFAULT_VALUE'], + 'autoincrement' => FALSE, + ); + } + $this->free(); + return $res; + } + + + + /** + * Returns metadata for all indexes in a table (the constraints are included). + * @param string + * @return array + */ + public function getIndexes($table) + { + $table = strtoupper($table); + $this->query(" + SELECT TRIM(s.RDB\$INDEX_NAME) AS INDEX_NAME, + TRIM(s.RDB\$FIELD_NAME) AS FIELD_NAME, + i.RDB\$UNIQUE_FLAG AS UNIQUE_FLAG, + i.RDB\$FOREIGN_KEY AS FOREIGN_KEY, + TRIM(r.RDB\$CONSTRAINT_TYPE) AS CONSTRAINT_TYPE, + s.RDB\$FIELD_POSITION AS FIELD_POSITION + FROM RDB\$INDEX_SEGMENTS s + LEFT JOIN RDB\$INDICES i ON i.RDB\$INDEX_NAME = s.RDB\$INDEX_NAME + LEFT JOIN RDB\$RELATION_CONSTRAINTS r ON r.RDB\$INDEX_NAME = s.RDB\$INDEX_NAME + WHERE UPPER(i.RDB\$RELATION_NAME) = '$table' + ORDER BY s.RDB\$FIELD_POSITION" + ); + $res = array(); + while ($row = $this->fetch(TRUE)) { + $key = $row['INDEX_NAME']; + $res[$key]['name'] = $key; + $res[$key]['unique'] = $row['UNIQUE_FLAG'] === 1; + $res[$key]['primary'] = $row['CONSTRAINT_TYPE'] === 'PRIMARY KEY'; + $res[$key]['table'] = $table; + $res[$key]['columns'][$row['FIELD_POSITION']] = $row['FIELD_NAME']; + } + $this->free(); + return $res; + } + + + + /** + * Returns metadata for all foreign keys in a table. + * @param string + * @return array + */ + public function getForeignKeys($table) + { + $table = strtoupper($table); + $this->query(" + SELECT TRIM(s.RDB\$INDEX_NAME) AS INDEX_NAME, + TRIM(s.RDB\$FIELD_NAME) AS FIELD_NAME, + FROM RDB\$INDEX_SEGMENTS s + LEFT JOIN RDB\$RELATION_CONSTRAINTS r ON r.RDB\$INDEX_NAME = s.RDB\$INDEX_NAME + WHERE UPPER(i.RDB\$RELATION_NAME) = '$table' + AND r.RDB\$CONSTRAINT_TYPE = 'FOREIGN KEY' + ORDER BY s.RDB\$FIELD_POSITION" + ); + $res = array(); + while ($row = $this->fetch(TRUE)) { + $key = $row['INDEX_NAME']; + $res[$key] = array( + 'name' => $key, + 'column' => $row['FIELD_NAME'], + 'table' => $table, + ); + } + $this->free(); + return $res; + } + + + + /** + * Returns list of indices in given table (the constraints are not listed). + * @param string + * @return array + */ + public function getIndices($table) + { + $this->query(" + SELECT TRIM(RDB\$INDEX_NAME) + FROM RDB\$INDICES + WHERE RDB\$RELATION_NAME = UPPER('$table') + AND RDB\$UNIQUE_FLAG IS NULL + AND RDB\$FOREIGN_KEY IS NULL;" + ); + $res = array(); + while ($row = $this->fetch(FALSE)) { + $res[] = $row[0]; + } + $this->free(); + return $res; + } + + + + /** + * Returns list of constraints in given table. + * @param string + * @return array + */ + public function getConstraints($table) + { + $this->query(" + SELECT TRIM(RDB\$INDEX_NAME) + FROM RDB\$INDICES + WHERE RDB\$RELATION_NAME = UPPER('$table') + AND ( + RDB\$UNIQUE_FLAG IS NOT NULL + OR RDB\$FOREIGN_KEY IS NOT NULL + );" + ); + $res = array(); + while ($row = $this->fetch(FALSE)) { + $res[] = $row[0]; + } + $this->free(); + return $res; + } + + + + /** + * Returns metadata for all triggers in a table or database. + * (Only if user has permissions on ALTER TABLE, INSERT/UPDATE/DELETE record in table) + * @param string + * @param string + * @return array + */ + public function getTriggersMeta($table = NULL) + { + $this->query(" + SELECT TRIM(RDB\$TRIGGER_NAME) AS TRIGGER_NAME, + TRIM(RDB\$RELATION_NAME) AS TABLE_NAME, + CASE RDB\$TRIGGER_TYPE + WHEN 1 THEN 'BEFORE' + WHEN 2 THEN 'AFTER' + WHEN 3 THEN 'BEFORE' + WHEN 4 THEN 'AFTER' + WHEN 5 THEN 'BEFORE' + WHEN 6 THEN 'AFTER' + END AS TRIGGER_TYPE, + CASE RDB\$TRIGGER_TYPE + WHEN 1 THEN 'INSERT' + WHEN 2 THEN 'INSERT' + WHEN 3 THEN 'UPDATE' + WHEN 4 THEN 'UPDATE' + WHEN 5 THEN 'DELETE' + WHEN 6 THEN 'DELETE' + END AS TRIGGER_EVENT, + CASE RDB\$TRIGGER_INACTIVE + WHEN 1 THEN 'FALSE' ELSE 'TRUE' + END AS TRIGGER_ENABLED + FROM RDB\$TRIGGERS + WHERE RDB\$SYSTEM_FLAG = 0" + . ($table === NULL ? ";" : " AND RDB\$RELATION_NAME = UPPER('$table');") + ); + $res = array(); + while ($row = $this->fetch(TRUE)) { + $res[$row['TRIGGER_NAME']] = array( + 'name' => $row['TRIGGER_NAME'], + 'table' => $row['TABLE_NAME'], + 'type' => trim($row['TRIGGER_TYPE']), + 'event' => trim($row['TRIGGER_EVENT']), + 'enabled' => trim($row['TRIGGER_ENABLED']) === 'TRUE', + ); + } + $this->free(); + return $res; + } + + + + /** + * Returns list of triggers for given table. + * (Only if user has permissions on ALTER TABLE, INSERT/UPDATE/DELETE record in table) + * @param string + * @return array + */ + public function getTriggers($table = NULL) + { + $q = "SELECT TRIM(RDB\$TRIGGER_NAME) + FROM RDB\$TRIGGERS + WHERE RDB\$SYSTEM_FLAG = 0"; + $q .= $table === NULL ? ";" : " AND RDB\$RELATION_NAME = UPPER('$table')"; + + $this->query($q); + $res = array(); + while ($row = $this->fetch(FALSE)) { + $res[] = $row[0]; + } + $this->free(); + return $res; + } + + + + /** + * Returns metadata from stored procedures and their input and output parameters. + * @param string + * @return array + */ + public function getProceduresMeta() + { + $this->query(" + SELECT + TRIM(p.RDB\$PARAMETER_NAME) AS PARAMETER_NAME, + TRIM(p.RDB\$PROCEDURE_NAME) AS PROCEDURE_NAME, + CASE p.RDB\$PARAMETER_TYPE + WHEN 0 THEN 'INPUT' + WHEN 1 THEN 'OUTPUT' + ELSE 'UNKNOWN' + END AS PARAMETER_TYPE, + CASE f.RDB\$FIELD_TYPE + WHEN 261 THEN 'BLOB' + WHEN 14 THEN 'CHAR' + WHEN 40 THEN 'CSTRING' + WHEN 11 THEN 'D_FLOAT' + WHEN 27 THEN 'DOUBLE' + WHEN 10 THEN 'FLOAT' + WHEN 16 THEN 'INT64' + WHEN 8 THEN 'INTEGER' + WHEN 9 THEN 'QUAD' + WHEN 7 THEN 'SMALLINT' + WHEN 12 THEN 'DATE' + WHEN 13 THEN 'TIME' + WHEN 35 THEN 'TIMESTAMP' + WHEN 37 THEN 'VARCHAR' + ELSE 'UNKNOWN' + END AS FIELD_TYPE, + f.RDB\$FIELD_LENGTH AS FIELD_LENGTH, + p.RDB\$PARAMETER_NUMBER AS PARAMETER_NUMBER + FROM RDB\$PROCEDURE_PARAMETERS p + LEFT JOIN RDB\$FIELDS f ON f.RDB\$FIELD_NAME = p.RDB\$FIELD_SOURCE + ORDER BY p.RDB\$PARAMETER_TYPE, p.RDB\$PARAMETER_NUMBER;" + ); + $res = array(); + while ($row = $this->fetch(TRUE)) { + $key = $row['PROCEDURE_NAME']; + $io = trim($row['PARAMETER_TYPE']); + $num = $row['PARAMETER_NUMBER']; + $res[$key]['name'] = $row['PROCEDURE_NAME']; + $res[$key]['params'][$io][$num]['name'] = $row['PARAMETER_NAME']; + $res[$key]['params'][$io][$num]['type'] = trim($row['FIELD_TYPE']); + $res[$key]['params'][$io][$num]['size'] = $row['FIELD_LENGTH']; + } + $this->free(); + return $res; + } + + + + /** + * Returns list of stored procedures. + * @return array + */ + public function getProcedures() + { + $this->query(" + SELECT TRIM(RDB\$PROCEDURE_NAME) + FROM RDB\$PROCEDURES;" + ); + $res = array(); + while ($row = $this->fetch(FALSE)) { + $res[] = $row[0]; + } + $this->free(); + return $res; + } + + + + /** + * Returns list of generators. + * @return array + */ + public function getGenerators() + { + $this->query(" + SELECT TRIM(RDB\$GENERATOR_NAME) + FROM RDB\$GENERATORS + WHERE RDB\$SYSTEM_FLAG = 0;" + ); + $res = array(); + while ($row = $this->fetch(FALSE)) { + $res[] = $row[0]; + } + $this->free(); + return $res; + } + + + + /** + * Returns list of user defined functions (UDF). + * @return array + */ + public function getFunctions() + { + $this->query(" + SELECT TRIM(RDB\$FUNCTION_NAME) + FROM RDB\$FUNCTIONS + WHERE RDB\$SYSTEM_FLAG = 0;" + ); + $res = array(); + while ($row = $this->fetch(FALSE)) { + $res[] = $row[0]; + } + $this->free(); + return $res; + } + +} + + + + +/** + * Database procedure exception. + * + * @author Roman Sklenář + * @copyright Copyright (c) 2009 + * @package dibi + */ +class DibiProcedureException extends DibiException +{ + /** @var string */ + protected $severity; + + + /** + * Construct the exception. + * @param string Message describing the exception + * @param int Some code + * @param string SQL command + */ + public function __construct($message = NULL, $code = 0, $severity = NULL, $sql = NULL) + { + parent::__construct($message, (int) $code, $sql); + $this->severity = $severity; + } + + + /** + * Gets the exception severity. + * @return string + */ + public function getSeverity() + { + $this->severity; + } + +}