<commit_before><?php
/**
 * This file is part of the PHPLucidFrame library.
 * SchemaManager manages your database schema.
 *
 * @package     PHPLucidFrame\Core
 * @since       PHPLucidFrame v 1.14.0
 * @copyright   Copyright (c), PHPLucidFrame.
 * @author      <NAME>. <<EMAIL>>
 * @link        http://phplucidframe.com
 * @license     http://www.opensource.org/licenses/mit-license.php MIT License
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE
 */

namespace LucidFrame\Core;

use LucidFrame\Console\Command;

/**
 * Schema Manager
 */
class SchemaManager
{
    /** @var array The schema definition */
    protected $schema = array();
    /** @var string The database driver; currently it allows "mysql" only */
    private $driver = 'mysql';
    /** @var array The global schema options */
    private $defaultOptions;
    /** @var array The data types for each db driver */
    private static $dataTypes = array(
        'mysql' => array(
            'smallint'  => 'SMALLINT',
            'int'       => 'INT',
            'integer'   => 'INT',
            'bigint'    => 'BIGINT',
            'decimal'   => 'NUMERIC',
            'float'     => 'DOUBLE',
            # For decimal and float
            # length => array(p, s) where p is the precision and s is the scale
            # The precision represents the number of significant digits that are stored for values, and
            # the scale represents the number of digits that can be stored following the decimal point.
            'string'    => 'VARCHAR',
            'char'      => 'CHAR',
            'binary'    => 'VARBINARY',
            'text'      => 'TEXT',
            'blob'      => 'BLOB',
            'array'     => 'TEXT',
            'json'      => 'TEXT',
            # For text, blob, array and json
            # length => tiny, medium or long
            # tiny for TINYTEXT, medium for MEDIUMTEXT, long for LONGTEXT
            # if no length is specified, default to TEXT
            'boolean'   => 'TINYINT', # TINYINT(1)
            'date'      => 'DATE',
            'datetime'  => 'DATETIME',
            'time'      => 'TIME',
        ),
    );
    /** @var array The relational database relationships */
    private static $relationships = array('1:m', 'm:1', 'm:m', '1:1');
    /** @var string The namespace for the database */
    private $dbNamespace = 'default';
    /** @var array The array of generated SQL statements */
    private $sqlStatements = array();
    /** @var string Version file name extension */
    private $sqlExtension = '.sqlc';
    /** @var array Dropped table names */
    private $droppedTables = array();
    /** @var array Added table names */
    private $addedTables = array();
    /** @var array Dropped field names */
    private $droppedColumns = array();
    /** @var array Added column names */
    private $addedColumns = array();
    /** @var array Renamed table names */
    private $tablesRenamed = array();
    /** @var array Renamed field names */
    private $columnsRenamed = array();

    /**
     * Constructor
     * @param array $schema The array of schema definition
     * @param string $dbNamespace The namespace for the database schema
     */
    public function __construct($schema = array(), $dbNamespace = null)
    {
        $this->defaultOptions = array(
            'timestamps'    => true,
            'constraints'   => true,
            'charset'       => 'utf8',
            'collate'       => 'utf8_general_ci',
            'engine'        => 'InnoDB',
        );

        $this->setSchema($schema);

        if ($dbNamespace) {
            $this->dbNamespace = $dbNamespace;
        }
    }

    /**
     * Setter for the property `schema`
     * @param  array $schema The array of schema definition
     * @return object SchemaManager
     */
    public function setSchema($schema)
    {
        if (!is_array($schema)) {
            $schema = array(
                '_options' => $this->defaultOptions
            );
        }

        $this->schema = $schema;

        return $this;
    }

    /**
     * Getter for the property `schema`
     * @return array The array of schema definition
     */
    public function getSchema()
    {
        return $this->schema;
    }

    /**
     * Setter for the property `driver`
     * Currently driver allows mysql only, that's why this method is private
     * @param  string $driver Database driver
     * @return object SchemaManager
     */
    private function setDriver($driver)
    {
        $this->driver = $driver;

        return $this;
    }

    /**
     * Getter for the property `driver`
     * @return string
     */
    public function getDriver()
    {
        return $this->driver;
    }

    /**
     * Setter for the property `dbNamespace`
     * @param  string $namespace The namespace
     * @return object SchemaManager
     */
    public function setDbNamespace($namespace)
    {
        $this->dbNamespace = $namespace;

        return $this;
    }

    /**
     * Getter for the property `dbNamespace`
     * @return string
     */
    public function getDbNamespace()
    {
        return $this->dbNamespace;
    }

    /**
     * Get default field type for primary key
     * @return array Array of field type options
     */
    private function getPKDefaultType()
    {
        return array(
            'type'      => 'int',
            'autoinc'   => true,
            'null'      => false,
            'unsigned'  => true
        );
    }

