Skip to content

Commit 6c49b5e

Browse files
Merge pull request #5 from keboola/add-bq-datatypes
Add bigquery
2 parents b49dea2 + 74096a8 commit 6c49b5e

2 files changed

Lines changed: 648 additions & 0 deletions

File tree

src/Definition/Bigquery.php

Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Keboola\Datatype\Definition;
6+
7+
use Keboola\Datatype\Definition\Exception\InvalidLengthException;
8+
use Keboola\Datatype\Definition\Exception\InvalidOptionException;
9+
use Keboola\Datatype\Definition\Exception\InvalidTypeException;
10+
use LogicException;
11+
12+
/**
13+
* Class Bigquery
14+
*
15+
* https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types
16+
*/
17+
class Bigquery extends Common
18+
{
19+
public const NUMERIC_LENGTH_CONST = 29;
20+
public const BIGNUMERIC_LENGTH_CONST = 38;
21+
22+
// public const TYPE_ARRAY = 'ARRAY'; // todo we decided not support array right now
23+
24+
public const TYPE_BOOL = 'BOOL'; // NULL,TRUE,FALSE
25+
26+
public const TYPE_BYTES = 'BYTES'; // BYTES(L) L is a positive INT64
27+
28+
/* Datetime */
29+
public const TYPE_DATE = 'DATE';
30+
public const TYPE_DATETIME = 'DATETIME';
31+
public const TYPE_TIME = 'TIME';
32+
public const TYPE_TIMESTAMP = 'TIMESTAMP';
33+
34+
public const TYPE_GEOGRAPHY = 'GEOGRAPHY';
35+
36+
public const TYPE_INTERVAL = 'INTERVAL';
37+
38+
public const TYPE_JSON = 'JSON';
39+
40+
/* Numeric */
41+
public const TYPE_INT64 = 'INT64';
42+
// aliases for INT64
43+
public const TYPE_INT = 'INT';
44+
public const TYPE_SMALLINT = 'SMALLINT';
45+
public const TYPE_INTEGER = 'INTEGER';
46+
public const TYPE_BIGINT = 'BIGINT';
47+
public const TYPE_TINYINT = 'TINYINT';
48+
public const TYPE_BYTEINT = 'BYTEINT';
49+
50+
public const TYPE_NUMERIC = 'NUMERIC'; // 0 ≤ S ≤ 9, max(1, S) ≤ P ≤ S + 29
51+
// alias for NUMERIC
52+
public const TYPE_DECIMAL = 'DECIMAL';
53+
54+
public const TYPE_BIGNUMERIC = 'BIGNUMERIC'; // 0 ≤ S ≤ 38, max(1, S) ≤ P ≤ S + 38
55+
// alias for BIGNUMERIC
56+
public const TYPE_BIGDECIMAL = 'BIGDECIMAL';
57+
58+
public const TYPE_FLOAT64 = 'FLOAT64';
59+
60+
public const TYPE_STRING = 'STRING'; // STRING(L) L is a positive INT64 value
61+
62+
// public const TYPE_STRUCT = 'STRUCT'; // todo we decided not support struct right now
63+
64+
public const TYPES = [
65+
self::TYPE_BOOL,
66+
self::TYPE_BYTES,
67+
self::TYPE_DATE,
68+
self::TYPE_DATETIME,
69+
self::TYPE_TIME,
70+
self::TYPE_TIMESTAMP,
71+
self::TYPE_GEOGRAPHY,
72+
self::TYPE_INTERVAL,
73+
self::TYPE_JSON,
74+
self::TYPE_INT64,
75+
self::TYPE_NUMERIC,
76+
self::TYPE_BIGNUMERIC,
77+
self::TYPE_FLOAT64,
78+
self::TYPE_STRING,
79+
80+
// aliases
81+
self::TYPE_INT,
82+
self::TYPE_SMALLINT,
83+
self::TYPE_INTEGER,
84+
self::TYPE_BIGINT,
85+
self::TYPE_TINYINT,
86+
self::TYPE_BYTEINT,
87+
self::TYPE_DECIMAL,
88+
self::TYPE_BIGDECIMAL,
89+
];
90+
91+
public const MAX_LENGTH = 9223372036854775807;
92+
93+
/**
94+
* @param array{length?:string|null, nullable?:bool, default?:string|null} $options
95+
* @throws InvalidLengthException
96+
* @throws InvalidOptionException
97+
* @throws InvalidTypeException
98+
*/
99+
public function __construct(string $type, array $options = [])
100+
{
101+
$this->validateType($type);
102+
$this->validateLength($type, $options['length'] ?? null);
103+
104+
$diff = array_diff(array_keys($options), ['length', 'nullable', 'default']);
105+
if ($diff !== []) {
106+
throw new InvalidOptionException("Option '{$diff[0]}' not supported");
107+
}
108+
109+
if (array_key_exists('default', $options) && $options['default'] === '') {
110+
unset($options['default']);
111+
}
112+
parent::__construct($type, $options);
113+
}
114+
115+
public function getTypeOnlySQLDefinition(): string
116+
{
117+
$out = $this->getType();
118+
$length = $this->getLength();
119+
if ($length !== null && $length !== '') {
120+
$out .= sprintf('(%s)', $length);
121+
}
122+
return $out;
123+
}
124+
125+
public function getSQLDefinition(): string
126+
{
127+
$definition = $this->getTypeOnlySQLDefinition();
128+
if ($this->getDefault() !== null) {
129+
$definition .= ' DEFAULT ' . $this->getDefault();
130+
}
131+
if (!$this->isNullable()) {
132+
$definition .= ' NOT NULL';
133+
}
134+
return $definition;
135+
}
136+
137+
public function getBasetype(): string
138+
{
139+
switch (strtoupper($this->type)) {
140+
case self::TYPE_INT64:
141+
case self::TYPE_INT:
142+
case self::TYPE_SMALLINT:
143+
case self::TYPE_INTEGER:
144+
case self::TYPE_BIGINT:
145+
case self::TYPE_TINYINT:
146+
case self::TYPE_BYTEINT:
147+
$basetype = BaseType::INTEGER;
148+
break;
149+
case self::TYPE_NUMERIC:
150+
case self::TYPE_DECIMAL:
151+
case self::TYPE_BIGNUMERIC:
152+
case self::TYPE_BIGDECIMAL:
153+
$basetype = BaseType::NUMERIC;
154+
break;
155+
case self::TYPE_FLOAT64:
156+
$basetype = BaseType::FLOAT;
157+
break;
158+
case self::TYPE_BOOL:
159+
$basetype = BaseType::BOOLEAN;
160+
break;
161+
case self::TYPE_DATE:
162+
$basetype = BaseType::DATE;
163+
break;
164+
case self::TYPE_DATETIME:
165+
case self::TYPE_TIME:
166+
case self::TYPE_TIMESTAMP:
167+
$basetype = BaseType::TIMESTAMP;
168+
break;
169+
default:
170+
$basetype = BaseType::STRING;
171+
break;
172+
}
173+
return $basetype;
174+
}
175+
176+
/**
177+
* @return array{type:string,length:string|null,nullable:bool}
178+
*/
179+
public function toArray(): array
180+
{
181+
return [
182+
'type' => $this->getType(),
183+
'length' => $this->getLength(),
184+
'nullable' => $this->isNullable(),
185+
];
186+
}
187+
188+
public static function getTypeByBasetype(string $basetype): string
189+
{
190+
$basetype = strtoupper($basetype);
191+
192+
if (!BaseType::isValid($basetype)) {
193+
throw new InvalidTypeException(sprintf('Base type "%s" is not valid.', $basetype));
194+
}
195+
196+
switch ($basetype) {
197+
case BaseType::BOOLEAN:
198+
return self::TYPE_BOOL;
199+
case BaseType::DATE:
200+
return self::TYPE_DATE;
201+
case BaseType::FLOAT:
202+
return self::TYPE_FLOAT64;
203+
case BaseType::INTEGER:
204+
return self::TYPE_INT64;
205+
case BaseType::NUMERIC:
206+
return self::TYPE_NUMERIC;
207+
case BaseType::STRING:
208+
return self::TYPE_STRING;
209+
case BaseType::TIMESTAMP:
210+
return self::TYPE_TIMESTAMP;
211+
}
212+
213+
throw new LogicException(sprintf('Definition for base type "%s" is missing.', $basetype));
214+
}
215+
216+
/**
217+
* @throws InvalidTypeException
218+
*/
219+
private function validateType(string $type): void
220+
{
221+
if (!in_array(strtoupper($type), self::TYPES, true)) {
222+
throw new InvalidTypeException(sprintf('"%s" is not a valid type', $type));
223+
}
224+
}
225+
226+
/**
227+
* @param null|int|string $length
228+
* @throws InvalidLengthException
229+
*/
230+
//phpcs:ignore SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
231+
private function validateLength(string $type, $length = null): void
232+
{
233+
$valid = true;
234+
switch (strtoupper($type)) {
235+
case self::TYPE_BYTES:
236+
case self::TYPE_STRING:
237+
$valid = $this->validateMaxLength($length, self::MAX_LENGTH);
238+
break;
239+
case self::TYPE_NUMERIC:
240+
case self::TYPE_DECIMAL:
241+
$valid = $this->validateBigqueryNumericLength($length, 38, 9);
242+
break;
243+
case self::TYPE_BIGNUMERIC:
244+
case self::TYPE_BIGDECIMAL:
245+
$valid = $this->validateBigNumericLength($length, 76, 38);
246+
break;
247+
default:
248+
if ($length !== null && $length !== '') {
249+
$valid = false;
250+
break;
251+
}
252+
break;
253+
}
254+
if (!$valid) {
255+
throw new InvalidLengthException("'{$length}' is not valid length for {$type}");
256+
}
257+
}
258+
259+
/**
260+
* @param null|int|string $length
261+
*/
262+
//phpcs:ignore SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
263+
protected function validateBigqueryNumericLength(
264+
$length,
265+
int $firstMax,
266+
int $secondMax
267+
): bool {
268+
if ($this->isEmpty($length)) {
269+
return true;
270+
}
271+
272+
$valid = $this->validateNumericLength($length, $firstMax, $secondMax);
273+
if (!$valid) {
274+
return false;
275+
}
276+
277+
return $this->validateNumericScaleAndPrecision((string) $length, self::NUMERIC_LENGTH_CONST);
278+
}
279+
280+
/**
281+
* @param null|int|string $length
282+
*/
283+
//phpcs:ignore SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
284+
protected function validateBigNumericLength(
285+
$length,
286+
int $firstMax,
287+
int $secondMax
288+
): bool {
289+
if ($this->isEmpty($length)) {
290+
return true;
291+
}
292+
293+
$valid = $this->validateNumericLength($length, $firstMax, $secondMax);
294+
if (!$valid) {
295+
return false;
296+
}
297+
298+
return $this->validateNumericScaleAndPrecision((string) $length, self::BIGNUMERIC_LENGTH_CONST);
299+
}
300+
301+
private function validateNumericScaleAndPrecision(string $length, int $decimalLengthConst): bool
302+
{
303+
$parts = explode(',', $length);
304+
$p = (int) $parts[0];
305+
$s = !isset($parts[1]) ? 0 : (int) $parts[1];
306+
// max(1, S) ≤ P ≤ S + <lengthConst NUMERIC=29|BIGNUMERIC=38>
307+
if ((max(1, $s) <= $p) && ($p <= ($s + $decimalLengthConst))) {
308+
return true;
309+
}
310+
311+
return false;
312+
}
313+
}

0 commit comments

Comments
 (0)