keboola/php-datatypes

View on GitHub
src/Definition/Snowflake.php

Summary

Maintainability
F
4 days
Test Coverage
<?php

declare(strict_types=1);

namespace Keboola\Datatype\Definition;

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

class Snowflake extends Common
{
    public const METADATA_BACKEND = 'snowflake';
    public const TYPE_NUMBER = 'NUMBER';
    public const TYPE_DECIMAL = 'DECIMAL';
    public const TYPE_NUMERIC = 'NUMERIC';
    public const TYPE_INT = 'INT';
    public const TYPE_INTEGER = 'INTEGER';
    public const TYPE_BIGINT = 'BIGINT';
    public const TYPE_SMALLINT = 'SMALLINT';
    public const TYPE_TINYINT = 'TINYINT';
    public const TYPE_BYTEINT = 'BYTEINT';
    public const TYPE_FLOAT = 'FLOAT';
    public const TYPE_FLOAT4 = 'FLOAT4';
    public const TYPE_FLOAT8 = 'FLOAT8';
    public const TYPE_DOUBLE = 'DOUBLE';
    public const TYPE_DOUBLE_PRECISION = 'DOUBLE PRECISION';
    public const TYPE_REAL = 'REAL';
    public const TYPE_VARCHAR = 'VARCHAR';
    public const TYPE_CHAR = 'CHAR';
    public const TYPE_CHARACTER = 'CHARACTER';
    public const TYPE_STRING = 'STRING';
    public const TYPE_TEXT = 'TEXT';
    public const TYPE_BOOLEAN = 'BOOLEAN';
    public const TYPE_DATE = 'DATE';
    public const TYPE_DATETIME = 'DATETIME';
    public const TYPE_TIME = 'TIME';
    public const TYPE_TIMESTAMP = 'TIMESTAMP';
    public const TYPE_TIMESTAMP_NTZ = 'TIMESTAMP_NTZ';
    public const TYPE_TIMESTAMP_LTZ = 'TIMESTAMP_LTZ';
    public const TYPE_TIMESTAMP_TZ = 'TIMESTAMP_TZ';
    public const TYPE_VARIANT = 'VARIANT';
    public const TYPE_BINARY = 'BINARY';
    public const TYPE_VARBINARY = 'VARBINARY';
    public const TYPES = [
        self::TYPE_NUMBER,
        self::TYPE_DECIMAL,
        self::TYPE_NUMERIC,
        self::TYPE_INT,
        self::TYPE_INTEGER,
        self::TYPE_BIGINT,
        self::TYPE_SMALLINT,
        self::TYPE_TINYINT,
        self::TYPE_BYTEINT,
        self::TYPE_FLOAT,
        self::TYPE_FLOAT4,
        self::TYPE_FLOAT8,
        self::TYPE_DOUBLE,
        self::TYPE_DOUBLE_PRECISION,
        self::TYPE_REAL,
        self::TYPE_VARCHAR,
        self::TYPE_CHAR,
        self::TYPE_CHARACTER,
        self::TYPE_STRING,
        self::TYPE_TEXT,
        self::TYPE_BOOLEAN,
        self::TYPE_DATE,
        self::TYPE_DATETIME,
        self::TYPE_TIME,
        self::TYPE_TIMESTAMP,
        self::TYPE_TIMESTAMP_NTZ,
        self::TYPE_TIMESTAMP_LTZ,
        self::TYPE_TIMESTAMP_TZ,
        self::TYPE_VARIANT,
        self::TYPE_BINARY,
        self::TYPE_VARBINARY,
    ];

    public const MAX_VARCHAR_LENGTH = 16777216;
    public const MAX_VARBINARY_LENGTH = 8388608;

    /**
     * Snowflake constructor.
     *
     * @param array{
     *     length?:string|null|array,
     *     nullable?:bool,
     *     default?:string|null
     * } $options
     * @throws InvalidLengthException
     * @throws InvalidOptionException
     * @throws InvalidTypeException
     */
    public function __construct(string $type, array $options = [])
    {
        $this->validateType($type);
        $options['length'] = $this->processLength($options);
        $this->validateLength($type, $options['length']);
        $diff = array_diff(array_keys($options), ['length', 'nullable', 'default']);
        if ($diff !== []) {
            throw new InvalidOptionException("Option '{$diff[0]}' not supported");
        }
        if (array_key_exists('default', $options) && $options['default'] === '') {
            unset($options['default']);
        }
        parent::__construct($type, $options);
    }


    public function getTypeOnlySQLDefinition(): string
    {
        $out = $this->getType();
        $length = $this->getLength();
        if ($length !== null && $length !== '') {
            $out .= ' (' . $length . ')';
        }
        return $out;
    }

