diff --git a/packages/php-datatypes/src/Definition/Bigquery.php b/packages/php-datatypes/src/Definition/Bigquery.php new file mode 100644 index 000000000..b7570fefb --- /dev/null +++ b/packages/php-datatypes/src/Definition/Bigquery.php @@ -0,0 +1,313 @@ +validateType($type); + $this->validateLength($type, $options['length'] ?? null); + + $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 .= sprintf('(%s)', $length); + } + return $out; + } + + public function getSQLDefinition(): string + { + $definition = $this->getTypeOnlySQLDefinition(); + if ($this->getDefault() !== null) { + $definition .= ' DEFAULT ' . $this->getDefault(); + } + if (!$this->isNullable()) { + $definition .= ' NOT NULL'; + } + return $definition; + } + + public function getBasetype(): string + { + switch (strtoupper($this->type)) { + case self::TYPE_INT64: + case self::TYPE_INT: + case self::TYPE_SMALLINT: + case self::TYPE_INTEGER: + case self::TYPE_BIGINT: + case self::TYPE_TINYINT: + case self::TYPE_BYTEINT: + $basetype = BaseType::INTEGER; + break; + case self::TYPE_NUMERIC: + case self::TYPE_DECIMAL: + case self::TYPE_BIGNUMERIC: + case self::TYPE_BIGDECIMAL: + $basetype = BaseType::NUMERIC; + break; + case self::TYPE_FLOAT64: + $basetype = BaseType::FLOAT; + break; + case self::TYPE_BOOL: + $basetype = BaseType::BOOLEAN; + break; + case self::TYPE_DATE: + $basetype = BaseType::DATE; + break; + case self::TYPE_DATETIME: + case self::TYPE_TIME: + case self::TYPE_TIMESTAMP: + $basetype = BaseType::TIMESTAMP; + break; + default: + $basetype = BaseType::STRING; + break; + } + return $basetype; + } + + /** + * @return array{type:string,length:string|null,nullable:bool} + */ + public function toArray(): array + { + return [ + 'type' => $this->getType(), + 'length' => $this->getLength(), + 'nullable' => $this->isNullable(), + ]; + } + + 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_BOOL; + case BaseType::DATE: + return self::TYPE_DATE; + case BaseType::FLOAT: + return self::TYPE_FLOAT64; + case BaseType::INTEGER: + return self::TYPE_INT64; + case BaseType::NUMERIC: + return self::TYPE_NUMERIC; + case BaseType::STRING: + return self::TYPE_STRING; + case BaseType::TIMESTAMP: + return self::TYPE_TIMESTAMP; + } + + throw new LogicException(sprintf('Definition for base type "%s" is missing.', $basetype)); + } + + /** + * @throws InvalidTypeException + */ + private function validateType(string $type): void + { + if (!in_array(strtoupper($type), self::TYPES, true)) { + throw new InvalidTypeException(sprintf('"%s" is not a valid type', $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_BYTES: + case self::TYPE_STRING: + $valid = $this->validateMaxLength($length, self::MAX_LENGTH); + break; + case self::TYPE_NUMERIC: + case self::TYPE_DECIMAL: + $valid = $this->validateBigqueryNumericLength($length, 38, 9); + break; + case self::TYPE_BIGNUMERIC: + case self::TYPE_BIGDECIMAL: + $valid = $this->validateBigNumericLength($length, 76, 38); + break; + default: + if ($length !== null && $length !== '') { + $valid = false; + break; + } + break; + } + if (!$valid) { + throw new InvalidLengthException("'{$length}' is not valid length for {$type}"); + } + } + + /** + * @param null|int|string $length + */ + //phpcs:ignore SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint + protected function validateBigqueryNumericLength( + $length, + int $firstMax, + int $secondMax + ): bool { + if ($this->isEmpty($length)) { + return true; + } + + $valid = $this->validateNumericLength($length, $firstMax, $secondMax); + if (!$valid) { + return false; + } + + return $this->validateNumericScaleAndPrecision((string) $length, self::NUMERIC_LENGTH_CONST); + } + + /** + * @param null|int|string $length + */ + //phpcs:ignore SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint + protected function validateBigNumericLength( + $length, + int $firstMax, + int $secondMax + ): bool { + if ($this->isEmpty($length)) { + return true; + } + + $valid = $this->validateNumericLength($length, $firstMax, $secondMax); + if (!$valid) { + return false; + } + + return $this->validateNumericScaleAndPrecision((string) $length, self::BIGNUMERIC_LENGTH_CONST); + } + + private function validateNumericScaleAndPrecision(string $length, int $decimalLengthConst): bool + { + $parts = explode(',', $length); + $p = (int) $parts[0]; + $s = !isset($parts[1]) ? 0 : (int) $parts[1]; + // max(1, S) ≤ P ≤ S + + if ((max(1, $s) <= $p) && ($p <= ($s + $decimalLengthConst))) { + return true; + } + + return false; + } +} diff --git a/packages/php-datatypes/tests/BigqueryDatatypeTest.php b/packages/php-datatypes/tests/BigqueryDatatypeTest.php new file mode 100644 index 000000000..f6d2fa69c --- /dev/null +++ b/packages/php-datatypes/tests/BigqueryDatatypeTest.php @@ -0,0 +1,335 @@ + + */ + public function invalidLengths(): array + { + return [ + ['string', 'notANumber'], + ['string', '0'], + ['string', 9223372036854775808], + + ['bytes', 'notANumber'], + ['bytes', '0'], + ['bytes', 9223372036854775808], + + ['decimal', 'notANumber'], + ['decimal', '0'], + ['decimal', '100'], + ['decimal', '38,8'], + ['decimal', '38'], + + ['numeric', 'notANumber'], + ['numeric', '0'], + ['numeric', '24,10'], + ['numeric', '38,8'], + + ['bignumeric', 'notANumber'], + ['bignumeric', '0'], + ['bignumeric', '100'], + ['bignumeric', '75,30'], + + ['bigdecimal', 'notANumber'], + ['bigdecimal', '0'], + ['bigdecimal', '78,10'], + ['bigdecimal', '75,30'], + ['bigdecimal', '78'], + + ['bool', 'anyLength'], + ['bytes', 'anyLength'], + ['date', 'anyLength'], + ['datetime', 'anyLength'], + ['time', 'anyLength'], + ['timestamp', 'anyLength'], + ['geography', 'anyLength'], + ['interval', 'anyLength'], + ['json', 'anyLength'], + ['int64', 'anyLength'], + ['int', 'anyLength'], + ['smallint', 'anyLength'], + ['integer', 'anyLength'], + ['bigint', 'anyLength'], + ['tinyint', 'anyLength'], + ['byteint', 'anyLength'], + ['float64', 'anyLength'], + ]; + } + + /** + * @dataProvider invalidLengths + * @param string|int|null $length + * @throws InvalidLengthException + * @throws InvalidOptionException + * @throws InvalidTypeException + */ + //phpcs:ignore SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint + public function testInvalidLengths(string $type, $length, ?array $extraOption = []): void + { + $options = $extraOption; + $options['length'] = $length; + + $this->expectException(InvalidLengthException::class); + new Bigquery($type, $options); + } + + public function testBasetypes(): void + { + foreach (Bigquery::TYPES as $type) { + $basetype = (new Bigquery($type))->getBasetype(); + switch ($type) { + case 'INT64': + case 'INT': + case 'SMALLINT': + case 'INTEGER': + case 'BIGINT': + case 'TINYINT': + case 'BYTEINT': + $this->assertEquals(BaseType::INTEGER, $basetype); + break; + case 'NUMERIC': + case 'DECIMAL': + case 'BIGNUMERIC': + case 'BIGDECIMAL': + $this->assertEquals(BaseType::NUMERIC, $basetype); + break; + case 'FLOAT64': + $this->assertEquals(BaseType::FLOAT, $basetype); + break; + case 'BOOL': + $this->assertEquals(BaseType::BOOLEAN, $basetype); + break; + case 'DATE': + $this->assertEquals(BaseType::DATE, $basetype); + break; + case 'DATETIME': + case 'TIME': + case 'TIMESTAMP': + $this->assertEquals(BaseType::TIMESTAMP, $basetype); + break; + default: + $this->assertEquals(BaseType::STRING, $basetype); + break; + } + } + } + + public function testInvalidOption(): void + { + try { + new Bigquery('numeric', ['myoption' => 'value']); + $this->fail('Exception not caught'); + } catch (Throwable $e) { + $this->assertEquals(InvalidOptionException::class, get_class($e)); + } + } + + public function testInvalidType(): void + { + $this->expectException(InvalidTypeException::class); + new Bigquery('UNKNOWN'); + } + + public function testGetTypeByBasetype(): void + { + $this->assertSame('BOOL', Bigquery::getTypeByBasetype('BOOLEAN')); + + $this->assertSame('STRING', Bigquery::getTypeByBasetype('STRING')); + + // not only upper case + $this->assertSame('BOOL', Bigquery::getTypeByBasetype('Boolean')); + + $this->expectException(InvalidTypeException::class); + $this->expectExceptionMessage('Base type "FOO" is not valid.'); + Bigquery::getTypeByBasetype('foo'); + } + + /** + * @return array + */ + public function expectedSqlDefinitions(): array + { + $tests = []; + + foreach (['numeric', 'decimal'] as $type) { + $tests[] = [ + $type, + ['length' => ''], + $type, + ]; + $tests[] = [ + $type, + [], + $type, + ]; + $tests[] = [ + $type, + ['length' => '30,2'], + $type . '(30,2)', + ]; + $tests[] = [ + $type, + ['length' => '30,2', 'default' => '10.00'], + $type . '(30,2) DEFAULT 10.00', + ]; + $tests[] = [ + $type, + ['length' => '30,2', 'default' => '10.00', 'nullable' => false], + $type . '(30,2) DEFAULT 10.00 NOT NULL', + ]; + } + + foreach (['bignumeric', 'bigdecimal'] as $type) { + $tests[] = [ + $type, + ['length' => ''], + $type, + ]; + $tests[] = [ + $type, + [], + $type, + ]; + $tests[] = [ + $type, + ['length' => '76,38'], + $type . '(76,38)', + ]; + $tests[] = [ + $type, + ['length' => '76,38', 'default' => '10.00'], + $type . '(76,38) DEFAULT 10.00', + ]; + $tests[] = [ + $type, + ['length' => '76,38', 'default' => '10.00', 'nullable' => false], + $type . '(76,38) DEFAULT 10.00 NOT NULL', + ]; + } + + foreach (['bytes', 'string'] as $type) { + $tests[] = [ + $type, + ['length' => ''], + $type, + ]; + $tests[] = [ + $type, + [], + $type, + ]; + $tests[] = [ + $type, + ['default' => '\'\'', 'nullable' => false], + $type . ' DEFAULT \'\' NOT NULL', + ]; + $tests[] = [ + $type, + ['default' => ''], + $type, + ]; + $tests[] = [ + $type, + ['default' => '', 'nullable' => false], + $type . ' NOT NULL', + ]; + $tests[] = [ + $type, + ['length' => '1000'], + $type . '(1000)', + ]; + } + + return $tests; + } + + /** + * @dataProvider expectedSqlDefinitions + */ + public function testSqlDefinition(string $type, ?array $options, string $expectedDefinition): void + { + $definition = new Bigquery($type, $options); + self::assertEquals($expectedDefinition, $definition->getSQLDefinition()); + } + + /** + * @return array> + */ + public function validLengths(): array + { + return [ + ['bytes', null], + ['bytes', ''], + ['bytes', '1'], + ['bytes', '42'], + ['bytes', 9223372036854775807], + ['bytes', '9223372036854775807'], + + ['string', null], + ['string', ''], + ['string', '1'], + ['string', '42'], + ['string', '9223372036854775807'], + + ['numeric', null], + ['numeric', ''], + ['numeric', '30,2'], + ['numeric', '38,9'], + ['numeric', '25'], + + ['bignumeric', null], + ['bignumeric', ''], + ['bignumeric', '38,9'], + ['bignumeric', '76,38'], + + ['bool', null], + ['date', null], + ['datetime', null], + ['time', null], + ['timestamp', null], + ['geography', null], + ['interval', null], + ['json', null], + ['int64', null], + ['int', null], + ['smallint', null], + ['integer', null], + ['bigint', null], + ['tinyint', null], + ['byteint', null], + ['float64', null], + ]; + } + + /** + * @dataProvider validLengths + * @param string|int|null $length + * @throws InvalidLengthException + * @throws InvalidOptionException + * @throws InvalidTypeException + */ + //phpcs:ignore SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint + public function testValidLengths(string $type, $length): void + { + $options = []; + if ($length !== null) { + $options['length'] = $length; + } + new Bigquery($type, $options); + $this->expectNotToPerformAssertions(); + } +}