From 3aab524218c3460e1466d04b55eb8aebcc3634c6 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 17 Sep 2026 14:23:55 +0200 Subject: [PATCH 1/4] Add more numeric types --- README.md | 2 +- doc/types.md | 18 ++++ docs/index.md | 2 +- src/Type/Decimal.php | 6 +- src/Type/Decimal128.php | 30 +++++++ src/Type/Decimal256.php | 30 +++++++ src/Type/Decimal32.php | 30 +++++++ src/Type/Decimal64.php | 30 +++++++ src/Type/Float32.php | 30 +++++++ src/Type/Float64.php | 30 +++++++ src/Type/Int128.php | 30 +++++++ src/Type/Int16.php | 30 +++++++ src/Type/Int256.php | 30 +++++++ src/Type/Int32.php | 30 +++++++ src/Type/Int8.php | 30 +++++++ src/Type/ScalarNumericType.php | 14 +++ src/Type/UInt128.php | 30 +++++++ src/Type/UInt16.php | 30 +++++++ src/Type/UInt256.php | 30 +++++++ src/Type/UInt8.php | 30 +++++++ tests/Type/ScalarNumericIntegrationTest.php | 97 +++++++++++++++++++++ tests/TypesTest.php | 50 +++++++++++ todo.md | 18 ++-- 23 files changed, 645 insertions(+), 12 deletions(-) create mode 100644 src/Type/Decimal128.php create mode 100644 src/Type/Decimal256.php create mode 100644 src/Type/Decimal32.php create mode 100644 src/Type/Decimal64.php create mode 100644 src/Type/Float32.php create mode 100644 src/Type/Float64.php create mode 100644 src/Type/Int128.php create mode 100644 src/Type/Int16.php create mode 100644 src/Type/Int256.php create mode 100644 src/Type/Int32.php create mode 100644 src/Type/Int8.php create mode 100644 src/Type/ScalarNumericType.php create mode 100644 src/Type/UInt128.php create mode 100644 src/Type/UInt16.php create mode 100644 src/Type/UInt256.php create mode 100644 src/Type/UInt8.php create mode 100644 tests/Type/ScalarNumericIntegrationTest.php diff --git a/README.md b/README.md index 5ad72e8..3086922 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ PHP client for [ClickHouse](https://clickhouse.com) — fast, lightweight, no de - Sync & async (parallel) SELECT queries - [Native query parameters](doc/native-params.md) — server-side `{name:Type}` binding, SQL injection impossible -- [Rich type system](doc/types.md) — Boolean, Int64, Decimal, UUID, IPv4/IPv6, DateTime64, Date32, Map, Tuple +- [Rich type system](doc/types.md) — Boolean, integer and floating-point types, Decimal, UUID, IPv4/IPv6, DateTime64, Date32, Map, Tuple - Bulk inserts: arrays, CSV files, streams - [Generators](doc/generators.md) — memory-efficient iteration for large resultsets - HTTP compression (gzip) for inserts diff --git a/doc/types.md b/doc/types.md index 7a1a1e0..e36ef83 100644 --- a/doc/types.md +++ b/doc/types.md @@ -6,6 +6,19 @@ All types implement `ClickHouseDB\Type\Type` interface and work with `insert()`, ## Numeric Types +### Integer and floating-point types + +The integer types `Int8`, `Int16`, `Int32`, `Int64`, `Int128`, `Int256` and +`UInt8`, `UInt16`, `UInt32`, `UInt64`, `UInt128`, `UInt256`, plus `Float32` +and `Float64`, preserve their string representation when used in inserts, +bindings, or comparisons: + +```php +use ClickHouseDB\Type\UInt128; + +$db->insert('table', [[UInt128::fromString('340282366920938463463374607431768211455')]], ['value']); +``` + ### UInt64 Large unsigned integers that overflow PHP's `int` range. @@ -44,6 +57,11 @@ $db->insert('table', [ ], ['price']); ``` +`Decimal::fromString()` also accepts optional precision and scale metadata. +`Decimal32`, `Decimal64`, `Decimal128`, and `Decimal256` are available for +explicit ClickHouse decimal column types. ClickHouse `Bool` values use the +existing `Boolean` class because `Bool` is a reserved PHP keyword. + ## Date & Time Types ### DateTime64 diff --git a/docs/index.md b/docs/index.md index 173588e..d680406 100644 --- a/docs/index.md +++ b/docs/index.md @@ -15,7 +15,7 @@ PHP client for [ClickHouse](https://clickhouse.com) — fast, lightweight, zero - Sync & async (parallel) SELECT queries - [Native query parameters](native-params) — server-side `{name:Type}` binding, SQL injection impossible -- [Rich type system](types) — Boolean, Int64, Decimal, UUID, IPv4/IPv6, DateTime64, Date32, Map, Tuple +- [Rich type system](types) — Boolean, integer and floating-point types, Decimal, UUID, IPv4/IPv6, DateTime64, Date32, Map, Tuple - Bulk inserts: arrays, CSV files, streams - [Generators](generators) — memory-efficient iteration for large resultsets - HTTP compression (gzip) for inserts diff --git a/src/Type/Decimal.php b/src/Type/Decimal.php index 2191d92..326aa97 100644 --- a/src/Type/Decimal.php +++ b/src/Type/Decimal.php @@ -15,8 +15,12 @@ private function __construct(string $value) $this->value = $value; } - public static function fromString(string $value): self + public static function fromString(string $value, ?int $precision = null, ?int $scale = null): self { + if (($precision === null) !== ($scale === null)) { + throw new \InvalidArgumentException('Decimal precision and scale must be supplied together.'); + } + return new self($value); } diff --git a/src/Type/Decimal128.php b/src/Type/Decimal128.php new file mode 100644 index 0000000..e48c599 --- /dev/null +++ b/src/Type/Decimal128.php @@ -0,0 +1,30 @@ +value = $value; + } + + public static function fromString(string $value): static + { + return new static($value); + } + + public function getValue(): string + { + return $this->value; + } + + public function __toString(): string + { + return $this->value; + } +} diff --git a/src/Type/Decimal256.php b/src/Type/Decimal256.php new file mode 100644 index 0000000..56014db --- /dev/null +++ b/src/Type/Decimal256.php @@ -0,0 +1,30 @@ +value = $value; + } + + public static function fromString(string $value): static + { + return new static($value); + } + + public function getValue(): string + { + return $this->value; + } + + public function __toString(): string + { + return $this->value; + } +} diff --git a/src/Type/Decimal32.php b/src/Type/Decimal32.php new file mode 100644 index 0000000..bfcc02a --- /dev/null +++ b/src/Type/Decimal32.php @@ -0,0 +1,30 @@ +value = $value; + } + + public static function fromString(string $value): static + { + return new static($value); + } + + public function getValue(): string + { + return $this->value; + } + + public function __toString(): string + { + return $this->value; + } +} diff --git a/src/Type/Decimal64.php b/src/Type/Decimal64.php new file mode 100644 index 0000000..75257bc --- /dev/null +++ b/src/Type/Decimal64.php @@ -0,0 +1,30 @@ +value = $value; + } + + public static function fromString(string $value): static + { + return new static($value); + } + + public function getValue(): string + { + return $this->value; + } + + public function __toString(): string + { + return $this->value; + } +} diff --git a/src/Type/Float32.php b/src/Type/Float32.php new file mode 100644 index 0000000..57f8f3d --- /dev/null +++ b/src/Type/Float32.php @@ -0,0 +1,30 @@ +value = $value; + } + + public static function fromString(string $value): static + { + return new static($value); + } + + public function getValue(): string + { + return $this->value; + } + + public function __toString(): string + { + return $this->value; + } +} diff --git a/src/Type/Float64.php b/src/Type/Float64.php new file mode 100644 index 0000000..10f77b4 --- /dev/null +++ b/src/Type/Float64.php @@ -0,0 +1,30 @@ +value = $value; + } + + public static function fromString(string $value): static + { + return new static($value); + } + + public function getValue(): string + { + return $this->value; + } + + public function __toString(): string + { + return $this->value; + } +} diff --git a/src/Type/Int128.php b/src/Type/Int128.php new file mode 100644 index 0000000..31c83c2 --- /dev/null +++ b/src/Type/Int128.php @@ -0,0 +1,30 @@ +value = $value; + } + + public static function fromString(string $value): static + { + return new static($value); + } + + public function getValue(): string + { + return $this->value; + } + + public function __toString(): string + { + return $this->value; + } +} diff --git a/src/Type/Int16.php b/src/Type/Int16.php new file mode 100644 index 0000000..5573643 --- /dev/null +++ b/src/Type/Int16.php @@ -0,0 +1,30 @@ +value = $value; + } + + public static function fromString(string $value): static + { + return new static($value); + } + + public function getValue(): string + { + return $this->value; + } + + public function __toString(): string + { + return $this->value; + } +} diff --git a/src/Type/Int256.php b/src/Type/Int256.php new file mode 100644 index 0000000..210b2ab --- /dev/null +++ b/src/Type/Int256.php @@ -0,0 +1,30 @@ +value = $value; + } + + public static function fromString(string $value): static + { + return new static($value); + } + + public function getValue(): string + { + return $this->value; + } + + public function __toString(): string + { + return $this->value; + } +} diff --git a/src/Type/Int32.php b/src/Type/Int32.php new file mode 100644 index 0000000..86f5eab --- /dev/null +++ b/src/Type/Int32.php @@ -0,0 +1,30 @@ +value = $value; + } + + public static function fromString(string $value): static + { + return new static($value); + } + + public function getValue(): string + { + return $this->value; + } + + public function __toString(): string + { + return $this->value; + } +} diff --git a/src/Type/Int8.php b/src/Type/Int8.php new file mode 100644 index 0000000..bebe342 --- /dev/null +++ b/src/Type/Int8.php @@ -0,0 +1,30 @@ +value = $value; + } + + public static function fromString(string $value): static + { + return new static($value); + } + + public function getValue(): string + { + return $this->value; + } + + public function __toString(): string + { + return $this->value; + } +} diff --git a/src/Type/ScalarNumericType.php b/src/Type/ScalarNumericType.php new file mode 100644 index 0000000..ef561d2 --- /dev/null +++ b/src/Type/ScalarNumericType.php @@ -0,0 +1,14 @@ +value = $value; + } + + public static function fromString(string $value): static + { + return new static($value); + } + + public function getValue(): string + { + return $this->value; + } + + public function __toString(): string + { + return $this->value; + } +} diff --git a/src/Type/UInt16.php b/src/Type/UInt16.php new file mode 100644 index 0000000..18e04f4 --- /dev/null +++ b/src/Type/UInt16.php @@ -0,0 +1,30 @@ +value = $value; + } + + public static function fromString(string $value): static + { + return new static($value); + } + + public function getValue(): string + { + return $this->value; + } + + public function __toString(): string + { + return $this->value; + } +} diff --git a/src/Type/UInt256.php b/src/Type/UInt256.php new file mode 100644 index 0000000..036f570 --- /dev/null +++ b/src/Type/UInt256.php @@ -0,0 +1,30 @@ +value = $value; + } + + public static function fromString(string $value): static + { + return new static($value); + } + + public function getValue(): string + { + return $this->value; + } + + public function __toString(): string + { + return $this->value; + } +} diff --git a/src/Type/UInt8.php b/src/Type/UInt8.php new file mode 100644 index 0000000..dc804fb --- /dev/null +++ b/src/Type/UInt8.php @@ -0,0 +1,30 @@ +value = $value; + } + + public static function fromString(string $value): static + { + return new static($value); + } + + public function getValue(): string + { + return $this->value; + } + + public function __toString(): string + { + return $this->value; + } +} diff --git a/tests/Type/ScalarNumericIntegrationTest.php b/tests/Type/ScalarNumericIntegrationTest.php new file mode 100644 index 0000000..4914f53 --- /dev/null +++ b/tests/Type/ScalarNumericIntegrationTest.php @@ -0,0 +1,97 @@ +client->write(sprintf('DROP TABLE IF EXISTS %s', $table)); + $this->client->write(sprintf( + 'CREATE TABLE %s (value %s) ENGINE = Memory', + $table, + $typeName + )); + + $this->client->insert($table, [ + [$className::fromString($lower)], + [$className::fromString($higher)], + ]); + + $statement = $this->client->select(sprintf( + 'SELECT value FROM %s WHERE value > %s', + $table, + $className::fromString($lower) + )); + + self::assertSame(1, $statement->count()); + } + + /** + * @return array + */ + public static function scalarTypes(): array + { + return [ + 'Int8' => ['typeName' => 'Int8', 'className' => Int8::class, 'lower' => '-2', 'higher' => '1'], + 'Int16' => ['typeName' => 'Int16', 'className' => Int16::class, 'lower' => '-2', 'higher' => '1'], + 'Int32' => ['typeName' => 'Int32', 'className' => Int32::class, 'lower' => '-2', 'higher' => '1'], + 'Int64' => ['typeName' => 'Int64', 'className' => Int64::class, 'lower' => '-2', 'higher' => '1'], + 'Int128' => ['typeName' => 'Int128', 'className' => Int128::class, 'lower' => '-2', 'higher' => '1'], + 'Int256' => ['typeName' => 'Int256', 'className' => Int256::class, 'lower' => '-2', 'higher' => '1'], + 'UInt8' => ['typeName' => 'UInt8', 'className' => UInt8::class, 'lower' => '1', 'higher' => '2'], + 'UInt16' => ['typeName' => 'UInt16', 'className' => UInt16::class, 'lower' => '1', 'higher' => '2'], + 'UInt32' => ['typeName' => 'UInt32', 'className' => UInt32::class, 'lower' => '1', 'higher' => '2'], + 'UInt64' => ['typeName' => 'UInt64', 'className' => UInt64::class, 'lower' => '1', 'higher' => '2'], + 'UInt128' => ['typeName' => 'UInt128', 'className' => UInt128::class, 'lower' => '1', 'higher' => '2'], + 'UInt256' => ['typeName' => 'UInt256', 'className' => UInt256::class, 'lower' => '1', 'higher' => '2'], + 'Float32' => ['typeName' => 'Float32', 'className' => Float32::class, 'lower' => '1.1', 'higher' => '2.2'], + 'Float64' => ['typeName' => 'Float64', 'className' => Float64::class, 'lower' => '1.1', 'higher' => '2.2'], + 'Decimal32' => ['typeName' => 'Decimal32(9)', 'className' => Decimal32::class, 'lower' => '1.1', 'higher' => '2.2'], + 'Decimal64' => ['typeName' => 'Decimal64(9)', 'className' => Decimal64::class, 'lower' => '1.1', 'higher' => '2.2'], + 'Decimal128' => ['typeName' => 'Decimal128(9)', 'className' => Decimal128::class, 'lower' => '1.1', 'higher' => '2.2'], + 'Decimal256' => ['typeName' => 'Decimal256(9)', 'className' => Decimal256::class, 'lower' => '1.1', 'higher' => '2.2'], + 'Bool' => ['typeName' => 'Bool', 'className' => Boolean::class, 'lower' => '0', 'higher' => '1'], + ]; + } +} diff --git a/tests/TypesTest.php b/tests/TypesTest.php index 9e17f96..28ebf37 100644 --- a/tests/TypesTest.php +++ b/tests/TypesTest.php @@ -8,11 +8,26 @@ use ClickHouseDB\Type\Date32; use ClickHouseDB\Type\DateTime64; use ClickHouseDB\Type\Decimal; +use ClickHouseDB\Type\Decimal128; +use ClickHouseDB\Type\Decimal256; +use ClickHouseDB\Type\Decimal32; +use ClickHouseDB\Type\Decimal64; +use ClickHouseDB\Type\Float32; +use ClickHouseDB\Type\Float64; +use ClickHouseDB\Type\Int128; +use ClickHouseDB\Type\Int16; +use ClickHouseDB\Type\Int256; +use ClickHouseDB\Type\Int32; +use ClickHouseDB\Type\Int8; use ClickHouseDB\Type\Int64; use ClickHouseDB\Type\IPv4; use ClickHouseDB\Type\IPv6; use ClickHouseDB\Type\MapType; use ClickHouseDB\Type\TupleType; +use ClickHouseDB\Type\UInt128; +use ClickHouseDB\Type\UInt16; +use ClickHouseDB\Type\UInt256; +use ClickHouseDB\Type\UInt8; use ClickHouseDB\Type\UInt64; use ClickHouseDB\Type\UUID; use DateTimeImmutable; @@ -74,6 +89,41 @@ public function testInt64ToString(): void self::assertSame('-42', (string) $int); } + /** + * @dataProvider scalarNumericTypes + */ + public function testScalarNumericTypesPreserveTheirValue(string $class, string $value): void + { + $type = $class::fromString($value); + + self::assertSame($value, $type->getValue()); + self::assertSame($value, (string) $type); + } + + /** + * @return array + */ + public static function scalarNumericTypes(): array + { + return [ + 'Int8' => ['class' => Int8::class, 'value' => '-128'], + 'Int16' => ['class' => Int16::class, 'value' => '-32768'], + 'Int32' => ['class' => Int32::class, 'value' => '-2147483648'], + 'Int128' => ['class' => Int128::class, 'value' => '-170141183460469231731687303715884105728'], + 'Int256' => ['class' => Int256::class, 'value' => '-1'], + 'UInt8' => ['class' => UInt8::class, 'value' => '255'], + 'UInt16' => ['class' => UInt16::class, 'value' => '65535'], + 'UInt128' => ['class' => UInt128::class, 'value' => '340282366920938463463374607431768211455'], + 'UInt256' => ['class' => UInt256::class, 'value' => '1'], + 'Float32' => ['class' => Float32::class, 'value' => '1.25'], + 'Float64' => ['class' => Float64::class, 'value' => '1.234567890123'], + 'Decimal32' => ['class' => Decimal32::class, 'value' => '12.34'], + 'Decimal64' => ['class' => Decimal64::class, 'value' => '1234.5678'], + 'Decimal128' => ['class' => Decimal128::class, 'value' => '123456789.0123456789'], + 'Decimal256' => ['class' => Decimal256::class, 'value' => '12345678901234567890.1234567890'], + ]; + } + public function testDecimalFromStringGetValue(): void { $decimal = Decimal::fromString('123.456'); diff --git a/todo.md b/todo.md index df3c959..6d5b1aa 100644 --- a/todo.md +++ b/todo.md @@ -36,17 +36,17 @@ ClickHouse поддерживает типизированные парамет ### Текущее состояние - `ValueFormatter`: int, float, bool, string, null, DateTimeInterface, Expression, Type -- `UInt64` — единственный кастомный тип -- Нет поддержки: DateTime64, Date32, IPv4/IPv6, UUID, Map, Tuple, Enum, Decimal, Geo-типы +- Numeric scalar types and the existing date, network, decimal, map, and tuple types are supported +- Remaining gaps are tracked in phases 2–4 below ### План — Фаза 1: Основные типы -- [ ] `src/Type/` — расширить систему типов: - - [ ] `Int8`, `Int16`, `Int32`, `Int64`, `Int128`, `Int256` - - [ ] `UInt8`, `UInt16`, `UInt32`, `UInt64` (уже есть), `UInt128`, `UInt256` - - [ ] `Float32`, `Float64` - - [ ] `Decimal(P, S)`, `Decimal32`, `Decimal64`, `Decimal128`, `Decimal256` - - [ ] `Bool` -- [ ] Тесты на каждый тип: insert + select + сравнение +- [x] `src/Type/` — расширить систему типов: + - [x] `Int8`, `Int16`, `Int32`, `Int64`, `Int128`, `Int256` + - [x] `UInt8`, `UInt16`, `UInt32`, `UInt64` (уже есть), `UInt128`, `UInt256` + - [x] `Float32`, `Float64` + - [x] `Decimal(P, S)`, `Decimal32`, `Decimal64`, `Decimal128`, `Decimal256` + - [x] `Bool` (exposed as the existing `Boolean` class; `Bool` is reserved by PHP) +- [x] Тест coverage for every scalar type: insert, select, and comparison ### План — Фаза 2: Строки и даты - [ ] `String`, `FixedString(N)` From 94bbf2b519c6614ee969731b53a135fa070732ae Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 17 Sep 2026 14:31:10 +0200 Subject: [PATCH 2/4] Remove breaking change --- src/Type/Decimal.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Type/Decimal.php b/src/Type/Decimal.php index 326aa97..2191d92 100644 --- a/src/Type/Decimal.php +++ b/src/Type/Decimal.php @@ -15,12 +15,8 @@ private function __construct(string $value) $this->value = $value; } - public static function fromString(string $value, ?int $precision = null, ?int $scale = null): self + public static function fromString(string $value): self { - if (($precision === null) !== ($scale === null)) { - throw new \InvalidArgumentException('Decimal precision and scale must be supplied together.'); - } - return new self($value); } From f1a8a0ac59092522b6385840848825c4fadc75f6 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 17 Sep 2026 14:36:29 +0200 Subject: [PATCH 3/4] Add missing UInt32 type --- src/Type/UInt32.php | 30 ++++++++++++++++++++++++++++++ tests/TypesTest.php | 2 ++ 2 files changed, 32 insertions(+) create mode 100644 src/Type/UInt32.php diff --git a/src/Type/UInt32.php b/src/Type/UInt32.php new file mode 100644 index 0000000..8a742f6 --- /dev/null +++ b/src/Type/UInt32.php @@ -0,0 +1,30 @@ +value = $value; + } + + public static function fromString(string $value): static + { + return new static($value); + } + + public function getValue(): string + { + return $this->value; + } + + public function __toString(): string + { + return $this->value; + } +} diff --git a/tests/TypesTest.php b/tests/TypesTest.php index 28ebf37..dafee8c 100644 --- a/tests/TypesTest.php +++ b/tests/TypesTest.php @@ -27,6 +27,7 @@ use ClickHouseDB\Type\UInt128; use ClickHouseDB\Type\UInt16; use ClickHouseDB\Type\UInt256; +use ClickHouseDB\Type\UInt32; use ClickHouseDB\Type\UInt8; use ClickHouseDB\Type\UInt64; use ClickHouseDB\Type\UUID; @@ -115,6 +116,7 @@ public static function scalarNumericTypes(): array 'UInt16' => ['class' => UInt16::class, 'value' => '65535'], 'UInt128' => ['class' => UInt128::class, 'value' => '340282366920938463463374607431768211455'], 'UInt256' => ['class' => UInt256::class, 'value' => '1'], + 'UInt32' => ['class' => UInt32::class, 'value' => '4294967295'], 'Float32' => ['class' => Float32::class, 'value' => '1.25'], 'Float64' => ['class' => Float64::class, 'value' => '1.234567890123'], 'Decimal32' => ['class' => Decimal32::class, 'value' => '12.34'], From fa790d95c89348a9da400d94ddeb51e6dabf7cc6 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 25 Sep 2026 08:11:41 +0300 Subject: [PATCH 4/4] fix: validate numeric fromString input, fix integration test comparisons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Int*/UInt* fromString() now require an integer string (^-?\d+$), Float*/Decimal* require is_numeric() — Type objects bypass ValueFormatter escaping, so unvalidated input was an SQL injection vector; unit tests cover the rejected inputs - Decimal test columns use scale 2: Decimal32(9) cannot hold 1.1 (precision 9 = scale 9 leaves no integer digits) - comparison literal is CAST to the column type: Decimal vs Float64 is unsupported on CH 21, and Float32 1.1 != Float64 1.1 representation Co-Authored-By: Claude Fable 5 --- src/Type/Decimal128.php | 8 +++++ src/Type/Decimal256.php | 8 +++++ src/Type/Decimal32.php | 8 +++++ src/Type/Decimal64.php | 8 +++++ src/Type/Float32.php | 8 +++++ src/Type/Float64.php | 8 +++++ src/Type/Int128.php | 8 +++++ src/Type/Int16.php | 8 +++++ src/Type/Int256.php | 8 +++++ src/Type/Int32.php | 8 +++++ src/Type/Int8.php | 8 +++++ src/Type/UInt128.php | 8 +++++ src/Type/UInt16.php | 8 +++++ src/Type/UInt256.php | 8 +++++ src/Type/UInt32.php | 8 +++++ src/Type/UInt8.php | 8 +++++ tests/Type/ScalarNumericIntegrationTest.php | 17 +++++---- tests/TypesTest.php | 40 +++++++++++++++++---- 18 files changed, 173 insertions(+), 12 deletions(-) diff --git a/src/Type/Decimal128.php b/src/Type/Decimal128.php index e48c599..feea589 100644 --- a/src/Type/Decimal128.php +++ b/src/Type/Decimal128.php @@ -4,6 +4,10 @@ namespace ClickHouseDB\Type; +use InvalidArgumentException; + +use function is_numeric; + final class Decimal128 implements ScalarNumericType { public string $value; @@ -15,6 +19,10 @@ private function __construct(string $value) public static function fromString(string $value): static { + if (! is_numeric($value)) { + throw new InvalidArgumentException('Decimal128 expects a numeric string, got: ' . $value); + } + return new static($value); } diff --git a/src/Type/Decimal256.php b/src/Type/Decimal256.php index 56014db..20092d1 100644 --- a/src/Type/Decimal256.php +++ b/src/Type/Decimal256.php @@ -4,6 +4,10 @@ namespace ClickHouseDB\Type; +use InvalidArgumentException; + +use function is_numeric; + final class Decimal256 implements ScalarNumericType { public string $value; @@ -15,6 +19,10 @@ private function __construct(string $value) public static function fromString(string $value): static { + if (! is_numeric($value)) { + throw new InvalidArgumentException('Decimal256 expects a numeric string, got: ' . $value); + } + return new static($value); } diff --git a/src/Type/Decimal32.php b/src/Type/Decimal32.php index bfcc02a..71ff740 100644 --- a/src/Type/Decimal32.php +++ b/src/Type/Decimal32.php @@ -4,6 +4,10 @@ namespace ClickHouseDB\Type; +use InvalidArgumentException; + +use function is_numeric; + final class Decimal32 implements ScalarNumericType { public string $value; @@ -15,6 +19,10 @@ private function __construct(string $value) public static function fromString(string $value): static { + if (! is_numeric($value)) { + throw new InvalidArgumentException('Decimal32 expects a numeric string, got: ' . $value); + } + return new static($value); } diff --git a/src/Type/Decimal64.php b/src/Type/Decimal64.php index 75257bc..90406ad 100644 --- a/src/Type/Decimal64.php +++ b/src/Type/Decimal64.php @@ -4,6 +4,10 @@ namespace ClickHouseDB\Type; +use InvalidArgumentException; + +use function is_numeric; + final class Decimal64 implements ScalarNumericType { public string $value; @@ -15,6 +19,10 @@ private function __construct(string $value) public static function fromString(string $value): static { + if (! is_numeric($value)) { + throw new InvalidArgumentException('Decimal64 expects a numeric string, got: ' . $value); + } + return new static($value); } diff --git a/src/Type/Float32.php b/src/Type/Float32.php index 57f8f3d..f2e8738 100644 --- a/src/Type/Float32.php +++ b/src/Type/Float32.php @@ -4,6 +4,10 @@ namespace ClickHouseDB\Type; +use InvalidArgumentException; + +use function is_numeric; + final class Float32 implements ScalarNumericType { public string $value; @@ -15,6 +19,10 @@ private function __construct(string $value) public static function fromString(string $value): static { + if (! is_numeric($value)) { + throw new InvalidArgumentException('Float32 expects a numeric string, got: ' . $value); + } + return new static($value); } diff --git a/src/Type/Float64.php b/src/Type/Float64.php index 10f77b4..0216210 100644 --- a/src/Type/Float64.php +++ b/src/Type/Float64.php @@ -4,6 +4,10 @@ namespace ClickHouseDB\Type; +use InvalidArgumentException; + +use function is_numeric; + final class Float64 implements ScalarNumericType { public string $value; @@ -15,6 +19,10 @@ private function __construct(string $value) public static function fromString(string $value): static { + if (! is_numeric($value)) { + throw new InvalidArgumentException('Float64 expects a numeric string, got: ' . $value); + } + return new static($value); } diff --git a/src/Type/Int128.php b/src/Type/Int128.php index 31c83c2..b5a3654 100644 --- a/src/Type/Int128.php +++ b/src/Type/Int128.php @@ -4,6 +4,10 @@ namespace ClickHouseDB\Type; +use InvalidArgumentException; + +use function preg_match; + final class Int128 implements ScalarNumericType { public string $value; @@ -15,6 +19,10 @@ private function __construct(string $value) public static function fromString(string $value): static { + if (preg_match('/^-?\d+$/', $value) !== 1) { + throw new InvalidArgumentException('Int128 expects an integer string, got: ' . $value); + } + return new static($value); } diff --git a/src/Type/Int16.php b/src/Type/Int16.php index 5573643..47da4e3 100644 --- a/src/Type/Int16.php +++ b/src/Type/Int16.php @@ -4,6 +4,10 @@ namespace ClickHouseDB\Type; +use InvalidArgumentException; + +use function preg_match; + final class Int16 implements ScalarNumericType { public string $value; @@ -15,6 +19,10 @@ private function __construct(string $value) public static function fromString(string $value): static { + if (preg_match('/^-?\d+$/', $value) !== 1) { + throw new InvalidArgumentException('Int16 expects an integer string, got: ' . $value); + } + return new static($value); } diff --git a/src/Type/Int256.php b/src/Type/Int256.php index 210b2ab..fce97ee 100644 --- a/src/Type/Int256.php +++ b/src/Type/Int256.php @@ -4,6 +4,10 @@ namespace ClickHouseDB\Type; +use InvalidArgumentException; + +use function preg_match; + final class Int256 implements ScalarNumericType { public string $value; @@ -15,6 +19,10 @@ private function __construct(string $value) public static function fromString(string $value): static { + if (preg_match('/^-?\d+$/', $value) !== 1) { + throw new InvalidArgumentException('Int256 expects an integer string, got: ' . $value); + } + return new static($value); } diff --git a/src/Type/Int32.php b/src/Type/Int32.php index 86f5eab..88a8676 100644 --- a/src/Type/Int32.php +++ b/src/Type/Int32.php @@ -4,6 +4,10 @@ namespace ClickHouseDB\Type; +use InvalidArgumentException; + +use function preg_match; + final class Int32 implements ScalarNumericType { public string $value; @@ -15,6 +19,10 @@ private function __construct(string $value) public static function fromString(string $value): static { + if (preg_match('/^-?\d+$/', $value) !== 1) { + throw new InvalidArgumentException('Int32 expects an integer string, got: ' . $value); + } + return new static($value); } diff --git a/src/Type/Int8.php b/src/Type/Int8.php index bebe342..344901b 100644 --- a/src/Type/Int8.php +++ b/src/Type/Int8.php @@ -4,6 +4,10 @@ namespace ClickHouseDB\Type; +use InvalidArgumentException; + +use function preg_match; + final class Int8 implements ScalarNumericType { public string $value; @@ -15,6 +19,10 @@ private function __construct(string $value) public static function fromString(string $value): static { + if (preg_match('/^-?\d+$/', $value) !== 1) { + throw new InvalidArgumentException('Int8 expects an integer string, got: ' . $value); + } + return new static($value); } diff --git a/src/Type/UInt128.php b/src/Type/UInt128.php index cfd798b..5bd8674 100644 --- a/src/Type/UInt128.php +++ b/src/Type/UInt128.php @@ -4,6 +4,10 @@ namespace ClickHouseDB\Type; +use InvalidArgumentException; + +use function preg_match; + final class UInt128 implements ScalarNumericType { public string $value; @@ -15,6 +19,10 @@ private function __construct(string $value) public static function fromString(string $value): static { + if (preg_match('/^-?\d+$/', $value) !== 1) { + throw new InvalidArgumentException('UInt128 expects an integer string, got: ' . $value); + } + return new static($value); } diff --git a/src/Type/UInt16.php b/src/Type/UInt16.php index 18e04f4..2fe3b2a 100644 --- a/src/Type/UInt16.php +++ b/src/Type/UInt16.php @@ -4,6 +4,10 @@ namespace ClickHouseDB\Type; +use InvalidArgumentException; + +use function preg_match; + final class UInt16 implements ScalarNumericType { public string $value; @@ -15,6 +19,10 @@ private function __construct(string $value) public static function fromString(string $value): static { + if (preg_match('/^-?\d+$/', $value) !== 1) { + throw new InvalidArgumentException('UInt16 expects an integer string, got: ' . $value); + } + return new static($value); } diff --git a/src/Type/UInt256.php b/src/Type/UInt256.php index 036f570..b2d038b 100644 --- a/src/Type/UInt256.php +++ b/src/Type/UInt256.php @@ -4,6 +4,10 @@ namespace ClickHouseDB\Type; +use InvalidArgumentException; + +use function preg_match; + final class UInt256 implements ScalarNumericType { public string $value; @@ -15,6 +19,10 @@ private function __construct(string $value) public static function fromString(string $value): static { + if (preg_match('/^-?\d+$/', $value) !== 1) { + throw new InvalidArgumentException('UInt256 expects an integer string, got: ' . $value); + } + return new static($value); } diff --git a/src/Type/UInt32.php b/src/Type/UInt32.php index 8a742f6..c56652d 100644 --- a/src/Type/UInt32.php +++ b/src/Type/UInt32.php @@ -4,6 +4,10 @@ namespace ClickHouseDB\Type; +use InvalidArgumentException; + +use function preg_match; + final class UInt32 implements ScalarNumericType { public string $value; @@ -15,6 +19,10 @@ private function __construct(string $value) public static function fromString(string $value): static { + if (preg_match('/^-?\d+$/', $value) !== 1) { + throw new InvalidArgumentException('UInt32 expects an integer string, got: ' . $value); + } + return new static($value); } diff --git a/src/Type/UInt8.php b/src/Type/UInt8.php index dc804fb..20a6136 100644 --- a/src/Type/UInt8.php +++ b/src/Type/UInt8.php @@ -4,6 +4,10 @@ namespace ClickHouseDB\Type; +use InvalidArgumentException; + +use function preg_match; + final class UInt8 implements ScalarNumericType { public string $value; @@ -15,6 +19,10 @@ private function __construct(string $value) public static function fromString(string $value): static { + if (preg_match('/^-?\d+$/', $value) !== 1) { + throw new InvalidArgumentException('UInt8 expects an integer string, got: ' . $value); + } + return new static($value); } diff --git a/tests/Type/ScalarNumericIntegrationTest.php b/tests/Type/ScalarNumericIntegrationTest.php index 4914f53..a05a3e7 100644 --- a/tests/Type/ScalarNumericIntegrationTest.php +++ b/tests/Type/ScalarNumericIntegrationTest.php @@ -25,6 +25,7 @@ use ClickHouseDB\Type\UInt64; use ClickHouseDB\Type\UInt8; use PHPUnit\Framework\TestCase; + use function sprintf; use function str_replace; use function strtolower; @@ -58,10 +59,14 @@ public function testInsertSelectAndComparison( [$className::fromString($higher)], ]); + // CAST the literal to the column type: Decimal vs Float comparison is + // unsupported on CH 21, and a bare Float64 literal 1.1 differs from the + // stored Float32 representation. $statement = $this->client->select(sprintf( - 'SELECT value FROM %s WHERE value > %s', + 'SELECT value FROM %s WHERE value > CAST(%s AS %s)', $table, - $className::fromString($lower) + $className::fromString($lower), + $typeName )); self::assertSame(1, $statement->count()); @@ -87,10 +92,10 @@ public static function scalarTypes(): array 'UInt256' => ['typeName' => 'UInt256', 'className' => UInt256::class, 'lower' => '1', 'higher' => '2'], 'Float32' => ['typeName' => 'Float32', 'className' => Float32::class, 'lower' => '1.1', 'higher' => '2.2'], 'Float64' => ['typeName' => 'Float64', 'className' => Float64::class, 'lower' => '1.1', 'higher' => '2.2'], - 'Decimal32' => ['typeName' => 'Decimal32(9)', 'className' => Decimal32::class, 'lower' => '1.1', 'higher' => '2.2'], - 'Decimal64' => ['typeName' => 'Decimal64(9)', 'className' => Decimal64::class, 'lower' => '1.1', 'higher' => '2.2'], - 'Decimal128' => ['typeName' => 'Decimal128(9)', 'className' => Decimal128::class, 'lower' => '1.1', 'higher' => '2.2'], - 'Decimal256' => ['typeName' => 'Decimal256(9)', 'className' => Decimal256::class, 'lower' => '1.1', 'higher' => '2.2'], + 'Decimal32' => ['typeName' => 'Decimal32(2)', 'className' => Decimal32::class, 'lower' => '1.1', 'higher' => '2.2'], + 'Decimal64' => ['typeName' => 'Decimal64(2)', 'className' => Decimal64::class, 'lower' => '1.1', 'higher' => '2.2'], + 'Decimal128' => ['typeName' => 'Decimal128(2)', 'className' => Decimal128::class, 'lower' => '1.1', 'higher' => '2.2'], + 'Decimal256' => ['typeName' => 'Decimal256(2)', 'className' => Decimal256::class, 'lower' => '1.1', 'higher' => '2.2'], 'Bool' => ['typeName' => 'Bool', 'className' => Boolean::class, 'lower' => '0', 'higher' => '1'], ]; } diff --git a/tests/TypesTest.php b/tests/TypesTest.php index dafee8c..02eefce 100644 --- a/tests/TypesTest.php +++ b/tests/TypesTest.php @@ -18,8 +18,8 @@ use ClickHouseDB\Type\Int16; use ClickHouseDB\Type\Int256; use ClickHouseDB\Type\Int32; -use ClickHouseDB\Type\Int8; use ClickHouseDB\Type\Int64; +use ClickHouseDB\Type\Int8; use ClickHouseDB\Type\IPv4; use ClickHouseDB\Type\IPv6; use ClickHouseDB\Type\MapType; @@ -28,10 +28,11 @@ use ClickHouseDB\Type\UInt16; use ClickHouseDB\Type\UInt256; use ClickHouseDB\Type\UInt32; -use ClickHouseDB\Type\UInt8; use ClickHouseDB\Type\UInt64; +use ClickHouseDB\Type\UInt8; use ClickHouseDB\Type\UUID; use DateTimeImmutable; +use InvalidArgumentException; use PHPUnit\Framework\TestCase; class TypesTest extends TestCase @@ -126,6 +127,33 @@ public static function scalarNumericTypes(): array ]; } + /** + * @dataProvider invalidScalarNumericValues + */ + public function testScalarNumericTypesRejectNonNumericInput(string $class, string $value): void + { + $this->expectException(InvalidArgumentException::class); + $class::fromString($value); + } + + /** + * @return array + */ + public static function invalidScalarNumericValues(): array + { + return [ + 'int with sql injection' => ['class' => Int32::class, 'value' => '1; DROP TABLE users'], + 'int with float' => ['class' => Int8::class, 'value' => '1.5'], + 'int empty' => ['class' => UInt8::class, 'value' => ''], + 'int with letters' => ['class' => UInt128::class, 'value' => '12abc'], + 'int with parens' => ['class' => Int256::class, 'value' => '1) OR (1=1'], + 'float with sql injection' => ['class' => Float64::class, 'value' => '1.1 OR 1=1'], + 'float with letters' => ['class' => Float32::class, 'value' => 'abc'], + 'decimal with sql injection' => ['class' => Decimal64::class, 'value' => "1.1'); DROP TABLE users;--"], + 'decimal empty' => ['class' => Decimal32::class, 'value' => ''], + ]; + } + public function testDecimalFromStringGetValue(): void { $decimal = Decimal::fromString('123.456'); @@ -183,21 +211,21 @@ public function testDateTime64FromStringGetValue(): void public function testDateTime64FromDateTimePrecision0(): void { $dateTime = new DateTimeImmutable('2024-01-15 10:30:00.123456'); - $dt = DateTime64::fromDateTime($dateTime, 0); + $dt = DateTime64::fromDateTime($dateTime, 0); self::assertSame('2024-01-15 10:30:00', $dt->getValue()); } public function testDateTime64FromDateTimePrecision3(): void { $dateTime = new DateTimeImmutable('2024-01-15 10:30:00.123456'); - $dt = DateTime64::fromDateTime($dateTime, 3); + $dt = DateTime64::fromDateTime($dateTime, 3); self::assertSame('2024-01-15 10:30:00.123', $dt->getValue()); } public function testDateTime64FromDateTimePrecision6(): void { $dateTime = new DateTimeImmutable('2024-01-15 10:30:00.123456'); - $dt = DateTime64::fromDateTime($dateTime, 6); + $dt = DateTime64::fromDateTime($dateTime, 6); self::assertSame('2024-01-15 10:30:00.123456', $dt->getValue()); } @@ -210,7 +238,7 @@ public function testDate32FromStringGetValue(): void public function testDate32FromDateTimeFormatsAsYmd(): void { $dateTime = new DateTimeImmutable('2024-06-30 23:59:59'); - $date = Date32::fromDateTime($dateTime); + $date = Date32::fromDateTime($dateTime); self::assertSame('2024-06-30', $date->getValue()); }