    /**
     * Get relationship options with defaults
     * @param  array  $relation The relationship options
     * @param  string $fkTable The FK table
     * @return array  The relationship options with defaults
     */
    private function getRelationOptions($relation, $fkTable = '')
    {
        if (!isset($relation['name'])) {
            $relation['name'] = $fkTable.'_id';
        }

        return $relation + array(
            'unique'  => false,
            'default' => null,
            'cascade' => false
        );
    }

    /**
     * Get field statement for CREATE TABLE
     * @param  string   $field        The field name
     * @param  array    $definition   SchemaManager field definition
     * @param  string   $collate      The collation for the field; if it is null, db collation is used
     * @return string   The field statement
     */
    public function getFieldStatement($field, $definition, $collate = null)
    {
        $type = $this->getVendorFieldType($definition);
        if ($type === null) {
            return '';
        }

        $statement = "`{$field}` {$type}";

        $length = $this->getFieldLength($definition);
        if ($length) {
            $statement .= "($length)";
        }

        if (in_array($definition['type'], array('string', 'char', 'text', 'array', 'json'))) {
            # COLLATE for text fields
            $statement .= ' COLLATE ';
            $statement .= $collate ? $collate : $this->schema['_options']['collate'];
        }

        if (isset($definition['unsigned'])) {
            # unsigned
            $statement .= ' unsigned';
        }

        if (isset($definition['null'])) {
            # true: DEFAULT NULL
            # false: NOT NULL
            $statement .= $definition['null'] ? ' DEFAULT NULL' : ' NOT NULL';
        }

        if (isset($definition['default'])) {
            $statement .= sprintf(" DEFAULT '%d'", (int) $definition['default']);
        }

        if (isset($definition['autoinc']) && $definition['autoinc']) {
            # AUTO_INCREMENT
            $statement .= ' AUTO_INCREMENT';
        }

        return $statement;
    }

    /**
     * Get field type
     * @param  array  $definition SchemaManager field definition
     * @return string The underlying db field type
     */
    public function getVendorFieldType(&$definition)
    {
        if (!isset(self::$dataTypes[$this->driver][$definition['type']])) {
            # if no data type is defined
            return null;
        }

        $type = self::$dataTypes[$this->driver][$definition['type']];

        if (in_array($definition['type'], array('text', 'blob', 'array', 'json'))) {
            if (isset($definition['length']) && in_array($definition['length'], array('tiny', 'medium', 'long'))) {
                return strtoupper($definition['length']).$type;
            } else {
                return $definition['type'] == 'blob' ? self::$dataTypes[$this->driver]['blob'] : self::$dataTypes[$this->driver]['text'];
            }
        }

        if ($definition['type'] == 'boolean') {
            # if type is boolean, force unsigned, not null and default 0
            $definition['unsigned'] = true;
            $definition['null']     = false;
            if (!isset($definition['default'])) {
                $definition['default'] = false;
            }
        }

        return $type;
    }

    /**
     * Get field length
     * @param  array    $definition SchemaManager field definition
     * @return integer  The field length
     */
    public function getFieldLength(&$definition)
    {
        $type = $definition['type'];

        if ($type == 'string' || $type == 'char') {
            $length = 255;
        } elseif ($type == 'int' || $type == 'integer') {
            $length = 11;
        } elseif ($type === 'boolean') {
            $length = 1;
        } elseif (in_array($type, array('text', 'blob', 'array', 'json'))) {
            $length = 0;
        } elseif ($type == 'decimal' || $type == 'float') {
            $length = isset($definition['length']) ? $definition['length'] : 0;
            $length = is_array($length) ? "$length[0], $length[1]" : '0, 0';
        } else {
            $length = 0;
        }

        if (isset($definition['length']) && is_numeric($definition['length'])) {
            $length = $definition['length'];
        }

        return $length;
    }

