Firebird a lastInsertId()

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
chloris
Člen | 23
+
0
-

Ahoj,

používám v jedné aplikaci databázi Firebird, která v PDO nemá podporu pro lastInsertId().Při pokusu o zavolání metody PHPko zakřičí:

Driver does not support this function: driver does not support lastInsertId()

Bohužel v Selection:737 se tahle funkce volá vždy:

$primaryKey = $this->connection->getInsertId($this->getPrimarySequence());

Takže insert vždycky spadne na té chybě.

Příklad takového insertu za předpokladu že tabulka má sloupce ID,MESSAGE,CODE:


$this->db->table('ERROR_LOG')->insert(array(
	'MESSAGE' => $message,
	'CODE' => $code
));

Nebylo by dobré do driverů do metod isSupported zavést také LAST_INSERT_ID a v případě, že nějaký DB stroj to nepodporuje se na IDčko nedotazovat nebo resp. mít možnost se třeba dotázat jinak? Viz http://www.firebirdfaq.org/faq243/ a klauzule RETURNING

Díky
Ondra

chloris
Člen | 23
+
0
-

Navrhuju tedy následující úpravu:

ISupplementalDriver konstanty rozšířit (spodní dvě):

	const SUPPORT_SEQUENCE = 'sequence',
		SUPPORT_SELECT_UNGROUPED_COLUMNS = 'ungrouped_cols',
		SUPPORT_MULTI_INSERT_AS_SELECT = 'insert_as_select',
		SUPPORT_MULTI_COLUMN_AS_OR_COND = 'multi_column_as_or',
		SUPPORT_SUBSELECT = 'subselect',
		SUPPORT_LAST_INSERT_ID = 'last_insert_id',
		SUPPORT_INSERT_RETURNING = 'insert_returning';

a metodu insert ve třídě Selection:

	/**
	 * Inserts row in a table.
	 * @param  array|\Traversable|Selection array($column => $value)|\Traversable|Selection for INSERT ... SELECT
	 * @return IRow|int|bool Returns IRow or number of affected rows for Selection or table without primary key
	 */
	public function insert($data)
	{
		if ($data instanceof Selection) {
			$data = new Nette\Database\SqlLiteral($data->getSql(), $data->getSqlBuilder()->getParameters());

		} elseif ($data instanceof \Traversable) {
			$data = iterator_to_array($data);
		}

		$driver = $this->getConnection()->getSupplementalDriver();

		// is lastInsertId() supported ?
		if ($driver->isSupported(ISupplementalDriver::SUPPORT_LAST_INSERT_ID)) {

			$return = $this->connection->query($this->sqlBuilder->buildInsertQuery(), $data);

			$this->loadRefCache();

			if ($data instanceof Nette\Database\SqlLiteral || $this->primary === NULL) {
				unset($this->refCache['referencing'][$this->getGeneralCacheKey()][$this->getSpecificCacheKey()]);
				return $return->getRowCount();
			}

			$primaryKey = $this->connection->getInsertId($this->getPrimarySequence());

			if ($primaryKey === FALSE) {
				unset($this->refCache['referencing'][$this->getGeneralCacheKey()][$this->getSpecificCacheKey()]);
				return $return->getRowCount();
			}

			if (is_array($this->getPrimary())) {
				$primaryKey = array();

				foreach ((array) $this->getPrimary() as $key) {
					if (!isset($data[$key])) {
						return $data;
					}

					$primaryKey[$key] = $data[$key];
				}
			}

		// is insert returning supported ?
		} elseif ($driver->isSupported(ISupplementalDriver::SUPPORT_INSERT_RETURNING)) {

			// delimite primary key fields
			if(is_array($this->getPrimary())) {
				$primaryColumns = array();
				foreach($this->getPrimary() as $primaryColumn) {
					$primaryColumns[] = $driver->delimite($primaryColumn);
				}
			} else {
				$primaryColumns = array($driver->delimite($this->getPrimary()));
			}

			// run insert query with RETURNING clause
			$return = $this->connection->query($this->sqlBuilder->buildInsertQuery(), $data, ' RETURNING '.implode(',', $primaryColumns));
			// fetch newly created primary key fields
			$primary = $return->fetch();

			$this->loadRefCache();

			if ($data instanceof Nette\Database\SqlLiteral || $this->primary === NULL) {
				unset($this->refCache['referencing'][$this->getGeneralCacheKey()][$this->getSpecificCacheKey()]);
				return $return->getRowCount();
			}

			if ($primary === FALSE) {
				unset($this->refCache['referencing'][$this->getGeneralCacheKey()][$this->getSpecificCacheKey()]);
				return $return->getRowCount();
			}

			// fetch primary key fields
			$primaryKey = array();
			foreach($primary as $pColumn => $pValue) {
				$primaryKey[$pColumn] = $pValue;
			}
		}

		// if primary key contains only one field, use its value
		if (count($primaryKey) === 1) {
			$primaryKey = reset($primaryKey);
		}
		$row = $this->createSelectionInstance()
			->select('*')
			->wherePrimary($primaryKey)
			->fetch();

		if ($this->rows !== NULL) {
			if ($signature = $row->getSignature(FALSE)) {
				$this->rows[$signature] = $row;
				$this->data[$signature] = $row;
			} else {
				$this->rows[] = $row;
				$this->data[] = $row;
			}
		}

		return $row;
	}

