Back to Home

Adding Firebird DBMS Support to the Laravel Framework

Firebird · Laravel · PHP

Adding Firebird DBMS Support to the Laravel Framework

    When writing an example (later there will be a link) of a PHP web application using the Firebird DBMS, the question arose of choosing a framework for development using the MVC architectural model. The choice of frameworks for PHP is very large, but the most convenient, simple and easily extensible seemed Laravel. However, this framework did not support Firebird DBMS out of the box. Laravel uses the PDO driver to work with the database. Since there is a PDO driver for Firebird, it made me think that it’s possible to make Laravel work with Firebird with some effort.

    Laravel- a free open-source web framework designed for development using the MVC architectural model (Eng. Model View Controller - model-view-controller). Laravel is a convenient and easily extensible framework for building your web applications. Out of the box, the Laravel framework supports 4 DBMSs: MySQL, Postgres, SQLite, and MS SQL Server. In this article I will tell you how to add another Firebird DBMS.

    FirebirdConnection Connection Class


    Each time you connect to the database, using the Illuminate \ Database \ Connectors \ ConnectionFactory factory, a specific connection instance is created depending on the type of DBMS that implements the Illuminate \ Database \ ConnectionInterface interface. In addition, the factory creates a connector, which, based on the configuration parameters, forms a connection string and passes it to the PDO connection designer. The connector is created in the createConnector method. We modify it a little so that it can create a connector for Firebird.

    public function createConnector(array $config)
    {
        if (! isset($config['driver'])) {
            throw new InvalidArgumentException('A driver must be specified.');
        }
        if ($this->container->bound($key = "db.connector.{$config['driver']}")) {
            return $this->container->make($key);
        }
        switch ($config['driver']) {
            case 'mysql':
                return new MySqlConnector;
            case 'pgsql':
                return new PostgresConnector;
            case 'sqlite':
                return new SQLiteConnector;
            case 'sqlsrv':
                return new SqlServerConnector;
            case 'firebird': // Add support Firebird
                return new FirebirdConnector;                
        }
        throw new InvalidArgumentException("Unsupported driver [{$config['driver']}]");
    }
    

    The Illuminate \ Database \ Connectors \ FirebirdConnector connector class itself will be defined a little later. In the meantime, we modify the createConnection method designed to create a connection that implements the Illuminate \ Database \ ConnectionInterface interface.

    protected function createConnection($driver, $connection, $database, $prefix = '', 
                                        array $config = [])
    {
        if ($this->container->bound($key = "db.connection.{$driver}")) {
            return $this->container->make($key, [$connection, $database, $prefix, $config]);
        }
        switch ($driver) {
            case 'mysql':
                return new MySqlConnection($connection, $database, $prefix, $config);
            case 'pgsql':
                return new PostgresConnection($connection, $database, $prefix, $config);
            case 'sqlite':
                return new SQLiteConnection($connection, $database, $prefix, $config);
            case 'sqlsrv':
                return new SqlServerConnection($connection, $database, $prefix, $config);
            case 'firebird': // Add support Firebird
                return new FirebirdConnection($connection, $database, $prefix, $config);                   
        }
        throw new InvalidArgumentException("Unsupported driver [$driver]");
     }
    

    Now let's move on to creating a connector for Firebird - the Illuminate \ Database \ Connectors \ FirebirdConnector class. You can take any of the existing connectors as a basis, for example, a connector for Postgres and remake it for Firebird.

    To begin with, we will change the method for forming the connection string in accordance with the format of the string described in the documentation :

    protected function getDsn(array $config) {
        $dsn = "firebird:dbname=";
        if (isset($config['host'])) {
            $dsn .= $config['host'];
        }
        if (isset($config['port'])) {
            $dsn .= "/" . $config['port'];
        }
        $dsn .= ":" . $config['database'];
        if (isset($config['charset'])) {
            $dsn .= ";charset=" . $config['charset'];
        }     
        if (isset($config['role'])) {
            $dsn .= ";role=" . $config['role'];
        }   
        return $dsn;
     }
    

    The method that creates the PDO connection, in this case will be simplified as much as possible

    public function connect(array $config) {
        $dsn = $this->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;
    }
    

    The connections themselves for the various DBMSs used in Laravel inherit the Illuminate \ Database \ Connection class. It is this class that encapsulates in itself all the possibilities for working with databases that are used in Laravel, in particular in ORM Eloquent. In the successor classes (for each type of DBMS), methods are implemented that return an instance of the class with the grammar description that is required when building DML queries, and a grammar instance for DDL queries (used in migration), also helpers to add the schema name to the table. This class will look like this:

    namespace Illuminate\Database;
    use Illuminate\Database\Query\Processors\FirebirdProcessor;
    use Doctrine\DBAL\Driver\PDOFirebird\Driver as DoctrineDriver;
    use Illuminate\Database\Query\Grammars\FirebirdGrammar as QueryGrammar;
    use Illuminate\Database\Schema\Grammars\FirebirdGrammar as SchemaGrammar;
    class FirebirdConnection extends Connection
    {
        /**
         * Get the default query grammar instance.
         *
         * @return \Illuminate\Database\Query\Grammars\FirebirdGrammar
         */
        protected function getDefaultQueryGrammar()
        {
            return $this->withTablePrefix(new QueryGrammar);
        }
        /**
         * Get the default schema grammar instance.
         *
         * @return \Illuminate\Database\Schema\Grammars\FirebirdGrammar
         */
        protected function getDefaultSchemaGrammar()
        {
            return $this->withTablePrefix(new SchemaGrammar);
        }
        /**
         * Get the default post processor instance.
         *
         * @return \Illuminate\Database\Query\Processors\FirebirdProcessor
         */
        protected function getDefaultPostProcessor()
        {
            return new FirebirdProcessor;
        }
        /**
         * Get the Doctrine DBAL driver.
         *
         * @return \Doctrine\DBAL\Driver\PDOFirebird\Driver
         */
        protected function getDoctrineDriver()
        {
            return new DoctrineDriver;
        }
    }
    

    The method for returning an instance of the Doctrine driver is formally required, so we introduce it, but I didn’t have a goal to work with Doctrine (only with Eloquent), so I did not implement it. If you wish, you can do it yourself.

    The Illuminate \ Database \ Query \ Processors \ FirebirdProcessor postprocessor is intended for additional processing of query results, in particular, it helps to extract the record identifier from an INSERT request. The definition of its implementation is fully copied from Illuminate \ Database \ Query \ Processors \ PostgresProcessor.

    Grammar class for building DML queries


    Now we turn to the most interesting and important, namely the description of the grammar for building DML queries. It is this class that is responsible for converting the expression written for the Laravel query builder into a dialect of the SQL language used in your DBMS. Knowing what a particular construction does in one DBMS, you can easily write this construction for Firebird. In fact, the DML part of the SQL language is fairly standard and not significantly different for different DBMSs, at least in terms of those queries that can be built using Laravel.

    DML query grammars inherit from the Illuminate \ Database \ Query \ Grammars \ Grammar class. The protected $ selectComponents property lists the parts of the SELECT query from which the query is assembled by the \ Illuminate \ Database \ Query \ Builder builder. In the compileComponents method, these parts are bypassed and for each of them a method is called with a name that consists of the name of the request part and the compile prefix.

        /**
         * Compile the components necessary for a select clause.
         *
         * @param  \Illuminate\Database\Query\Builder  $query
         * @return array
         */
        protected function compileComponents(Builder $query)
        {
            $sql = [];
            foreach ($this->selectComponents as $component) {
                // To compile the query, we'll spin through each component of the query and
                // see if that component exists. If it does we'll just call the compiler
                // function for the component which is responsible for making the SQL.
                if (! is_null($query->$component)) {
                    $method = 'compile'.ucfirst($component);
                    $sql[$component] = $this->$method($query, $query->$component);
                }
            }
            return $sql;
        }
    

    Knowing this fact, it becomes clear what and where to modify. Now is the time to create a descendant of the Illuminate \ Database \ Query \ Grammars \ Grammar class to define the Firebird grammar - Illuminate \ Database \ Query \ Grammars \ FirebirdGrammar. Define the main distinguishing features of Firebird.

    For a simple INSERT query, the main difference is the ability to return the newly added row using the RETURNING clause. In Laravel, this is used to return the identifier of the string just added. However, MySQL does not have this capability, and therefore the compileInsertGetId method looks different for different DBMSs. Firebird supports the RETURNING clause, as does Postgres DBMS, and therefore this method can be taken from the grammar for Postgres. It will look like this:

        /**
         * 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);
        }
    

    Perhaps the most important difference for a SELECT query is the limit on the number of records returned by the query, which is often used in paged navigation. For example, the following expression:

    DB::table('goods')->orderBy('name')->skip(10)->take(20)->get();
    

    In various DBMSs it will look in SQL very differently. In MySQL, it looks like this:

    SELECT * 
    FROM goods
    ORDER BY name
    LIMIT 10, 20
    

    in Postgres like this:

    SELECT * 
    FROM goods
    ORDER BY name
    LIMIT 20 OFFSET 10
    

    There are three options in Firebird. Starting from version 1.5:

    SELECT FIRST(10) SKIP(20) * 
    FROM goods
    ORDER BY name
    

    Starting with version 2.0, one more construction is added:
    SELECT * 
    FROM goods
    ORDER BY name
    ROWS 21, 30
    

    Starting with version 3.0, a construction from the SQL-2011 standard has been added:

    SELECT * 
    FROM color
    ORDER BY name
    OFFSET 20 ROWS
    FETCH FIRST 10 ROWS ONLY
    

    The most convenient and correct option, of course, is the one from the standard, but I wanted Laravel to support both Firebird 2.5 and 3.0, so we will choose the second option. In this case, the compileLimit and compileOffset methods will look like this:

        /**
         * 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 "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';
            }
        }
    

    The next thing that differentiates the queries is by extracting the parts of the date, this is done using the dateBasedWhere method. Firebird uses the standard EXTRACT function for this. With this in mind, our method will look like this:

        /**
         * 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;
        }
    

    That's all, all the main distinguishing features have been taken into account. You can find a fully implemented class Illuminate \ Database \ Query \ Grammars \ FirebirdGrammar in the source codes attached to the article.

    Grammar class for building DDL queries


    Now we turn to a more complex grammar used in constructing database schemas. This grammar is used for the so-called migrations (in terms of Laravel). There are much more differences between different DBMSs, from data types to auto-increment fields. In addition, here you will need to rewrite queries to a number of system tables here to determine the presence of a table or column.

    Grammar DDL queries inherit from the class Illuminate \ Database \ Schema \ Grammars \ Grammar. Create your own grammar Illuminate \ Database \ Schema \ Grammars \ FirebirdGrammar. The protected property $ modifiers lists table field modifiers, for each of the modifiers listed in the array there should be a method that starts with modify, followed by the name of the modifier. We build these methods by analogy with the grammar for MySQL, but taking into account the specifics of Firebird.

    Column Modifier Support
    class FirebirdGrammar extends Grammar {
        /**
         * The possible column modifiers.
         *
         * @var array
         */
        protected $modifiers = ['Charset', 'Collate', 'Increment', 'Nullable', 'Default'];
        /**
         * The columns available as serials.
         *
         * @var array
         */
        protected $serials = ['bigInteger', 'integer', 
            'mediumInteger', 'smallInteger', 'tinyInteger'];
    // …………………… пропущено ………………
        /**
         * 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) {
                return ' primary key';
            }
        }
    // …………………… пропущено ………………
    }
    


    The $ serials array lists the types (available in Laravel) for which the Increment modifier (auto-increment column) is available. The types available in Laravel should be considered separately. The types available in Laravel are listed in the migration documentation under "Available column types." To convert the type available in Laravel to the data type of a specific DBMS inside the grammar, methods starting with the word type followed by the type name are used.

    Data Type Support
        /**
         * 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 $column->autoIncrement ? 'INTEGER GENERATED BY DEFAULT AS IDENTITY' : 'INTEGER';
        }
        /**
         * Create the column definition for a big integer type.
         *
         * @param  \Illuminate\Support\Fluent  $column
         * @return string
         */
        protected function typeBigInteger(Fluent $column) {
            return $column->autoIncrement ? 'BIGINT GENERATED BY DEFAULT AS IDENTITY' : 'BIGINT';
        }
        /**
         * Create the column definition for a medium integer type.
         *
         * @param  \Illuminate\Support\Fluent  $column
         * @return string
         */
        protected function typeMediumInteger(Fluent $column) {
            return $column->autoIncrement ? 'INTEGER GENERATED BY DEFAULT AS IDENTITY' : 'INTEGER';
        }
        /**
         * Create the column definition for a tiny integer type.
         *
         * @param  \Illuminate\Support\Fluent  $column
         * @return string
         */
        protected function typeTinyInteger(Fluent $column) {
            return $column->autoIncrement ? 'SMALLINT GENERATED BY DEFAULT AS IDENTITY' : 'SMALLINT';
        }
        /**
         * Create the column definition for a small integer type.
         *
         * @param  \Illuminate\Support\Fluent  $column
         * @return string
         */
        protected function typeSmallInteger(Fluent $column) {
            return $column->autoIncrement ? 'SMALLINT GENERATED BY DEFAULT AS IDENTITY' : 'SMALLINT';
        }
        /**
         * Create the column definition for a float type.
         *
         * @param  \Illuminate\Support\Fluent  $column
         * @return string
         */
        protected function typeFloat(Fluent $column) {
            return $this->typeDouble($column);
        }
        /**
         * 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) {
            return 'boolean';
        }
        /**
         * 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)';
        }
        /**
         * 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) {
            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) {
            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 'varchar(8191) CHARACTER SET OCTETS';
        }
        /**
         * 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)';
        }

    Note on auto-incrementing columns


    Identity columns are available starting with Firebird 3.0. Prior to version 3.0, a generator and a BEFORE INSERT trigger were used for similar functionality. Example:

    CREATE TABLE USERS (
        ID INTEGER GENERATED BY DEFAULT AS IDENTITY,
    …
    );
    

    Similar functionality can be obtained like this:

    CREATE TABLE USERS (
        ID INTEGER,
    …
    );
    CREATE SEQUENCE SEQ_USERS;
    CREATE TRIGGER TR_USERS_BI FOR USERS
    ACTIVE BEFORE INSERT
    AS
    BEGIN
      IF (NEW.ID IS NULL) THEN
        NEW.ID = NEXT VALUE FOR SEQ_USERS;
    END
    

    Laravel migrations do not support any schema objects other than tables. Those. creation and modification of sequences, and even more so triggers, is not supported. However, sequences are part of the functionality of a fairly large number of DBMSs, including Postgres and MS SQL (since 2012). How to add sequence support to Laravel migration will be described later in this article.

    Now add two methods that the query returns to check for the existence of the table and the column inside the table.

    /**
      * Compile the query to determine if a table exists.
      *
      * @return string
      */
    public function compileTableExists() {
        return 'select * from RDB$RELATIONS where RDB$RELATION_NAME = ?';
    }
    /**
      * Compile the query to determine the list of columns.
      *
      * @param  string  $table
      * @return string
      */
    public function compileColumnExists($table) {
        return "select TRIM(RDB\$FIELD_NAME) AS \"column_name\" from RDB\$RELATION_FIELDS where RDB\$RELATION_NAME = '$table'";
    }
    

    Add a compileCreate method to create a CREATE TABLE statement. The same method is used to create temporary GTT tables. Quite strange, but even for Postgres DBMS there is only one type of GTT created - ON COMMIT DELETE ROWS, but we implement support for both types of GTT at once.

        /**
         * Compile a create table command.
         *
         * @param  \Illuminate\Database\Schema\Blueprint  $blueprint
         * @param  \Illuminate\Support\Fluent  $command
         * @return string
         */
        public function compileCreate(Blueprint $blueprint, Fluent $command) {
            $columns = implode(', ', $this->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;
        }
    

    The Illuminate \ Database \ Schema \ Blueprint class does not contain the $ preserve property, so we will add it, as well as a method for installing it. The Blueprint class is designed to generate a query or set of queries to support the creation, modification, and deletion of table metadata.

    class Blueprint
    {
    // …………… Пропущено
        /**
         * Whether a temporary table such as ON COMMIT PRESERVE ROWS
         * 
         * @var bool 
         */
        public $preserve = false;
    // …………… Пропущено
        /**
         * Indicate that the temporary table as ON COMMIT PRESERVE ROWS.
         * 
         * @return void
         */
        public function preserveRows() {
            $this->preserve = true;
        }
    // …………… Пропущено
    }
    

    Back to the Illuminate \ Database \ Schema \ Grammars \ FirebirdGrammar grammar class. Add a method to it to create a table delete operator.

        /**
         * 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);
        }
    

    Laravel has another method that tries to delete a table only if it exists. This is done using the SQL DROP TABLE IF EXISTS statement. Firebird does not have an operator with similar functionality, however we can emulate it using an anonymous block (EXECUTE BLOCK + EXECUTE STATEMENT).

        /**
         * Compile a drop table (if exists) command.
         *
         * @param  \Illuminate\Database\Schema\Blueprint  $blueprint
         * @param  \Illuminate\Support\Fluent  $command
         * @return string
         */
        public function compileDropIfExists(Blueprint $blueprint, Fluent $command) {
            $sql = 'EXECUTE BLOCK' . "\n";
            $sql .= 'AS' . "\n";
            $sql .= 'BEGIN' . "\n";
            $sql .= "  IF (EXISTS(select * from RDB\$RELATIONS where RDB\$RELATION_NAME = '" . $blueprint->getTable() . "')) THEN" . "\n";
            $sql .= "    EXECUTE STATEMENT 'DROP TABLE " . $this->wrapTable($blueprint) . "';" . "\n";
            $sql .= 'END';
            return $sql;
        }
    

    Now add methods to add and remove columns, constraints, and indexes.

    Methods for adding and removing columns, constraints, and indexes
        /**
         * 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 column', $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($command->index);
            $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($command->index);
            return "create index {$index} on " . $this->wrapTable($blueprint) . " ({$columns})";
        }
        /**
         * Compile a drop column command.
         *
         * @param  \Illuminate\Database\Schema\Blueprint  $blueprint
         * @param  \Illuminate\Support\Fluent  $command
         * @return string
         */
        public function compileDropColumn(Blueprint $blueprint, Fluent $command) {
            $columns = $this->prefixArray('drop column', $this->wrapArray($command->columns));
            $table = $this->wrapTable($blueprint);
            return 'alter table ' . $table . ' ' . implode(', ', $columns);
        }
        /**
         * Compile a drop primary key command.
         *
         * @param  \Illuminate\Database\Schema\Blueprint  $blueprint
         * @param  \Illuminate\Support\Fluent  $command
         * @return string
         */
        public function compileDropPrimary(Blueprint $blueprint, Fluent $command) {
            $table = $blueprint->getTable();
            $index = $this->wrap("{$table}_pkey");
            return 'alter table ' . $this->wrapTable($blueprint) . " drop constraint {$index}";
        }
        /**
         * Compile a drop unique key command.
         *
         * @param  \Illuminate\Database\Schema\Blueprint  $blueprint
         * @param  \Illuminate\Support\Fluent  $command
         * @return string
         */
        public function compileDropUnique(Blueprint $blueprint, Fluent $command) {
            $table = $this->wrapTable($blueprint);
            $index = $this->wrap($command->index);
            return "alter table {$table} drop constraint {$index}";
        }
        /**
         * Compile a drop index command.
         *
         * @param  \Illuminate\Database\Schema\Blueprint  $blueprint
         * @param  \Illuminate\Support\Fluent  $command
         * @return string
         */
        public function compileDropIndex(Blueprint $blueprint, Fluent $command) {
            $index = $this->wrap($command->index);
            return "drop index {$index}";
        }
        /**
         * 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);
            $index = $this->wrap($command->index);
            return "alter table {$table} drop constraint {$index}";
        }
    


    You can test the performance of our classes by creating and running a migration with the following contents:

    use Illuminate\Database\Schema\Blueprint;
    use Illuminate\Database\Migrations\Migration;
    class CreateUsersTable extends Migration
    {
        /**
         * Run the migrations.
         *
         * @return void
         */
        public function up()
        {
            Schema::create('users', function (Blueprint $table) {
                $table->increments('id');
                $table->string('name');
                $table->string('email')->unique();
                $table->string('password');
                $table->rememberToken();
                $table->timestamps();
            });
        }
        /**
         * Reverse the migrations.
         *
         * @return void
         */
        public function down()
        {
            Schema::drop('users');
        }
    }
    

    As a result of starting with the command:

    php artisan migrate
    

    a table will be created with the following DDL:

    CREATE TABLE "users" (
        "id"              INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
        "name"            VARCHAR(255) NOT NULL,
        "email"           VARCHAR(255) NOT NULL,
        "password"        VARCHAR(255) NOT NULL,
        "remember_token"  VARCHAR(100),
        "created_at"      TIMESTAMP,
        "updated_at"      TIMESTAMP
    );
    ALTER TABLE "users" ADD CONSTRAINT "users_email_unique" UNIQUE ("email");
    

    You can roll back the migration using the command:

    php artisan migrate:reset
    

    Adding Sequence Support


    By analogy with the Blueprint class, designed to generate a query or a set of queries to support the creation, modification, and deletion of table metadata, we will create the SequenceBlueprint class to support the same actions for sequences. This class is quite simple, I will not give it in its entirety, only the main differences from the similar Blueprint class.

    Sequences in Firebird have the following attributes: sequence name, starting value, and increment, which is used by the NEXT VALUE FOR statement. The last attribute is available starting with version 3.0. Thus, our class will contain the following properties:

        /**
         * The sequence the blueprint describes.
         *
         * @var string
         */
        protected $sequence;   
        /**
         * Initial sequence value
         * 
         * @var int 
         */
        protected $start_with = 0;
        /**
         * Increment for sequence
         * 
         * @var int 
         */
        protected $increment = 1;
        /**
         * Restart flag that indicates that the sequence should be reset
         * 
         * @var bool 
         */
        protected $restart = false;
    

    The methods for obtaining values ​​and setting these properties are elementary, so we will not give them here. I will give only the restart method, which will be used when generating the RESTART clause in the ALTER SEQUENCE statement.

        /**
         * Restart sequence and set initial value
         * 
         * @param int $startWith
         */
        public function restart($startWith = null) {
            $this->restart = true;
            $this->start_with = $startWith;
        }
    

    By analogy with the Blueprint class, if create or drop are not given, the alter sequence command is executed.

        /**
         * 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;
        } 
        /**
         * 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;
        }
    

    The full code of the Illuminate \ Database \ Schema \ SequenceBlueprint class can be found in the source code attached to the article.

    Now it's time to go back to the Illuminate \ Database \ Schema \ Grammars \ FirebirdGrammar grammar class and add methods to create the {CREATE | ALTER | DROP} SEQUENCE.

    Operator Support {CREATE | ALTER | DROP} SEQUENCE
        /**
         * Compile a create sequence command.
         *
         * @param  \Illuminate\Database\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 alter sequence command.
         *
         * @param  \Illuminate\Database\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 \Illuminate\Database\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 \Illuminate\Database\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);
        }
    


    Well, now the addition of sequence support in Laravel migration is complete. Let's see how it works, for this we slightly modify the migration given earlier.

    Migration for testing DDL over sequences
    class CreateUsersTable extends Migration
    {
        /**
         * Run the migrations.
         *
         * @return void
         */
        public function up()
        {
            // выполнит оператор 
            // CREATE SEQUENCE "seq_users_id"
            Schema::createSequence('seq_users_id');
            // выполнит операторы
            // CREATE TABLE "users" (
            //   "id"              INTEGER NOT NULL,
            //   "name"            VARCHAR(255) NOT NULL,
            //   "email"           VARCHAR(255) NOT NULL,
            //   "password"        VARCHAR(255) NOT NULL,
            //   "remember_token"  VARCHAR(100),
            //   "created_at"      TIMESTAMP,
            //   "updated_at"      TIMESTAMP
            // );
            // ALTER TABLE "users" ADD PRIMARY KEY ("id");
            // ALTER TABLE "users" ADD CONSTRAINT "users_email_unique" UNIQUE ("email");
            Schema::create('users', function (Blueprint $table) {
                //$table->increments('id');
                $table->integer('id')->primary();
                $table->string('name');
                $table->string('email')->unique();
                $table->string('password');
                $table->rememberToken();
                $table->timestamps();
            });
            // выполнит оператор
            // ALTER SEQUENCE "seq_users_id" RESTART WITH 10 INCREMENT BY 5
            Schema::sequence('seq_users_id', function (SequenceBlueprint $sequence) {
                $sequence->increment(5);
                $sequence->restart(10);
            });
        }
        /**
         * Reverse the migrations.
         *
         * @return void
         */
        public function down()
        {
            // выполнит оператор 
            // DROP SEQUENCE "seq_users_id"
            Schema::dropSequence('seq_users_id');
            // выполнит оператор 
            // DROP TABLE "users"        
            Schema::drop('users');
        }
    }
    


    You can go further and make support for creating a BEFORE INSERT trigger and sequence (generator) to support auto-increment fields in Firebird 2.5 and lower. But in most cases, it’s enough to get the next value of the sequence and pass it to the INSERT request.

    Conclusion


    These changes are enough for developing web applications using the Firebird DBMS using the Laravel framework. Of course, it would be nice to design this as a package and connect it to expand the functionality, as this is done with the rest of the Laravel modules.

    In the next article I will talk about how to create a small application using Laravel and Firebird DBMS. If you are interested in integrating Firebird into the Laravel framework or if you find an error, please write in a personal reply. Files modified for Laravel support for Firebird can be downloaded from here .

    Comment


    At the time of writing, I was not aware of the existence of the github.com/jacquestvanzuydam/laravel-firebird package. I hope the article is still useful for understanding what is going on inside Laravel. Thanks to the user ellrion for the comment and the link provided.

    Read Next