keboola/php-datatypes

View on GitHub
src/Definition/Redshift.php

Summary

Maintainability
F
4 days
Test Coverage
<?php

declare(strict_types=1);

namespace Keboola\Datatype\Definition;

use Keboola\Datatype\Definition\Exception\InvalidCompressionException;
use Keboola\Datatype\Definition\Exception\InvalidLengthException;
use Keboola\Datatype\Definition\Exception\InvalidOptionException;
use Keboola\Datatype\Definition\Exception\InvalidTypeException;
use LogicException;

class Redshift extends Common
{
    public const TYPES = [
        'SMALLINT', 'INT2', 'INTEGER', 'INT', 'INT4', 'BIGINT', 'INT8',
        'DECIMAL', 'NUMERIC',
        'REAL', 'FLOAT4', 'DOUBLE PRECISION', 'FLOAT8', 'FLOAT',
        'BOOLEAN', 'BOOL',
        'CHAR', 'CHARACTER', 'NCHAR', 'BPCHAR',
        'VARCHAR', 'CHARACTER VARYING', 'NVARCHAR', 'TEXT',
        'DATE',
        'TIMESTAMP', 'TIMESTAMP WITHOUT TIME ZONE',
        'TIMESTAMPTZ', 'TIMESTAMP WITH TIME ZONE',
    ];

    protected ?string $compression = null;

    /**
     * Redshift constructor.
     *
     * @param array{length?:string|null, nullable?:bool, default?:string|null, compression?:string|null} $options
     * @throws InvalidOptionException
     */
    public function __construct(string $type, array $options = [])
    {
        $this->validateType($type);
        $options['length'] = $this->processLength($options);
        $this->validateLength($type, $options['length']);

        if (isset($options['compression'])) {
            $this->validateCompression($type, $options['compression']);
            $this->compression = $options['compression'];
        }
        $diff = array_diff(array_keys($options), ['length', 'nullable', 'default', 'compression']);
        if ($diff !== []) {
            throw new InvalidOptionException("Option '{$diff[0]}' not supported");
        }
        parent::__construct($type, $options);
    }

    public function getCompression(): ?string
    {
        return $this->compression;
    }

    public function getSQLDefinition(): string
    {
        $definition =  $this->getType();
        if ($this->getLength() && $this->getLength() !== '') {
            $definition .= '(' . $this->getLength() . ')';
        }
        if (!$this->isNullable()) {
            $definition .= ' NOT NULL';
        }
        if ($this->getCompression() && $this->getCompression() !== '') {
            $definition .= ' ENCODE ' . $this->getCompression();
        }
        return $definition;
    }

    /**
     * @return array{type:string,length:string|null,nullable:bool,compression:string|null}
     */
    public function toArray(): array
    {
        return [
            'type' => $this->getType(),
            'length' => $this->getLength(),
            'nullable' => $this->isNullable(),
            'compression' => $this->getCompression(),
        ];
    }

    /**
     * @param array{length?:string|array|null} $options
     * @throws InvalidOptionException
     */
    private function processLength(array $options): ?string
    {
        if (!isset($options['length'])) {
            return null;
        }
        if (is_array($options['length'])) {
            return $this->getLengthFromArray($options['length']);
        }
        return (string) $options['length'];
    }

    /**
     * @param array{
     *     character_maximum?:string|int|null,
     *     numeric_precision?:string|int|null,
     *     numeric_scale?:string|int|null
     * } $lengthOptions
     * @throws InvalidOptionException
     */
    private function getLengthFromArray(array $lengthOptions): ?string
    {
        $expectedOptions = ['character_maximum', 'numeric_precision', 'numeric_scale'];
        $diff = array_diff(array_keys($lengthOptions), $expectedOptions);
        if ($diff !== []) {
            throw new InvalidOptionException(sprintf('Length option "%s" not supported', $diff[0]));
        }

        $characterMaximum = $lengthOptions['character_maximum'] ?? null;
        $numericPrecision = $lengthOptions['numeric_precision'] ?? null;
        $numericScale = $lengthOptions['numeric_scale'] ?? null;

        if (!is_null($characterMaximum)) {
            return (string) $characterMaximum;
        }
        if (!is_null($numericPrecision) && !is_null($numericScale)) {
            return $numericPrecision . ',' . $numericScale;
        }
        return $numericPrecision === null ? null : (string) $numericPrecision;
    }

