From 235334a1f29143c39c745429cffbfabeecf44bf3 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Tue, 22 Apr 2025 12:07:05 +0700 Subject: [PATCH 1/3] Use `DateTimeColumn` --- src/Column/ColumnDefinitionBuilder.php | 10 +-- src/Column/ColumnFactory.php | 17 ++--- tests/ColumnTest.php | 92 ++++++++++++------------ tests/Provider/ColumnFactoryProvider.php | 21 +++--- tests/Provider/ColumnProvider.php | 27 +++++++ tests/Provider/QueryBuilderProvider.php | 3 + tests/Provider/SchemaProvider.php | 26 +++---- tests/Support/Fixture/pgsql.sql | 4 +- 8 files changed, 120 insertions(+), 80 deletions(-) diff --git a/src/Column/ColumnDefinitionBuilder.php b/src/Column/ColumnDefinitionBuilder.php index 91751292..43df8c57 100644 --- a/src/Column/ColumnDefinitionBuilder.php +++ b/src/Column/ColumnDefinitionBuilder.php @@ -25,10 +25,10 @@ final class ColumnDefinitionBuilder extends AbstractColumnDefinitionBuilder 'bpchar', 'character varying', 'varchar', - 'time', - 'timetz', 'timestamp', 'timestamptz', + 'time', + 'timetz', 'interval', ]; @@ -97,10 +97,12 @@ protected function getDbType(ColumnInterface $column): string ColumnType::TEXT => 'text', ColumnType::BINARY => 'bytea', ColumnType::UUID => 'uuid', - ColumnType::DATETIME => 'timestamp', ColumnType::TIMESTAMP => 'timestamp', - ColumnType::DATE => 'date', + ColumnType::DATETIME => 'timestamp', + ColumnType::DATETIMETZ => 'timestamptz', ColumnType::TIME => 'time', + ColumnType::TIMETZ => 'timetz', + ColumnType::DATE => 'date', ColumnType::STRUCTURED => 'jsonb', ColumnType::JSON => 'jsonb', default => 'varchar', diff --git a/src/Column/ColumnFactory.php b/src/Column/ColumnFactory.php index e74dd669..3cb92818 100644 --- a/src/Column/ColumnFactory.php +++ b/src/Column/ColumnFactory.php @@ -24,6 +24,7 @@ * columns?: array, * comment?: string|null, * computed?: bool|string, + * dbTimezone?: string, * db_type?: string|null, * default_value?: mixed, * dimension?: int|string, @@ -88,16 +89,16 @@ final class ColumnFactory extends AbstractColumnFactory 'varchar' => ColumnType::STRING, 'text' => ColumnType::TEXT, 'bytea' => ColumnType::BINARY, - 'date' => ColumnType::DATE, + 'abstime' => ColumnType::DATETIME, + 'timestamp' => ColumnType::DATETIME, + 'timestamp without time zone' => ColumnType::DATETIME, + 'timestamp with time zone' => ColumnType::DATETIMETZ, + 'timestamptz' => ColumnType::DATETIMETZ, 'time' => ColumnType::TIME, 'time without time zone' => ColumnType::TIME, - 'time with time zone' => ColumnType::TIME, - 'timetz' => ColumnType::TIME, - 'timestamp' => ColumnType::TIMESTAMP, - 'timestamp without time zone' => ColumnType::TIMESTAMP, - 'timestamp with time zone' => ColumnType::TIMESTAMP, - 'timestamptz' => ColumnType::TIMESTAMP, - 'abstime' => ColumnType::TIMESTAMP, + 'time with time zone' => ColumnType::TIMETZ, + 'timetz' => ColumnType::TIMETZ, + 'date' => ColumnType::DATE, 'interval' => ColumnType::STRING, 'box' => ColumnType::STRING, 'circle' => ColumnType::STRING, diff --git a/tests/ColumnTest.php b/tests/ColumnTest.php index 15cc2508..81ff75b0 100644 --- a/tests/ColumnTest.php +++ b/tests/ColumnTest.php @@ -4,6 +4,9 @@ namespace Yiisoft\Db\Pgsql\Tests; +use DateTimeImmutable; +use DateTimeZone; +use PHPUnit\Framework\Attributes\DataProviderExternal; use Throwable; use Yiisoft\Db\Constant\ColumnType; use Yiisoft\Db\Exception\Exception; @@ -20,14 +23,17 @@ use Yiisoft\Db\Pgsql\Column\IntegerColumn; use Yiisoft\Db\Pgsql\Column\StructuredColumn; use Yiisoft\Db\Pgsql\Connection; +use Yiisoft\Db\Pgsql\Tests\Provider\ColumnProvider; use Yiisoft\Db\Pgsql\Tests\Support\TestTrait; use Yiisoft\Db\Query\Query; use Yiisoft\Db\Schema\Column\ColumnInterface; use Yiisoft\Db\Schema\Column\DoubleColumn; use Yiisoft\Db\Schema\Column\JsonColumn; use Yiisoft\Db\Schema\Column\StringColumn; -use Yiisoft\Db\Tests\AbstractColumnTest; +use Yiisoft\Db\Tests\Common\CommonColumnTest; +use Yiisoft\Db\Tests\Support\Assert; +use function str_repeat; use function stream_get_contents; /** @@ -35,10 +41,12 @@ * * @psalm-suppress PropertyNotSetInConstructor */ -final class ColumnTest extends AbstractColumnTest +final class ColumnTest extends CommonColumnTest { use TestTrait; + protected const COLUMN_BUILDER = ColumnBuilder::class; + private function insertTypeValues(Connection $db): void { $db->createCommand()->insert( @@ -49,6 +57,8 @@ private function insertTypeValues(Connection $db): void 'char_col3' => null, 'float_col' => 1.234, 'blob_col' => "\x10\x11\x12", + 'timestamp_col' => '2023-07-11 14:50:23', + 'timestamp_default' => new DateTimeImmutable('2023-07-11 14:50:23'), 'bool_col' => false, 'bit_col' => 0b0110_0100, // 100 'varbit_col' => 0b1_1100_1000, // 456 @@ -64,12 +74,14 @@ private function insertTypeValues(Connection $db): void )->execute(); } - private function assertResultValues(array $result): void + private function assertTypecastedValues(array $result): void { $this->assertSame(1, $result['int_col']); $this->assertSame(str_repeat('x', 100), $result['char_col']); $this->assertSame(1.234, $result['float_col']); $this->assertSame("\x10\x11\x12", stream_get_contents($result['blob_col'])); + $this->assertEquals(new DateTimeImmutable('2023-07-11 14:50:23', new DateTimeZone('UTC')), $result['timestamp_col']); + $this->assertEquals(new DateTimeImmutable('2023-07-11 14:50:23'), $result['timestamp_default']); $this->assertFalse($result['bool_col']); $this->assertSame(0b0110_0100, $result['bit_col']); $this->assertSame(0b1_1100_1000, $result['varbit_col']); @@ -93,11 +105,11 @@ public function testQueryWithTypecasting(): void $result = $query->one(); - $this->assertResultValues($result); + $this->assertTypecastedValues($result); $result = $query->all(); - $this->assertResultValues($result[0]); + $this->assertTypecastedValues($result[0]); $db->close(); } @@ -112,11 +124,11 @@ public function testCommandWithPhpTypecasting(): void $result = $command->queryOne(); - $this->assertResultValues($result); + $this->assertTypecastedValues($result); $result = $command->queryAll(); - $this->assertResultValues($result[0]); + $this->assertTypecastedValues($result[0]); $db->close(); } @@ -190,43 +202,19 @@ public function testPhpTypeCast(): void { $db = $this->getConnection(true); $schema = $db->getSchema(); - $tableSchema = $schema->getTableSchema('type'); + $columns = $schema->getTableSchema('type')->getColumns(); $this->insertTypeValues($db); $query = (new Query($db))->from('type')->one(); - $intColPhpTypeCast = $tableSchema->getColumn('int_col')?->phpTypecast($query['int_col']); - $charColPhpTypeCast = $tableSchema->getColumn('char_col')?->phpTypecast($query['char_col']); - $floatColPhpTypeCast = $tableSchema->getColumn('float_col')?->phpTypecast($query['float_col']); - $blobColPhpTypeCast = $tableSchema->getColumn('blob_col')?->phpTypecast($query['blob_col']); - $boolColPhpTypeCast = $tableSchema->getColumn('bool_col')?->phpTypecast($query['bool_col']); - $bitColPhpTypeCast = $tableSchema->getColumn('bit_col')?->phpTypecast($query['bit_col']); - $varbitColPhpTypeCast = $tableSchema->getColumn('varbit_col')?->phpTypecast($query['varbit_col']); - $numericColPhpTypeCast = $tableSchema->getColumn('numeric_col')?->phpTypecast($query['numeric_col']); - $intArrayColPhpType = $tableSchema->getColumn('intarray_col')?->phpTypecast($query['intarray_col']); - $numericArrayColPhpTypeCast = $tableSchema->getColumn('numericarray_col')?->phpTypecast($query['numericarray_col']); - $varcharArrayColPhpTypeCast = $tableSchema->getColumn('varchararray_col')?->phpTypecast($query['varchararray_col']); - $textArray2ColPhpType = $tableSchema->getColumn('textarray2_col')?->phpTypecast($query['textarray2_col']); - $jsonColPhpType = $tableSchema->getColumn('json_col')?->phpTypecast($query['json_col']); - $jsonBColPhpType = $tableSchema->getColumn('jsonb_col')?->phpTypecast($query['jsonb_col']); - $jsonArrayColPhpType = $tableSchema->getColumn('jsonarray_col')?->phpTypecast($query['jsonarray_col']); - - $this->assertSame(1, $intColPhpTypeCast); - $this->assertSame(str_repeat('x', 100), $charColPhpTypeCast); - $this->assertSame(1.234, $floatColPhpTypeCast); - $this->assertSame("\x10\x11\x12", stream_get_contents($blobColPhpTypeCast)); - $this->assertFalse($boolColPhpTypeCast); - $this->assertSame(0b0110_0100, $bitColPhpTypeCast); - $this->assertSame(0b1_1100_1000, $varbitColPhpTypeCast); - $this->assertSame(33.22, $numericColPhpTypeCast); - $this->assertSame([1, -2, null, 42], $intArrayColPhpType); - $this->assertSame([null, 1.2, -2.2, null, null], $numericArrayColPhpTypeCast); - $this->assertSame(['', 'some text', '""', '\\\\', '[",","null",true,"false","f"]', null], $varcharArrayColPhpTypeCast); - $this->assertNull($textArray2ColPhpType); - $this->assertSame([['a' => 1, 'b' => null, 'c' => [1, 3, 5]]], $jsonColPhpType); - $this->assertSame(['1', '2', '3'], $jsonBColPhpType); - $this->assertSame([[[',', 'null', true, 'false', 'f']]], $jsonArrayColPhpType); + $result = []; + + foreach ($columns as $columnName => $column) { + $result[$columnName] = $column->phpTypecast($query[$columnName]); + } + + $this->assertTypecastedValues($result); $db->close(); } @@ -430,32 +418,48 @@ public function testColumnInstance() $db->close(); } - /** @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\ColumnProvider::predefinedTypes */ + #[DataProviderExternal(ColumnProvider::class, 'predefinedTypes')] public function testPredefinedType(string $className, string $type, string $phpType) { parent::testPredefinedType($className, $type, $phpType); } - /** @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\ColumnProvider::dbTypecastColumns */ + #[DataProviderExternal(ColumnProvider::class, 'dbTypecastColumns')] public function testDbTypecastColumns(ColumnInterface $column, array $values) { parent::testDbTypecastColumns($column, $values); } - /** @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\ColumnProvider::phpTypecastColumns */ + #[DataProviderExternal(ColumnProvider::class, 'phpTypecastColumns')] public function testPhpTypecastColumns(ColumnInterface $column, array $values) { parent::testPhpTypecastColumns($column, $values); } - /** @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\ColumnProvider::phpTypecastArrayColumns */ + #[DataProviderExternal(ColumnProvider::class, 'dbTypecastArrayColumns')] + public function testArrayColumnDbTypecast(ColumnInterface $column, array $values): void + { + $arrayCol = (new ArrayColumn())->column($column); + + foreach ($values as [$dimension, $expected, $value]) { + $arrayCol->dimension($dimension); + $dbValue = $arrayCol->dbTypecast($value); + + $this->assertInstanceOf(ArrayExpression::class, $dbValue); + $this->assertSame($arrayCol, $dbValue->getType()); + $this->assertEquals($value, $dbValue->getValue()); + } + } + + #[DataProviderExternal(ColumnProvider::class, 'phpTypecastArrayColumns')] public function testPhpTypecastArrayColumn(ColumnInterface $column, array $values): void { $arrayCol = ColumnBuilder::array($column); foreach ($values as [$dimension, $expected, $value]) { $arrayCol->dimension($dimension); - $this->assertSame($expected, $arrayCol->phpTypecast($value)); + + Assert::arraysEquals($expected, $arrayCol->phpTypecast($value)); } } diff --git a/tests/Provider/ColumnFactoryProvider.php b/tests/Provider/ColumnFactoryProvider.php index 8a0516ca..58243c40 100644 --- a/tests/Provider/ColumnFactoryProvider.php +++ b/tests/Provider/ColumnFactoryProvider.php @@ -12,6 +12,7 @@ use Yiisoft\Db\Pgsql\Column\BooleanColumn; use Yiisoft\Db\Pgsql\Column\IntegerColumn; use Yiisoft\Db\Pgsql\Column\StructuredColumn; +use Yiisoft\Db\Schema\Column\DateTimeColumn; use Yiisoft\Db\Schema\Column\DoubleColumn; use Yiisoft\Db\Schema\Column\JsonColumn; use Yiisoft\Db\Schema\Column\StringColumn; @@ -55,16 +56,16 @@ public static function dbTypes(): array ['varchar', ColumnType::STRING, StringColumn::class], ['text', ColumnType::TEXT, StringColumn::class], ['bytea', ColumnType::BINARY, BinaryColumn::class], - ['date', ColumnType::DATE, StringColumn::class], - ['time', ColumnType::TIME, StringColumn::class], - ['time without time zone', ColumnType::TIME, StringColumn::class], - ['time with time zone', ColumnType::TIME, StringColumn::class], - ['timetz', ColumnType::TIME, StringColumn::class], - ['timestamp', ColumnType::TIMESTAMP, StringColumn::class], - ['timestamp without time zone', ColumnType::TIMESTAMP, StringColumn::class], - ['timestamp with time zone', ColumnType::TIMESTAMP, StringColumn::class], - ['timestamptz', ColumnType::TIMESTAMP, StringColumn::class], - ['abstime', ColumnType::TIMESTAMP, StringColumn::class], + ['abstime', ColumnType::DATETIME, DateTimeColumn::class], + ['timestamp', ColumnType::DATETIME, DateTimeColumn::class], + ['timestamp without time zone', ColumnType::DATETIME, DateTimeColumn::class], + ['timestamp with time zone', ColumnType::DATETIMETZ, DateTimeColumn::class], + ['timestamptz', ColumnType::DATETIMETZ, DateTimeColumn::class], + ['time', ColumnType::TIME, DateTimeColumn::class], + ['time without time zone', ColumnType::TIME, DateTimeColumn::class], + ['time with time zone', ColumnType::TIMETZ, DateTimeColumn::class], + ['timetz', ColumnType::TIMETZ, DateTimeColumn::class], + ['date', ColumnType::DATE, DateTimeColumn::class], ['interval', ColumnType::STRING, StringColumn::class], ['box', ColumnType::STRING, StringColumn::class], ['circle', ColumnType::STRING, StringColumn::class], diff --git a/tests/Provider/ColumnProvider.php b/tests/Provider/ColumnProvider.php index 348b6911..bdae4a0e 100644 --- a/tests/Provider/ColumnProvider.php +++ b/tests/Provider/ColumnProvider.php @@ -4,6 +4,8 @@ namespace Yiisoft\Db\Pgsql\Tests\Provider; +use DateTimeImmutable; +use DateTimeZone; use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Pgsql\Column\ArrayColumn; use Yiisoft\Db\Pgsql\Column\ArrayLazyColumn; @@ -16,6 +18,7 @@ use Yiisoft\Db\Pgsql\Column\StructuredLazyColumn; use Yiisoft\Db\Pgsql\Data\LazyArray; use Yiisoft\Db\Pgsql\Data\StructuredLazyArray; +use Yiisoft\Db\Schema\Column\DateTimeColumn; use Yiisoft\Db\Schema\Column\DoubleColumn; use Yiisoft\Db\Schema\Column\JsonColumn; use Yiisoft\Db\Schema\Column\StringColumn; @@ -125,6 +128,8 @@ public static function phpTypecastColumns(): array public static function phpTypecastArrayColumns() { + $utcTimezone = new DateTimeZone('UTC'); + return [ // [column, values] [ @@ -170,6 +175,28 @@ public static function phpTypecastArrayColumns() [2, [["\x10\x11"], ['', null]], '{{\x1011},{"",}}'], ], ], + [ + new DateTimeColumn(), + [ + [ + 1, + [ + new DateTimeImmutable('2025-04-19 14:11:35', $utcTimezone), + new DateTimeImmutable('2025-04-19 00:00:00', $utcTimezone), + null, + ], + '{2025-04-19 14:11:35,2025-04-19 00:00:00,}', + ], + [ + 2, + [ + [new DateTimeImmutable('2025-04-19 14:11:35', $utcTimezone)], + [new DateTimeImmutable('2025-04-19 00:00:00', $utcTimezone), null], + ], + '{{2025-04-19 14:11:35},{2025-04-19 00:00:00,}}', + ], + ], + ], [ new JsonColumn(), [ diff --git a/tests/Provider/QueryBuilderProvider.php b/tests/Provider/QueryBuilderProvider.php index a8b392df..f14fbe6b 100644 --- a/tests/Provider/QueryBuilderProvider.php +++ b/tests/Provider/QueryBuilderProvider.php @@ -374,6 +374,9 @@ public static function buildColumnDefinition(): array $values['datetime()'][0] = 'timestamp(0)'; $values['datetime(6)'][0] = 'timestamp(6)'; $values['datetime(null)'][0] = 'timestamp'; + $values['datetimeWithTimezone()'][0] = 'timestamptz(0)'; + $values['datetimeWithTimezone(6)'][0] = 'timestamptz(6)'; + $values['datetimeWithTimezone(null)'][0] = 'timestamptz'; $values['array()'][0] = 'varchar[]'; $values['structured()'][0] = 'jsonb'; $values['json()'][0] = 'jsonb'; diff --git a/tests/Provider/SchemaProvider.php b/tests/Provider/SchemaProvider.php index b538045d..d929e4fc 100644 --- a/tests/Provider/SchemaProvider.php +++ b/tests/Provider/SchemaProvider.php @@ -4,6 +4,8 @@ namespace Yiisoft\Db\Pgsql\Tests\Provider; +use DateTimeImmutable; +use DateTimeZone; use Yiisoft\Db\Constant\ColumnType; use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Pgsql\Column\ArrayColumn; @@ -11,6 +13,7 @@ use Yiisoft\Db\Pgsql\Column\BitColumn; use Yiisoft\Db\Pgsql\Column\BooleanColumn; use Yiisoft\Db\Pgsql\Column\IntegerColumn; +use Yiisoft\Db\Schema\Column\DateTimeColumn; use Yiisoft\Db\Schema\Column\DoubleColumn; use Yiisoft\Db\Schema\Column\JsonColumn; use Yiisoft\Db\Schema\Column\StringColumn; @@ -82,12 +85,18 @@ public static function columns(): array scale: 2, defaultValue: 33.22, ), - 'time' => new StringColumn( - ColumnType::TIMESTAMP, + 'timestamp_col' => new DateTimeColumn( dbType: 'timestamp', notNull: true, size: 6, - defaultValue: '2002-01-01 00:00:00', + defaultValue: new DateTimeImmutable('2002-01-01 00:00:00', new DateTimeZone('UTC')), + shouldConvertTimezone: true, + ), + 'timestamp_default' => new DateTimeColumn( + dbType: 'timestamp', + notNull: true, + size: 6, + defaultValue: new Expression('now()'), ), 'bool_col' => new BooleanColumn( dbType: 'bool', @@ -97,13 +106,6 @@ public static function columns(): array dbType: 'bool', defaultValue: true, ), - 'ts_default' => new StringColumn( - ColumnType::TIMESTAMP, - dbType: 'timestamp', - notNull: true, - size: 6, - defaultValue: new Expression('now()'), - ), 'bit_col' => new BitColumn( dbType: 'bit', notNull: true, @@ -362,7 +364,7 @@ public static function resultColumns(): array 'len' => -1, 'precision' => 327686, ]], - [new StringColumn(ColumnType::TIMESTAMP, dbType: 'timestamp', name: 'time'), [ + [new DateTimeColumn(dbType: 'timestamp', name: 'time'), [ 'pgsql:oid' => 1114, 'pgsql:table_oid' => 40133105, 'table' => 'type', @@ -448,7 +450,7 @@ public static function resultColumns(): array 'len' => 1, 'precision' => -1, ]], - [new StringColumn(ColumnType::TIMESTAMP, dbType: 'timestamptz', name: 'timestamp(3)', size: 3), [ + [new DateTimeColumn(ColumnType::DATETIMETZ, dbType: 'timestamptz', name: 'timestamp(3)', size: 3), [ 'pgsql:oid' => 1184, 'pgsql:table_oid' => 0, 'native_type' => 'timestamptz', diff --git a/tests/Support/Fixture/pgsql.sql b/tests/Support/Fixture/pgsql.sql index b33d745b..ebf64d18 100644 --- a/tests/Support/Fixture/pgsql.sql +++ b/tests/Support/Fixture/pgsql.sql @@ -153,10 +153,10 @@ CREATE TABLE "type" ( float_col2 double precision DEFAULT '1.23', blob_col bytea DEFAULT 'a binary value', numeric_col decimal(5,2) DEFAULT '33.22', - time timestamp NOT NULL DEFAULT '2002-01-01 00:00:00', + timestamp_col timestamp NOT NULL DEFAULT '2002-01-01 00:00:00', + timestamp_default TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, bool_col boolean NOT NULL, bool_col2 boolean DEFAULT TRUE, - ts_default TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, bit_col BIT(8) NOT NULL DEFAULT B'10000010', -- 130 varbit_col VARBIT NOT NULL DEFAULT '100'::bit, -- 4 bigint_col BIGINT, From 823dd188da1e3475be85fa61ce595cdf7c841742 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Sun, 4 May 2025 15:18:15 +0700 Subject: [PATCH 2/3] Fix test --- tests/SchemaTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/SchemaTest.php b/tests/SchemaTest.php index 84635873..2decc535 100644 --- a/tests/SchemaTest.php +++ b/tests/SchemaTest.php @@ -68,7 +68,7 @@ public function testColumns(array $columns, string $tableName): void if (version_compare($db->getServerInfo()->getVersion(), '10', '>')) { if ($tableName === 'type') { - $columns['ts_default']->defaultValue(new Expression('CURRENT_TIMESTAMP')); + $columns['timestamp_default']->defaultValue(new Expression('CURRENT_TIMESTAMP')); } } From 5308692becfc93f2d0cd582f5ca5e201b2b7e985 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Tue, 13 May 2025 17:10:13 +0700 Subject: [PATCH 3/3] Add line to CHANGELOG.md [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fef951e3..6985c0be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ - Enh #396: Remove `getCacheKey()` and `getCacheTag()` methods from `Schema` class (@Tigrov) - Enh #403, #404: Use `DbArrayHelper::arrange()` instead of `DbArrayHelper::index()` method (@Tigrov) - New #397: Realize `Schema::loadResultColumn()` method (@Tigrov) +- New #407: Use `DateTimeColumn` class for datetime column types (@Tigrov) ## 1.3.0 March 21, 2024