    public function getSQLDefinition(): string
    {
        $definition = $this->getTypeOnlySQLDefinition();
        if (!$this->isNullable()) {
            $definition .= ' NOT NULL';
        }
        if ($this->getDefault() !== null) {
            $definition .= ' DEFAULT ' . $this->getDefault();
        }
        return $definition;
    }

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

    /**
     * @param array{length?: string|null|array} $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 self::TYPE_NUMBER:
            case self::TYPE_DECIMAL:
            case self::TYPE_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] > 38) {
                    $valid = false;
                    break;
                }
                if (isset($parts[1]) && ((int) $parts[1] > 38 || (int) $parts[1] > (int) $parts[0])) {
                    $valid = false;
                    break;
                }
                break;
            case self::TYPE_VARCHAR:
            case self::TYPE_CHAR:
            case self::TYPE_CHARACTER:
            case self::TYPE_STRING:
            case self::TYPE_TEXT:
                if (is_null($length) || $length === '') {
                    break;
                }
                if (!is_numeric($length)) {
                    $valid = false;
                    break;
                }
                if ((int) $length <= 0 || (int) $length > self::MAX_VARCHAR_LENGTH) {
                    $valid = false;
                    break;
                }
                break;
            case self::TYPE_TIME:
            case self::TYPE_DATETIME:
            case self::TYPE_TIMESTAMP:
            case self::TYPE_TIMESTAMP_NTZ:
            case self::TYPE_TIMESTAMP_LTZ:
            case self::TYPE_TIMESTAMP_TZ:
                if (is_null($length) || $length === '') {
                    break;
                }
                if (!is_numeric($length)) {
                    $valid = false;
                    break;
                }
                if ((int) $length < 0 || (int) $length > 9) {
                    $valid = false;
                    break;
                }
                break;
            case self::TYPE_BINARY:
            case self::TYPE_VARBINARY:
                if (is_null($length) || $length === '') {
                    break;
                }
                if (!is_numeric($length)) {
                    $valid = false;
                    break;
                }
                if ((int) $length < 1 || (int) $length > self::MAX_VARBINARY_LENGTH) {
                    $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}");
        }
    }

    public function getBasetype(): string
    {
        switch (strtoupper($this->type)) {
            case self::TYPE_INT:
            case self::TYPE_INTEGER:
            case self::TYPE_BIGINT:
            case self::TYPE_SMALLINT:
            case self::TYPE_TINYINT:
            case self::TYPE_BYTEINT:
                $basetype = BaseType::INTEGER;
                break;
            case self::TYPE_NUMBER:
            case self::TYPE_DECIMAL:
            case self::TYPE_NUMERIC:
                $basetype = BaseType::NUMERIC;
                break;
            case self::TYPE_FLOAT:
            case self::TYPE_FLOAT4:
            case self::TYPE_FLOAT8:
            case self::TYPE_DOUBLE:
            case self::TYPE_DOUBLE_PRECISION:
            case self::TYPE_REAL:
                $basetype = BaseType::FLOAT;
                break;
            case self::TYPE_BOOLEAN:
                $basetype = BaseType::BOOLEAN;
                break;
            case self::TYPE_DATE:
                $basetype = BaseType::DATE;
                break;
            case self::TYPE_DATETIME:
            case self::TYPE_TIMESTAMP:
            case self::TYPE_TIMESTAMP_NTZ:
            case self::TYPE_TIMESTAMP_LTZ:
            case self::TYPE_TIMESTAMP_TZ:
                $basetype = BaseType::TIMESTAMP;
                break;
            default:
                $basetype = BaseType::STRING;
                break;
        }
        return $basetype;
    }

    public static function getTypeByBasetype(string $basetype): string
    {
        $basetype = strtoupper($basetype);

        if (!BaseType::isValid($basetype)) {
            throw new InvalidTypeException(sprintf('Base type "%s" is not valid.', $basetype));
        }

        switch ($basetype) {
            case BaseType::BOOLEAN:
                return self::TYPE_BOOLEAN;
            case BaseType::DATE:
                return self::TYPE_DATE;
            case BaseType::FLOAT:
                return self::TYPE_FLOAT;
            case BaseType::INTEGER:
                return self::TYPE_INTEGER;
            case BaseType::NUMERIC:
                return self::TYPE_NUMBER;
            case BaseType::STRING:
                return self::TYPE_VARCHAR;
            case BaseType::TIMESTAMP:
                return self::TYPE_TIMESTAMP;
        }

        throw new LogicException(sprintf('Definition for base type "%s" is missing.', $basetype));
    }
}