    /**
     * Get foreign key schema definition
     * @param  string $table    The table where the FK field will be added
     * @param  string $fkTable  The reference table name
     * @param  array  $relation The relationship definition
     * @return array Foreign key schema definition
     */
    protected function getFKField($table, $fkTable, $relation)
    {
        $field = $relation['name'];
        $pkFields = $this->schema['_options']['pk'];

        if (isset($pkFields[$fkTable][$field])) {
            $fkField = $pkFields[$fkTable][$field];
        } else {
            $keys = array_keys($pkFields[$fkTable]);
            $firstPKField = array_shift($keys);<commit_msg>Fix default length for decimal and float data type in SchemaManager<commit_after><?php
/**
 * This file is part of the PHPLucidFrame library.
 * SchemaManager manages your database schema.
 *
 * @package     PHPLucidFrame\Core
 * @since       PHPLucidFrame v 1.14.0
 * @copyright   Copyright (c), PHPLucidFrame.
 * @author      <NAME>. <<EMAIL>>
 * @link        http://phplucidframe.com
 * @license     http://www.opensource.org/licenses/mit-license.php MIT License
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE
 */

namespace LucidFrame\Core;

use LucidFrame\Console\Command;

/**
 * Schema Manager
 */
class SchemaManager
{
    /** @var array The schema definition */
    protected $schema = array();
    /** @var string The database driver; currently it allows "mysql" only */
    private $driver = 'mysql';
    /** @var array The global schema options */
    private $defaultOptions;
    /** @var array The data types for each db driver */
    private static $dataTypes = array(
        'mysql' => array(
            'smallint'  => 'SMALLINT',
            'int'       => 'INT',
            'integer'   => 'INT',
            'bigint'    => 'BIGINT',
            'decimal'   => 'NUMERIC',
            'float'     => 'DOUBLE',
            # For decimal and float
            # length => array(p, s) where p is the precision and s is the scale
            # The precision represents the number of significant digits that are stored for values, and
            # the scale represents the number of digits that can be stored following the decimal point.
            'string'    => 'VARCHAR',
            'char'      => 'CHAR',
            'binary'    => 'VARBINARY',
            'text'      => 'TEXT',
            'blob'      => 'BLOB',
            'array'     => 'TEXT',
            'json'      => 'TEXT',
            # For text, blob, array and json
            # length => tiny, medium or long
            # tiny for TINYTEXT, medium for MEDIUMTEXT, long for LONGTEXT
            # if no length is specified, default to TEXT
            'boolean'   => 'TINYINT', # TINYINT(1)
            'date'      => 'DATE',
            'datetime'  => 'DATETIME',
            'time'      => 'TIME',
        ),
    );
    /** @var array The relational database relationships */
    private static $relationships = array('1:m', 'm:1', 'm:m', '1:1');
    /** @var string The namespace for the database */
    private $dbNamespace = 'default';
    /** @var array The array of generated SQL statements */
    private $sqlStatements = array();
    /** @var string Version file name extension */
    private $sqlExtension = '.sqlc';
    /** @var array Dropped table names */
    private $droppedTables = array();
    /** @var array Added table names */
    private $addedTables = array();
    /** @var array Dropped field names */
    private $droppedColumns = array();
    /** @var array Added column names */
    private $addedColumns = array();
    /** @var array Renamed table names */
    private $tablesRenamed = array();
    /** @var array Renamed field names */
    private $columnsRenamed = array();

    /**
     * Constructor
     * @param array $schema The array of schema definition
     * @param string $dbNamespace The namespace for the database schema
     */
    public function __construct($schema = array(), $dbNamespace = null)
    {
        $this->defaultOptions = array(
            'timestamps'    => true,
            'constraints'   => true,
            'charset'       => 'utf8',
            'collate'       => 'utf8_general_ci',
            'engine'        => 'InnoDB',
        );

        $this->setSchema($schema);

        if ($dbNamespace) {
            $this->dbNamespace = $dbNamespace;
        }
    }

    /**
     * Setter for the property `schema`
     * @param  array $schema The array of schema definition
     * @return object SchemaManager
     */
    public function setSchema($schema)
    {
        if (!is_array($schema)) {
            $schema = array(
                '_options' => $this->defaultOptions
            );
        }

        $this->schema = $schema;

        return $this;
    }

    /**
     * Getter for the property `schema`
     * @return array The array of schema definition
     */
    public function getSchema()
    {
        return $this->schema;
    }

    /**
     * Setter for the property `driver`
     * Currently driver allows mysql only, that's why this method is private
     * @param  string $driver Database driver
     * @return object SchemaManager
     */
    private function setDriver($driver)
    {
        $this->driver = $driver;

        return $this;
    }

    /**
     * Getter for the property `driver`
     * @return string
     */
    public function getDriver()
    {
        return $this->driver;
    }

    /**
     * Setter for the property `dbNamespace`
     * @param  string $namespace The namespace
     * @return object SchemaManager
     */
    public function setDbNamespace($namespace)
    {
        $this->dbNamespace = $namespace;

        return $this;
    }

    /**
     * Getter for the property `dbNamespace`
     * @return string
     */
    public function getDbNamespace()
    {
        return $this->dbNamespace;
    }

    /**
     * Get default field type for primary key
     * @return array Array of field type options
     */
    private function getPKDefaultType()
    {
        return array(
            'type'      => 'int',
            'autoinc'   => true,
            'null'      => false,
            'unsigned'  => true
        );
    }

    /**
     * Get relationship options with defaults
     * @param  array  $relation The relationship options
     * @param  string $fkTable The FK table
     * @return array  The relationship options with defaults
     */
    private function getRelationOptions($relation, $fkTable = '')
    {
        if (!isset($relation['name'])) {
            $relation['name'] = $fkTable.'_id';
        }

        return $relation + array(
            'unique'  => false,
            'default' => null,
            'cascade' => false
        );
    }

    /**
     * Get field statement for CREATE TABLE
     * @param  string   $field        The field name
     * @param  array    $definition   SchemaManager field definition
     * @param  string   $collate      The collation for the field; if it is null, db collation is used
     * @return string   The field statement
     */
    public function getFieldStatement($field, $definition, $collate = null)
    {
        $type = $this->getVendorFieldType($definition);
        if ($type === null) {
            return '';
        }

        $statement = "`{$field}` {$type}";

        $length = $this->getFieldLength($definition);
        if ($length) {
            $statement .= "($length)";
        }

        if (in_array($definition['type'], array('string', 'char', 'text', 'array', 'json'))) {
            # COLLATE for text fields
            $statement .= ' COLLATE ';
            $statement .= $collate ? $collate : $this->schema['_options']['collate'];
        }

        if (isset($definition['unsigned'])) {
            # unsigned
            $statement .= ' unsigned';
        }

        if (isset($definition['null'])) {
            # true: DEFAULT NULL
            # false: NOT NULL
            $statement .= $definition['null'] ? ' DEFAULT NULL' : ' NOT NULL';
        }

        if (isset($definition['default'])) {
            $statement .= sprintf(" DEFAULT '%d'", (int) $definition['default']);
        }

        if (isset($definition['autoinc']) && $definition['autoinc']) {
            # AUTO_INCREMENT
            $statement .= ' AUTO_INCREMENT';
        }

        return $statement;
    }

    /**
     * Get field type
     * @param  array  $definition SchemaManager field definition
     * @return string The underlying db field type
     */
    public function getVendorFieldType(&$definition)
    {
        if (!isset(self::$dataTypes[$this->driver][$definition['type']])) {
            # if no data type is defined
            return null;
        }

        $type = self::$dataTypes[$this->driver][$definition['type']];

        if (in_array($definition['type'], array('text', 'blob', 'array', 'json'))) {
            if (isset($definition['length']) && in_array($definition['length'], array('tiny', 'medium', 'long'))) {
                return strtoupper($definition['length']).$type;
            } else {
                return $definition['type'] == 'blob' ? self::$dataTypes[$this->driver]['blob'] : self::$dataTypes[$this->driver]['text'];
            }
        }

        if ($definition['type'] == 'boolean') {
            # if type is boolean, force unsigned, not null and default 0
            $definition['unsigned'] = true;
            $definition['null']     = false;
            if (!isset($definition['default'])) {
                $definition['default'] = false;
            }
        }

        return $type;
    }

    /**
     * Get field length
     * @param  array    $definition SchemaManager field definition
     * @return integer  The field length
     */
    public function getFieldLength(&$definition)
    {
        $type = $definition['type'];

        if ($type == 'string' || $type == 'char') {
            $length = 255;
        } elseif ($type == 'int' || $type == 'integer') {
            $length = 11;
        } elseif ($type === 'boolean') {
            $length = 1;
        } elseif (in_array($type, array('text', 'blob', 'array', 'json'))) {
            $length = 0;
        } elseif ($type == 'decimal' || $type == 'float') {
            $length = isset($definition['length']) ? $definition['length'] : 0;
            if (is_array($length) && count($length) == 2) {
                $length = implode(', ', $length);
            }
        } else {
            $length = 0;
        }

        if (isset($definition['length']) && is_numeric($definition['length'])) {
            $length = $definition['length'];
        }

        return $length;
    }

    /**
     * Get foreign key schema definition
     * @param  string $table    The table where the FK field will be added
     * @param  string $fkTable  The reference table name
     * @param  array  $relation The relationship definition
     * @return array Foreign key schema definition
     */
    protected function getFKField($table, $fkTable, $relation)
    {
        $field = $relation['name'];
        $pkFields = $this->schema['_options']['pk'];

        if (isset($pkFields[$fkTable][$field])) {
            $fkField = $pkFields[$fkTable][$field];
        } else {
            $keys = array_keys($pkFields[$fkTable]);
            $firstPKField = array_shift($keys);
{'fraction_non_alphanumeric': 0.13654843684284312, 'fraction_numerical': 0.002196364316089537, 'mean_word_length': 1.9627578568461859, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 111, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '750213', 'n_tokens_mistral': 5859, 'n_tokens_neox': 5740, 'n_words': 1704}