Pak je potřeba upravit u jednotlivých driverů, zda podporují lastInsertId() či nikoliv, např. u MySQL ano:

	/**
	 * @return bool
	 */
	public function isSupported($item)
	{
		return $item === self::SUPPORT_SUBSELECT || $item === self::SUPPORT_LAST_INSERT_ID;
	}

A nakonec přikládám FirebirdDriver, který používám. Firebird PDO nepodporuje meta informace v metodě getColumnTypes, vyhodí výjimku „SQLSTATE[IM001]: Driver does not support this function: driver doesn't support meta data“, takže tam jsou dvě možnosti, buď to nepoužívat a připravit se o přetypování vytažených dat z databáze, nebo cesta, kterou je driver tvořen a sice (a teď mne nekamenujte :-)) vytvoření supportConnection přes ibase extension, kterou lze spustit SQL dotaz znovu (není-li to insert) a meta data vytáhnout:

chloris
Člen | 23
+
0
-

/**
 * Nette Database Firebird Driver
 *
 * Example configuration:
 *
 * nette:
 *	database:
 *		dsn: 'firebird:dbname=[HOST]:[PATH];charset=utf-8'
 *		user: SYSDBA
 *		password: masterkey
 *		options:
 *			lazy: yes
 *			supportConnection:
 *				database: '[HOST]:[PATH]'
 *				user: SYSDBA
 *				password: masterkey
 *
 * supportConnection parameters needs to be equivalent to dsn/user/password parameters
 */

namespace Nette\Database\Drivers;

use Nette;


/**
 * Supplemental Firebird database driver.
 *
 * @see http://www.php.net/manual/en/ref.pdo-firebird.connection.php
 */
class FirebirdDriver extends Nette\Object implements Nette\Database\ISupplementalDriver
{
	/** @var Nette\Database\Connection */
	private $connection;

	/** Support connection to firebird database for obtaining META information */
	private $supportConnection = null;

	private $supportConnectionOptions = null;

	/**
	 * Constructor
	 */
	public function __construct(Nette\Database\Connection $connection, array $options)
	{
		$this->connection = $connection;

		// Check if we have configured the supportConnection
		if(empty($options['supportConnection']) || empty($options['supportConnection']['user']) || !isset($options['supportConnection']['password'])) {
			throw new Nette\InvalidArgumentException('Database connection "options" must contain "supportConnection" with subsequent keys "database", "user" and "password".');
		}

		$this->supportConnectionOptions = $options['supportConnection'];
	}


	/********************* SQL *******************/


	/**
	 * Delimites identifier for use in a SQL statement.
	 */
	public function delimite($name)
	{
		// @see http://dev.mysql.com/doc/refman/5.0/en/identifiers.html
		return '"' . str_replace('"', '""', strtoupper($name)). '"';
	}


	/**
	 * Formats boolean for use in a SQL statement.
	 */
	public function formatBool($value)
	{
		return $value ? '1' : '0';
	}


	/**
	 * Formats date-time for use in a SQL statement.
	 */
	public function formatDateTime(/*\DateTimeInterface*/ $value)
	{
		return $value->format("'Y-m-d H:i:s'");
	}