    /**
     * @throws InvalidTypeException
     */
    private function validateType(string $type): void
    {
        if (!in_array(strtoupper($type), $this::TYPES)) {
            throw new InvalidTypeException("'{$type}' is not a valid type");
        }
    }

    /**
     * @param null|int|string $length
     * @throws InvalidLengthException
     */
    //phpcs:ignore SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
    private function validateLength(string $type, $length = null): void
    {
        $valid = true;
        switch (strtoupper($type)) {
            case 'DECIMAL':
            case 'NUMERIC':
                if (is_null($length) || $length === '') {
                    break;
                }
                $parts = explode(',', (string) $length);
                if (!in_array(count($parts), [1, 2])) {
                    $valid = false;
                    break;
                }
                if (!is_numeric($parts[0])) {
                    $valid = false;
                    break;
                }
                if (isset($parts[1]) && !is_numeric($parts[1])) {
                    $valid = false;
                    break;
                }
                if ((int) $parts[0] <= 0 || (int) $parts[0] > 37) {
                    $valid = false;
                    break;
                }
                if (isset($parts[1]) && ((int) $parts[1] > 37 || (int) $parts[1] > (int) $parts[0])) {
                    $valid = false;
                    break;
                }
                break;
            case 'VARCHAR':
            case 'CHARACTER VARYING':
            case 'TEXT':
            case 'NVARCHAR':
                if (is_null($length) || $length === '') {
                    break;
                }
                if (!is_numeric($length)) {
                    $valid = false;
                    break;
                }
                if ((int) $length <= 0 || (int) $length > 65535) {
                    $valid = false;
                    break;
                }
                break;
            case 'CHAR':
            case 'CHARACTER':
            case 'NCHAR':
            case 'BPCHAR':
                if (is_null($length)) {
                    break;
                }
                if (!is_numeric($length)) {
                    $valid = false;
                    break;
                }
                if ((int) $length <= 0 || (int) $length > 4096) {
                    $valid = false;
                    break;
                }
                break;
            case 'TIMESTAMP':
            case 'TIMESTAMP WITHOUT TIME ZONE':
            case 'TIMESTAMPTZ':
            case 'TIMESTAMP WITH TIME ZONE':
                if (is_null($length) || $length === '') {
                    break;
                }
                if (!is_numeric($length)) {
                    $valid = false;
                    break;
                }
                if ((int) $length <= 0 || (int) $length > 11) {
                    $valid = false;
                    break;
                }
                break;
            default:
                if (!is_null($length) && $length !== '') {
                    $valid = false;
                    break;
                }
                break;
        }
        if (!$valid) {
            throw new InvalidLengthException("'{$length}' is not valid length for {$type}");
        }
    }

