This is an automated email from the ASF dual-hosted git repository.
mgrigorov pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/avro.git
The following commit(s) were added to refs/heads/main by this push:
new 1e9a16ab77 AVRO-4190: [php] Add logical types (#3526)
1e9a16ab77 is described below
commit 1e9a16ab77d1019a5a62a1f671982894b479c044
Author: Mattia Basone <[email protected]>
AuthorDate: Fri Oct 24 14:58:34 2025 +0200
AVRO-4190: [php] Add logical types (#3526)
* wip
* update logical types
* wip - added logical types
* update PHP workflow
* add composer.json to workflow path
* bump composer version
* add license to AvroLogicalType.php
* add test on logical types
* update test and fix default decimal value
* improve exception messages
* fix lint
* improve types
* add test for decimal schema validation
* add test for decimal schema validation
* remove is_numeric
* add some test for decimal logical type (bytes)
* Add AvroDuration type and extend fixed schema
* additional tests for decimal
---
.github/workflows/test-lang-php.yml | 13 +-
composer.json | 2 +-
lang/php/lib/DataFile/AvroDataIOWriter.php | 2 +-
lang/php/lib/Datum/AvroIOBinaryEncoder.php | 72 ++++-
lang/php/lib/Datum/AvroIODatumReader.php | 94 ++++--
lang/php/lib/Datum/AvroIODatumWriter.php | 141 +++++---
lang/php/lib/Datum/Type/AvroDuration.php | 114 +++++++
lang/php/lib/Schema/AvroFixedSchema.php | 53 +++
lang/php/lib/Schema/AvroLogicalType.php | 128 ++++++++
lang/php/lib/Schema/AvroPrimitiveSchema.php | 74 +++++
lang/php/lib/Schema/AvroRecordSchema.php | 6 +-
lang/php/lib/Schema/AvroSchema.php | 161 +++++++++-
lang/php/phpunit.xml | 2 +-
lang/php/test/DatumIOTest.php | 483 +++++++++++++++++++++-------
lang/php/test/IODatumReaderTest.php | 220 +++++++++++--
lang/php/test/SchemaTest.php | 255 +++++++++++----
16 files changed, 1518 insertions(+), 302 deletions(-)
diff --git a/.github/workflows/test-lang-php.yml
b/.github/workflows/test-lang-php.yml
index 94d19d68f1..883ef5ce11 100644
--- a/.github/workflows/test-lang-php.yml
+++ b/.github/workflows/test-lang-php.yml
@@ -22,6 +22,7 @@ on:
branches: [ main ]
paths:
- .github/workflows/test-lang-php.yml
+ - composer.json
- lang/php/**
defaults:
@@ -44,6 +45,10 @@ jobs:
- '7.3'
- '7.4'
- '8.0'
+ - '8.1'
+ - '8.2'
+ - '8.3'
+ - '8.4'
steps:
- uses: actions/checkout@v5
@@ -52,7 +57,7 @@ jobs:
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
- tools: composer:2.2.5
+ tools: composer:2.8.12
- name: Get Composer Cache Directory
id: composer-cache
@@ -82,6 +87,10 @@ jobs:
- '7.3'
- '7.4'
- '8.0'
+ - '8.1'
+ - '8.2'
+ - '8.3'
+ - '8.4'
steps:
- uses: actions/checkout@v5
@@ -90,7 +99,7 @@ jobs:
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
- tools: composer:2.2.5
+ tools: composer:2.8.12
- name: Cache Local Maven Repository
uses: actions/cache@v4
diff --git a/composer.json b/composer.json
index f48c7d594e..3b5e06ef21 100644
--- a/composer.json
+++ b/composer.json
@@ -23,7 +23,7 @@
"issues": "https://issues.apache.org/jira/browse/AVRO"
},
"require": {
- "php": "^7.1 || ^8.0"
+ "php": "^7.3 || ^8.0"
},
"deps": [
"vendor/phpunit/phpunit",
diff --git a/lang/php/lib/DataFile/AvroDataIOWriter.php
b/lang/php/lib/DataFile/AvroDataIOWriter.php
index 730a2ef2dd..b28143723d 100644
--- a/lang/php/lib/DataFile/AvroDataIOWriter.php
+++ b/lang/php/lib/DataFile/AvroDataIOWriter.php
@@ -136,7 +136,7 @@ class AvroDataIOWriter
/**
* Writes the header of the AvroIO object container
*/
- private function writeHeader()
+ private function writeHeader(): void
{
$this->write(AvroDataIO::magic());
$this->datum_writer->writeData(
diff --git a/lang/php/lib/Datum/AvroIOBinaryEncoder.php
b/lang/php/lib/Datum/AvroIOBinaryEncoder.php
index d4c47248c1..5f289aafa9 100644
--- a/lang/php/lib/Datum/AvroIOBinaryEncoder.php
+++ b/lang/php/lib/Datum/AvroIOBinaryEncoder.php
@@ -21,6 +21,7 @@
namespace Apache\Avro\Datum;
use Apache\Avro\Avro;
+use Apache\Avro\AvroException;
use Apache\Avro\AvroGMP;
use Apache\Avro\AvroIO;
@@ -41,7 +42,7 @@ class AvroIOBinaryEncoder
* @param AvroIO $io object to which data is to be written.
*
*/
- public function __construct($io)
+ public function __construct(AvroIO $io)
{
Avro::checkPlatform();
$this->io = $io;
@@ -50,9 +51,9 @@ class AvroIOBinaryEncoder
/**
* @param null $datum actual value is ignored
*/
- public function writeNull($datum)
+ public function writeNull($datum): void
{
- return null;
+ return;
}
/**
@@ -67,7 +68,7 @@ class AvroIOBinaryEncoder
/**
* @param string $datum
*/
- public function write($datum)
+ public function write($datum): void
{
$this->io->write($datum);
}
@@ -75,7 +76,7 @@ class AvroIOBinaryEncoder
/**
* @param int $datum
*/
- public function writeInt($datum)
+ public function writeInt($datum): void
{
$this->writeLong($datum);
}
@@ -83,7 +84,7 @@ class AvroIOBinaryEncoder
/**
* @param int $n
*/
- public function writeLong($n)
+ public function writeLong($n): void
{
if (Avro::usesGmp()) {
$this->write(AvroGMP::encodeLong($n));
@@ -125,7 +126,7 @@ class AvroIOBinaryEncoder
* @param float $datum
* @uses self::floatToIntBits()
*/
- public function writeFloat($datum)
+ public function writeFloat($datum): void
{
$this->write(self::floatToIntBits($datum));
}
@@ -142,7 +143,7 @@ class AvroIOBinaryEncoder
* @returns string bytes
* @see Avro::checkPlatform()
*/
- public static function floatToIntBits($float)
+ public static function floatToIntBits($float): string
{
return pack('g', (float) $float);
}
@@ -151,7 +152,7 @@ class AvroIOBinaryEncoder
* @param float $datum
* @uses self::doubleToLongBits()
*/
- public function writeDouble($datum)
+ public function writeDouble($datum): void
{
$this->write(self::doubleToLongBits($datum));
}
@@ -165,7 +166,7 @@ class AvroIOBinaryEncoder
* @param double $double
* @returns string bytes
*/
- public static function doubleToLongBits($double)
+ public static function doubleToLongBits($double): string
{
return pack('e', (double) $double);
}
@@ -174,7 +175,7 @@ class AvroIOBinaryEncoder
* @param string $str
* @uses self::writeBytes()
*/
- public function writeString($str)
+ public function writeString($str): void
{
$this->writeBytes($str);
}
@@ -182,9 +183,56 @@ class AvroIOBinaryEncoder
/**
* @param string $bytes
*/
- public function writeBytes($bytes)
+ public function writeBytes($bytes): void
{
$this->writeLong(strlen($bytes));
$this->write($bytes);
}
+
+ public function writeDecimal($decimal, int $scale, int $precision): void
+ {
+ if (!is_numeric($decimal)) {
+ throw new AvroException("Decimal value '{$decimal}' must be
numeric");
+ }
+
+ $value = $decimal * (10 ** $scale);
+ if (!is_int($value)) {
+ $value = (int) round($value);
+ }
+
+ $maxValue = 10 ** $precision;
+ if (abs($value) >= $maxValue) {
+ throw new AvroException(
+ "Decimal value '{$decimal}' is out of range for
precision={$precision}, scale={$scale}"
+ );
+ }
+
+ $packed = pack('J', $value);
+
+ $significantBit = self::getMostSignificantBitAt($packed, 0);
+ $trimByte = $significantBit ? 0xff : 0x00;
+
+ $offset = 0;
+ $packedLength = strlen($packed);
+ while ($offset < $packedLength - 1) {
+ if (ord($packed[$offset]) !== $trimByte) {
+ break;
+ }
+
+ if (self::getMostSignificantBitAt($packed, $offset + 1) !==
$significantBit) {
+ break;
+ }
+
+ $offset++;
+ }
+
+ $value = substr($packed, $offset);
+
+ $this->writeBytes($value);
+ }
+
+ private static function getMostSignificantBitAt($bytes, $offset): int
+ {
+ return ord($bytes[$offset]) & 0x80;
+ }
}
diff --git a/lang/php/lib/Datum/AvroIODatumReader.php
b/lang/php/lib/Datum/AvroIODatumReader.php
index c9a5ae3953..24278a3dfb 100644
--- a/lang/php/lib/Datum/AvroIODatumReader.php
+++ b/lang/php/lib/Datum/AvroIODatumReader.php
@@ -21,6 +21,8 @@
namespace Apache\Avro\Datum;
use Apache\Avro\AvroException;
+use Apache\Avro\Datum\Type\AvroDuration;
+use Apache\Avro\Schema\AvroLogicalType;
use Apache\Avro\Schema\AvroName;
use Apache\Avro\Schema\AvroSchema;
@@ -43,20 +45,13 @@ class AvroIODatumReader
*/
private $readers_schema;
- /**
- * @param AvroSchema $writers_schema
- * @param AvroSchema $readers_schema
- */
- public function __construct($writers_schema = null, $readers_schema = null)
+ public function __construct(?AvroSchema $writers_schema = null,
?AvroSchema $readers_schema = null)
{
$this->writers_schema = $writers_schema;
$this->readers_schema = $readers_schema;
}
- /**
- * @param AvroSchema $readers_schema
- */
- public function setWritersSchema($readers_schema)
+ public function setWritersSchema(AvroSchema $readers_schema): void
{
$this->writers_schema = $readers_schema;
}
@@ -80,7 +75,7 @@ class AvroIODatumReader
/**
* @returns mixed
*/
- public function readData($writers_schema, $readers_schema, $decoder)
+ public function readData(AvroSchema $writers_schema, AvroSchema
$readers_schema, AvroIOBinaryDecoder $decoder)
{
// Schema resolution: reader's schema is a union, writer's schema is
not
if (
@@ -111,7 +106,8 @@ class AvroIODatumReader
case AvroSchema::STRING_TYPE:
return $decoder->readString();
case AvroSchema::BYTES_TYPE:
- return $decoder->readBytes();
+ $bytes = $decoder->readBytes();
+ return $this->readBytes($writers_schema, $readers_schema,
$bytes);
case AvroSchema::ARRAY_SCHEMA:
return $this->readArray($writers_schema, $readers_schema,
$decoder);
case AvroSchema::MAP_SCHEMA:
@@ -136,12 +132,10 @@ class AvroIODatumReader
/**
*
- * @param AvroSchema $writers_schema
- * @param AvroSchema $readers_schema
* @returns boolean true if the schemas are consistent with
* each other and false otherwise.
*/
- public static function schemasMatch($writers_schema, $readers_schema)
+ public static function schemasMatch(AvroSchema $writers_schema, AvroSchema
$readers_schema)
{
$writers_schema_type = $writers_schema->type;
$readers_schema_type = $readers_schema->type;
@@ -167,12 +161,6 @@ class AvroIODatumReader
$readers_schema->items(),
[AvroSchema::TYPE_ATTR]
);
- case AvroSchema::ENUM_SCHEMA:
- return self::attributesMatch(
- $writers_schema,
- $readers_schema,
- [AvroSchema::FULLNAME_ATTR]
- );
case AvroSchema::FIXED_SCHEMA:
return self::attributesMatch(
$writers_schema,
@@ -182,6 +170,7 @@ class AvroIODatumReader
AvroSchema::SIZE_ATTR
]
);
+ case AvroSchema::ENUM_SCHEMA:
case AvroSchema::RECORD_SCHEMA:
case AvroSchema::ERROR_SCHEMA:
return self::attributesMatch(
@@ -255,10 +244,28 @@ class AvroIODatumReader
return true;
}
+ public function readBytes(AvroSchema $writers_schema, AvroSchema
$readers_schema, string $bytes): string
+ {
+ $logicalTypeWriters = $writers_schema->logicalType();
+ if (
+ $logicalTypeWriters instanceof AvroLogicalType
+ && $logicalTypeWriters->name() === AvroSchema::DECIMAL_LOGICAL_TYPE
+ ) {
+ if ($logicalTypeWriters !== $readers_schema->logicalType()) {
+ throw new AvroIOSchemaMatchException($writers_schema,
$readers_schema);
+ }
+
+ $scale = $logicalTypeWriters->attributes()['scale'] ?? 0;
+ $bytes = $this->readDecimal($bytes, $scale);
+ }
+
+ return $bytes;
+ }
+
/**
* @return array
*/
- public function readArray($writers_schema, $readers_schema, $decoder)
+ public function readArray(AvroSchema $writers_schema, AvroSchema
$readers_schema, AvroIOBinaryDecoder $decoder)
{
$items = array();
$block_count = $decoder->readLong();
@@ -282,7 +289,7 @@ class AvroIODatumReader
/**
* @returns array
*/
- public function readMap($writers_schema, $readers_schema, $decoder)
+ public function readMap(AvroSchema $writers_schema, AvroSchema
$readers_schema, AvroIOBinaryDecoder $decoder)
{
$items = array();
$pair_count = $decoder->readLong();
@@ -309,7 +316,7 @@ class AvroIODatumReader
/**
* @returns mixed
*/
- public function readUnion($writers_schema, $readers_schema, $decoder)
+ public function readUnion(AvroSchema $writers_schema, AvroSchema
$readers_schema, AvroIOBinaryDecoder $decoder)
{
$schema_index = $decoder->readLong();
$selected_writers_schema =
$writers_schema->schemaByIndex($schema_index);
@@ -319,7 +326,7 @@ class AvroIODatumReader
/**
* @returns string
*/
- public function readEnum($writers_schema, $readers_schema, $decoder)
+ public function readEnum(AvroSchema $writers_schema, AvroSchema
$readers_schema, AvroIOBinaryDecoder $decoder)
{
$symbol_index = $decoder->readInt();
$symbol = $writers_schema->symbolByIndex($symbol_index);
@@ -330,17 +337,37 @@ class AvroIODatumReader
}
/**
- * @returns string
+ * @returns string|AvroDuration
*/
- public function readFixed($writers_schema, $readers_schema, $decoder)
+ public function readFixed(AvroSchema $writers_schema, AvroSchema
$readers_schema, AvroIOBinaryDecoder $decoder)
{
+ $logicalTypeWriters = $writers_schema->logicalType();
+ if ($logicalTypeWriters instanceof AvroLogicalType) {
+ if ($logicalTypeWriters !== $readers_schema->logicalType()) {
+ throw new AvroIOSchemaMatchException($writers_schema,
$readers_schema);
+ }
+
+ switch ($logicalTypeWriters->name()) {
+ case AvroSchema::DECIMAL_LOGICAL_TYPE:
+ $scale = $logicalTypeWriters->attributes()['scale'] ?? 0;
+ return $this->readDecimal($decoder->readBytes(), $scale);
+ case AvroSchema::DURATION_LOGICAL_TYPE:
+ $encodedDuration = $decoder->read($writers_schema->size());
+ if (strlen($encodedDuration) !== 12) {
+ throw new AvroException('Invalid duration fixed size:
' . strlen($encodedDuration));
+ }
+
+ return AvroDuration::fromBytes($encodedDuration);
+ }
+ }
+
return $decoder->read($writers_schema->size());
}
/**
* @returns array
*/
- public function readRecord($writers_schema, $readers_schema, $decoder)
+ public function readRecord(AvroSchema $writers_schema, AvroSchema
$readers_schema, AvroIOBinaryDecoder $decoder)
{
$readers_fields = $readers_schema->fieldsHash();
$record = [];
@@ -419,13 +446,12 @@ class AvroIODatumReader
}
/**
- * @param AvroSchema $field_schema
* @param null|boolean|int|float|string|array $default_value
* @returns null|boolean|int|float|string|array
*
* @throws AvroException if $field_schema type is unknown.
*/
- public function readDefaultValue($field_schema, $default_value)
+ public function readDefaultValue(AvroSchema $field_schema, $default_value)
{
switch ($field_schema->type()) {
case AvroSchema::NULL_TYPE:
@@ -440,7 +466,7 @@ class AvroIODatumReader
return (float) $default_value;
case AvroSchema::STRING_TYPE:
case AvroSchema::BYTES_TYPE:
- return $default_value;
+ return $this->readBytes($field_schema, $field_schema,
$default_value);
case AvroSchema::ARRAY_SCHEMA:
$array = array();
foreach ($default_value as $json_val) {
@@ -483,4 +509,12 @@ class AvroIODatumReader
throw new AvroException(sprintf('Unknown type: %s',
$field_schema->type()));
}
}
+
+ private function readDecimal(string $bytes, int $scale): string
+ {
+ $mostSignificantBit = ord($bytes[0]) & 0x80;
+ $padded = str_pad($bytes, 8, $mostSignificantBit ? "\xff" : "\x00",
STR_PAD_LEFT);
+ $int = unpack('J', $padded)[1];
+ return (string) ($scale > 0 ? ($int / (10 ** $scale)) : $int);
+ }
}
diff --git a/lang/php/lib/Datum/AvroIODatumWriter.php
b/lang/php/lib/Datum/AvroIODatumWriter.php
index ef8031f763..13e2bbe388 100644
--- a/lang/php/lib/Datum/AvroIODatumWriter.php
+++ b/lang/php/lib/Datum/AvroIODatumWriter.php
@@ -21,7 +21,10 @@
namespace Apache\Avro\Datum;
use Apache\Avro\AvroException;
+use Apache\Avro\Datum\Type\AvroDuration;
+use Apache\Avro\Schema\AvroLogicalType;
use Apache\Avro\Schema\AvroSchema;
+use Apache\Avro\Schema\AvroSchemaParseException;
/**
* Handles schema-specific writing of data to the encoder.
@@ -38,19 +41,16 @@ class AvroIODatumWriter
*/
public $writersSchema;
- /**
- * @param AvroSchema $writers_schema
- */
- public function __construct($writers_schema = null)
+ public function __construct(?AvroSchema $writers_schema = null)
{
$this->writersSchema = $writers_schema;
}
/**
- * @param $datum
+ * @param mixed $datum
* @param AvroIOBinaryEncoder $encoder
*/
- public function write($datum, $encoder)
+ public function write($datum, AvroIOBinaryEncoder $encoder)
{
$this->writeData($this->writersSchema, $datum, $encoder);
}
@@ -62,57 +62,72 @@ class AvroIODatumWriter
* @return mixed
*
* @throws AvroIOTypeException if $datum is invalid for $writers_schema
+ * @throws AvroException if the type is invalid
*/
- public function writeData($writers_schema, $datum, $encoder)
+ public function writeData(AvroSchema $writers_schema, $datum,
AvroIOBinaryEncoder $encoder): void
{
if (!AvroSchema::isValidDatum($writers_schema, $datum)) {
throw new AvroIOTypeException($writers_schema, $datum);
}
- return $this->writeValidatedData($writers_schema, $datum, $encoder);
+ $this->writeValidatedData($writers_schema, $datum, $encoder);
}
/**
* @param AvroSchema $writers_schema
* @param $datum
* @param AvroIOBinaryEncoder $encoder
- * @return mixed
+ * @return void
*
* @throws AvroIOTypeException if $datum is invalid for $writers_schema
*/
- private function writeValidatedData($writers_schema, $datum, $encoder)
+ private function writeValidatedData(AvroSchema $writers_schema, $datum,
AvroIOBinaryEncoder $encoder)
{
switch ($writers_schema->type()) {
case AvroSchema::NULL_TYPE:
- return $encoder->writeNull($datum);
+ $encoder->writeNull($datum);
+ return;
case AvroSchema::BOOLEAN_TYPE:
- return $encoder->writeBoolean($datum);
+ $encoder->writeBoolean($datum);
+ return;
case AvroSchema::INT_TYPE:
- return $encoder->writeInt($datum);
+ $encoder->writeInt($datum);
+ return;
case AvroSchema::LONG_TYPE:
- return $encoder->writeLong($datum);
+ $encoder->writeLong($datum);
+ return;
case AvroSchema::FLOAT_TYPE:
- return $encoder->writeFloat($datum);
+ $encoder->writeFloat($datum);
+ return;
case AvroSchema::DOUBLE_TYPE:
- return $encoder->writeDouble($datum);
+ $encoder->writeDouble($datum);
+ return;
case AvroSchema::STRING_TYPE:
- return $encoder->writeString($datum);
+ $encoder->writeString($datum);
+ return;
case AvroSchema::BYTES_TYPE:
- return $encoder->writeBytes($datum);
+ $this->writeBytes($writers_schema, $datum, $encoder);
+ return;
case AvroSchema::ARRAY_SCHEMA:
- return $this->writeArray($writers_schema, $datum, $encoder);
+ $this->writeArray($writers_schema, $datum, $encoder);
+ return;
case AvroSchema::MAP_SCHEMA:
- return $this->writeMap($writers_schema, $datum, $encoder);
+ $this->writeMap($writers_schema, $datum, $encoder);
+ return;
case AvroSchema::FIXED_SCHEMA:
- return $this->writeFixed($writers_schema, $datum, $encoder);
+ $this->writeFixed($writers_schema, $datum, $encoder);
+ return;
case AvroSchema::ENUM_SCHEMA:
- return $this->writeEnum($writers_schema, $datum, $encoder);
+ $this->writeEnum($writers_schema, $datum, $encoder);
+ return;
case AvroSchema::RECORD_SCHEMA:
case AvroSchema::ERROR_SCHEMA:
case AvroSchema::REQUEST_SCHEMA:
- return $this->writeRecord($writers_schema, $datum, $encoder);
+ $this->writeRecord($writers_schema, $datum, $encoder);
+ return;
case AvroSchema::UNION_SCHEMA:
- return $this->writeUnion($writers_schema, $datum, $encoder);
+ $this->writeUnion($writers_schema, $datum, $encoder);
+ return;
default:
throw new AvroException(sprintf(
'Unknown type: %s',
@@ -121,12 +136,28 @@ class AvroIODatumWriter
}
}
+ private function writeBytes(AvroSchema $writers_schema, $datum,
AvroIOBinaryEncoder $encoder): void
+ {
+ $logicalType = $writers_schema->logicalType();
+ if (
+ $logicalType instanceof AvroLogicalType
+ && $logicalType->name() === AvroSchema::DECIMAL_LOGICAL_TYPE
+ ) {
+ $scale = $logicalType->attributes()['scale'] ?? 0;
+ $precision = $logicalType->attributes()['precision'] ?? null;
+
+ $encoder->writeDecimal($datum, $scale, $precision);
+ return;
+ }
+
+ $encoder->writeBytes($datum);
+ }
+
/**
- * @param AvroSchema $writers_schema
* @param null|boolean|int|float|string|array $datum item to be written
- * @param AvroIOBinaryEncoder $encoder
+ * @throws AvroIOTypeException
*/
- private function writeArray($writers_schema, $datum, $encoder)
+ private function writeArray(AvroSchema $writers_schema, $datum,
AvroIOBinaryEncoder $encoder): void
{
$datum_count = count($datum);
if (0 < $datum_count) {
@@ -136,16 +167,14 @@ class AvroIODatumWriter
$this->writeValidatedData($items, $item, $encoder);
}
}
- return $encoder->writeLong(0);
+ $encoder->writeLong(0);
}
/**
- * @param $writers_schema
* @param $datum
- * @param $encoder
* @throws AvroIOTypeException
*/
- private function writeMap($writers_schema, $datum, $encoder)
+ private function writeMap(AvroSchema $writers_schema, $datum,
AvroIOBinaryEncoder $encoder): void
{
$datum_count = count($datum);
if ($datum_count > 0) {
@@ -158,29 +187,59 @@ class AvroIODatumWriter
$encoder->writeLong(0);
}
- private function writeFixed($writers_schema, $datum, $encoder)
+ private function writeFixed(AvroSchema $writers_schema, $datum,
AvroIOBinaryEncoder $encoder): void
{
- /**
- * NOTE Unused $writers_schema parameter included for consistency
- * with other write_* methods.
- */
- return $encoder->write($datum);
+ $logicalType = $writers_schema->logicalType();
+ if (
+ $logicalType instanceof AvroLogicalType
+ ) {
+ switch ($logicalType->name()) {
+ case AvroSchema::DECIMAL_LOGICAL_TYPE:
+ $scale = $logicalType->attributes()['scale'] ?? 0;
+ $precision = $logicalType->attributes()['precision'] ??
null;
+
+ $encoder->writeDecimal($datum, $scale, $precision);
+ return;
+ case AvroSchema::DURATION_LOGICAL_TYPE:
+ if (!$datum instanceof AvroDuration) {
+ throw new AvroException(
+ "Duration datum must be an instance of
AvroDuration"
+ );
+ }
+ $duration = (string) $datum;
+
+ if (strlen($duration) !== 12) {
+ throw new AvroException(
+ "Fixed duration size mismatch. Expected 12 bytes,
got " . strlen($duration)
+ );
+ }
+
+ $encoder->write($duration);
+ return;
+ }
+ }
+
+ $encoder->write($datum);
}
- private function writeEnum($writers_schema, $datum, $encoder)
+ private function writeEnum(AvroSchema $writers_schema, $datum,
AvroIOBinaryEncoder $encoder): void
{
$datum_index = $writers_schema->symbolIndex($datum);
- return $encoder->writeInt($datum_index);
+ $encoder->writeInt($datum_index);
}
- private function writeRecord($writers_schema, $datum, $encoder)
+ private function writeRecord(AvroSchema $writers_schema, $datum,
AvroIOBinaryEncoder $encoder): void
{
foreach ($writers_schema->fields() as $field) {
$this->writeValidatedData($field->type(), $datum[$field->name()]
?? null, $encoder);
}
}
- private function writeUnion($writers_schema, $datum, $encoder)
+ /**
+ * @throws AvroIOTypeException
+ * @throws AvroSchemaParseException
+ */
+ private function writeUnion(AvroSchema $writers_schema, $datum,
AvroIOBinaryEncoder $encoder): void
{
$datum_schema_index = -1;
$datum_schema = null;
diff --git a/lang/php/lib/Datum/Type/AvroDuration.php
b/lang/php/lib/Datum/Type/AvroDuration.php
new file mode 100644
index 0000000000..7bf40d4a11
--- /dev/null
+++ b/lang/php/lib/Datum/Type/AvroDuration.php
@@ -0,0 +1,114 @@
+<?php
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+declare(strict_types=1);
+
+namespace Apache\Avro\Datum\Type;
+
+use Apache\Avro\AvroException;
+use Apache\Avro\Schema\AvroSchema;
+
+class AvroDuration
+{
+ /** @var int */
+ private $months;
+
+ /** @var int */
+ private $days;
+
+ /** @var int */
+ private $milliseconds;
+
+ /**
+ * @throws AvroException
+ */
+ public function __construct(
+ int $months,
+ int $days,
+ int $milliseconds
+ ) {
+ self::validateComponent($months, 'months');
+ self::validateComponent($days, 'days');
+ self::validateComponent($milliseconds, 'milliseconds');
+
+ $this->months = $months;
+ $this->days = $days;
+ $this->milliseconds = $milliseconds;
+ }
+
+ public static function fromBytes(string $bytes): self
+ {
+ $unpackedData = unpack('Vmonths/Vdays/Vmilliseconds', $bytes);
+ $months = (int) $unpackedData['months'];
+ $days = (int) $unpackedData['days'];
+ $milliseconds = (int) $unpackedData['milliseconds'];
+
+ // Correct the sign for each component if it was read as a negative
number
+ // (i.e., if the value is 2^31 or greater, which means the sign bit
was set)
+ if ($months > AvroSchema::INT_MAX_VALUE) {
+ $months -= AvroSchema::INT_RANGE;
+ }
+ if ($days > AvroSchema::INT_MAX_VALUE) {
+ $days -= AvroSchema::INT_RANGE;
+ }
+ if ($milliseconds > AvroSchema::INT_MAX_VALUE) {
+ $milliseconds -= AvroSchema::INT_RANGE;
+ }
+
+ return new self(
+ $months,
+ $days,
+ $milliseconds
+ );
+ }
+
+ public function toBytes(): string
+ {
+ $months = pack('V', $this->months);
+ $days = pack('V', $this->days);
+ $milliseconds = pack('V', $this->milliseconds);
+
+ return $months . $days . $milliseconds;
+ }
+
+ public function __toString(): string
+ {
+ return $this->toBytes();
+ }
+
+ /**
+ * Helper to check if a value is within the 32-bit signed range.
+ * @throws AvroException If the value is out of bounds.
+ */
+ private static function validateComponent(int $value, string $name): void
+ {
+ if ($value > AvroSchema::INT_MAX_VALUE || $value <
AvroSchema::INT_MIN_VALUE) {
+ // Throw an appropriate exception for an out-of-bounds value
+ // You may need to replace \Exception with your specific
AvroException
+ throw new AvroException(
+ sprintf(
+ "Duration component '%s' value (%d) is out of bounds for a
32-bit signed integer.",
+ $name,
+ $value
+ )
+ );
+ }
+ }
+}
diff --git a/lang/php/lib/Schema/AvroFixedSchema.php
b/lang/php/lib/Schema/AvroFixedSchema.php
index 3ace00e46b..a48073ad36 100644
--- a/lang/php/lib/Schema/AvroFixedSchema.php
+++ b/lang/php/lib/Schema/AvroFixedSchema.php
@@ -20,6 +20,8 @@
namespace Apache\Avro\Schema;
+use Apache\Avro\AvroException;
+
/**
* AvroNamedSchema with fixed-length data values
* @package Avro
@@ -67,4 +69,55 @@ class AvroFixedSchema extends AvroNamedSchema
$avro[AvroSchema::SIZE_ATTR] = $this->size;
return $avro;
}
+
+ /**
+ * @param array<int, string>|null $aliases
+ * @throws AvroSchemaParseException
+ */
+ public static function duration(
+ AvroName $name,
+ ?string $doc,
+ AvroNamedSchemata &$schemata = null,
+ ?array $aliases = null
+ ): self {
+ $fixedSchema = new self($name, $doc, 12, $schemata, $aliases);
+
+ $fixedSchema->logicalType = AvroLogicalType::duration();
+
+ return $fixedSchema;
+ }
+
+ /**
+ * @param array<int, string>|null $aliases
+ * @throws AvroSchemaParseException
+ * @throws AvroException
+ */
+ public static function decimal(
+ AvroName $name,
+ ?string $doc,
+ int $size,
+ int $precision,
+ int $scale,
+ ?AvroNamedSchemata &$schemata = null,
+ ?array $aliases = null
+ ): self {
+ $self = new self($name, $doc, $size, $schemata, $aliases);
+
+ $maxPrecision = (int) floor(log10(self::maxDecimalMagnitude($size)));
+
+ if ($precision > $maxPrecision) {
+ throw new AvroException(
+ "Invalid precision for specified fixed size (size='{$size}',
precision='{$precision}')."
+ );
+ }
+
+ $self->logicalType = AvroLogicalType::decimal($precision, $scale);
+
+ return $self;
+ }
+
+ public static function maxDecimalMagnitude(int $size): float
+ {
+ return (float) (2 ** ((8 * $size) - 1)) - 1;
+ }
}
diff --git a/lang/php/lib/Schema/AvroLogicalType.php
b/lang/php/lib/Schema/AvroLogicalType.php
new file mode 100644
index 0000000000..9b19794473
--- /dev/null
+++ b/lang/php/lib/Schema/AvroLogicalType.php
@@ -0,0 +1,128 @@
+<?php
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Apache\Avro\Schema;
+
+use Apache\Avro\AvroException;
+
+class AvroLogicalType
+{
+ public const ATTRIBUTE_DECIMAL_PRECISION = 'precision';
+ public const ATTRIBUTE_DECIMAL_SCALE = 'scale';
+
+ /** @var string */
+ private $name;
+
+ /** @var array */
+ private $attributes;
+
+ public function __construct(string $name, array $attributes = [])
+ {
+ $this->name = $name;
+ $this->attributes = $attributes;
+ }
+
+ public function name(): string
+ {
+ return $this->name;
+ }
+
+ public function attributes(): array
+ {
+ return $this->attributes;
+ }
+
+ public function toAvro(): array
+ {
+ $avro[AvroSchema::LOGICAL_TYPE_ATTR] = $this->name;
+ return array_merge($avro, $this->attributes);
+ }
+
+ public static function decimal(int $precision, int $scale): self
+ {
+ if ($precision <= 0) {
+ throw new AvroException("Precision '{$precision}' is invalid. It
must be a positive integer.");
+ }
+
+ if ($scale < 0) {
+ throw new AvroException("Scale '{$scale}' is invalid. It must be a
non-negative integer.");
+ }
+
+ if ($scale >= $precision) {
+ throw new AvroException(
+ "Scale must be a lower than precision (scale='{$scale}',
precision='{$precision}')."
+ );
+ }
+
+ return new AvroLogicalType(
+ AvroSchema::DECIMAL_LOGICAL_TYPE,
+ [
+ self::ATTRIBUTE_DECIMAL_PRECISION => $precision,
+ self::ATTRIBUTE_DECIMAL_SCALE => $scale,
+
+ ]
+ );
+ }
+
+ public static function uuid(): self
+ {
+ return new AvroLogicalType(AvroSchema::UUID_LOGICAL_TYPE);
+ }
+
+ public static function date(): self
+ {
+ return new AvroLogicalType(AvroSchema::DATE_LOGICAL_TYPE);
+ }
+
+ public static function timeMillis(): self
+ {
+ return new AvroLogicalType(AvroSchema::TIME_MILLIS_LOGICAL_TYPE);
+ }
+
+ public static function timeMicros(): self
+ {
+ return new AvroLogicalType(AvroSchema::TIME_MICROS_LOGICAL_TYPE);
+ }
+
+ public static function timestampMillis(): self
+ {
+ return new AvroLogicalType(AvroSchema::TIMESTAMP_MILLIS_LOGICAL_TYPE);
+ }
+
+ public static function timestampMicros(): self
+ {
+ return new AvroLogicalType(AvroSchema::TIMESTAMP_MICROS_LOGICAL_TYPE);
+ }
+
+ public static function localTimestampMillis(): self
+ {
+ return new
AvroLogicalType(AvroSchema::LOCAL_TIMESTAMP_MILLIS_LOGICAL_TYPE);
+ }
+
+ public static function localTimestampMicros(): self
+ {
+ return new
AvroLogicalType(AvroSchema::LOCAL_TIMESTAMP_MICROS_LOGICAL_TYPE);
+ }
+
+ public static function duration(): self
+ {
+ return new AvroLogicalType(AvroSchema::DURATION_LOGICAL_TYPE);
+ }
+}
diff --git a/lang/php/lib/Schema/AvroPrimitiveSchema.php
b/lang/php/lib/Schema/AvroPrimitiveSchema.php
index 94686d8ec0..1c331a3037 100644
--- a/lang/php/lib/Schema/AvroPrimitiveSchema.php
+++ b/lang/php/lib/Schema/AvroPrimitiveSchema.php
@@ -39,16 +39,90 @@ class AvroPrimitiveSchema extends AvroSchema
parent::__construct($type);
}
+ public static function decimal(int $precision, int $scale): self
+ {
+ $self = new self(AvroSchema::BYTES_TYPE);
+ $self->logicalType = AvroLogicalType::decimal($precision, $scale);
+
+ return $self;
+ }
+
+ public static function uuid(): self
+ {
+ $self = new self(AvroSchema::STRING_TYPE);
+ $self->logicalType = AvroLogicalType::uuid();
+
+ return $self;
+ }
+
+ public static function date(): self
+ {
+ $self = new self(AvroSchema::INT_TYPE);
+ $self->logicalType = AvroLogicalType::date();
+
+ return $self;
+ }
+
+ public static function timeMillis(): self
+ {
+ $self = new self(AvroSchema::INT_TYPE);
+ $self->logicalType = AvroLogicalType::timeMillis();
+
+ return $self;
+ }
+
+ public static function timeMicros(): self
+ {
+ $self = new self(AvroSchema::LONG_TYPE);
+ $self->logicalType = AvroLogicalType::timeMicros();
+
+ return $self;
+ }
+
+ public static function timestampMillis(): self
+ {
+ $self = new self(AvroSchema::LONG_TYPE);
+ $self->logicalType = AvroLogicalType::timestampMillis();
+
+ return $self;
+ }
+
+ public static function timestampMicros(): self
+ {
+ $self = new self(AvroSchema::LONG_TYPE);
+ $self->logicalType = AvroLogicalType::timestampMicros();
+
+ return $self;
+ }
+
+ public static function localTimestampMillis(): self
+ {
+ $self = new self(AvroSchema::LONG_TYPE);
+ $self->logicalType = AvroLogicalType::localTimestampMillis();
+
+ return $self;
+ }
+
+ public static function localTimestampMicros(): self
+ {
+ $self = new self(AvroSchema::LONG_TYPE);
+ $self->logicalType = AvroLogicalType::localTimestampMicros();
+
+ return $self;
+ }
+
/**
* @returns mixed
*/
public function toAvro()
{
$avro = parent::toAvro();
+
// FIXME: Is this if really necessary? When *wouldn't* this be the
case?
if (1 == count($avro)) {
return $this->type;
}
+
return $avro;
}
}
diff --git a/lang/php/lib/Schema/AvroRecordSchema.php
b/lang/php/lib/Schema/AvroRecordSchema.php
index 354aebd2a8..959ce5904e 100644
--- a/lang/php/lib/Schema/AvroRecordSchema.php
+++ b/lang/php/lib/Schema/AvroRecordSchema.php
@@ -110,7 +110,11 @@ class AvroRecordSchema extends AvroNamedSchema
) {
$is_schema_from_schemata = true;
} else {
- $field_schema = self::subparse($type, $default_namespace,
$schemata);
+ if (self::isPrimitiveType($type)) {
+ $field_schema = self::subparse($field, $default_namespace,
$schemata);
+ } else {
+ $field_schema = self::subparse($type, $default_namespace,
$schemata);
+ }
}
$new_field = new AvroField(
diff --git a/lang/php/lib/Schema/AvroSchema.php
b/lang/php/lib/Schema/AvroSchema.php
index dc4bb8e6e1..5a33ce1c9e 100644
--- a/lang/php/lib/Schema/AvroSchema.php
+++ b/lang/php/lib/Schema/AvroSchema.php
@@ -21,6 +21,7 @@
namespace Apache\Avro\Schema;
use Apache\Avro\AvroUtil;
+use Apache\Avro\Datum\Type\AvroDuration;
/** TODO
* - ARRAY have only type and item attributes (what about metadata?)
@@ -64,12 +65,17 @@ class AvroSchema
public const INT_MAX_VALUE = 2147483647;
/**
- * @var long lower bound of long values: -(1 << 63)
+ * @var int upper bound of integer values: (1 << 31) - 1
+ */
+ public const INT_RANGE = 4294967296;
+
+ /**
+ * @var int lower bound of long values: -(1 << 63)
*/
public const LONG_MIN_VALUE = -9223372036854775808;
/**
- * @var long upper bound of long values: (1 << 63) - 1
+ * @var int upper bound of long values: (1 << 63) - 1
*/
public const LONG_MAX_VALUE = 9223372036854775807;
@@ -119,6 +125,37 @@ class AvroSchema
*/
public const BYTES_TYPE = 'bytes';
+ // Logical Types
+ /** @var string */
+ public const DECIMAL_LOGICAL_TYPE = 'decimal';
+
+ /** @var string */
+ public const UUID_LOGICAL_TYPE = 'uuid';
+
+ /** @var string */
+ public const DATE_LOGICAL_TYPE = 'date';
+
+ /** @var string */
+ public const TIME_MILLIS_LOGICAL_TYPE = 'time-millis';
+
+ /** @var string */
+ public const TIME_MICROS_LOGICAL_TYPE = 'time-micros';
+
+ /** @var string */
+ public const TIMESTAMP_MILLIS_LOGICAL_TYPE = 'timestamp-millis';
+
+ /** @var string */
+ public const TIMESTAMP_MICROS_LOGICAL_TYPE = 'timestamp-micros';
+
+ /** @var string */
+ public const LOCAL_TIMESTAMP_MILLIS_LOGICAL_TYPE =
'local-timestamp-millis';
+
+ /** @var string */
+ public const LOCAL_TIMESTAMP_MICROS_LOGICAL_TYPE =
'local-timestamp-micros';
+
+ /** @var string */
+ public const DURATION_LOGICAL_TYPE = 'duration';
+
// Complex Types
// Unnamed Schema
/**
@@ -225,6 +262,9 @@ class AvroSchema
/** @var string aliases string attribute name */
public const ALIASES_ATTR = 'aliases';
+ /** @var string logical type attribute name */
+ public const LOGICAL_TYPE_ATTR = 'logicalType';
+
/**
* @var array list of primitive schema type names
*/
@@ -259,13 +299,17 @@ class AvroSchema
self::ITEMS_ATTR,
self::SIZE_ATTR,
self::SYMBOLS_ATTR,
- self::VALUES_ATTR
+ self::VALUES_ATTR,
+ self::LOGICAL_TYPE_ATTR,
);
/**
* @var string|AvroNamedSchema
*/
public $type;
+ /** @var null|AvroLogicalType */
+ protected $logicalType = null;
+
/**
* @param string $type a schema type name
* @internal Should only be called from within the constructor of
@@ -304,7 +348,29 @@ class AvroSchema
$type = $avro[self::TYPE_ATTR] ?? null;
if (self::isPrimitiveType($type)) {
- return new AvroPrimitiveSchema($type);
+ switch ($avro[self::LOGICAL_TYPE_ATTR] ?? null) {
+ case self::DECIMAL_LOGICAL_TYPE:
+ [$precision, $scale] =
self::extractPrecisionAndScaleForDecimal($avro);
+ return AvroPrimitiveSchema::decimal($precision,
$scale);
+ case self::UUID_LOGICAL_TYPE:
+ return AvroPrimitiveSchema::uuid();
+ case self::DATE_LOGICAL_TYPE:
+ return AvroPrimitiveSchema::date();
+ case self::TIME_MILLIS_LOGICAL_TYPE:
+ return AvroPrimitiveSchema::timeMillis();
+ case self::TIME_MICROS_LOGICAL_TYPE:
+ return AvroPrimitiveSchema::timeMicros();
+ case self::TIMESTAMP_MILLIS_LOGICAL_TYPE:
+ return AvroPrimitiveSchema::timestampMillis();
+ case self::TIMESTAMP_MICROS_LOGICAL_TYPE:
+ return AvroPrimitiveSchema::timestampMicros();
+ case self::LOCAL_TIMESTAMP_MILLIS_LOGICAL_TYPE:
+ return AvroPrimitiveSchema::localTimestampMillis();
+ case self::LOCAL_TIMESTAMP_MICROS_LOGICAL_TYPE:
+ return AvroPrimitiveSchema::localTimestampMicros();
+ default:
+ return new AvroPrimitiveSchema($type);
+ }
}
if (self::isNamedType($type)) {
@@ -316,6 +382,29 @@ class AvroSchema
switch ($type) {
case self::FIXED_SCHEMA:
$size = $avro[self::SIZE_ATTR] ?? null;
+ if (array_key_exists(self::LOGICAL_TYPE_ATTR, $avro)) {
+ switch ($avro[self::LOGICAL_TYPE_ATTR]) {
+ case self::DURATION_LOGICAL_TYPE:
+ return AvroFixedSchema::duration(
+ $new_name,
+ $doc,
+ $schemata,
+ $aliases
+ );
+ case self::DECIMAL_LOGICAL_TYPE:
+ [$precision, $scale] =
self::extractPrecisionAndScaleForDecimal($avro);
+ return AvroFixedSchema::decimal(
+ $new_name,
+ $doc,
+ $size,
+ $precision,
+ $scale,
+ $schemata,
+ $aliases
+ );
+ }
+ }
+
return new AvroFixedSchema(
$new_name,
$doc,
@@ -450,7 +539,7 @@ class AvroSchema
* and false otherwise.
* @throws AvroSchemaParseException
*/
- public static function isValidDatum($expected_schema, $datum)
+ public static function isValidDatum(AvroSchema $expected_schema, $datum):
bool
{
switch ($expected_schema->type) {
case self::NULL_TYPE:
@@ -504,8 +593,28 @@ class AvroSchema
case self::ENUM_SCHEMA:
return in_array($datum, $expected_schema->symbols());
case self::FIXED_SCHEMA:
+ if (
+ $expected_schema->logicalType() instanceof AvroLogicalType
+ ) {
+ switch ($expected_schema->logicalType->name()) {
+ case self::DECIMAL_LOGICAL_TYPE:
+ $value = abs((float) $datum);
+ $maxMagnitude =
AvroFixedSchema::maxDecimalMagnitude((int) $expected_schema->size());
+ return $value <= $maxMagnitude;
+ case self::DURATION_LOGICAL_TYPE:
+ return $datum instanceof AvroDuration;
+ default:
+ throw new AvroSchemaParseException(
+ sprintf(
+ 'Logical type %s not supported for fixed
schema validation.',
+ $expected_schema->logicalType->name()
+ )
+ );
+ }
+ }
+
return (is_string($datum)
- && (strlen($datum) == $expected_schema->size()));
+ && (strlen($datum) === $expected_schema->size()));
case self::RECORD_SCHEMA:
case self::ERROR_SCHEMA:
case self::REQUEST_SCHEMA:
@@ -548,13 +657,18 @@ class AvroSchema
}
/**
- * @returns string schema type name of this schema
+ * @returns string|AvroNamedSchema schema type name of this schema
*/
public function type()
{
return $this->type;
}
+ public function logicalType(): ?AvroLogicalType
+ {
+ return $this->logicalType;
+ }
+
/**
* @returns string the JSON-encoded representation of this Avro schema.
*/
@@ -563,12 +677,15 @@ class AvroSchema
return (string) json_encode($this->toAvro());
}
- /**
- * @returns mixed
- */
public function toAvro()
{
- return array(self::TYPE_ATTR => $this->type);
+ $avro = [self::TYPE_ATTR => $this->type];
+
+ if (!is_null($this->logicalType)) {
+ $avro = array_merge($avro, $this->logicalType->toAvro());
+ }
+
+ return $avro;
}
/**
@@ -578,4 +695,26 @@ class AvroSchema
{
return $this->$attribute();
}
+
+ /**
+ * @return array{0: int, 1: int} [precision, scale]
+ * @throws AvroSchemaParseException
+ */
+ private static function extractPrecisionAndScaleForDecimal(array $avro):
array
+ {
+ $precision = $avro[AvroLogicalType::ATTRIBUTE_DECIMAL_PRECISION] ??
null;
+ if (!is_int($precision)) {
+ throw new AvroSchemaParseException(
+ "Invalid value '{$precision}' for 'precision' attribute of
decimal logical type."
+ );
+ }
+ $scale = $avro[AvroLogicalType::ATTRIBUTE_DECIMAL_SCALE] ?? 0;
+ if (!is_int($scale)) {
+ throw new AvroSchemaParseException(
+ "Invalid value '{$scale}' for 'scale' attribute of decimal
logical type."
+ );
+ }
+
+ return [$precision, $scale];
+ }
}
diff --git a/lang/php/phpunit.xml b/lang/php/phpunit.xml
index bdf201da73..10584bc4aa 100644
--- a/lang/php/phpunit.xml
+++ b/lang/php/phpunit.xml
@@ -19,7 +19,7 @@
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.1/phpunit.xsd"
bootstrap="test/test_helper.php"
executionOrder="depends,defects"
- forceCoversAnnotation="true"
+ forceCoversAnnotation="false"
beStrictAboutCoversAnnotation="true"
beStrictAboutOutputDuringTests="true"
beStrictAboutTodoAnnotatedTests="true"
diff --git a/lang/php/test/DatumIOTest.php b/lang/php/test/DatumIOTest.php
index b2b879c1c8..a29cf06f06 100644
--- a/lang/php/test/DatumIOTest.php
+++ b/lang/php/test/DatumIOTest.php
@@ -20,10 +20,12 @@
namespace Apache\Avro\Tests;
use Apache\Avro\AvroDebug;
+use Apache\Avro\AvroException;
use Apache\Avro\Datum\AvroIOBinaryDecoder;
use Apache\Avro\Datum\AvroIOBinaryEncoder;
use Apache\Avro\Datum\AvroIODatumReader;
use Apache\Avro\Datum\AvroIODatumWriter;
+use Apache\Avro\Datum\Type\AvroDuration;
use Apache\Avro\IO\AvroStringIO;
use Apache\Avro\Schema\AvroSchema;
use PHPUnit\Framework\TestCase;
@@ -39,157 +41,380 @@ class DatumIOTest extends TestCase
/**
* @dataProvider data_provider
*/
- function test_datum_round_trip($schema_json, $datum, $binary)
+ public function test_datum_round_trip(string $schema_json, $datum, string
$binary): void
{
- $schema = AvroSchema::parse($schema_json);
- $written = new AvroStringIO();
- $encoder = new AvroIOBinaryEncoder($written);
- $writer = new AvroIODatumWriter($schema);
-
- $writer->write($datum, $encoder);
- $output = (string) $written;
- $this->assertEquals($binary, $output,
- sprintf("expected: %s\n actual: %s",
- AvroDebug::asciiString($binary, 'hex'),
- AvroDebug::asciiString($output, 'hex')));
-
- $read = new AvroStringIO($binary);
- $decoder = new AvroIOBinaryDecoder($read);
- $reader = new AvroIODatumReader($schema);
- $read_datum = $reader->read($decoder);
- $this->assertEquals($datum, $read_datum);
+ $this->assertIsValidDatumForSchema($schema_json, $datum, $binary);
}
- function data_provider()
+ public static function data_provider(): array
{
- return array(
- array('"null"', null, ''),
-
- array('"boolean"', true, "\001"),
- array('"boolean"', false, "\000"),
-
- array('"int"', (int) -2147483648, "\xFF\xFF\xFF\xFF\x0F"),
- array('"int"', -1, "\001"),
- array('"int"', 0, "\000"),
- array('"int"', 1, "\002"),
- array('"int"', 2147483647, "\xFE\xFF\xFF\xFF\x0F"),
-
- array('"long"', (int) -9223372036854775808,
"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x01"),
- array('"long"', -(1<<62), "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x7F"),
- array('"long"', -(1<<61), "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x3F"),
- array('"long"', -4294967295, "\xFD\xFF\xFF\xFF\x1F"),
- array('"long"', -1<<24, "\xFF\xFF\xFF\x0F"),
- array('"long"', -1<<16, "\xFF\xFF\x07"),
- array('"long"', -255, "\xFD\x03"),
- array('"long"', -128, "\xFF\x01"),
- array('"long"', -127, "\xFD\x01"),
- array('"long"', -10, "\x13"),
- array('"long"', -3, "\005"),
- array('"long"', -2, "\003"),
- array('"long"', -1, "\001"),
- array('"long"', 0, "\000"),
- array('"long"', 1, "\002"),
- array('"long"', 2, "\004"),
- array('"long"', 3, "\006"),
- array('"long"', 10, "\x14"),
- array('"long"', 127, "\xFE\x01"),
- array('"long"', 128, "\x80\x02"),
- array('"long"', 255, "\xFE\x03"),
- array('"long"', 1<<16, "\x80\x80\x08"),
- array('"long"', 1<<24, "\x80\x80\x80\x10"),
- array('"long"', 4294967295, "\xFE\xFF\xFF\xFF\x1F"),
- array('"long"', 1<<61, "\x80\x80\x80\x80\x80\x80\x80\x80\x40"),
- array('"long"', 1<<62, "\x80\x80\x80\x80\x80\x80\x80\x80\x80\x01"),
- array('"long"', 9223372036854775807,
"\xFE\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x01"),
-
- array('"float"', (float) -10.0, "\000\000 \301"),
- array('"float"', (float) -1.0, "\000\000\200\277"),
- array('"float"', (float) 0.0, "\000\000\000\000"),
- array('"float"', (float) 2.0, "\000\000\000@"),
- array('"float"', (float) 9.0, "\000\000\020A"),
-
- array('"double"', (double) -10.0, "\000\000\000\000\000\000$\300"),
- array('"double"', (double) -1.0,
"\000\000\000\000\000\000\360\277"),
- array('"double"', (double) 0.0,
"\000\000\000\000\000\000\000\000"),
- array('"double"', (double) 2.0, "\000\000\000\000\000\000\000@"),
- array('"double"', (double) 9.0, "\000\000\000\000\000\000\"@"),
-
- array('"string"', 'foo', "\x06foo"),
- array('"bytes"', "\x01\x02\x03", "\x06\x01\x02\x03"),
-
- array(
+ return [
+ ['"null"', null, ''],
+
+ ['"boolean"', true, "\001"],
+ ['"boolean"', false, "\000"],
+
+ ['"int"', (int) -2147483648, "\xFF\xFF\xFF\xFF\x0F"],
+ ['"int"', -1, "\001"],
+ ['"int"', 0, "\000"],
+ ['"int"', 1, "\002"],
+ ['"int"', 2147483647, "\xFE\xFF\xFF\xFF\x0F"],
+
+ ['"long"', (int) -9223372036854775808,
"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x01"],
+ ['"long"', -(1<<62), "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x7F"],
+ ['"long"', -(1<<61), "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x3F"],
+ ['"long"', -4294967295, "\xFD\xFF\xFF\xFF\x1F"],
+ ['"long"', -1<<24, "\xFF\xFF\xFF\x0F"],
+ ['"long"', -1<<16, "\xFF\xFF\x07"],
+ ['"long"', -255, "\xFD\x03"],
+ ['"long"', -128, "\xFF\x01"],
+ ['"long"', -127, "\xFD\x01"],
+ ['"long"', -10, "\x13"],
+ ['"long"', -3, "\005"],
+ ['"long"', -2, "\003"],
+ ['"long"', -1, "\001"],
+ ['"long"', 0, "\000"],
+ ['"long"', 1, "\002"],
+ ['"long"', 2, "\004"],
+ ['"long"', 3, "\006"],
+ ['"long"', 10, "\x14"],
+ ['"long"', 127, "\xFE\x01"],
+ ['"long"', 128, "\x80\x02"],
+ ['"long"', 255, "\xFE\x03"],
+ ['"long"', 1<<16, "\x80\x80\x08"],
+ ['"long"', 1<<24, "\x80\x80\x80\x10"],
+ ['"long"', 4294967295, "\xFE\xFF\xFF\xFF\x1F"],
+ ['"long"', 1<<61, "\x80\x80\x80\x80\x80\x80\x80\x80\x40"],
+ ['"long"', 1<<62, "\x80\x80\x80\x80\x80\x80\x80\x80\x80\x01"],
+ ['"long"', 9223372036854775807,
"\xFE\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x01"],
+
+ ['"float"', (float) -10.0, "\000\000 \301"],
+ ['"float"', (float) -1.0, "\000\000\200\277"],
+ ['"float"', (float) 0.0, "\000\000\000\000"],
+ ['"float"', (float) 2.0, "\000\000\000@"],
+ ['"float"', (float) 9.0, "\000\000\020A"],
+
+ ['"double"', (double) -10.0, "\000\000\000\000\000\000$\300"],
+ ['"double"', (double) -1.0, "\000\000\000\000\000\000\360\277"],
+ ['"double"', (double) 0.0, "\000\000\000\000\000\000\000\000"],
+ ['"double"', (double) 2.0, "\000\000\000\000\000\000\000@"],
+ ['"double"', (double) 9.0, "\000\000\000\000\000\000\"@"],
+
+ ['"string"', 'foo', "\x06foo"],
+ ['"bytes"', "\x01\x02\x03", "\x06\x01\x02\x03"],
+
+ [
'{"type":"array","items":"int"}',
- array(1, 2, 3),
+ [1, 2, 3],
"\x06\x02\x04\x06\x00"
- ),
- array(
+ ],
+ [
'{"type":"map","values":"int"}',
- array('foo' => 1, 'bar' => 2, 'baz' => 3),
+ ['foo' => 1, 'bar' => 2, 'baz' => 3],
"\x06\x06foo\x02\x06bar\x04\x06baz\x06\x00"
- ),
- array('["null", "int"]', 1, "\x02\x02"),
- array(
+ ],
+ ['["null", "int"]', 1, "\x02\x02"],
+ [
'{"name":"fix","type":"fixed","size":3}',
"\xAA\xBB\xCC",
"\xAA\xBB\xCC"
- ),
- array(
+ ],
+ [
'{"name":"enm","type":"enum","symbols":["A","B","C"]}',
'B',
"\x02"
- ),
- array(
+ ],
+ [
'{"name":"rec","type":"record","fields":[{"name":"a","type":"int"},{"name":"b","type":"boolean"}]}',
- array('a' => 1, 'b' => false),
+ ['a' => 1, 'b' => false],
"\x02\x00"
- )
+ ],
+ ];
+ }
+
+ public static function validBytesDecimalLogicalType(): array
+ {
+ return [
+ 'positive' => [
+ '10.95',
+ 4,
+ 2,
+ "\x04\x04\x47",
+ ],
+ 'negative' => [
+ '-10.95',
+ 4,
+ 2,
+ "\x04\xFB\xB9",
+ ],
+ 'small positive' => [
+ '0.05',
+ 3,
+ 2,
+ "\x02\x05",
+ ],
+ 'zero value' => [
+ '0',
+ 4,
+ 2,
+ "\x02\x00",
+ ],
+ 'unscaled positive' => [
+ '12345',
+ 6,
+ 0,
+ "\x04\x30\x39",
+ ],
+ 'trimming edge case 127' => [
+ '127',
+ 3,
+ 0,
+ "\x02\x7F",
+ ],
+ 'trimming edge case 128' => [
+ '128',
+ 3,
+ 0,
+ "\x04\x00\x80",
+ ],
+ 'negative trimming -1' => [
+ '-1',
+ 3,
+ 0,
+ "\x02\xFF",
+ ],
+ 'negative trimming -129' => [
+ '-129',
+ 3,
+ 0,
+ "\x04\xFF\x7F",
+ ],
+ 'high precision positive number' => [
+ '549755813887',
+ 12,
+ 0,
+ "\x0A\x7F\xFF\xFF\xFF\xFF",
+ ],
+ 'high precision positive number with scale' => [
+ '54975581.3887',
+ 12,
+ 4,
+ "\x0A\x7F\xFF\xFF\xFF\xFF",
+ ],
+ 'high precision negative number' => [
+ '-549755813888',
+ 12,
+ 0,
+ "\x0A\x80\x00\x00\x00\x00",
+ ],
+ 'high precision negative number with scale' => [
+ '-54975581.3888',
+ 12,
+ 4,
+ "\x0A\x80\x00\x00\x00\x00",
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider validBytesDecimalLogicalType
+ */
+ public function testValidBytesDecimalLogicalType(string $datum, int
$precision, int $scale, string $expected): void
+ {
+ $bytesSchemaJson = <<<JSON
+ {
+ "name": "number",
+ "type": "bytes",
+ "logicalType": "decimal",
+ "precision": {$precision},
+ "scale": {$scale}
+ }
+ JSON;
+
+ $this->assertIsValidDatumForSchema($bytesSchemaJson, $datum,
$expected);
+
+ $fixedSchemaJson = <<<JSON
+ {
+ "name": "number",
+ "type": "fixed",
+ "size": 8,
+ "logicalType": "decimal",
+ "precision": {$precision},
+ "scale": {$scale}
+ }
+ JSON;
+
+ $this->assertIsValidDatumForSchema($fixedSchemaJson, $datum,
$expected);
+ }
+
+ public function testInvalidBytesLogicalTypeOutOfRange(): void
+ {
+ $schemaJson = <<<JSON
+ {
+ "name": "number",
+ "type": "bytes",
+ "logicalType": "decimal",
+ "precision": 4,
+ "scale": 0
+ }
+ JSON;
+
+ $schema = AvroSchema::parse($schemaJson);
+ $written = new AvroStringIO();
+ $encoder = new AvroIOBinaryEncoder($written);
+ $writer = new AvroIODatumWriter($schema);
+
+ $this->expectException(AvroException::class);
+ $this->expectExceptionMessage("Decimal value '10000' is out of range
for precision=4, scale=0");
+ $writer->write("10000", $encoder);
+ }
+
+ public static function validDurationLogicalTypes(): array
+ {
+ return [
+ [new AvroDuration(1, 2, 3)],
+ [new AvroDuration(-1, -2, -3)],
+ [new AvroDuration(AvroSchema::INT_MAX_VALUE,
AvroSchema::INT_MAX_VALUE, AvroSchema::INT_MIN_VALUE)],
+ ];
+ }
+
+ /**
+ * @dataProvider validDurationLogicalTypes
+ */
+ public function testDurationLogicalType(AvroDuration $avroDuration): void
+ {
+ $bytesSchemaJson = <<<JSON
+ {
+ "name": "number",
+ "type": "fixed",
+ "size": 12,
+ "logicalType": "duration"
+ }
+ JSON;
+
+ $this->assertIsValidDatumForSchema(
+ $bytesSchemaJson,
+ $avroDuration,
+ $avroDuration
);
}
- function default_provider()
+ public static function durationLogicalTypeOutOfBounds(): array
+ {
+ return [
+ [AvroSchema::INT_MIN_VALUE - 1, 0, 0],
+ [0, AvroSchema::INT_MIN_VALUE - 1, 0],
+ [0, 0, AvroSchema::INT_MIN_VALUE - 1],
+ [AvroSchema::INT_MAX_VALUE + 1, 0, 0],
+ [0, AvroSchema::INT_MAX_VALUE + 1, 0],
+ [0, 0, AvroSchema::INT_MAX_VALUE + 1],
+ ];
+ }
+
+ /**
+ * @dataProvider durationLogicalTypeOutOfBounds
+ */
+ public function testDurationLogicalTypeOutOfBounds(int $months, int $days,
int $milliseconds): void
+ {
+ $this->expectException(AvroException::class);
+ new AvroDuration($months, $days, $milliseconds);
+ }
+
+ public static function default_provider(): array
{
- return array(
- array('"null"', 'null', null),
- array('"boolean"', 'true', true),
- array('"int"', '1', 1),
- array('"long"', '2000', 2000),
- array('"float"', '1.1', (float) 1.1),
- array('"double"', '200.2', (double) 200.2),
- array('"string"', '"quux"', 'quux'),
- array('"bytes"', '"\u00FF"', "\xC3\xBF"),
- array(
+ return [
+ [
+ '"null"',
+ 'null',
+ null,
+ ],
+ [
+ '"boolean"',
+ 'true',
+ true,
+ ],
+ [
+ '"int"',
+ '1',
+ 1,
+ ],
+ [
+ '"long"',
+ '2000',
+ 2000,
+ ],
+ [
+ '"float"',
+ '1.1',
+ 1.1,
+ ],
+ [
+ '"double"',
+ '200.2',
+ 200.2,
+ ],
+ [
+ '"string"',
+ '"quux"',
+ 'quux',
+ ],
+ [
+ '"bytes"',
+ '"\u00FF"',
+ "\xC3\xBF"],
+ [
'{"type":"array","items":"int"}',
'[5,4,3,2]',
- array(5, 4, 3, 2)
- ),
- array(
+ [5, 4, 3, 2]
+ ],
+ [
'{"type":"map","values":"int"}',
'{"a":9}',
- array('a' => 9)
- ),
- array('["int","string"]', '8', 8),
- array(
+ ['a' => 9],
+ ],
+ [
+ '["int","string"]',
+ '8',
+ 8,
+ ],
+ [
'{"name":"x","type":"enum","symbols":["A","V"]}',
'"A"',
- 'A'
- ),
- array('{"name":"x","type":"fixed","size":4}', '"\u00ff"',
"\xC3\xBF"),
- array(
+ 'A',
+ ],
+ [
+ '{"name":"x","type":"fixed","size":4}',
+ '"\u00ff"',
+ "\xC3\xBF",
+ ],
+ [
'{"name":"x","type":"record","fields":[{"name":"label","type":"int"}]}',
'{"label":7}',
- array('label' => 7)
- )
- );
+ ['label' => 7],
+ ],
+ 'logical type' => [
+ '{"type":"string","logicalType":"uuid"}',
+ '"550e8400-e29b-41d4-a716-446655"',
+ '550e8400-e29b-41d4-a716-446655',
+
+ ],
+ 'logical type in a record' => [
+
'{"name":"x","type":"record","fields":[{"name":"price","type":"bytes","logicalType":"decimal","precision":4,"scale":2}]}',
+ '{"price": "\u0000\u0000"}',
+ ['price' => "0"],
+
+ ],
+ ];
}
/**
* @dataProvider default_provider
*/
- function test_field_default_value(
+ public function test_field_default_value(
$field_schema_json,
$default_json,
$default_value
- ) {
+ ): void {
$writers_schema_json = '{"name":"foo","type":"record","fields":[]}';
$writers_schema = AvroSchema::parse($writers_schema_json);
@@ -207,4 +432,32 @@ class DatumIOTest extends TestCase
print_r($record, true)));
}
}
+
+ /**
+ * @param string $schemaJson
+ * @param string $datum
+ * @param string $expected
+ * @return void
+ * @throws \Apache\Avro\IO\AvroIOException
+ */
+ private function assertIsValidDatumForSchema(string $schemaJson, $datum,
$expected): void
+ {
+ $schema = AvroSchema::parse($schemaJson);
+ $written = new AvroStringIO();
+ $encoder = new AvroIOBinaryEncoder($written);
+ $writer = new AvroIODatumWriter($schema);
+
+ $writer->write($datum, $encoder);
+ $output = (string) $written;
+ $this->assertEquals($expected, $output,
+ sprintf("expected: %s\n actual: %s",
+ AvroDebug::asciiString($expected, 'hex'),
+ AvroDebug::asciiString($output, 'hex')));
+
+ $read = new AvroStringIO((string) $expected);
+ $decoder = new AvroIOBinaryDecoder($read);
+ $reader = new AvroIODatumReader($schema);
+ $read_datum = $reader->read($decoder);
+ $this->assertEquals($datum, $read_datum);
+ }
}
diff --git a/lang/php/test/IODatumReaderTest.php
b/lang/php/test/IODatumReaderTest.php
index acdc8c484d..0135751d80 100644
--- a/lang/php/test/IODatumReaderTest.php
+++ b/lang/php/test/IODatumReaderTest.php
@@ -23,31 +23,43 @@ use Apache\Avro\Datum\AvroIOBinaryDecoder;
use Apache\Avro\Datum\AvroIOBinaryEncoder;
use Apache\Avro\Datum\AvroIODatumReader;
use Apache\Avro\Datum\AvroIODatumWriter;
+use Apache\Avro\Datum\Type\AvroDuration;
use Apache\Avro\IO\AvroStringIO;
use Apache\Avro\Schema\AvroSchema;
use PHPUnit\Framework\TestCase;
class IODatumReaderTest extends TestCase
{
- public function testSchemaMatching()
+ public function testSchemaMatching(): void
{
$writers_schema = <<<JSON
- { "type": "map",
- "values": "bytes" }
-JSON;
+ {
+ "type": "map",
+ "values": "bytes"
+ }
+ JSON;
$readers_schema = $writers_schema;
$this->assertTrue(AvroIODatumReader::schemasMatch(
AvroSchema::parse($writers_schema),
AvroSchema::parse($readers_schema)));
}
- public function test_aliased()
+ public function test_aliased(): void
{
- $writers_schema = AvroSchema::parse(<<<SCHEMA
-{"type":"record", "name":"Rec1", "fields":[
-{"name":"field1", "type":"int"}
-]}
-SCHEMA);
+ $writers_schema = AvroSchema::parse(
+ <<<JSON
+ {
+ "type": "record",
+ "name": "Rec1",
+ "fields": [
+ {
+ "name": "field1",
+ "type": "int"
+ }
+ ]
+ }
+ JSON
+ );
$readers_schema = AvroSchema::parse(<<<SCHEMA
{"type":"record", "name":"Rec2", "aliases":["Rec1"], "fields":[
{"name":"field2", "aliases":["field1"], "type":"int"}
@@ -69,15 +81,18 @@ SCHEMA);
$this->assertEquals(['field2' => 1], $record);
}
- public function testRecordNullField()
+ public function testRecordNullField(): void
{
- $schema_json = <<<_JSON
-{"name":"member",
- "type":"record",
- "fields":[{"name":"one", "type":"int"},
- {"name":"two", "type":["null", "string"]}
- ]}
-_JSON;
+ $schema_json = <<<JSON
+ {
+ "name":"member",
+ "type":"record",
+ "fields":[
+ {"name":"one", "type":"int"},
+ {"name":"two", "type":["null", "string"]}
+ ]
+ }
+ JSON;
$schema = AvroSchema::parse($schema_json);
$datum = array("one" => 1);
@@ -91,21 +106,22 @@ _JSON;
$this->assertSame('0200', bin2hex($bin));
}
- public function testRecordFieldWithDefault()
- {
- $schema = AvroSchema::parse(<<<_JSON
-{
- "name": "RecordWithDefaultValue",
- "type": "record",
- "fields": [
+ public function testRecordFieldWithDefault(): void
{
- "name": "field1",
- "type": "string",
- "default": "default"
- }
- ]
-}
-_JSON
+ $schema = AvroSchema::parse(
+ <<<JSON
+ {
+ "name": "RecordWithDefaultValue",
+ "type": "record",
+ "fields": [
+ {
+ "name": "field1",
+ "type": "string",
+ "default": "default"
+ }
+ ]
+ }
+ JSON
);
$io = new AvroStringIO();
@@ -122,4 +138,144 @@ _JSON
$this->assertEquals(['field1' => "foobar"], $record);
}
+
+ public function testRecordWithLogicalTypes(): void
+ {
+ $schema = AvroSchema::parse(
+ <<<JSON
+ {
+ "name": "RecordWithLogicalTypes",
+ "type": "record",
+ "fields": [
+ {
+ "name": "decimal_field",
+ "type": "bytes",
+ "logicalType": "decimal",
+ "precision": 4,
+ "scale": 2
+ },
+ {
+ "name": "uuid_field",
+ "type": "string",
+ "logicalType": "uuid"
+ },
+ {
+ "name": "date_field",
+ "type": {
+ "type": "int",
+ "logicalType": "date"
+ }
+ },
+ {
+ "name": "time_millis_field",
+ "type": {
+ "type": "int",
+ "logicalType": "time-millis"
+ }
+ },
+ {
+ "name": "time_micros_field",
+ "type": {
+ "type": "long",
+ "logicalType": "time-micros"
+ }
+ },
+ {
+ "name": "timestamp_millis_field",
+ "type": {
+ "type": "long",
+ "logicalType": "timestamp-millis"
+ }
+ },
+ {
+ "name": "timestamp_micros_field",
+ "type": {
+ "type": "long",
+ "logicalType": "timestamp-micros"
+ }
+ },
+ {
+ "name": "local_timestamp_millis_field",
+ "type": {
+ "type": "long",
+ "logicalType": "local-timestamp-millis"
+ }
+ },
+ {
+ "name": "local_timestamp_micros_field",
+ "type": {
+ "type": "long",
+ "logicalType": "local-timestamp-micros"
+ }
+ },
+ {
+ "name": "duration_field",
+ "type": {
+ "name": "duration_field",
+ "type": "fixed",
+ "size": 12,
+ "logicalType": "duration"
+ }
+ },
+ {
+ "name": "decimal_fixed_field",
+ "type": {
+ "name": "decimal_fixed_field",
+ "type": "fixed",
+ "logicalType": "decimal",
+ "size": 3,
+ "precision": 4,
+ "scale": 2
+ }
+ }
+ ]
+ }
+ JSON
+ );
+
+ $io = new AvroStringIO();
+ $writer = new AvroIODatumWriter();
+ $writer->writeData(
+ $schema,
+ [
+ 'decimal_field' => '10.91',
+ 'uuid_field' => '9fb9ea49-2f7e-4df3-b02b-96d881e27a6b',
+ 'date_field' => 20251023,
+ 'time_millis_field' => 86400000,
+ 'time_micros_field' => 86400000000,
+ 'timestamp_millis_field' => 1761224729109,
+ 'timestamp_micros_field' => 1761224729109000,
+ 'local_timestamp_millis_field' => 1751224729109,
+ 'local_timestamp_micros_field' => 1751224729109000,
+ 'duration_field' => new AvroDuration(5, 3600, 1234),
+ 'decimal_fixed_field' => '10.91',
+ ],
+ new AvroIOBinaryEncoder($io)
+ );
+
+ $bin = $io->string();
+ $reader = new AvroIODatumReader();
+ $record = $reader->readRecord(
+ $schema,
+ $schema,
+ new AvroIOBinaryDecoder(new AvroStringIO($bin))
+ );
+
+ $this->assertEquals(
+ [
+ 'decimal_field' => '10.91',
+ 'uuid_field' => '9fb9ea49-2f7e-4df3-b02b-96d881e27a6b',
+ 'date_field' => 20251023,
+ 'time_millis_field' => 86400000,
+ 'time_micros_field' => 86400000000,
+ 'timestamp_millis_field' => 1761224729109,
+ 'timestamp_micros_field' => 1761224729109000,
+ 'local_timestamp_millis_field' => 1751224729109,
+ 'local_timestamp_micros_field' => 1751224729109000,
+ 'duration_field' => new AvroDuration(5, 3600, 1234),
+ 'decimal_fixed_field' => '10.91',
+ ],
+ $record
+ );
+ }
}
diff --git a/lang/php/test/SchemaTest.php b/lang/php/test/SchemaTest.php
index aaa2240fc1..e0d7be75b0 100644
--- a/lang/php/test/SchemaTest.php
+++ b/lang/php/test/SchemaTest.php
@@ -19,6 +19,7 @@
namespace Apache\Avro\Tests;
+use Apache\Avro\AvroException;
use Apache\Avro\Schema\AvroSchema;
use Apache\Avro\Schema\AvroSchemaParseException;
use PHPUnit\Framework\TestCase;
@@ -49,65 +50,65 @@ class SchemaExample
class SchemaTest extends TestCase
{
- static $examples = array();
- static $valid_examples = array();
+ private static $examples = [];
+ private static $valid_examples = [];
- public function test_json_decode()
+ public function test_json_decode(): void
{
- $this->assertEquals(json_decode('null', true), null);
- $this->assertEquals(json_decode('32', true), 32);
- $this->assertEquals(json_decode('"32"', true), '32');
- $this->assertEquals((array) json_decode('{"foo": 27}'), array("foo" =>
27));
- $this->assertTrue(is_array(json_decode('{"foo": 27}', true)));
- $this->assertEquals(json_decode('{"foo": 27}', true), array("foo" =>
27));
- $this->assertEquals(json_decode('["bar", "baz", "blurfl"]', true),
- array("bar", "baz", "blurfl"));
- $this->assertFalse(is_array(json_decode('null', true)));
- $this->assertEquals(json_decode('{"type": "null"}', true),
array("type" => 'null'));
+ $this->assertEquals(null, json_decode('null', true));
+ $this->assertEquals(32, json_decode('32', true));
+ $this->assertEquals('32', json_decode('"32"', true));
+ $this->assertEquals(["foo" => 27], (array) json_decode('{"foo": 27}'));
+ $this->assertIsArray(json_decode('{"foo": 27}', true));
+ $this->assertEquals(["foo" => 27], json_decode('{"foo": 27}', true));
+ $this->assertEquals(["bar", "baz", "blurfl"],
+ json_decode('["bar", "baz", "blurfl"]', true));
+ $this->assertIsNotArray(json_decode('null', true));
+ $this->assertEquals(["type" => 'null'], json_decode('{"type":
"null"}', true));
// PHP now only accept lowercase true, and rejects TRUE etc.
//
https://php.net/manual/en/migration56.incompatible.php#migration56.incompatible.json-decode
- $this->assertEquals(json_decode('true', true), true, 'true');
+ $this->assertEquals(true, json_decode('true', true), 'true');
- $this->assertEquals(json_decode('"boolean"'), 'boolean');
+ $this->assertEquals('boolean', json_decode('"boolean"'));
}
- public function schema_examples_provider()
+ public function schema_examples_provider(): array
{
self::make_examples();
- $ary = array();
+ $ary = [];
foreach (self::$examples as $example) {
- $ary[] = array($example);
+ $ary[] = [$example];
}
return $ary;
}
- protected static function make_examples()
+ protected static function make_examples(): void
{
- $primitive_examples = array_merge(array(
+ $primitive_examples = array_merge([
new SchemaExample('"True"', false),
new SchemaExample('{"no_type": "test"}', false),
new SchemaExample('{"type": "panther"}', false)
- ),
+ ],
self::make_primitive_examples());
- $array_examples = array(
+ $array_examples = [
new SchemaExample('{"type": "array", "items": "long"}', true),
new SchemaExample('
{"type": "array",
"items": {"type": "enum", "name": "Test", "symbols": ["A", "B"]}}
', true)
- );
+ ];
- $map_examples = array(
+ $map_examples = [
new SchemaExample('{"type": "map", "values": "long"}', true),
new SchemaExample('
{"type": "map",
"values": {"type": "enum", "name": "Test", "symbols": ["A", "B"]}}
', true)
- );
+ ];
- $union_examples = array(
+ $union_examples = [
new SchemaExample('["string", "null", "long"]', true),
new SchemaExample('["null", "null"]', false),
new SchemaExample('["long", "long"]', false),
@@ -151,7 +152,7 @@ class SchemaTest extends TestCase
{"type": "array", "items": "string"}]
', true,
'[{"type":"record","name":"subtract","namespace":"com.example","fields":[{"name":"minuend","type":"int"},{"name":"subtrahend","type":"int"}]},{"type":"record","name":"divide","namespace":"com.example","fields":[{"name":"quotient","type":"int"},{"name":"dividend","type":"int"}]},{"type":"array","items":"string"}]'),
- );
+ ];
$fixed_examples = [
new SchemaExample('{"type": "fixed", "name": "Test", "size": 1}',
true),
@@ -447,7 +448,7 @@ class SchemaTest extends TestCase
protected static function make_primitive_examples()
{
- $examples = array();
+ $examples = [];
foreach ([
'null',
'boolean',
@@ -468,7 +469,7 @@ class SchemaTest extends TestCase
/**
* @dataProvider schema_examples_provider
*/
- function test_parse($example)
+ function test_parse($example): void
{
$schema_string = $example->schema_string;
try {
@@ -486,19 +487,19 @@ class SchemaTest extends TestCase
}
}
- public function testToAvroIncludesAliases()
+ public function testToAvroIncludesAliases(): void
{
$hash = <<<SCHEMA
-{
- "type": "record",
- "name": "test_record",
- "aliases": ["alt_record"],
- "fields": [
- { "name": "f", "type": { "type": "fixed", "size": 2, "name":
"test_fixed", "aliases": ["alt_fixed"] } },
- { "name": "e", "type": { "type": "enum", "symbols": ["A", "B"],
"name": "test_enum", "aliases": ["alt_enum"] } }
- ]
-}
-SCHEMA;
+ {
+ "type": "record",
+ "name": "test_record",
+ "aliases": ["alt_record"],
+ "fields": [
+ { "name": "f", "type": { "type": "fixed", "size": 2,
"name": "test_fixed", "aliases": ["alt_fixed"] } },
+ { "name": "e", "type": { "type": "enum", "symbols":
["A", "B"], "name": "test_enum", "aliases": ["alt_enum"] } }
+ ]
+ }
+ SCHEMA;
$schema = AvroSchema::parse($hash);
$this->assertEquals($schema->toAvro(), json_decode($hash, true));
}
@@ -584,20 +585,164 @@ SCHEMA);
{
$this->expectNotToPerformAssertions();
AvroSchema::parse(<<<SCHEMA
-{
- "type": "record",
- "name": "fruits",
- "fields": [
- {
- "name": "banana",
- "type": "string",
- "aliases": [
- "yellow",
- "yellow"
- ]
- }
- ]
-}
-SCHEMA);
+ {
+ "type": "record",
+ "name": "fruits",
+ "fields": [
+ {
+ "name": "banana",
+ "type": "string",
+ "aliases": [
+ "yellow",
+ "yellow"
+ ]
+ }
+ ]
+ }
+ SCHEMA
+ );
+ }
+
+ public function testLogicalTypesInRecord(): void
+ {
+ $avro = <<<AVRO
+ {
+ "type": "record",
+ "name": "fruits",
+ "fields": [
+ {
+ "name": "UUID",
+ "type": {
+ "type": "string",
+ "logicalType": "uuid"
+ }
+ },
+ {
+ "name": "decimal",
+ "type": {
+ "type": "bytes",
+ "logicalType": "decimal",
+ "precision": 6,
+ "scale": 2
+ }
+ },
+ {
+ "name": "date",
+ "type": {
+ "type": "int",
+ "logicalType": "date"
+ }
+ },
+ {
+ "name": "timeMillis",
+ "type": {
+ "type": "int",
+ "logicalType": "time-millis"
+ }
+ },
+ {
+ "name": "timeMicros",
+ "type": {
+ "type": "long",
+ "logicalType": "time-micros"
+ }
+ },
+ {
+ "name": "timestampMillis",
+ "type": {
+ "type": "long",
+ "logicalType": "timestamp-millis"
+ }
+ },
+ {
+ "name": "timestampMicros",
+ "type": {
+ "type": "long",
+ "logicalType": "timestamp-micros"
+ }
+ },
+ {
+ "name": "localTimestampMillis",
+ "type": {
+ "type": "long",
+ "logicalType": "local-timestamp-millis"
+ }
+ },
+ {
+ "name": "localTimestampMicros",
+ "type": {
+ "type": "long",
+ "logicalType": "local-timestamp-micros"
+ }
+ },
+ {
+ "name": "duration",
+ "type": {
+ "name": "inner_fixed",
+ "type": "fixed",
+ "size": 12,
+ "logicalType": "duration"
+ }
+ }
+ ]
+ }
+ AVRO;
+
+
+ $schema = AvroSchema::parse($avro);
+
+ self::assertEquals($schema->toAvro(), json_decode($avro, true));
+ }
+
+ public static function invalidDecimalLogicalTypeDataProvider(): array
+ {
+ return [
+ 'bytes - invalid precision' => [
+ '{"type": "bytes", "logicalType": "decimal", "precision": -1,
"scale": 2}',
+ new AvroException("Precision '-1' is invalid. It must be a
positive integer."),
+ ],
+ 'bytes - invalid value for precision (float)' => [
+ '{"type": "bytes", "logicalType": "decimal", "precision":
11.23, "scale": 2}',
+ new AvroException("Invalid value '11.23' for 'precision'
attribute of decimal logical type."),
+ ],
+ 'bytes - invalid value for precision (string)' => [
+ '{"type": "bytes", "logicalType": "decimal", "precision":
"banana", "scale": 2}',
+ new AvroException("Invalid value 'banana' for 'precision'
attribute of decimal logical type."),
+ ],
+ 'bytes - invalid scale' => [
+ '{"type": "bytes", "logicalType": "decimal", "precision": 2,
"scale": -1}',
+ new AvroException("Scale '-1' is invalid. It must be a
non-negative integer."),
+ ],
+ 'bytes - invalid scale for precision' => [
+ '{"type": "bytes", "logicalType": "decimal", "precision": 2,
"scale": 2}',
+ new AvroException("Scale must be a lower than precision
(scale='2', precision='2')."),
+ ],
+ 'bytes - invalid value for scale (float)' => [
+ '{"type": "bytes", "logicalType": "decimal", "precision": 2,
"scale": 9.12}',
+ new AvroException("Invalid value '9.12' for 'scale' attribute
of decimal logical type."),
+ ],
+ 'bytes - invalid value for scale (string)' => [
+ '{"type": "bytes", "logicalType": "decimal", "precision": 2,
"scale": "two"}',
+ new AvroException("Invalid value 'two' for 'scale' attribute
of decimal logical type."),
+ ],
+ 'fixed - invalid precision' => [
+ '{"name": "fixed_decimal", "type": "fixed", "logicalType":
"decimal", "size": 2, "precision": -1, "scale": 2}',
+ new AvroException("Precision '-1' is invalid. It must be a
positive integer."),
+ ],
+ 'fixed - invalid value for precision with specified size' => [
+ '{"name": "fixed_decimal", "type": "fixed", "logicalType":
"decimal", "size": 2, "precision": 6, "scale": 2}',
+ new AvroException("Invalid precision for specified fixed size
(size='2', precision='6')."),
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider invalidDecimalLogicalTypeDataProvider
+ */
+ public function testDecimalLogicalTypeWithInvalidParameters(string
$schema, AvroException $exception): void
+ {
+ $this->expectException(get_class($exception));
+ $this->expectExceptionMessage($exception->getMessage());
+ AvroSchema::parse($schema);
}
}