diff --git a/README.md b/README.md index 5dc8b5e..ff32c46 100644 --- a/README.md +++ b/README.md @@ -78,9 +78,12 @@ $newTable = $toSchema->createTable('new_table'); // add columns $newTable->addColumn('id', 'integer', ['unsigned' => true]); -$newTable->addColumn('payload', 'string'); +$newTable->addColumn('payload', 'string', ['notnull' => false]); +// *option 'notnull' in false mode allows you to insert NULL into the column; +// in this case, the column will be represented in the ClickHouse as Nullable(String) $newTable->addColumn('hash', 'string', ['length' => 32, 'fixed' => true]); -// *option 'fixed' sets the fixed length of a string column as specified; if specified, the type of the column is FixedString +// *option 'fixed' sets the fixed length of a string column as specified; +// if specified, the type of the column is FixedString //set primary key $newTable->setPrimaryKey(['id']); diff --git a/src/ClickHouseConnection.php b/src/ClickHouseConnection.php index dbd54cc..15cd109 100644 --- a/src/ClickHouseConnection.php +++ b/src/ClickHouseConnection.php @@ -21,6 +21,8 @@ use Doctrine\DBAL\Driver\ServerInfoAwareConnection; use Doctrine\DBAL\ParameterType; use Doctrine\DBAL\Platforms\AbstractPlatform; +use function array_merge; +use function func_get_args; /** * ClickHouse implementation for the Connection interface. @@ -36,7 +38,7 @@ class ClickHouseConnection implements Connection, PingableConnection, ServerInfo /** * Connection constructor * - * @param array $params Array with connection params. + * @param mixed[] $params */ public function __construct( array $params, @@ -70,7 +72,7 @@ public function prepare($prepareString) */ public function query() { - $args = \func_get_args(); + $args = func_get_args(); $stmt = $this->prepare($args[0]); $stmt->execute(); @@ -164,7 +166,7 @@ public function getServerVersion() try { return $this->smi2CHClient->getServerVersion(); } catch (TransportException $exception) { - return null; + return ''; } } @@ -173,6 +175,6 @@ public function getServerVersion() */ public function requiresQueryForServerVersion() { - return true; + return true; } } diff --git a/src/ClickHousePlatform.php b/src/ClickHousePlatform.php index e7a0d81..ea3c98b 100644 --- a/src/ClickHousePlatform.php +++ b/src/ClickHousePlatform.php @@ -14,19 +14,6 @@ namespace FOD\DBALClickHouse; -use Doctrine\DBAL\Types\{ - BlobType, - DecimalType, - FloatType, - StringType, - TextType, - Type, - DateType, - IntegerType, - SmallIntType, - BigIntType, - DateTimeType -}; use Doctrine\DBAL\DBALException; use Doctrine\DBAL\ParameterType; use Doctrine\DBAL\Platforms\AbstractPlatform; @@ -34,6 +21,37 @@ use Doctrine\DBAL\Schema\ForeignKeyConstraint; use Doctrine\DBAL\Schema\Index; use Doctrine\DBAL\Schema\TableDiff; +use Doctrine\DBAL\Types\BigIntType; +use Doctrine\DBAL\Types\BlobType; +use Doctrine\DBAL\Types\DateTimeType; +use Doctrine\DBAL\Types\DateType; +use Doctrine\DBAL\Types\DecimalType; +use Doctrine\DBAL\Types\FloatType; +use Doctrine\DBAL\Types\IntegerType; +use Doctrine\DBAL\Types\SmallIntType; +use Doctrine\DBAL\Types\StringType; +use Doctrine\DBAL\Types\TextType; +use Doctrine\DBAL\Types\Type; +use FOD\DBALClickHouse\Types\BitNumericalClickHouseType; +use FOD\DBALClickHouse\Types\DatableClickHouseType; +use FOD\DBALClickHouse\Types\NumericalClickHouseType; +use FOD\DBALClickHouse\Types\StringClickHouseType; +use FOD\DBALClickHouse\Types\UnsignedNumericalClickHouseType; +use function addslashes; +use function array_filter; +use function array_key_exists; +use function array_keys; +use function array_merge; +use function array_unique; +use function array_values; +use function count; +use function func_get_args; +use function get_class; +use function implode; +use function in_array; +use function sprintf; +use function stripos; +use function trim; /** * Provides the behavior, features and SQL dialect of the ClickHouse database platform. @@ -50,7 +68,11 @@ class ClickHousePlatform extends AbstractPlatform */ public function getBooleanTypeDeclarationSQL(array $columnDef) : string { - return 'UInt8'; + return $this->prepareDeclarationSQL( + UnsignedNumericalClickHouseType::UNSIGNED_CHAR . + NumericalClickHouseType::TYPE_INT . BitNumericalClickHouseType::EIGHT_BIT, + $columnDef + ); } /** @@ -58,7 +80,11 @@ public function getBooleanTypeDeclarationSQL(array $columnDef) : string */ public function getIntegerTypeDeclarationSQL(array $columnDef) : string { - return $this->_getCommonIntegerTypeDeclarationSQL($columnDef) . 'Int32'; + return $this->prepareDeclarationSQL( + $this->_getCommonIntegerTypeDeclarationSQL($columnDef) . + NumericalClickHouseType::TYPE_INT . BitNumericalClickHouseType::THIRTY_TWO_BIT, + $columnDef + ); } /** @@ -66,7 +92,7 @@ public function getIntegerTypeDeclarationSQL(array $columnDef) : string */ public function getBigIntTypeDeclarationSQL(array $columnDef) : string { - return 'String'; + return $this->prepareDeclarationSQL(StringClickHouseType::TYPE_STRING, $columnDef); } /** @@ -74,7 +100,11 @@ public function getBigIntTypeDeclarationSQL(array $columnDef) : string */ public function getSmallIntTypeDeclarationSQL(array $columnDef) : string { - return $this->_getCommonIntegerTypeDeclarationSQL($columnDef) . 'Int16'; + return $this->prepareDeclarationSQL( + $this->_getCommonIntegerTypeDeclarationSQL($columnDef) . + NumericalClickHouseType::TYPE_INT . BitNumericalClickHouseType::SIXTEEN_BIT, + $columnDef + ); } /** @@ -86,7 +116,7 @@ protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef) : strin throw new \Exception('Clickhouse do not support AUTO_INCREMENT fields'); } - return empty($columnDef['unsigned']) ? '' : 'U'; + return empty($columnDef['unsigned']) ? '' : UnsignedNumericalClickHouseType::UNSIGNED_CHAR; } /** @@ -128,6 +158,37 @@ protected function initializeDoctrineTypeMappings() : void 'enum8' => 'string', 'enum16' => 'string', + + 'nullable(int8)' => 'smallint', + 'nullable(int16)' => 'integer', + 'nullable(int32)' => 'integer', + 'nullable(int64)' => 'bigint', + 'nullable(uint8)' => 'smallint', + 'nullable(uint16)' => 'integer', + 'nullable(uint32)' => 'integer', + 'nullable(uint64)' => 'bigint', + 'nullable(float32)' => 'decimal', + 'nullable(float64)' => 'float', + + 'nullable(string)' => 'string', + 'nullable(fixedstring)' => 'string', + 'nullable(date)' => 'date', + 'nullable(datetime)' => 'datetime', + + 'array(nullable(int8))' => 'array', + 'array(nullable(int16))' => 'array', + 'array(nullable(int32))' => 'array', + 'array(nullable(int64))' => 'array', + 'array(nullable(uint8))' => 'array', + 'array(nullable(uint16))' => 'array', + 'array(nullable(uint32))' => 'array', + 'array(nullable(uint64))' => 'array', + 'array(nullable(float32))' => 'array', + 'array(nullable(float64))' => 'array', + + 'array(nullable(string))' => 'array', + 'array(nullable(date))' => 'array', + 'array(nullable(datetime))' => 'array', ]; } @@ -137,8 +198,33 @@ protected function initializeDoctrineTypeMappings() : void protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed) : string { return $fixed - ? 'FixedString(' . $length . ')' - : 'String'; + ? (StringClickHouseType::TYPE_FIXED_STRING . '(' . $length . ')') + : StringClickHouseType::TYPE_STRING; + } + + /** + * {@inheritDoc} + */ + public function getVarcharTypeDeclarationSQL(array $field) + { + if (! isset($field['length'])) { + $field['length'] = $this->getVarcharDefaultLength(); + } + + $fixed = $field['fixed'] ?? false; + + $maxLength = $fixed + ? $this->getCharMaxLength() + : $this->getVarcharMaxLength(); + + if ($field['length'] > $maxLength) { + return $this->getClobTypeDeclarationSQL($field); + } + + return $this->prepareDeclarationSQL( + $this->getVarcharTypeDeclarationSQLSnippet($field['length'], $fixed), + $field + ); } /** @@ -146,7 +232,7 @@ protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed) : string */ protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed) : string { - return 'String'; + return StringClickHouseType::TYPE_STRING; } /** @@ -154,7 +240,7 @@ protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed) : string */ public function getClobTypeDeclarationSQL(array $field) : string { - return 'String'; + return $this->prepareDeclarationSQL(StringClickHouseType::TYPE_STRING, $field); } /** @@ -162,7 +248,7 @@ public function getClobTypeDeclarationSQL(array $field) : string */ public function getBlobTypeDeclarationSQL(array $field) : string { - return 'String'; + return $this->prepareDeclarationSQL(StringClickHouseType::TYPE_STRING, $field); } /** @@ -323,7 +409,7 @@ public function getSubstringExpression($value, $from, $length = null) : string */ public function getConcatExpression() : string { - return 'concat(' . implode(', ', \func_get_args()) . ')'; + return 'concat(' . implode(', ', func_get_args()) . ')'; } /** @@ -331,7 +417,7 @@ public function getConcatExpression() : string */ public function getIsNullExpression($expression) { - throw DBALException::notSupported(__METHOD__); + return 'isNull(' . $expression . ')'; } /** @@ -339,7 +425,7 @@ public function getIsNullExpression($expression) */ public function getIsNotNullExpression($expression) { - throw DBALException::notSupported(__METHOD__); + return 'isNotNull(' . $expression . ')'; } /** @@ -561,7 +647,7 @@ protected function _getCreateTableSQL($tableName, array $columns, array $options /** * MergeTree* specific section */ - if (\in_array( + if (in_array( $engine, [ 'MergeTree', @@ -639,7 +725,7 @@ protected function _getCreateTableSQL($tableName, array $columns, array $options throw new \Exception( 'In table `' . $tableName . '` you have set field `' . $options['eventDateColumn'] . - '` (' . \get_class($columns[$options['eventDateColumn']]['type']) . ') + '` (' . get_class($columns[$options['eventDateColumn']]['type']) . ') as `eventDateColumn`, but it is not instance of DateType' ); } @@ -650,7 +736,8 @@ protected function _getCreateTableSQL($tableName, array $columns, array $options $eventDateColumnName = $options['eventDateColumn']; } $dateColumnParams['name'] = $eventDateColumnName; - $columns = [$eventDateColumnName => $dateColumnParams] + $columns; // insert into very beginning + // insert into very beginning + $columns = [$eventDateColumnName => $dateColumnParams] + $columns; /** * Primary key section @@ -682,8 +769,9 @@ protected function _getCreateTableSQL($tableName, array $columns, array $options ! $columns[$options['versionColumn']]['type'] instanceof DateTimeType ) { throw new \Exception( - 'For ReplacingMergeTree tables `versionColumn` must be any of UInt* family, or Date, or DateTime types. ' . - \get_class($columns[$options['versionColumn']]['type']) . ' given.' + 'For ReplacingMergeTree tables `versionColumn` must be any of UInt* family, + or Date, or DateTime types. ' . + get_class($columns[$options['versionColumn']]['type']) . ' given.' ); } @@ -693,10 +781,13 @@ protected function _getCreateTableSQL($tableName, array $columns, array $options $engineOptions .= ')'; } - $columnListSql = $this->getColumnDeclarationListSQL($columns); - $query = 'CREATE TABLE ' . $tableName . ' (' . $columnListSql . ') ENGINE = ' . $engine . $engineOptions; - - $sql[] = $query; + $sql[] = sprintf( + 'CREATE TABLE %s (%s) ENGINE = %s%s', + $tableName, + $this->getColumnDeclarationListSQL($columns), + $engine, + $engineOptions + ); return $sql; } @@ -804,6 +895,18 @@ protected function _getAlterTableIndexForeignKeySQL(TableDiff $diff) throw DBALException::notSupported(__METHOD__); } + /** + * @param mixed[] $columnDef + */ + protected function prepareDeclarationSQL(string $declarationSQL, array $columnDef) : string + { + if (array_key_exists('notnull', $columnDef) && $columnDef['notnull'] === false) { + return 'Nullable(' . $declarationSQL . ')'; + } + + return $declarationSQL; + } + /** * {@inheritDoc} */ @@ -825,7 +928,7 @@ public function getColumnDeclarationSQL($name, array $field) : string */ public function getDecimalTypeDeclarationSQL(array $columnDef) : string { - return 'String'; + return $this->prepareDeclarationSQL(StringClickHouseType::TYPE_STRING, $columnDef); } /** @@ -930,7 +1033,10 @@ public function getListDatabasesSQL() : string */ public function getListTableColumnsSQL($table, $database = null) : string { - return 'DESCRIBE TABLE ' . ($database ? $this->quoteSingleIdentifier($database) . '.' : '') . $this->quoteSingleIdentifier($table); + return sprintf( + 'DESCRIBE TABLE %s', + ($database ? $this->quoteSingleIdentifier($database) . '.' : '') . $this->quoteSingleIdentifier($table) + ); } /** @@ -978,7 +1084,7 @@ public function getCreateDatabaseSQL($database) : string */ public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration) : string { - return 'DateTime'; + return $this->prepareDeclarationSQL(DatableClickHouseType::TYPE_DATE_TIME, $fieldDeclaration); } /** @@ -986,12 +1092,15 @@ public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration) : string */ public function getDateTimeTzTypeDeclarationSQL(array $fieldDeclaration) : string { - return 'DateTime'; + return $this->prepareDeclarationSQL(DatableClickHouseType::TYPE_DATE_TIME, $fieldDeclaration); } + /** + * {@inheritDoc} + */ public function getTimeTypeDeclarationSQL(array $fieldDeclaration) : string { - return 'String'; + return $this->prepareDeclarationSQL(StringClickHouseType::TYPE_STRING, $fieldDeclaration); } /** @@ -999,7 +1108,7 @@ public function getTimeTypeDeclarationSQL(array $fieldDeclaration) : string */ public function getDateTypeDeclarationSQL(array $fieldDeclaration) : string { - return 'Date'; + return $this->prepareDeclarationSQL(DatableClickHouseType::TYPE_DATE, $fieldDeclaration); } /** @@ -1007,7 +1116,10 @@ public function getDateTypeDeclarationSQL(array $fieldDeclaration) : string */ public function getFloatDeclarationSQL(array $fieldDeclaration) : string { - return 'Float64'; + return $this->prepareDeclarationSQL( + NumericalClickHouseType::TYPE_FLOAT . BitNumericalClickHouseType::SIXTY_FOUR_BIT, + $fieldDeclaration + ); } /** @@ -1141,23 +1253,30 @@ public function getDefaultValueDeclarationSQL($field) : string if (! isset($field['default'])) { return ''; } - - $default = " DEFAULT '" . $field['default'] . "'"; - if ($fieldType = (string) ($field['type'] ?? null)) { - if (\in_array($fieldType, [ + $defaultValue = $this->quoteStringLiteral($field['default']); + $fieldType = $field['type'] ?: null; + if ($fieldType !== null) { + if ($fieldType === DatableClickHouseType::TYPE_DATE || + $fieldType instanceof DateType || + in_array($fieldType, [ 'Integer', 'SmallInt', 'Float', - ]) || ($fieldType === 'BigInt' && Type::getType('BigInt')->getBindingType() === ParameterType::INTEGER)) { - $default = ' DEFAULT ' . $field['default']; - } elseif ($fieldType === 'DateTime' && $field['default'] === $this->getCurrentTimestampSQL()) { - $default = ' DEFAULT ' . $this->getCurrentTimestampSQL(); - } elseif ($fieldType === 'Date') { // TODO check if string matches constant date like 'dddd-yy-mm' and quote it - $default = ' DEFAULT ' . $field['default']; + ]) || + ( + $fieldType === 'BigInt' + && Type::getType('BigInt')->getBindingType() === ParameterType::INTEGER + ) + ) { + $defaultValue = $field['default']; + } elseif ($fieldType === DatableClickHouseType::TYPE_DATE_TIME && + $field['default'] === $this->getCurrentTimestampSQL() + ) { + $defaultValue = $this->getCurrentTimestampSQL(); } } - return $default; + return sprintf(' DEFAULT %s', $defaultValue); } /** diff --git a/src/ClickHouseSchemaManager.php b/src/ClickHouseSchemaManager.php index f3ba5d0..69cc2b4 100644 --- a/src/ClickHouseSchemaManager.php +++ b/src/ClickHouseSchemaManager.php @@ -18,6 +18,12 @@ use Doctrine\DBAL\Schema\Column; use Doctrine\DBAL\Schema\View; use Doctrine\DBAL\Types\Type; +use const CASE_LOWER; +use function array_change_key_case; +use function preg_replace; +use function stripos; +use function strtolower; +use function trim; /** * Schema manager for the ClickHouse DBMS. diff --git a/src/ClickHouseStatement.php b/src/ClickHouseStatement.php index 516ed60..d9532be 100644 --- a/src/ClickHouseStatement.php +++ b/src/ClickHouseStatement.php @@ -19,6 +19,25 @@ use Doctrine\DBAL\FetchMode; use Doctrine\DBAL\ParameterType; use Doctrine\DBAL\Platforms\AbstractPlatform; +use function array_key_exists; +use function array_keys; +use function array_map; +use function array_replace; +use function array_shift; +use function array_values; +use function array_walk; +use function count; +use function current; +use function explode; +use function implode; +use function in_array; +use function is_array; +use function is_bool; +use function is_float; +use function is_int; +use function preg_replace; +use function stripos; +use function trim; /** * ClickHouse Statement @@ -34,18 +53,18 @@ class ClickHouseStatement implements \IteratorAggregate, Statement /** @var AbstractPlatform */ protected $platform; - /** @var array|null */ - protected $rows; + /** @var mixed[] */ + protected $rows = []; /** * Query parameters for prepared statement (key => value) - * @var array + * @var mixed[] */ protected $values = []; /** * Query parameters' types for prepared statement (key => value) - * @var array + * @var mixed[] */ protected $types = []; @@ -68,7 +87,7 @@ public function __construct(Client $client, string $statement, AbstractPlatform public function getIterator() : \ArrayIterator { if (! $this->iterator) { - $this->iterator = new \ArrayIterator($this->rows ?: []); + $this->iterator = new \ArrayIterator($this->rows); } return $this->iterator; @@ -79,7 +98,7 @@ public function getIterator() : \ArrayIterator */ public function closeCursor() { - $this->rows = null; + $this->rows = []; $this->iterator = null; return true; @@ -108,7 +127,7 @@ public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null) protected function assumeFetchMode(?int $fetchMode = null) : int { $mode = $fetchMode ?: $this->fetchMode; - if (! \in_array($mode, [ + if (! in_array($mode, [ FetchMode::ASSOCIATIVE, FetchMode::NUMERIC, FetchMode::STANDARD_OBJECT, @@ -147,7 +166,9 @@ public function fetch($fetchMode = null, $cursorOrientation = \PDO::FETCH_ORI_NE if ($this->assumeFetchMode($fetchMode) === \PDO::FETCH_KEY_PAIR) { if (count($data) < 2) { - throw new \Exception('To fetch in \PDO::FETCH_KEY_PAIR mode, result set must contain at least 2 columns'); + throw new \Exception( + 'To fetch in \PDO::FETCH_KEY_PAIR mode, result set must contain at least 2 columns' + ); } return [array_shift($data) => array_shift($data)]; @@ -190,7 +211,9 @@ function ($row) { return array_map( function ($row) { if (count($row) < 2) { - throw new \Exception('To fetch in \PDO::FETCH_KEY_PAIR mode, result set must contain at least 2 columns'); + throw new \Exception( + 'To fetch in \PDO::FETCH_KEY_PAIR mode, result set must contain at least 2 columns' + ); } return [array_shift($row) => array_shift($row)]; @@ -207,7 +230,8 @@ function ($row) { */ public function fetchColumn($columnIndex = 0) { - if ($elem = $this->fetch(FetchMode::NUMERIC)) { + $elem = $this->fetch(FetchMode::NUMERIC); + if (is_array($elem)) { return $elem[$columnIndex] ?? $elem[0]; } @@ -248,7 +272,7 @@ public function errorInfo() : void public function execute($params = null) : bool { $hasZeroIndex = false; - if (\is_array($params)) { + if (is_array($params)) { $this->values = array_replace($this->values, $params);//TODO array keys must be all strings or all integers? $hasZeroIndex = array_key_exists(0, $params); } @@ -268,7 +292,7 @@ public function execute($params = null) : bool } else { foreach (array_keys($this->values) as $key) { $sql = preg_replace( - '/(' . (\is_int($key) ? '\?' : ':' . $key) . ')/i', + '/(' . (is_int($key) ? '\?' : ':' . $key) . ')/i', $this->getTypedParam($key), $sql, 1 @@ -317,40 +341,44 @@ protected function processViaSMI2(string $sql) : void */ protected function getTypedParam($key) : string { + if ($this->values[$key] === null) { + return 'NULL'; + } + $type = $this->types[$key] ?? null; // if param type was not setted - trying to get db-type by php-var-type if ($type === null) { - if (\is_bool($this->values[$key])) { + if (is_bool($this->values[$key])) { $type = ParameterType::BOOLEAN; - } elseif (\is_int($this->values[$key]) || \is_float($this->values[$key])) { + } elseif (is_int($this->values[$key]) || is_float($this->values[$key])) { $type = ParameterType::INTEGER; - } elseif (\is_array($this->values[$key])) { + } elseif (is_array($this->values[$key])) { /* * ClickHouse Arrays */ $values = $this->values[$key]; - if (\is_int(current($values)) || \is_float(current($values))) { + if (is_int(current($values)) || is_float(current($values))) { array_map( function ($value) : void { - if (! \is_int($value) && ! \is_float($value)) { - throw new ClickHouseException('Array values must all be int/float or string, mixes not allowed'); + if (! is_int($value) && ! is_float($value)) { + throw new ClickHouseException( + 'Array values must all be int/float or string, mixes not allowed' + ); } }, $values ); } else { - $values = array_map([$this->platform, 'quoteStringLiteral'], $values); + $values = array_map(function ($value) { + return $value === null ? 'NULL' : $this->platform->quoteStringLiteral($value); + }, $values); } return '[' . implode(', ', $values) . ']'; } } - if ($type === ParameterType::NULL) { - throw new ClickHouseException('NULLs are not supported by ClickHouse'); - } - if ($type === ParameterType::INTEGER) { return (string) $this->values[$key]; } diff --git a/src/Connection.php b/src/Connection.php index 35bfdaa..80f8e72 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -15,6 +15,9 @@ namespace FOD\DBALClickHouse; use Doctrine\DBAL\DBALException; +use function strtoupper; +use function substr; +use function trim; /** * ClickHouse Connection diff --git a/src/Types/AbstractArrayFloatType.php b/src/Types/AbstractArrayFloatType.php deleted file mode 100644 index 88f2970..0000000 --- a/src/Types/AbstractArrayFloatType.php +++ /dev/null @@ -1,39 +0,0 @@ -) - * - * (c) FriendsOfDoctrine . - * - * For the full copyright and license inflormation, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace FOD\DBALClickHouse\Types; - -use Doctrine\DBAL\Platforms\AbstractPlatform; - -/** - * Array(Float*) Types basic class - */ -abstract class AbstractArrayFloatType extends AbstractArrayNumType -{ - /** - * {@inheritDoc} - */ - public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) : string - { - return 'Array(Float' . $this->getBitness() . ')'; - } - - /** - * {@inheritDoc} - */ - public function getName() : string - { - return 'array(float' . $this->getBitness() . ')'; - } -} diff --git a/src/Types/AbstractArrayIntType.php b/src/Types/AbstractArrayIntType.php deleted file mode 100644 index 06d3b55..0000000 --- a/src/Types/AbstractArrayIntType.php +++ /dev/null @@ -1,41 +0,0 @@ -) - * - * (c) FriendsOfDoctrine . - * - * For the full copyright and license inflormation, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace FOD\DBALClickHouse\Types; - -use Doctrine\DBAL\Platforms\AbstractPlatform; - -/** - * Array(Int*) Types basic class - */ -abstract class AbstractArrayIntType extends AbstractArrayNumType -{ - public const UNSIGNED = false; - - /** - * {@inheritDoc} - */ - public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) : string - { - return 'Array(' . (static::UNSIGNED ? 'U' : '') . 'Int' . $this->getBitness() . ')'; - } - - /** - * {@inheritDoc} - */ - public function getName() : string - { - return 'array(' . (static::UNSIGNED ? 'u' : '') . 'int' . $this->getBitness() . ')'; - } -} diff --git a/src/Types/ArrayDateTimeType.php b/src/Types/ArrayDateTimeType.php index d7daffa..76cf715 100644 --- a/src/Types/ArrayDateTimeType.php +++ b/src/Types/ArrayDateTimeType.php @@ -16,26 +16,18 @@ use Doctrine\DBAL\ParameterType; use Doctrine\DBAL\Platforms\AbstractPlatform; +use function array_filter; +use function array_map; +use function implode; /** * Array(DateTime) Type class */ -class ArrayDateTimeType extends AbstractArrayType +class ArrayDateTimeType extends ArrayType implements DatableClickHouseType { - /** - * {@inheritDoc} - */ - public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) : string - { - return 'Array(DateTime)'; - } - - /** - * {@inheritDoc} - */ - public function getName() : string + public function getBaseClickHouseType() : string { - return 'array(datetime)'; + return DatableClickHouseType::TYPE_DATE_TIME; } /** @@ -43,9 +35,12 @@ public function getName() : string */ public function convertToPHPValue($value, AbstractPlatform $platform) { - return array_map(function ($stringDatetime) use ($platform) { - return \DateTime::createFromFormat($platform->getDateTimeFormatString(), $stringDatetime); - }, (array) $value); + return array_map( + function ($stringDatetime) use ($platform) { + return \DateTime::createFromFormat($platform->getDateTimeFormatString(), $stringDatetime); + }, + (array) $value + ); } /** @@ -55,11 +50,17 @@ public function convertToDatabaseValue($value, AbstractPlatform $platform) { return '[' . implode( ', ', - array_map(function (\DateTime $datetime) use ($platform) { + array_map( + function (\DateTime $datetime) use ($platform) { return "'" . $datetime->format($platform->getDateTimeFormatString()) . "'"; - }, array_filter((array) $value, function ($datetime) { - return $datetime instanceof \DateTime; - })) + }, + array_filter( + (array) $value, + function ($datetime) { + return $datetime instanceof \DateTime; + } + ) + ) ) . ']'; } diff --git a/src/Types/ArrayDateType.php b/src/Types/ArrayDateType.php index e546cbd..f96829d 100644 --- a/src/Types/ArrayDateType.php +++ b/src/Types/ArrayDateType.php @@ -16,26 +16,18 @@ use Doctrine\DBAL\ParameterType; use Doctrine\DBAL\Platforms\AbstractPlatform; +use function array_filter; +use function array_map; +use function implode; /** * Array(Date) Type class */ -class ArrayDateType extends AbstractArrayType +class ArrayDateType extends ArrayType implements DatableClickHouseType { - /** - * {@inheritDoc} - */ - public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) : string - { - return 'Array(Date)'; - } - - /** - * {@inheritDoc} - */ - public function getName() : string + public function getBaseClickHouseType() : string { - return 'array(date)'; + return DatableClickHouseType::TYPE_DATE; } /** @@ -43,9 +35,12 @@ public function getName() : string */ public function convertToPHPValue($value, AbstractPlatform $platform) { - return array_map(function ($stringDatetime) use ($platform) { - return \DateTime::createFromFormat($platform->getDateFormatString(), $stringDatetime); - }, (array) $value); + return array_map( + function ($stringDatetime) use ($platform) { + return \DateTime::createFromFormat($platform->getDateFormatString(), $stringDatetime); + }, + (array) $value + ); } /** @@ -55,11 +50,17 @@ public function convertToDatabaseValue($value, AbstractPlatform $platform) { return '[' . implode( ', ', - array_map(function (\DateTime $datetime) use ($platform) { + array_map( + function (\DateTime $datetime) use ($platform) { return "'" . $datetime->format($platform->getDateFormatString()) . "'"; - }, array_filter((array) $value, function ($datetime) { - return $datetime instanceof \DateTime; - })) + }, + array_filter( + (array) $value, + function ($datetime) { + return $datetime instanceof \DateTime; + } + ) + ) ) . ']'; } diff --git a/src/Types/ArrayFloat32Type.php b/src/Types/ArrayFloat32Type.php index c5e4467..fcc0b27 100644 --- a/src/Types/ArrayFloat32Type.php +++ b/src/Types/ArrayFloat32Type.php @@ -17,13 +17,15 @@ /** * Array(Float32) Type */ -class ArrayFloat32Type extends AbstractArrayFloatType +class ArrayFloat32Type extends ArrayType implements BitNumericalClickHouseType { - public const BITNESS = 32; + public function getBits() : int + { + return BitNumericalClickHouseType::THIRTY_TWO_BIT; + } - /** {@inheritdoc} */ - protected function getBitness() : int + public function getBaseClickHouseType() : string { - return self::BITNESS; + return NumericalClickHouseType::TYPE_FLOAT; } } diff --git a/src/Types/ArrayFloat64Type.php b/src/Types/ArrayFloat64Type.php index f51ee29..01c8790 100644 --- a/src/Types/ArrayFloat64Type.php +++ b/src/Types/ArrayFloat64Type.php @@ -17,13 +17,15 @@ /** * Array(Float64) Type */ -class ArrayFloat64Type extends AbstractArrayFloatType +class ArrayFloat64Type extends ArrayType implements BitNumericalClickHouseType { - public const BITNESS = 64; + public function getBits() : int + { + return BitNumericalClickHouseType::SIXTY_FOUR_BIT; + } - /** {@inheritdoc} */ - protected function getBitness() : int + public function getBaseClickHouseType() : string { - return self::BITNESS; + return NumericalClickHouseType::TYPE_FLOAT; } } diff --git a/src/Types/ArrayInt16Type.php b/src/Types/ArrayInt16Type.php index 034563a..0c78d76 100644 --- a/src/Types/ArrayInt16Type.php +++ b/src/Types/ArrayInt16Type.php @@ -17,13 +17,15 @@ /** * Array(Int16) Type */ -class ArrayInt16Type extends AbstractArrayIntType +class ArrayInt16Type extends ArrayType implements BitNumericalClickHouseType { - public const BITNESS = 16; + public function getBits() : int + { + return BitNumericalClickHouseType::SIXTEEN_BIT; + } - /** {@inheritdoc} */ - protected function getBitness() : int + public function getBaseClickHouseType() : string { - return self::BITNESS; + return NumericalClickHouseType::TYPE_INT; } } diff --git a/src/Types/ArrayInt32Type.php b/src/Types/ArrayInt32Type.php index 81a74a6..201278e 100644 --- a/src/Types/ArrayInt32Type.php +++ b/src/Types/ArrayInt32Type.php @@ -17,13 +17,15 @@ /** * Array(Int32) Type */ -class ArrayInt32Type extends AbstractArrayIntType +class ArrayInt32Type extends ArrayType implements BitNumericalClickHouseType { - public const BITNESS = 32; + public function getBits() : int + { + return BitNumericalClickHouseType::THIRTY_TWO_BIT; + } - /** {@inheritdoc} */ - protected function getBitness() : int + public function getBaseClickHouseType() : string { - return self::BITNESS; + return NumericalClickHouseType::TYPE_INT; } } diff --git a/src/Types/ArrayInt64Type.php b/src/Types/ArrayInt64Type.php index 863cb4d..2e1860d 100644 --- a/src/Types/ArrayInt64Type.php +++ b/src/Types/ArrayInt64Type.php @@ -17,13 +17,15 @@ /** * Array(Int64) Type */ -class ArrayInt64Type extends AbstractArrayIntType +class ArrayInt64Type extends ArrayType implements BitNumericalClickHouseType { - public const BITNESS = 64; + public function getBits() : int + { + return BitNumericalClickHouseType::SIXTY_FOUR_BIT; + } - /** {@inheritdoc} */ - protected function getBitness() : int + public function getBaseClickHouseType() : string { - return self::BITNESS; + return NumericalClickHouseType::TYPE_INT; } } diff --git a/src/Types/ArrayInt8Type.php b/src/Types/ArrayInt8Type.php index 8099a9d..9f18e6f 100644 --- a/src/Types/ArrayInt8Type.php +++ b/src/Types/ArrayInt8Type.php @@ -17,13 +17,15 @@ /** * Array(Int8) Type */ -class ArrayInt8Type extends AbstractArrayIntType +class ArrayInt8Type extends ArrayType implements BitNumericalClickHouseType { - public const BITNESS = 8; + public function getBits() : int + { + return BitNumericalClickHouseType::EIGHT_BIT; + } - /** {@inheritdoc} */ - protected function getBitness() : int + public function getBaseClickHouseType() : string { - return self::BITNESS; + return NumericalClickHouseType::TYPE_INT; } } diff --git a/src/Types/ArrayStringType.php b/src/Types/ArrayStringType.php index 64cb5c6..8b39e5f 100644 --- a/src/Types/ArrayStringType.php +++ b/src/Types/ArrayStringType.php @@ -16,26 +16,17 @@ use Doctrine\DBAL\ParameterType; use Doctrine\DBAL\Platforms\AbstractPlatform; +use function array_map; +use function implode; /** * Array(String) Type class */ -class ArrayStringType extends AbstractArrayType +class ArrayStringType extends ArrayType implements StringClickHouseType { - /** - * {@inheritDoc} - */ - public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) : string - { - return 'Array(String)'; - } - - /** - * {@inheritDoc} - */ - public function getName() : string + public function getBaseClickHouseType() : string { - return 'array(string)'; + return StringClickHouseType::TYPE_STRING; } /** diff --git a/src/Types/AbstractArrayType.php b/src/Types/ArrayType.php similarity index 64% rename from src/Types/AbstractArrayType.php rename to src/Types/ArrayType.php index dd19594..2c185fc 100644 --- a/src/Types/AbstractArrayType.php +++ b/src/Types/ArrayType.php @@ -17,13 +17,16 @@ use Doctrine\DBAL\DBALException; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Types\Type; +use function array_key_exists; +use function sprintf; +use function strtolower; /** * Array(*) Types basic class */ -abstract class AbstractArrayType extends Type +abstract class ArrayType extends Type implements ClickHouseType { - public const ARRAY_TYPES = [ + protected const ARRAY_TYPES = [ 'array(int8)' => ArrayInt8Type::class, 'array(int16)' => ArrayInt16Type::class, 'array(int32)' => ArrayInt32Type::class, @@ -65,4 +68,36 @@ public function getMappedDatabaseTypes(AbstractPlatform $platform) : array { return [$this->getName()]; } + + /** + * {@inheritDoc} + */ + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) : string + { + return $this->getDeclaration($fieldDeclaration); + } + + /** + * {@inheritDoc} + */ + public function getName() : string + { + return strtolower($this->getDeclaration()); + } + + /** + * @param mixed[] $fieldDeclaration + */ + protected function getDeclaration(array $fieldDeclaration = []) : string + { + return sprintf( + array_key_exists( + 'notnull', + $fieldDeclaration + ) && $fieldDeclaration['notnull'] === false ? 'Array(Nullable(%s%s%s))' : 'Array(%s%s%s)', + $this instanceof UnsignedNumericalClickHouseType ? 'U' : '', + $this->getBaseClickHouseType(), + $this instanceof BitNumericalClickHouseType ? $this->getBits() : '' + ); + } } diff --git a/src/Types/ArrayUInt16Type.php b/src/Types/ArrayUInt16Type.php index 98c2847..3fb8d4c 100644 --- a/src/Types/ArrayUInt16Type.php +++ b/src/Types/ArrayUInt16Type.php @@ -17,7 +17,15 @@ /** * Array(UInt16) Type */ -class ArrayUInt16Type extends ArrayInt16Type +class ArrayUInt16Type extends ArrayType implements BitNumericalClickHouseType, UnsignedNumericalClickHouseType { - public const UNSIGNED = true; + public function getBits() : int + { + return BitNumericalClickHouseType::SIXTEEN_BIT; + } + + public function getBaseClickHouseType() : string + { + return NumericalClickHouseType::TYPE_INT; + } } diff --git a/src/Types/ArrayUInt32Type.php b/src/Types/ArrayUInt32Type.php index 39bec16..ef6794c 100644 --- a/src/Types/ArrayUInt32Type.php +++ b/src/Types/ArrayUInt32Type.php @@ -17,7 +17,15 @@ /** * Array(UInt32) Type */ -class ArrayUInt32Type extends ArrayInt32Type +class ArrayUInt32Type extends ArrayType implements BitNumericalClickHouseType, UnsignedNumericalClickHouseType { - public const UNSIGNED = true; + public function getBits() : int + { + return BitNumericalClickHouseType::THIRTY_TWO_BIT; + } + + public function getBaseClickHouseType() : string + { + return NumericalClickHouseType::TYPE_INT; + } } diff --git a/src/Types/ArrayUInt64Type.php b/src/Types/ArrayUInt64Type.php index 5c42123..a21c280 100644 --- a/src/Types/ArrayUInt64Type.php +++ b/src/Types/ArrayUInt64Type.php @@ -17,7 +17,15 @@ /** * Array(UInt64) Type */ -class ArrayUInt64Type extends ArrayInt64Type +class ArrayUInt64Type extends ArrayType implements BitNumericalClickHouseType, UnsignedNumericalClickHouseType { - public const UNSIGNED = true; + public function getBits() : int + { + return BitNumericalClickHouseType::SIXTY_FOUR_BIT; + } + + public function getBaseClickHouseType() : string + { + return NumericalClickHouseType::TYPE_INT; + } } diff --git a/src/Types/ArrayUInt8Type.php b/src/Types/ArrayUInt8Type.php index 832e886..626eafa 100644 --- a/src/Types/ArrayUInt8Type.php +++ b/src/Types/ArrayUInt8Type.php @@ -17,7 +17,15 @@ /** * Array(UInt8) Type */ -class ArrayUInt8Type extends ArrayInt8Type +class ArrayUInt8Type extends ArrayType implements BitNumericalClickHouseType, UnsignedNumericalClickHouseType { - public const UNSIGNED = true; + public function getBits() : int + { + return BitNumericalClickHouseType::EIGHT_BIT; + } + + public function getBaseClickHouseType() : string + { + return NumericalClickHouseType::TYPE_INT; + } } diff --git a/src/Types/AbstractArrayNumType.php b/src/Types/BitNumericalClickHouseType.php similarity index 61% rename from src/Types/AbstractArrayNumType.php rename to src/Types/BitNumericalClickHouseType.php index 0d412f2..ab843b8 100644 --- a/src/Types/AbstractArrayNumType.php +++ b/src/Types/BitNumericalClickHouseType.php @@ -14,13 +14,12 @@ namespace FOD\DBALClickHouse\Types; -/** - * Array(Numeric) Types basic class - */ -abstract class AbstractArrayNumType extends AbstractArrayType +interface BitNumericalClickHouseType extends NumericalClickHouseType { - /** - * @return int Bitness of integers or floats in Array (Array(Int{bitness}) or Array(Float{bitness})) - */ - abstract protected function getBitness() : int; + public const EIGHT_BIT = 8; + public const SIXTEEN_BIT = 16; + public const THIRTY_TWO_BIT = 32; + public const SIXTY_FOUR_BIT = 64; + + public function getBits() : int; } diff --git a/src/Types/ClickHouseType.php b/src/Types/ClickHouseType.php new file mode 100644 index 0000000..9f12543 --- /dev/null +++ b/src/Types/ClickHouseType.php @@ -0,0 +1,20 @@ +) + * + * (c) FriendsOfDoctrine . + * + * For the full copyright and license inflormation, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOD\DBALClickHouse\Types; + +interface ClickHouseType +{ + public function getBaseClickHouseType() : string; +} diff --git a/src/Types/DatableClickHouseType.php b/src/Types/DatableClickHouseType.php new file mode 100644 index 0000000..e2e7e5a --- /dev/null +++ b/src/Types/DatableClickHouseType.php @@ -0,0 +1,21 @@ +) + * + * (c) FriendsOfDoctrine . + * + * For the full copyright and license inflormation, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOD\DBALClickHouse\Types; + +interface DatableClickHouseType extends ClickHouseType +{ + public const TYPE_DATE = 'Date'; + public const TYPE_DATE_TIME = 'DateTime'; +} diff --git a/src/Types/NumericalClickHouseType.php b/src/Types/NumericalClickHouseType.php new file mode 100644 index 0000000..ebee8ed --- /dev/null +++ b/src/Types/NumericalClickHouseType.php @@ -0,0 +1,21 @@ +) + * + * (c) FriendsOfDoctrine . + * + * For the full copyright and license inflormation, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOD\DBALClickHouse\Types; + +interface NumericalClickHouseType extends ClickHouseType +{ + public const TYPE_INT = 'Int'; + public const TYPE_FLOAT = 'Float'; +} diff --git a/src/Types/StringClickHouseType.php b/src/Types/StringClickHouseType.php new file mode 100644 index 0000000..524d7e9 --- /dev/null +++ b/src/Types/StringClickHouseType.php @@ -0,0 +1,21 @@ +) + * + * (c) FriendsOfDoctrine . + * + * For the full copyright and license inflormation, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOD\DBALClickHouse\Types; + +interface StringClickHouseType extends ClickHouseType +{ + public const TYPE_STRING = 'String'; + public const TYPE_FIXED_STRING = 'FixedString'; +} diff --git a/src/Types/UnsignedNumericalClickHouseType.php b/src/Types/UnsignedNumericalClickHouseType.php new file mode 100644 index 0000000..f17132e --- /dev/null +++ b/src/Types/UnsignedNumericalClickHouseType.php @@ -0,0 +1,20 @@ +) + * + * (c) FriendsOfDoctrine . + * + * For the full copyright and license inflormation, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOD\DBALClickHouse\Types; + +interface UnsignedNumericalClickHouseType extends NumericalClickHouseType +{ + public const UNSIGNED_CHAR = 'U'; +} diff --git a/tests/ArraysTest.php b/tests/ArraysTest.php index fb0225e..dfc412d 100644 --- a/tests/ArraysTest.php +++ b/tests/ArraysTest.php @@ -12,7 +12,7 @@ namespace FOD\DBALClickHouse\Tests; use FOD\DBALClickHouse\Connection; -use FOD\DBALClickHouse\Types\AbstractArrayType; +use FOD\DBALClickHouse\Types\ArrayType; use PHPUnit\Framework\TestCase; /** @@ -28,7 +28,7 @@ class ArraysTest extends TestCase public function setUp() { $this->connection = CreateConnectionTest::createConnection(); - AbstractArrayType::registerArrayTypes($this->connection->getDatabasePlatform()); + ArrayType::registerArrayTypes($this->connection->getDatabasePlatform()); } public function tearDown() diff --git a/tests/ConnectionTest.php b/tests/ConnectionTest.php index 63bd06d..5603b10 100644 --- a/tests/ConnectionTest.php +++ b/tests/ConnectionTest.php @@ -150,7 +150,7 @@ public function testGetServerVersion() { $conn = $this->connection->getWrappedConnection(); if ($conn instanceof ServerInfoAwareConnection) { - $this->assertRegExp('/(^[0-9]+.[0-9]+.[0-9]+.[0-9]$)/mi', $conn->getServerVersion()); + $this->assertRegExp('/(^[0-9]+.[0-9]+.[0-9]+(.[0-9]$|$))/mi', $conn->getServerVersion()); } else { $this->fail(sprintf('`%s` does not implement the `%s` interface', \get_class($conn), ServerInfoAwareConnection::class)); diff --git a/tests/CreateSchemaTest.php b/tests/CreateSchemaTest.php index 59dbaa5..3b3ad5e 100644 --- a/tests/CreateSchemaTest.php +++ b/tests/CreateSchemaTest.php @@ -15,6 +15,7 @@ use Doctrine\DBAL\Types\Type; use FOD\DBALClickHouse\ClickHouseSchemaManager; use FOD\DBALClickHouse\Connection; +use FOD\DBALClickHouse\Types\ArrayType; use PHPUnit\Framework\TestCase; /** @@ -238,4 +239,75 @@ public function testEventDateProviderColumnBadOption() } $this->connection->exec('DROP TABLE test_table'); } + + public function testNullableColumns() + { + $fromSchema = $this->connection->getSchemaManager()->createSchema(); + ArrayType::registerArrayTypes($this->connection->getDatabasePlatform()); + + $toSchema = clone $fromSchema; + + $newTable = $toSchema->createTable('test_table_nullable'); + + $newTable->addColumn('id', 'integer', ['unsigned' => true, 'notnull' => false]); + $newTable->addColumn('payload', 'string', ['notnull' => false]); + $newTable->addColumn('price', 'float', ['notnull' => false]); + $newTable->addColumn('transactions', 'array(datetime)', ['notnull' => false]); + $newTable->addColumn('status', 'boolean', ['notnull' => false]); + $newTable->setPrimaryKey(['id']); + $newTable->addOption('engine', 'Memory'); + + $migrationSQLs = $fromSchema->getMigrateToSql($toSchema, $this->connection->getDatabasePlatform()); + $generatedSQL = implode(';', $migrationSQLs); + $this->assertEquals("CREATE TABLE test_table_nullable (id UInt32, payload Nullable(String), price Nullable(Float64), transactions Array(Nullable(DateTime)), status Nullable(UInt8)) ENGINE = Memory", + $generatedSQL); + foreach ($migrationSQLs as $sql) { + $this->connection->exec($sql); + } + $this->connection->insert('test_table_nullable', + [ + 'id' => 1, + 'payload' => 's1', + 'price' => 1.5, + 'transactions' => [date('Y-m-d H:i:s'), null], + 'status' => null + ]); + $this->connection->insert('test_table_nullable', + [ + 'id' => 2, + 'payload' => 's2', + 'price' => 120, + 'transactions' => [null, null], + 'status' => false + ]); + $this->connection->insert('test_table_nullable', + [ + 'id' => 3, + 'payload' => null, + 'price' => 1000, + 'transactions' => [date('Y-m-d H:i:s')], + 'status' => true + ]); + $this->connection->insert('test_table_nullable', + [ + 'id' => 4, + 'payload' => 's4', + 'price' => null, + 'transactions' => [date('Y-m-d H:i:s'), date('Y-m-d H:i:s')], + 'status' => null + ]); + $this->connection->insert('test_table_nullable', + [ + 'id' => 5, + 'payload' => 's5', + 'price' => 100, + 'transactions' => [date('Y-m-d H:i:s')], + 'status' => true + ]); + + $this->assertEquals(2, + (int)$this->connection->fetchColumn("SELECT count() from test_table_nullable WHERE {$this->connection->getDatabasePlatform()->getIsNullExpression('status')}")); + + $this->connection->exec('DROP TABLE test_table_nullable'); + } }