	/**
	 * Encodes string for use in a LIKE statement.
	 */
	public function formatLike($value, $pos)
	{
		$value = addcslashes(str_replace('\\', '\\\\', $value), "\x00\n\r\\'%_");
		return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'");
	}


	/**
	 * Injects LIMIT/OFFSET to the SQL query.
	 */
	public function applyLimit(&$sql, $limit, $offset)
	{
		if ($limit >= 0 || $offset > 0) {
			$sql = preg_replace('#^\s*(SELECT|UPDATE|DELETE)#i',
								'$0 FIRST '.($limit < 0 ? '18446744073709551615' : (int) $limit).($offset > 0 ? ' SKIP ' . (int) $offset : ''),
								$sql, 1, $count);
			if (!$count) {
				throw new Nette\InvalidArgumentException('SQL query must begin with SELECT, UPDATE or DELETE command.');
			}
		}
	}


	/**
	 * Normalizes result row.
	 */
	public function normalizeRow($row)
	{
		return $row;
	}


	/********************* reflection *******************/


	/**
	 * Returns list of tables.
	 */
	public function getTables()
	{
		$tables = array();
		foreach ($this->connection->query('SELECT RDB$RELATION_NAME NAME, (CASE COALESCE(RDB$VIEW_SOURCE,0) WHEN 0 THEN 0 ELSE 1 END) IS_VIEW FROM RDB$RELATIONS') as $row) {
			$tables[] = array(
				'name' => trim($row['NAME']),
				'view' => !empty($row['IS_VIEW']),
			);
		}
		return $tables;
	}


	/**
	 * Returns metadata for all columns in a table.
	 */
	public function getColumns($table)
	{
		$columns = array();

		$query =
			'SELECT '.
				'rf.RDB$RELATION_NAME AS relationname, '.
				'rf.RDB$FIELD_NAME AS fieldname, '.
				't.RDB$TYPE_NAME AS typename, '.
				'f.RDB$FIELD_LENGTH AS fieldlength, '.
				'f.RDB$DEFAULT_VALUE AS defaultvalue, '.
				'rf.RDB$NULL_FLAG AS nullflag, '.
				'(SELECT COUNT(*) '.
					  'FROM RDB$RELATION_CONSTRAINTS rc '.
					  'JOIN RDB$INDEX_SEGMENTS ins ON rc.RDB$INDEX_NAME = ins.RDB$INDEX_NAME '.
					  'WHERE rc.RDB$CONSTRAINT_TYPE = \'PRIMARY KEY\' '.
					  'AND rc.RDB$RELATION_NAME = rf.RDB$RELATION_NAME '.
					  'AND ins.RDB$FIELD_NAME = rf.RDB$FIELD_NAME '.
				') AS isprimary '.
			'FROM RDB$RELATION_FIELDS rf '.
			'JOIN RDB$FIELDS f ON f.RDB$FIELD_NAME = rf.RDB$FIELD_SOURCE '.
			'JOIN RDB$TYPES t ON f.RDB$FIELD_TYPE = t.RDB$TYPE AND t.RDB$FIELD_NAME = \'RDB$FIELD_TYPE\' '.
			'WHERE rf.RDB$RELATION_NAME = \''.$table.'\'';


		foreach ($this->connection->query($query) as $row) {
			//$type = explode('(', $row['Type']);
			$columns[] = array(
				'name' => trim($row->FIELDNAME),
				'table' => $table,
				'nativetype' => strtoupper(trim($row->TYPENAME)),
				'size' => $row->FIELDLENGTH,
				'unsigned' => FALSE, //(bool) strstr($row['Type'], 'unsigned'),
				'nullable' => !empty($row->NULLFLAG),
				'default' => $row->DEFAULTVALUE,
				'autoincrement' => FALSE, //$row['Extra'] === 'auto_increment',
				'primary' => (bool)$row->ISPRIMARY,
				'vendor' => (array) $row,
			);
		}
		return $columns;
	}


