diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1c3ef20 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/vendor +.DS_Store +.*.sw? +.idea +/nbproject +/node_modules diff --git a/README.md b/README.md new file mode 100644 index 0000000..cff5a40 --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# Laravel Firebird + +--- +![GitHub last commit](https://img.shields.io/github/last-commit/ejetar/laravel-firebird) +![GitHub release (latest by date)](https://img.shields.io/github/v/release/ejetar/laravel-firebird) +![GitHub](https://img.shields.io/github/license/ejetar/laravel-firebird) + +* [About](#about) +* [Compability](#compability) +* [Installation](#installation) +* [Changelog](#changelog) +* [Contributing](#contributing) +* [Credits](#credits) +* [License](#license) + +## About +With this package you can use Eloquent and QueryBuilder with a Firebird database. 🔥 + +## Compability +Support for Laravel 5.5 to 8.x with PHP 7.1+ and Firebird 1.5, 2.5, 3.0. + +## Installation + +1. Install/enable the Firebird PDO driver for PHP (`pdo_firebird`); +2. Install the package with composer: +```bash +composer require ejetar/laravel-firebird +``` +3. As of Laravel 5.5, it is not necessary to inform service providers in `config/app.php`. But if you want to inform, enter the file `config/app.php` and include the class below in the section `providers`: +```php +Ejetar\LaravelFirebird\FirebirdServiceProvider::class +``` +4. Declare your connection in section `connections` in file `config/database.php`, using firebird driver: +```php +'firebird' => [ + 'driver' => 'firebird', + 'host' => env('DB_HOST', 'localhost'), + 'database' => env('DB_DATABASE','/storage/firebird/APPLICATION.FDB'), + 'username' => env('DB_USERNAME', 'sysdba'), + 'password' => env('DB_PASSWORD', 'masterkey'), + 'charset' => env('DB_CHARSET', 'UTF8'), + 'role' => 'RDB$ADMIN', + //'engine_version' => '3.0', //it will be discovered automatically +] +``` +If you do not enter `engine_version`, it will be discovered automatically. + +## Changelog +Nothing for now... + +## Contributing +Contribute to this wonderful project, it will be a pleasure to have you with us. Let's help the free software community. You are invited to incorporate new features, make corrections, report bugs, and any other form of support. Don't forget to star in this repository! 😀 + +## Credits +This package was based on the repository [marcha/laravel-firebird](https://github.com/marcha/laravel-firebird) and its predecessors, forked and extended: +* [sim1984/laravel-firebird](https://github.com/sim1984/laravel-firebird) +* [jacquestvanzuydam/laravel-firebird](https://github.com/jacquestvanzuydam/laravel-firebird) +* [KKSzymanowski/laravel-6-firebird](https://github.com/KKSzymanowski/laravel-6-firebird) +* [harrygulliford/laravel-firebird](https://github.com/harrygulliford/laravel-firebird) + +## License +This library is a open-source software licensed under the MIT license. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..84332db --- /dev/null +++ b/composer.json @@ -0,0 +1,35 @@ +{ + "name": "ejetar/laravel-firebird", + "description": "Use Eloquent and QueryBuilder with a Firebird database \uD83D\uDD25", + "license": "MIT", + "authors": [ + { + "name": "Guilherme A. Girardi", + "email": "guilhermeagirardi@gmail.com" + } + ], + "require": { + "php": ">=7.1", + "illuminate/support": "~5.5.0|~5.6.0|~5.7.0|~5.8.0|^6.0|^7.0|^8.0", + "illuminate/container": "~5.5.0|~5.6.0|~5.7.0|~5.8.0|^6.0|^7.0|^8.0", + "illuminate/database": "~5.5.0|~5.6.0|~5.7.0|~5.8.0|^6.0|^7.0|^8.0", + "illuminate/events": "~5.5.0|~5.6.0|~5.7.0|~5.8.0|^6.0|^7.0|^8.0" + }, + "require-dev": { + "mockery/mockery": "~1.0", + "phpunit/phpunit": "~7.0|^8.0|^9.0", + "orchestra/testbench": "~3.7|^4.0|^5.0|^6.0" + }, + "autoload": { + "psr-4": { + "Ejetar\\LaravelFirebird\\": "src/" + } + }, + "extra": { + "laravel": { + "providers": [ + "Ejetar\\LaravelFirebird\\FirebirdServiceProvider" + ] + } + } +} diff --git a/src/Connection.php b/src/Connection.php new file mode 100644 index 0000000..0932743 --- /dev/null +++ b/src/Connection.php @@ -0,0 +1,203 @@ +engine_version) { + $this->engine_version = isset($this->config['engine_version']) ? $this->config['engine_version'] : null; + } + if (!$this->engine_version) { + $sql = "SELECT RDB\$GET_CONTEXT(?, ?) FROM RDB\$DATABASE"; + $sth = $this->getPdo()->prepare($sql); + $sth->execute(['SYSTEM', 'ENGINE_VERSION']); + $this->engine_version = $sth->fetchColumn(); + $sth->closeCursor(); + } + return $this->engine_version; + } + + /** + * Get major engine version + * It allows you to determine the features of the engine. + * + * @return int + */ + protected function getMajorEngineVersion() + { + $version = $this->getEngineVersion(); + $parts = explode('.', $version); + return (int)$parts[0]; + } + + /** + * Get the default query grammar instance + * + * @return QueryGrammar10|QueryGrammar20|QueryGrammar30 + */ + protected function getDefaultQueryGrammar() + { + switch ($this->getMajorEngineVersion()){ + case 1: + return new QueryGrammar10; + break; + case 3: + return new QueryGrammar30; + break; + default: + return new QueryGrammar20; + break; + } + } + + /** + * Get the default post processor instance. + * + * @return \Ejetar\LaravelFirebird\Query\Processors\FirebirdProcessor + */ + protected function getDefaultPostProcessor() + { + return new Processor; + } + + /** + * Get a schema builder instance for this connection. + * + * @return \Ejetar\LaravelFirebird\Schema\Builder + */ + public function getSchemaBuilder() + { + if (is_null($this->schemaGrammar)) { + $this->useDefaultSchemaGrammar(); + } + + return new SchemaBuilder($this); + } + + /** + * Get the default schema grammar instance. + * + * @return \Illuminate\Database\Grammar + */ + protected function getDefaultSchemaGrammar() + { + return $this->withTablePrefix(new SchemaGrammar); + } + + /** + * Get query builder + * + * @return \Ejetar\LaravelFirebird\Query\Builder + */ + protected function getQueryBuilder() + { + $processor = $this->getPostProcessor(); + $grammar = $this->getQueryGrammar(); + + return new QueryBuilder($this, $grammar, $processor); + } + + /** + * Get a new query builder instance. + * + * @return \Ejetar\LaravelFirebird\Query\Builder + */ + public function query() + { + return $this->getQueryBuilder(); + } + + /** + * Execute stored function + * + * @param string $function + * @param array $values + * @return mixed + */ + public function executeFunction($function, array $values = null) + { + $query = $this->getQueryBuilder(); + + return $query->executeFunction($function, $values); + } + + /** + * Execute stored procedure + * + * @param string $procedure + * @param array $values + */ + public function executeProcedure($procedure, array $values = null) + { + $query = $this->getQueryBuilder(); + + $query->executeProcedure($procedure, $values); + } + + /** + * Start a new database transaction. + * + * @return void + * @throws \Exception + */ + public function beginTransaction() + { + if ($this->transactions == 0 && $this->pdo->getAttribute(PDO::ATTR_AUTOCOMMIT) == 1) { + $this->pdo->setAttribute(PDO::ATTR_AUTOCOMMIT, 0); + } + parent::beginTransaction(); + } + + /** + * Commit the active database transaction. + * + * @return void + */ + public function commit() + { + parent::commit(); + if ($this->transactions == 0 && $this->pdo->getAttribute(PDO::ATTR_AUTOCOMMIT) == 0) { + $this->pdo->setAttribute(PDO::ATTR_AUTOCOMMIT, 1); + } + } + + /** + * Rollback the active database transaction. + * + * @param int|null $toLevel + * @return void + * @throws \Exception + */ + public function rollBack($toLevel = null) + { + parent::rollBack($toLevel); + if ($this->transactions == 0 && $this->pdo->getAttribute(PDO::ATTR_AUTOCOMMIT) == 0) { + $this->pdo->setAttribute(PDO::ATTR_AUTOCOMMIT, 1); + } + } + +} diff --git a/src/Eloquent/Model.php b/src/Eloquent/Model.php new file mode 100644 index 0000000..8ba0f73 --- /dev/null +++ b/src/Eloquent/Model.php @@ -0,0 +1,65 @@ +getTable(), 0, 31); + return $this->sequence ? $this->sequence : $autoSequence; + } + + /** + * Get next sequence value + * + * @param string $sequence + * + * @return int + */ + protected function nextSequenceValue($sequence = null) + { + $query = $this->getConnection()->getQueryBuilder(); + + $id = $query->nextSequenceValue($sequence ? $sequence : $this->getSequence()); + + return $id; + } + + /** + * Insert the given attributes and set the ID on the model. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param array $attributes + * @return void + */ + protected function insertAndSetId(Builder $query, $attributes) + { + $id = $this->nextSequenceValue(); + + $keyName = $this->getKeyName(); + + $attributes[$keyName] = $id; + + $query->insert($attributes); + + $this->setAttribute($keyName, $id); + } + +} diff --git a/src/FirebirdConnector.php b/src/FirebirdConnector.php new file mode 100644 index 0000000..51a29c1 --- /dev/null +++ b/src/FirebirdConnector.php @@ -0,0 +1,65 @@ +getDsn($config); + + $options = $this->getOptions($config); + + // We need to grab the PDO options that should be used while making the brand + // new connection instance. The PDO options control various aspects of the + // connection's behavior, and some might be specified by the developers. + $connection = $this->createConnection($dsn, $config, $options); + + return $connection; + } + + /** + * Create a DSN string from a configuration. + * + * @param array $config + * @return string + */ + protected function getDsn(array $config) + { + $dsn = ''; + if (isset($config['host'])) { + $dsn .= $config['host']; + } + if (isset($config['port'])) { + $dsn .= "/" . $config['port']; + } + if (!isset($config['database'])) { + throw new InvalidArgumentException("Database not given, required."); + } + if ($dsn) { + $dsn .= ':'; + } + $dsn .= $config['database'] . ';'; + if (isset($config['charset'])) { + $dsn .= "charset=" . $config['charset']; + } + if (isset($config['role'])) { + $dsn .= ";role=" . $config['role']; + } + $dsn = 'firebird:dbname=' . $dsn; + + return $dsn; + } +} diff --git a/src/FirebirdServiceProvider.php b/src/FirebirdServiceProvider.php new file mode 100644 index 0000000..ce7429d --- /dev/null +++ b/src/FirebirdServiceProvider.php @@ -0,0 +1,25 @@ +app->bind('db.connector.firebird', FirebirdConnector::class); + } +} diff --git a/src/Query/Builder.php b/src/Query/Builder.php new file mode 100644 index 0000000..9caa2c7 --- /dev/null +++ b/src/Query/Builder.php @@ -0,0 +1,76 @@ +grammar->compileGetContext($this, $namespace, $name); + + return $this->processor->processGetContextValue($this, $sql); + } + + /** + * Get next sequence value + * + * @param string $sequence + * @param int $increment + * @return int + */ + public function nextSequenceValue($sequence = null, $increment = null) + { + $sql = $this->grammar->compileNextSequenceValue($this, $sequence, $increment); + + return $this->processor->processNextSequenceValue($this, $sql); + } + + /** + * Execute stored procedure + * + * @param string $procedure + * @param array $values + */ + public function executeProcedure($procedure, array $values = null) + { + if (!$values) { + $values = []; + } + + $bindings = array_values($values); + + $sql = $this->grammar->compileExecProcedure($this, $procedure, $values); + + $this->connection->statement($sql, $this->cleanBindings($bindings)); + } + + /** + * Execute stored function + * + * @param string $function + * @param array $values + * + * @return mixed + */ + public function executeFunction($function, array $values = null) + { + if (!$values) { + $values = []; + } + + $sql = $this->grammar->compileExecProcedure($this, $function, $values); + + return $this->processor->processExecuteFunction($this, $sql, $values); + } + +} diff --git a/src/Query/Grammars/Firebird15Grammar.php b/src/Query/Grammars/Firebird15Grammar.php new file mode 100644 index 0000000..2a5f7cc --- /dev/null +++ b/src/Query/Grammars/Firebird15Grammar.php @@ -0,0 +1,112 @@ +aggregate)) return; + $select = ''; + if (count($columns) > 0 && $query->limit == null && $query->aggregate == null) + { + $select = $query->distinct ? 'select distinct ' : 'select '; + } + + return $select.$this->columnize($columns); + } + + /** + * Compile a select query into SQL. + * + * @param Illuminate\Database\Query\Builder + * + * @return string + */ + public function compileSelect(Builder $query) + { + if (is_null($query->columns)) $query->columns = array('*'); + + return trim($this->concatenate($this->compileComponents($query))); + } + + /** + * Compile an aggregated select clause. + * + * @param \Illuminate\Database\Query\Builder $query + * @param array $aggregate + * @return string + */ + protected function compileAggregate(Builder $query, $aggregate) + { + $column = $this->columnize($aggregate['columns']); + + // If the query has a "distinct" constraint and we're not asking for all columns + // we need to prepend "distinct" onto the column name so that the query takes + // it into account when it performs the aggregating operations on the data. + if ($query->distinct && $column !== '*') + { + $column = 'distinct '.$column; + } + + return 'select '.$aggregate['function'].'('.$column.') as aggregate'; + } + + /** + * Compile first instead of limit + * + * @param \Illuminate\Database\Query\Builder $query + * @param int $limit + * @return string + */ + protected function compileLimit(Builder $query, $limit) + { + return 'select first '.(int) $limit; + } + + /** + * Compile skip instead of offset + * + * @param \Illuminate\Database\Query\Builder $query + * @param int $limit + * @return string + */ + protected function compileOffset(Builder $query, $limit) + { + return 'skip '.(int) $limit; + } + +} diff --git a/src/Query/Grammars/Firebird25Grammar.php b/src/Query/Grammars/Firebird25Grammar.php new file mode 100644 index 0000000..2ebdcde --- /dev/null +++ b/src/Query/Grammars/Firebird25Grammar.php @@ -0,0 +1,232 @@ +', '<=', '>=', '<>', '!=', + 'like', 'not like', 'between', 'containing', 'starting with', + 'similar to', 'not similar to', + ]; + + /** + * Compile an aggregated select clause. + * + * @param \Illuminate\Database\Query\Builder $query + * @param array $aggregate + * @return string + */ + protected function compileAggregate(Builder $query, $aggregate) + { + $column = $this->columnize($aggregate['columns']); + + // If the query has a "distinct" constraint and we're not asking for all columns + // we need to prepend "distinct" onto the column name so that the query takes + // it into account when it performs the aggregating operations on the data. + if ($query->distinct && $column !== '*') { + $column = 'distinct ' . $column; + } + + return 'select ' . $aggregate['function'] . '(' . $column . ') as "aggregate"'; + } + + /** + * Compile SQL statement for get context variable value + * + * @param \Illuminate\Database\Query\Builder $query + * @param string $namespace + * @param string $name + * @return string + */ + public function compileGetContext(Builder $query, $namespace, $name) + { + return "SELECT RDB\$GET_CONTEXT('{$namespace}', '{$name}' AS VAL FROM RDB\$DATABASE"; + } + + /** + * Compile SQL statement for execute function + * + * @param \Illuminate\Database\Query\Builder $query + * @param string $function + * @param array $values + * @return string + */ + public function compileExecFunction(Builder $query, $function, array $values = null) + { + $function = $this->wrap($function); + + return "SELECT {$function} (" . $this->parameterize($values) . ") AS VAL FROM RDB\$DATABASE"; + } + + /** + * Compile SQL statement for execute procedure + * + * @param \Illuminate\Database\Query\Builder $query + * @param string $procedure + * @param array $values + * @return string + */ + public function compileExecProcedure(Builder $query, $procedure, array $values = null) + { + $procedure = $this->wrap($procedure); + + return "EXECUTE PROCEDURE {$$procedure} (" . $this->parameterize($values) . ')'; + } + + /** + * Compile an insert and get ID statement into SQL. + * + * @param \Illuminate\Database\Query\Builder $query + * @param array $values + * @param string $sequence + * @return string + */ + public function compileInsertGetId(Builder $query, $values, $sequence) + { + if (is_null($sequence)) { + $sequence = 'ID'; + } + + return $this->compileInsert($query, $values) . ' RETURNING ' . $this->wrap($sequence); + } + + /** + * Compile the "limit" portions of the query. + * + * @param \Illuminate\Database\Query\Builder $query + * @param int $limit + * @return string + */ + protected function compileLimit(Builder $query, $limit) + { + if ($query->offset) { + $first = (int)$query->offset + 1; + return 'ROWS ' . (int)$first; + } else { + return 'ROWS ' . (int)$limit; + } + } + + /** + * Compile the lock into SQL. + * + * @param \Illuminate\Database\Query\Builder $query + * @param bool|string $value + * @return string + */ + protected function compileLock(Builder $query, $value) + { + if (is_string($value)) { + return $value; + } + + return $value ? 'FOR UPDATE' : ''; + } + + /** + * Compile SQL statement for get next sequence value + * + * @param \Illuminate\Database\Query\Builder $query + * @param string $sequence + * @param int $increment + * @return string + */ + public function compileNextSequenceValue(Builder $query, $sequence = null, $increment = null) + { + if (!$sequence) { + $sequence = $this->wrap(substr('seq_' . $query->from, 0, 31)); + } + if ($increment) { + return "SELECT GEN_ID({$sequence}, {$increment}) AS ID FROM RDB\$DATABASE"; + } + return "SELECT NEXT VALUE FOR {$sequence} AS ID FROM RDB\$DATABASE"; + } + + /** + * Compile the "offset" portions of the query. + * + * @param \Illuminate\Database\Query\Builder $query + * @param int $offset + * @return string + */ + protected function compileOffset(Builder $query, $offset) + { + if ($query->limit) { + if ($offset) { + $end = (int)$query->limit + (int)$offset; + return 'TO ' . $end; + } else { + return ''; + } + } else { + $begin = (int)$offset + 1; + return 'ROWS ' . $begin . ' TO 2147483647'; + } + } + + /** + * Compile the additional where clauses for updates with joins. + * + * @param \Illuminate\Database\Query\Builder $query + * @return string + */ + protected function compileUpdateWheres(Builder $query) + { + $baseWhere = $this->compileWheres($query); + + return $baseWhere; + } + + /** + * Compile a date based where clause. + * + * @param string $type + * @param \Illuminate\Database\Query\Builder $query + * @param array $where + * @return string + */ + protected function dateBasedWhere($type, Builder $query, $where) + { + $value = $this->parameter($where['value']); + + return 'EXTRACT(' . $type . ' FROM ' . $this->wrap($where['column']) . ') ' . $where['operator'] . ' ' . $value; + } + + /** + * Wrap a single string in keyword identifiers. + * + * @param string $value + * @return string + */ + protected function wrapValue($value) + { + if ($value === '*') { + return $value; + } + + return "$value"; //without quotes + } + + /** + * Compile an exists statement into SQL. + * + * @param \Illuminate\Database\Query\Builder $query + * @return string + */ + public function compileExists(Builder $query) + { + $select = $this->compileSelect($query); + return "SELECT 1 as \"exists\" FROM RDB\$DATABASE WHERE EXISTS ({$select})"; + // return "select exists({$select}) as {$this->wrap('exists')}"; + } +} diff --git a/src/Query/Grammars/Firebird30Grammar.php b/src/Query/Grammars/Firebird30Grammar.php new file mode 100644 index 0000000..cb14951 --- /dev/null +++ b/src/Query/Grammars/Firebird30Grammar.php @@ -0,0 +1,61 @@ +getConnection()->selectFromWriteConnection($sql, $values); + + $sequence = $sequence ?: 'ID'; + + $result = (array)$results[0]; + + $id = $result[$sequence]; + + return is_numeric($id) ? (int)$id : $id; + } + + /** + * Process an "next sequence value" query. + * + * @param \Illuminate\Database\Query\Builder $query + * @param string $sql + * @return int + */ + public function processNextSequenceValue(Builder $query, $sql) + { + $results = $query->getConnection()->selectFromWriteConnection($sql); + + $result = (array)$results[0]; + + $id = $result['ID']; + + return is_numeric($id) ? (int)$id : $id; + } + + /** + * Process an "get context variable value" query. + * + * @param \Illuminate\Database\Query\Builder $query + * @param string $sql + * @return int + */ + public function processGetContextValue(Builder $query, $sql) + { + $result = $query->getConnection()->selectOne($sql); + + return $result['VAL']; + } + + /** + * Process an "execute function" query. + * + * @param \Illuminate\Database\Query\Builder $query + * @param string $sql + * @param array $values + * + * @return mixed + */ + public function processExecuteFunction(Builder $query, $sql, $values) + { + $result = $query->getConnection()->selectOne($sql, $values); + + return $result['VAL']; + } + + /** + * Process the results of a column listing query. + * + * @param array $results + * @return array + */ + public function processColumnListing($results) + { + $mapping = function ($r) { + $r = (object)$r; + + return trim($r->{'RDB$FIELD_NAME'}); + }; + + return array_map($mapping, $results); + } + +} diff --git a/src/Schema/Blueprint.php b/src/Schema/Blueprint.php new file mode 100644 index 0000000..23235e8 --- /dev/null +++ b/src/Schema/Blueprint.php @@ -0,0 +1,138 @@ +preserve = true; + } + + /** + * Indicate that it is necessary to use a identity modifier for increment columns + * + * @return void + */ + public function useIdentity() + { + $this->use_identity = true; + } + + /** + * Indicate that it is necessary to use native boolean type + * Reserved for future versions. Now Firebird PDO driver + * does not support the type BOOLEAN + * + * @return void + */ + public function nativeBoolean() + { + $this->use_native_boolean = true; + } + + /** + * Determine if the blueprint has a create command. + * + * @return bool + */ + protected function droping() + { + foreach ($this->commands as $command) { + if (($command->name == 'drop') || ($command->name == 'dropIfExists')) { + return true; + } + } + + return false; + } + + /** + * Add the commands that are implied by the blueprint. + * + * @param \Illuminate\Database\Schema\Grammars\Grammar $grammar + * @return void + */ + protected function addImpliedCommands(Grammar $grammar) + { + parent::addImpliedCommands($grammar); + + if (!$this->use_identity) { + $this->addSequence(); + $this->addAutoIncrementTrigger(); + } + + if ($this->droping() && !$this->use_identity) { + $this->dropSequence(); + } + } + + /** + * Add the command for create sequence for table + * + * @return void + */ + protected function addSequence() + { + foreach ($this->columns as $column) { + if ($column->autoIncrement) { + array_push($this->commands, $this->createCommand('sequenceForTable')); + break; + } + } + } + + /** + * Add the command for drop sequence for table + * + * @return void + */ + protected function dropSequence() + { + array_push($this->commands, $this->createCommand('dropSequenceForTable')); + } + + /** + * Add the command for create trigger + * + * @return void + */ + protected function addAutoIncrementTrigger() + { + foreach ($this->columns as $column) { + if ($column->autoIncrement) { + array_push($this->commands, $this->createCommand('triggerForAutoincrement', ['columnname' => $column->name])); + break; + } + } + } + +} diff --git a/src/Schema/Builder.php b/src/Schema/Builder.php new file mode 100644 index 0000000..9d30d51 --- /dev/null +++ b/src/Schema/Builder.php @@ -0,0 +1,127 @@ +resolver)) { + return call_user_func($this->resolver, $table, $callback); + } + + return new Blueprint($table, $callback); + } + + /** + * Create a new command for Sequence set with a Closure. + * + * @param string $sequence + * @param \Closure|null $callback + * @return \Ejetar\LaravelFirebird\Schema\SequenceBlueprint + */ + protected function createSequenceBlueprint($sequence, Closure $callback = null) + { + if (isset($this->resolver)) { + return call_user_func($this->resolver, $sequence, $callback); + } + + return new SequenceBlueprint($sequence, $callback); + } + + /** + * Execute the blueprint to build / modify the sequence. + * + * @param \Ejetar\LaravelFirebird\Schema\SequenceBlueprint $seqprint + * @return void + */ + protected function buildSequence(SequenceBlueprint $seqprint) + { + $seqprint->build($this->connection, $this->grammar); + } + + /** + * Determine if the given sequence exists. + * + * @param string $sequence + * @return bool + */ + public function hasSequence($sequence) + { + $sql = $this->grammar->compileSequenceExists(); + + return count($this->connection->select($sql, [$sequence])) > 0; + } + + /** + * Create a new sequence on the schema + * + * @param string $sequence + * @param \Closure $callback + * @return void + */ + public function createSequence($sequence, Closure $callback = null) + { + $seqprint = $this->createSequenceBlueprint($sequence); + + $seqprint->create(); + + if ($callback) { + $callback($seqprint); + } + + $this->buildSequence($seqprint); + } + + /** + * Drop a sequence from the schema. + * + * @param string $sequence + * @param \Closure $callback + */ + public function dropSequence($sequence) + { + $seqprint = $this->createSequenceBlueprint($sequence); + + $seqprint->drop(); + + $this->buildSequence($seqprint); + } + + /** + * Modify a sequence on the schema. + * + * @param string $sequence + * @param \Closure $callback + * @return void + */ + public function sequence($sequence, Closure $callback) + { + $this->buildSequence($this->createSequenceBlueprint($sequence, $callback)); + } + + /** + * Drop a sequence from the schema if it exists. + * + * @param string $sequence + * @return void + */ + public function dropSequenceIfExists($sequence) + { + $blueprint = $this->createSequenceBlueprint($sequence); + + $blueprint->dropIfExists(); + + $this->buildSequence($blueprint); + } + +} diff --git a/src/Schema/Grammars/FirebirdGrammar.php b/src/Schema/Grammars/FirebirdGrammar.php new file mode 100644 index 0000000..e7cbc69 --- /dev/null +++ b/src/Schema/Grammars/FirebirdGrammar.php @@ -0,0 +1,762 @@ +getColumns($blueprint)); + + $sql = $blueprint->temporary ? 'CREATE TEMPORARY' : 'CREATE'; + + $sql .= ' TABLE ' . $this->wrapTable($blueprint) . " ($columns)"; + + if ($blueprint->temporary) { + if ($blueprint->preserve) { + $sql .= ' ON COMMIT DELETE ROWS'; + } else { + $sql .= ' ON COMMIT PRESERVE ROWS'; + } + } + + return $sql; + } + + /** + * Compile a drop table command. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $command + * @return string + */ + public function compileDrop(Blueprint $blueprint, Fluent $command) + { + return 'DROP TABLE ' . $this->wrapTable($blueprint); + } + + /** + * Compile a column addition command. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $command + * @return string + */ + public function compileAdd(Blueprint $blueprint, Fluent $command) + { + $table = $this->wrapTable($blueprint); + + $columns = $this->prefixArray('ADD', $this->getColumns($blueprint)); + + return 'ALTER TABLE ' . $table . ' ' . implode(', ', $columns); + } + + /** + * Compile a primary key command. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $command + * @return string + */ + public function compilePrimary(Blueprint $blueprint, Fluent $command) + { + $columns = $this->columnize($command->columns); + + return 'ALTER TABLE ' . $this->wrapTable($blueprint) . " ADD PRIMARY KEY ({$columns})"; + } + + /** + * Compile a unique key command. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $command + * @return string + */ + public function compileUnique(Blueprint $blueprint, Fluent $command) + { + $table = $this->wrapTable($blueprint); + + $index = $this->wrap(substr($command->index, 0, 31)); + + $columns = $this->columnize($command->columns); + + return "ALTER TABLE {$table} ADD CONSTRAINT {$index} UNIQUE ({$columns})"; + } + + /** + * Compile a plain index key command. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $command + * @return string + */ + public function compileIndex(Blueprint $blueprint, Fluent $command) + { + $columns = $this->columnize($command->columns); + + $index = $this->wrap(substr($command->index, 0, 31)); + + $table = $this->wrapTable($blueprint); + + return "CREATE INDEX {$index} ON {$table} ($columns)"; + } + + /** + * Compile a foreign key command. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $command + * @return string + */ + public function compileForeign(Blueprint $blueprint, Fluent $command) + { + $table = $this->wrapTable($blueprint); + + $on = $this->wrapTable($command->on); + + // We need to prepare several of the elements of the foreign key definition + // before we can create the SQL, such as wrapping the tables and convert + // an array of columns to comma-delimited strings for the SQL queries. + $columns = $this->columnize($command->columns); + + $onColumns = $this->columnize((array)$command->references); + + $fkName = substr($command->index, 0, 31); + + $sql = "ALTER TABLE {$table} ADD CONSTRAINT {$fkName} "; + + $sql .= "FOREIGN KEY ({$columns}) REFERENCES {$on} ({$onColumns})"; + + // Once we have the basic foreign key creation statement constructed we can + // build out the syntax for what should happen on an update or delete of + // the affected columns, which will get something like "cascade", etc. + if (!is_null($command->onDelete)) { + $sql .= " ON DELETE {$command->onDelete}"; + } + + if (!is_null($command->onUpdate)) { + $sql .= " ON UPDATE {$command->onUpdate}"; + } + + return $sql; + } + + /** + * Compile a drop foreign key command. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $command + * @return string + */ + public function compileDropForeign(Blueprint $blueprint, Fluent $command) + { + $table = $this->wrapTable($blueprint); + + return "ALTER TABLE {$table} DROP CONSTRAINT {$command->index}"; + } + + /** + * Get the SQL for a character set column modifier. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $column + * @return string|null + */ + protected function modifyCharset(Blueprint $blueprint, Fluent $column) + { + if (!is_null($column->charset)) { + return ' CHARACTER SET ' . $column->charset; + } + } + + /** + * Get the SQL for a collation column modifier. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $column + * @return string|null + */ + protected function modifyCollate(Blueprint $blueprint, Fluent $column) + { + if (!is_null($column->collation)) { + return ' COLLATE ' . $column->collation; + } + } + + /** + * Get the SQL for a nullable column modifier. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $column + * @return string|null + */ + protected function modifyNullable(Blueprint $blueprint, Fluent $column) + { + return $column->nullable ? '' : ' NOT NULL'; + } + + /** + * Get the SQL for a default column modifier. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $column + * @return string|null + */ + protected function modifyDefault(Blueprint $blueprint, Fluent $column) + { + if (!is_null($column->default)) { + return ' DEFAULT ' . $this->getDefaultValue($column->default); + } + } + + /** + * Get the SQL for an auto-increment column modifier. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $column + * @return string|null + */ + protected function modifyIncrement(Blueprint $blueprint, Fluent $column) + { + if (in_array($column->type, $this->serials) && $column->autoIncrement) { + // identity columns support beginning Firebird 3.0 and above + return $blueprint->use_identity ? ' GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY' : ' PRIMARY KEY'; + } + } + + /** + * Create the column definition for a char type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeChar(Fluent $column) + { + return "CHAR({$column->length})"; + } + + /** + * Create the column definition for a string type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeString(Fluent $column) + { + return "VARCHAR({$column->length})"; + } + + /** + * Create the column definition for a text type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeText(Fluent $column) + { + return 'BLOB SUB_TYPE TEXT'; + } + + /** + * Create the column definition for a medium text type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeMediumText(Fluent $column) + { + return 'BLOB SUB_TYPE TEXT'; + } + + /** + * Create the column definition for a long text type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeLongText(Fluent $column) + { + return 'BLOB SUB_TYPE TEXT'; + } + + /** + * Create the column definition for a integer type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeInteger(Fluent $column) + { + return 'INTEGER'; + } + + /** + * Create the column definition for a big integer type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeBigInteger(Fluent $column) + { + return 'BIGINT'; + } + + /** + * Create the column definition for a medium integer type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeMediumInteger(Fluent $column) + { + return 'INTEGER'; + } + + /** + * Create the column definition for a tiny integer type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeTinyInteger(Fluent $column) + { + return 'SMALLINT'; + } + + /** + * Create the column definition for a small integer type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeSmallInteger(Fluent $column) + { + return 'SMALLINT'; + } + + /** + * Create the column definition for a float type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeFloat(Fluent $column) + { + return 'FLOAT'; + } + + /** + * Create the column definition for a double type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeDouble(Fluent $column) + { + return 'DOUBLE PRECISION'; + } + + /** + * Create the column definition for a decimal type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeDecimal(Fluent $column) + { + return "DECIMAL({$column->total}, {$column->places})"; + } + + /** + * Create the column definition for a boolean type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeBoolean(Fluent $column) + { + // Firebird 3.0 support native type BOOLEAN, but + // PDO dosn't support + return 'CHAR(1)'; + } + + /** + * Create the column definition for an enum type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeEnum(Fluent $column) + { + $allowed = array_map(function ($a) { + return "'" . $a . "'"; + }, $column->allowed); + + return "VARCHAR(255) CHECK (\"{$column->name}\" IN (" . implode(', ', $allowed) . '))'; + } + + /** + * Create the column definition for a json type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeJson(Fluent $column) + { + return 'VARCHAR(8191)'; + } + + /** + * Create the column definition for a jsonb type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeJsonb(Fluent $column) + { + return 'VARCHAR(8191) CHARACTER SET OCTETS'; + } + + /** + * Create the column definition for a date type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeDate(Fluent $column) + { + return 'DATE'; + } + + /** + * Create the column definition for a date-time type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeDateTime(Fluent $column) + { + return 'TIMESTAMP'; + } + + /** + * Create the column definition for a date-time type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeDateTimeTz(Fluent $column) + { + // Firebird don't support timezones + return 'TIMESTAMP'; + } + + /** + * Create the column definition for a time type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeTime(Fluent $column) + { + return 'TIME'; + } + + /** + * Create the column definition for a time type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeTimeTz(Fluent $column) + { + // Firebird don't support timezones + return 'TIME'; + } + + /** + * Create the column definition for a timestamp type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeTimestamp(Fluent $column) + { + if ($column->useCurrent) { + return 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP'; + } + + return 'TIMESTAMP'; + } + + /** + * Create the column definition for a timestamp type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeTimestampTz(Fluent $column) + { + if ($column->useCurrent) { + return 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP'; + } + + return 'TIMESTAMP'; + } + + /** + * Create the column definition for a binary type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeBinary(Fluent $column) + { + return 'BLOB SUB_TYPE BINARY'; + } + + /** + * Create the column definition for a uuid type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeUuid(Fluent $column) + { + return 'CHAR(36)'; + } + + /** + * Create the column definition for an IP address type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeIpAddress(Fluent $column) + { + return 'VARCHAR(45)'; + } + + /** + * Create the column definition for a MAC address type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeMacAddress(Fluent $column) + { + return 'VARCHAR(17)'; + } + + /** + * Compile a create sequence command. + * + * @param \Ejetar\LaravelFirebird\Schema\SequenceBlueprint $blueprint + * @param \Illuminate\Support\Fluent $command + * @return string + */ + public function compileCreateSequence(SequenceBlueprint $blueprint, Fluent $command) + { + $sql = 'CREATE SEQUENCE '; + $sql .= $this->wrapSequence($blueprint); + if ($blueprint->getInitialValue() !== 0) { + $sql .= ' START WITH ' . $blueprint->getInitialValue(); + } + if ($blueprint->getIncrement() !== 1) { + $sql .= ' INCREMENT BY ' . $blueprint->getIncrement(); + } + return $sql; + } + + /** + * Compile a create sequence command for table. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $command + * @return string + */ + public function compileSequenceForTable(Blueprint $blueprint, Fluent $command) + { + + $sequence = strtoupper(substr('SEQ_' . $blueprint->getTable(), 0, 31)); + + return "CREATE SEQUENCE {$sequence}"; + } + + /** + * Compile a drop sequence command for table. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $command + * @return string + */ + public function compileDropSequenceForTable(Blueprint $blueprint, Fluent $command) + { + $sequenceName = substr('SEQ_' . $blueprint->getTable(), 0, 31); + $sequence = strtoupper($sequenceName); + + $sql = 'EXECUTE BLOCK' . "\n"; + $sql .= 'AS' . "\n"; + $sql .= 'BEGIN' . "\n"; + $sql .= " IF (EXISTS(SELECT * FROM RDB\$GENERATORS WHERE RDB\$GENERATOR_NAME = '{$sequenceName}')) THEN" . "\n"; + $sql .= " EXECUTE STATEMENT 'DROP SEQUENCE {$sequence}';" . "\n"; + $sql .= 'END'; + return $sql; + } + + /** + * Compile a create trigger for support autoincrement. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $command + * @return string + */ + public function compileTriggerForAutoincrement(Blueprint $blueprint, Fluent $command) + { + $table = $this->wrapTable($blueprint); + $trigger = strtoupper(substr('TR_' . $blueprint->getTable() . '_BI', 0, 31)); + $column = strtoupper($command->columnname); + $sequence = strtoupper(substr('SEQ_' . $blueprint->getTable(), 0, 31)); + + $sql = "CREATE OR ALTER TRIGGER {$trigger} FOR {$table}\n"; + $sql .= "ACTIVE BEFORE INSERT POSITION 0\n"; + $sql .= "AS\n"; + $sql .= "BEGIN\n"; + $sql .= " IF (NEW.{$column} IS NULL) THEN \n"; + $sql .= " NEW.{$column} = NEXT VALUE FOR {$sequence};\n"; + $sql .= 'END'; + + return $sql; + } + + /** + * Compile a alter sequence command. + * + * @param \Ejetar\LaravelFirebird\Schema\SequenceBlueprint $blueprint + * @param \Illuminate\Support\Fluent $command + * @return string + */ + public function compileAlterSequence(SequenceBlueprint $blueprint, Fluent $command) + { + $sql = 'ALTER SEQUENCE '; + $sql .= $this->wrapSequence($blueprint); + if ($blueprint->isRestart()) { + $sql .= ' RESTART'; + if ($blueprint->getInitialValue() !== null) { + $sql .= ' WITH ' . $blueprint->getInitialValue(); + } + } + if ($blueprint->getIncrement() !== 1) { + $sql .= ' INCREMENT BY ' . $blueprint->getIncrement(); + } + return $sql; + } + + /** + * Compile a drop sequence command. + * + * @param \Ejetar\LaravelFirebird\Schema\SequenceBlueprint $blueprint + * @param \Illuminate\Support\Fluent $command + * @return string + */ + public function compileDropSequence(SequenceBlueprint $blueprint, Fluent $command) + { + return 'DROP SEQUENCE ' . $this->wrapSequence($blueprint); + } + + /** + * Compile a drop sequence command. + * + * @param \Ejetar\LaravelFirebird\Schema\SequenceBlueprint $blueprint + * @param \Illuminate\Support\Fluent $command + * @return string + */ + public function compileDropSequenceIfExists(SequenceBlueprint $blueprint, Fluent $command) + { + $sql = 'EXECUTE BLOCK' . "\n"; + $sql .= 'AS' . "\n"; + $sql .= 'BEGIN' . "\n"; + $sql .= " IF (EXISTS(SELECT * FROM RDB\$GENERATORS WHERE RDB\$GENERATOR_NAME = '" . $blueprint->getSequence() . "')) THEN" . "\n"; + $sql .= " EXECUTE STATEMENT 'DROP SEQUENCE " . $this->wrapSequence($blueprint) . "';" . "\n"; + $sql .= 'END'; + return $sql; + } + + /** + * Wrap a sequence in keyword identifiers. + * + * @param mixed $sequence + * @return string + */ + public function wrapSequence($sequence) + { + if ($sequence instanceof SequenceBlueprint) { + $sequence = $sequence->getSequence(); + } + + if ($this->isExpression($sequence)) { + return $this->getValue($sequence); + } + + return $this->wrap($this->tablePrefix . $sequence, true); + } +} diff --git a/src/Schema/SequenceBlueprint.php b/src/Schema/SequenceBlueprint.php new file mode 100644 index 0000000..e92d25e --- /dev/null +++ b/src/Schema/SequenceBlueprint.php @@ -0,0 +1,293 @@ +sequence = $sequence; + + if (!is_null($callback)) { + $callback($this); + } + } + + /** + * Execute the blueprint against the database. + * + * @param \Illuminate\Database\Connection $connection + * @param \Illuminate\Database\Schema\Grammars\Grammar $grammar + * @return void + */ + public function build(Connection $connection, Grammar $grammar) + { + foreach ($this->toSql($connection, $grammar) as $statement) { + $connection->statement($statement); + } + } + + /** + * Indicate that the table needs to be created. + * + * @return \Illuminate\Support\Fluent + */ + public function create() + { + return $this->addCommand('createSequence'); + } + + /** + * Determine if the blueprint has a create command. + * + * @return bool + */ + protected function creating() + { + foreach ($this->commands as $command) { + if ($command->name == 'createSequence') { + return true; + } + } + + return false; + } + + /** + * Determine if the blueprint has a drop command. + * + * @return bool + */ + protected function dropping() + { + foreach ($this->commands as $command) { + if ($command->name == 'dropSequence') { + return true; + } + if ($command->name == 'dropSequenceIfExists') { + return true; + } + } + + return false; + } + + /** + * Indicate that the table should be dropped. + * + * @return \Illuminate\Support\Fluent + */ + public function drop() + { + return $this->addCommand('dropSequence'); + } + + /** + * Indicate that the table should be dropped if it exists. + * + * @return \Illuminate\Support\Fluent + */ + public function dropIfExists() + { + return $this->addCommand('dropSequenceIfExists'); + } + + /** + * Add a new command to the blueprint. + * + * @param string $name + * @param array $parameters + * @return \Illuminate\Support\Fluent + */ + protected function addCommand($name, array $parameters = []) + { + $this->commands[] = $command = $this->createCommand($name, $parameters); + + return $command; + } + + /** + * Create a new Fluent command. + * + * @param string $name + * @param array $parameters + * @return \Illuminate\Support\Fluent + */ + protected function createCommand($name, array $parameters = []) + { + return new Fluent(array_merge(compact('name'), $parameters)); + } + + /** + * Get the commands on the blueprint. + * + * @return array + */ + public function getCommands() + { + return $this->commands; + } + + /** + * Get increment for the sequence + * + * @return int + */ + public function getIncrement() + { + return $this->increment; + } + + /** + * Get initial value for the sequence + * + * @return int + */ + public function getInitialValue() + { + return $this->start_with; + } + + /** + * Get the sequence the blueprint describes. + * + * @return string + */ + public function getSequence() + { + return $this->sequence; + } + + /** + * Set the sequence increment + * + * @param int $increment + */ + public function increment($increment) + { + $this->increment = $increment; + } + + /** + * Get the sequence restart flag + * + * @return bool + */ + public function isRestart() + { + return $this->restart; + } + + /** + * Set initial value for the sequence + * + * @param int $startWith + */ + public function startWith($startWith) + { + $this->start_with = $startWith; + } + + /** + * Restart sequence and set initial value + * + * @param int $startWith + */ + public function restart($startWith = null) + { + $this->restart = true; + $this->start_with = $startWith; + } + + /** + * Add the commands that are implied by the blueprint. + * + * @return void + */ + protected function addImpliedCommands() + { + if (($this->restart || ($this->increment !== 1)) && + !$this->creating() && + !$this->dropping()) { + array_unshift($this->commands, $this->createCommand('alterSequence')); + } + } + + /** + * Get the raw SQL statements for the blueprint. + * + * @param \Illuminate\Database\Connection $connection + * @param \Illuminate\Database\Schema\Grammars\Grammar $grammar + * @return array + */ + public function toSql(Connection $connection, Grammar $grammar) + { + $this->addImpliedCommands(); + + $statements = []; + + // Each type of command has a corresponding compiler function on the schema + // grammar which is used to build the necessary SQL statements to build + // the sequence blueprint element, so we'll just call that compilers function. + foreach ($this->commands as $command) { + $method = 'compile' . ucfirst($command->name); + + if (method_exists($grammar, $method)) { + if (!is_null($sql = $grammar->$method($this, $command, $connection))) { + $statements = array_merge($statements, (array)$sql); + } + } + } + + return $statements; + } + +}