    /**
     * @throws InvalidCompressionException
     */
    private function validateCompression(string $type, string $compression): void
    {
        $valid = true;
        $type = strtoupper($type);
        switch (strtoupper($compression)) {
            case 'RAW':
            case 'ZSTD':
            case 'RUNLENGTH':
            case null:
            case '':
                break;
            case 'BYTEDICT':
                if (in_array($type, ['BOOLEAN', 'BOOL'])) {
                    $valid = false;
                }
                break;
            case 'DELTA':
                if (!in_array($type, [
                    'SMALLINT',
                    'INT2',
                    'INT',
                    'INTEGER',
                    'INT4',
                    'BIGINT',
                    'INT8',
                    'DATE',
                    'TIMESTAMP',
                    'TIMESTAMP WITHOUT TIME ZONE',
                    'TIMESTAMPTZ',
                    'TIMESTAMP WITH TIMEZONE',
                    'DECIMAL',
                    'NUMERIC',
                ])) {
                    $valid = false;
                }
                break;
            case 'DELTA32K':
                if (!in_array($type, [
                    'INT',
                    'INTEGER',
                    'INT4',
                    'BIGINT',
                    'INT8',
                    'DATE',
                    'TIMESTAMP',
                    'TIMESTAMP WITHOUT TIME ZONE',
                    'TIMESTAMPTZ',
                    'TIMESTAMP WITH TIMEZONE',
                    'DECIMAL',
                    'NUMERIC',
                ])) {
                    $valid = false;
                }
                break;
            case 'LZO':
                if (in_array($type, ['BOOLEAN', 'BOOL', 'REAL', 'FLOAT4', 'DOUBLE PRECISION', 'FLOAT8', 'FLOAT'])) {
                    $valid = false;
                }
                break;
            case 'MOSTLY8':
                if (!in_array($type, [
                    'SMALLINT',
                    'INT2',
                    'INT',
                    'INTEGER',
                    'INT4',
                    'BIGINT',
                    'INT8',
                    'DECIMAL',
                    'NUMERIC',
                ])) {
                    $valid = false;
                }
                break;
            case 'MOSTLY16':
                if (!in_array($type, ['INT', 'INTEGER', 'INT4', 'BIGINT', 'INT8', 'DECIMAL', 'NUMERIC'])) {
                    $valid = false;
                }
                break;
            case 'MOSTLY32':
                if (!in_array($type, ['BIGINT', 'INT8', 'DECIMAL', 'NUMERIC'])) {
                    $valid = false;
                }
                break;
            case 'TEXT255':
            case 'TEXT32K':
                if (!in_array($type, ['VARCHAR', 'CHARACTER VARYING', 'NVARCHAR', 'TEXT'])) {
                    $valid = false;
                }
                break;
            default:
                $valid = false;
                break;
        }
        if (!$valid) {
            throw new InvalidCompressionException("'{$compression}' is not valid compression for {$type}");
        }
    }

    public function getBasetype(): string
    {
        switch (strtoupper($this->type)) {
            case 'SMALLINT':
            case 'INT2':
            case 'INTEGER':
            case 'INT':
            case 'INT4':
            case 'BIGINT':
            case 'INT8':
                $basetype = BaseType::INTEGER;
                break;
            case 'DECIMAL':
            case 'NUMERIC':
                $basetype = BaseType::NUMERIC;
                break;
            case 'REAL':
            case 'FLOAT4':
            case 'DOUBLE PRECISION':
            case 'FLOAT8':
            case 'FLOAT':
                $basetype = BaseType::FLOAT;
                break;
            case 'BOOLEAN':
            case 'BOOL':
                $basetype = BaseType::BOOLEAN;
                break;
            case 'DATE':
                $basetype = BaseType::DATE;
                break;
            case 'TIMESTAMP':
            case 'TIMESTAMP WITHOUT TIME ZONE':
            case 'TIMESTAMPTZ':
            case 'TIMESTAMP WITH TIME ZONE':
                $basetype = BaseType::TIMESTAMP;
                break;
            default:
                $basetype = BaseType::STRING;
                break;
        }
        return $basetype;
    }

    public static function getTypeByBasetype(string $basetype): string
    {
        throw new LogicException('Method is not implemented yet.');
    }

    /**
     * @return array<int, array{key:string,value:mixed}>
     */
    public function toMetadata(): array
    {
        $metadata = parent::toMetadata();
        if ($this->getCompression()) {
            $metadata[] = [
                'key' => Common::KBC_METADATA_KEY_COMPRESSION,
                'value' => $this->getCompression(),
            ];
        }
        return $metadata;
    }
}