	/**
	 * Returns metadata for all indexes in a table.
	 *
	 * @param string $table Table name
	 * @return array array of indices
	 */
	public function getIndexes($table)
	{
		$query =
			'SELECT '.
				'ixs.RDB$FIELD_POSITION AS FIELD_POSITION, '.
				'ix.RDB$INDEX_NAME AS INDEX_NAME, '.
				'ix.RDB$UNIQUE_FLAG AS UNIQUE_FLAG, '.
				'ixs.RDB$FIELD_NAME AS FIELD_NAME, '.
				'CASE COALESCE(rc.RDB$CONSTRAINT_TYPE,\'\') WHEN \'PRIMARY KEY\' THEN 1 ELSE 0 END AS IS_PRIMARY '.
			'FROM RDB$INDICES ix '.
			'LEFT JOIN RDB$INDEX_SEGMENTS ixs ON ix.RDB$INDEX_NAME = ixs.RDB$INDEX_NAME '.
			'LEFT JOIN RDB$RELATION_CONSTRAINTS rc ON ix.RDB$RELATION_NAME = rc.RDB$RELATION_NAME AND ix.RDB$INDEX_NAME = rc.RDB$INDEX_NAME '.
			'WHERE ix.RDB$RELATION_NAME = \''.$table.'\'';

		$indexes = array();
		foreach ($this->connection->query($query) as $row) {
			$ixName = trim($row['INDEX_NAME']);
			$indexes[$ixName]['name'] = $ixName;
			$indexes[$ixName]['unique'] = (bool)$row['UNIQUE_FLAG'];
			$indexes[$ixName]['primary'] = (bool)$row['IS_PRIMARY'];
			$indexes[$ixName]['columns'][$row['FIELD_POSITION']] = $row['FIELD_NAME'];
		}
		return array_values($indexes);
	}


	/**
	 * Returns metadata for all foreign keys in a table.
	 */
	public function getForeignKeys($table)
	{
		$query =
			'SELECT '.
				'rc.RDB$CONSTRAINT_NAME AS constraint_name, '.
				'i.RDB$RELATION_NAME AS table_name, '.
				's.RDB$FIELD_NAME AS field_name, '.
				'i2.RDB$RELATION_NAME AS references_table, '.
				's2.RDB$FIELD_NAME AS references_field '.
			'FROM RDB$INDEX_SEGMENTS s '.
				'LEFT JOIN RDB$INDICES i ON i.RDB$INDEX_NAME = s.RDB$INDEX_NAME '.
				'LEFT JOIN RDB$RELATION_CONSTRAINTS rc ON rc.RDB$INDEX_NAME = s.RDB$INDEX_NAME '.
				'LEFT JOIN RDB$REF_CONSTRAINTS refc ON rc.RDB$CONSTRAINT_NAME = refc.RDB$CONSTRAINT_NAME '.
				'LEFT JOIN RDB$RELATION_CONSTRAINTS rc2 ON rc2.RDB$CONSTRAINT_NAME = refc.RDB$CONST_NAME_UQ '.
				'LEFT JOIN RDB$INDICES i2 ON i2.RDB$INDEX_NAME = rc2.RDB$INDEX_NAME '.
				'LEFT JOIN RDB$INDEX_SEGMENTS s2 ON i2.RDB$INDEX_NAME = s2.RDB$INDEX_NAME '.
			'WHERE rc.RDB$CONSTRAINT_TYPE = \'FOREIGN KEY\' AND rc.RDB$RELATION_NAME = \''.$table.'\' '.
			'ORDER BY s.RDB$FIELD_POSITION';

		$keys = array();
		foreach ($this->connection->query($query) as $id => $row) {
			$keys[$id]['name'] = trim($row['CONSTRAINT_NAME']); // foreign key name
			$keys[$id]['local'] = trim($row['FIELD_NAME']); // local columns
			$keys[$id]['table'] = trim($row['REFERENCES_TABLE']); // referenced table
			$keys[$id]['foreign'] = trim($row['REFERENCES_FIELD']); // referenced columns
		}

		return array_values($keys);
	}


	/**
	 * Returns associative array of detected types (IReflection::FIELD_*) in result set.
	 */
	public function getColumnTypes(\PDOStatement $statement)
	{
		$types = array();
		$count = $statement->columnCount();

		// it's not a good idea to run insert two-times !
		if(Nette\Utils\Strings::contains(strtoupper($statement->queryString), 'INSERT INTO'))
		{
			// SQLSTATE[IM001]: Driver does not support this function: driver doesn't support meta data
		}
		else
		{
			// run the same query on support connection which supports meta data
			if(empty($this->supportConnection)) {
				$this->supportConnection = ibase_connect(
						$this->supportConnectionOptions['database'],
						$this->supportConnectionOptions['user'],
						$this->supportConnectionOptions['password']
					);
			}
			$res = ibase_query($this->supportConnection, $statement->queryString);
			// for each column get metadata
			for ($col = 0; $col < $count; $col++) {
				$meta = $this->getColumnMeta($res, $col);
				if (isset($meta['native_type'])) {
					$types[$meta['name']] = $type = Nette\Database\Helpers::detectType($meta['native_type']);
					if ($type === Nette\Database\IReflection::FIELD_TIME) {
						$types[$meta['name']] = Nette\Database\IReflection::FIELD_TIME_INTERVAL;
					}
				}
			}
		}
		return $types;
	}


	/**
	 * @return bool
	 */
	public function isSupported($item)
	{
		return $item === self::SUPPORT_SELECT_UNGROUPED_COLUMNS || $item === self::SUPPORT_MULTI_COLUMN_AS_OR_COND || $item == self::SUPPORT_INSERT_RETURNING;
	}


	/**
	 * Reads metadata of given resultset and columnIndex
	 * @param mixed $res Result from helper connection
	 * @param mixed $col Result column index
	 * @return array
	 */
	public function getColumnMeta($res, $col)
	{
		$colInfo = ibase_field_info($res, $col);
		return array(
			'native_type' => $colInfo['type'],
			'name' => empty($colInfo['alias']) ? $colInfo['name'] : $colInfo['alias'],
			'len' => $colInfo['length'],
		);
	}



	/**
	 * Disconnect helper databse connection
	 */
	public function __destruct()
	{
		if(!empty($this->supportConnection))
		{
			ibase_close($this->supportConnection);
		}
	}

}
stenly
Člen | 6
+
0
-

+1

verex
Člen | 12
+
0
-

+1 i like a palec nahoru.

chloris
Člen | 23
+
0
-

Tak vzhledem k mně neznámému této podmínky:

https://api.nette.org/…sor.php.html#111


108:     private function formatValue($value)
109:     {
110:         if (is_string($value)) {
111:             if (strlen($value) > 20) {        // proč 20? co to je a komu to slouží, druhé straně?
112:                 $this->remaining[] = $value;
113:                 return '?';
114:
115:             } else {
116:                 return $this->connection->quote($value);
117:             }

nefunguje vždy supportConnection.

verex
Člen | 12
+
0
-
<?php
public function delimite($name)
    {
        // @see http://dev.mysql.com/doc/refman/5.0/en/identifiers.html
        return '"' . str_replace('"', '""', strtoupper($name)). '"';
    }
?>

Proč je tam použito strtoupper(…)?
Kdyz totiz mam sloupec treba nazevSloupce a dam ho do uvozovek, firebird bere nazeb sloupce case sensitive a ten strtoupper mi to jaksi porusi. Radeji bych fci delimite videl takto:

<?php
public function delimite($name)
    {
        // @see http://dev.mysql.com/doc/refman/5.0/en/identifiers.html
        return '"' . str_replace('"', '""', $name). '"';
    }
?>
chloris
Člen | 23
+
0
-

Je to tak. „strtoupper“ tam být nemá. Žil jsem v domnění, že Firebird má vše povinně v uppercase. Nicméně tento driver do Nette Database nebude fungovat vždy na 100%, protože Nette Database jako taková funguje nad PDO, které má experimental podporu pro Firebird. Takže se často stává, že aplikace volající např. proceduru „spadne“ na segfault bez chyby v logu. Řešením je opustit PDO a používat ibase extension pro PHP a tu naroubovat na Nette Database. Připravil jsem pro to extension, dám jí v nejbližších dnech do addons. Takže pak doporučuju pro větší stabilitu používat ji.

verex
Člen | 12
+
0
-

Aha, to zní lákavě.
Aktuálně mi driver dostačuje. Potřebuji jej jen na čtení dat z několika pohledů, přičemž pouze nad jedním se zatím provádí update. Replikuju si totiž data z FB databáze do MySQL pro webovou aplikaci. S tou opravou mi driver dostačuje.

Narazil jsem teda ještě na jedno úskalí: Firebird má striktně typ DATE a TIME zvlášť a když se pro něj formátuje zápis DATE naformátuje se včetně času a to se Firebirdu nelíbí – neumí tu hodnotu jako DATE použít.