Page MenuHomeDevCentral

No OneTemporary

diff --git a/src/Collections/BaseCollection.php b/src/Collections/BaseCollection.php
index 594652c..25b2de4 100644
--- a/src/Collections/BaseCollection.php
+++ b/src/Collections/BaseCollection.php
@@ -1,28 +1,28 @@
<?php
declare(strict_types=1);
namespace Keruald\OmniTools\Collections;
-interface BaseCollection {
+abstract class BaseCollection {
///
/// Constructors
///
- public static function from (iterable $items) : static;
+ public static abstract function from (iterable $items) : static;
///
/// Getters
///
- public function toArray () : array;
+ public abstract function toArray () : array;
///
/// Properties
///
- public function count () : int;
+ public abstract function count () : int;
- public function isEmpty () : bool;
+ public abstract function isEmpty () : bool;
}
diff --git a/src/Collections/BaseMap.php b/src/Collections/BaseMap.php
index a588c71..d0d4df5 100644
--- a/src/Collections/BaseMap.php
+++ b/src/Collections/BaseMap.php
@@ -1,17 +1,46 @@
<?php
namespace Keruald\OmniTools\Collections;
-interface BaseMap {
+use ArrayAccess;
- public function get (mixed $key) : mixed;
+abstract class BaseMap extends BaseCollection implements ArrayAccess {
- public function getOr (mixed $key, mixed $defaultValue): mixed;
+ ///
+ /// Methods to implement
+ ///
- public function set (mixed $key, mixed $value) : static;
+ public abstract function get (mixed $key) : mixed;
- public function has (mixed $key) : bool;
+ public abstract function getOr (mixed $key, mixed $defaultValue): mixed;
- public function contains (mixed $value) : bool;
+ public abstract function set (mixed $key, mixed $value) : static;
+
+ public abstract function unset (mixed $key) : static;
+
+ public abstract function has (mixed $key) : bool;
+
+ public abstract function contains (mixed $value) : bool;
+
+ ///
+ /// ArrayAccess
+ /// Interface to provide accessing objects as arrays.
+ ///
+
+ public function offsetExists (mixed $offset) : bool {
+ return $this->has($offset);
+ }
+
+ public function offsetGet (mixed $offset) : mixed {
+ return $this->get($offset);
+ }
+
+ public function offsetSet (mixed $offset, mixed $value) : void {
+ $this->set($offset, $value);
+ }
+
+ public function offsetUnset (mixed $offset) : void {
+ $this->unset($offset);
+ }
}
diff --git a/src/Collections/HashMap.php b/src/Collections/HashMap.php
index 0a06b9e..2a1d0d9 100644
--- a/src/Collections/HashMap.php
+++ b/src/Collections/HashMap.php
@@ -1,171 +1,181 @@
<?php
declare(strict_types=1);
namespace Keruald\OmniTools\Collections;
use Keruald\OmniTools\Reflection\CallableElement;
use InvalidArgumentException;
/**
* An associative array allowing the use of chained
*
*
* This class can be used as a service container,
* an application context, to store configuration.
*/
-class HashMap implements BaseCollection, BaseMap {
+class HashMap extends BaseMap {
///
/// Properties
///
private array $map;
///
/// Constructor
///
public function __construct (iterable $iterable = []) {
if (is_array($iterable)) {
$this->map = (array)$iterable;
return;
}
foreach ($iterable as $key => $value) {
$this->map[$key] = $value;
}
}
public static function from (iterable $items) : static {
return new self($items);
}
///
/// Interact with map content at key level
///
public function get (mixed $key) : mixed {
if (!array_key_exists($key, $this->map)) {
throw new InvalidArgumentException("Key not found.");
}
return $this->map[$key];
}
public function getOr (mixed $key, mixed $defaultValue) : mixed {
return $this->map[$key] ?? $defaultValue;
}
public function set (mixed $key, mixed $value) : static {
+ if ($key === null) {
+ throw new InvalidArgumentException("Key can't be null");
+ }
+
$this->map[$key] = $value;
return $this;
}
+ public function unset (mixed $key) : static {
+ unset($this->map[$key]);
+
+ return $this;
+ }
+
public function has (mixed $key) : bool {
return array_key_exists($key, $this->map);
}
public function contains (mixed $value) : bool {
return in_array($value, $this->map);
}
///
/// Interact with collection content at collection level
///
public function count () : int {
return count($this->map);
}
public function isEmpty () : bool {
return $this->count() === 0;
}
public function clear () : self {
$this->map = [];
return $this;
}
/**
* Merge the specified map with the current map.
*
* If a key already exists, the value already set is kept.
*
* @see update() when you need to update with the new value.
*/
public function merge (iterable $iterable) : self {
foreach ($iterable as $key => $value) {
$this->map[$key] ??= $value;
}
return $this;
}
/**
* Merge the specified map with the current bag.
*
* If a key already exists, the value is updated with the new one.
*
* @see merge() when you need to keep old value.
*/
public function update (iterable $iterable) : self {
foreach ($iterable as $key => $value) {
$this->map[$key] = $value;
}
return $this;
}
/**
* Gets a copy of the internal map.
*
* Scalar values (int, strings) are cloned.
* Objects are references to a specific objet, not a clone.
*
* @return array<string, mixed>
*/
public function toArray () : array {
return $this->map;
}
///
/// HOF
///
public function map (callable $callable) : self {
return new self(array_map($callable, $this->map));
}
public function filter (callable $callable) : self {
$argc = (new CallableElement($callable))->countArguments();
if ($argc === 0) {
throw new InvalidArgumentException(
"Callback should have at least one argument"
);
}
$mode = (int)($argc > 1);
return new self(
array_filter($this->map, $callable, $mode)
);
}
public function mapKeys (callable $callable) : self {
$mappedMap = [];
foreach ($this->map as $key => $value) {
$mappedMap[$callable($key)] = $value;
}
return new self($mappedMap);
}
public function filterKeys (callable $callable) : self {
return new self(
array_filter($this->map, $callable, ARRAY_FILTER_USE_KEY)
);
}
}
diff --git a/src/Collections/Vector.php b/src/Collections/Vector.php
index c8c3733..9c9724c 100644
--- a/src/Collections/Vector.php
+++ b/src/Collections/Vector.php
@@ -1,210 +1,266 @@
<?php
declare(strict_types=1);
namespace Keruald\OmniTools\Collections;
use Keruald\OmniTools\Reflection\CallableElement;
use Keruald\OmniTools\Strings\Multibyte\OmniString;
+use ArrayAccess;
use InvalidArgumentException;
-class Vector implements BaseCollection {
+class Vector extends BaseCollection implements ArrayAccess {
///
/// Properties
///
private array $items;
///
/// Constructors
///
public function __construct (iterable $items = []) {
if (is_array($items)) {
$this->items = $items;
return;
}
foreach ($items as $item) {
$this->items[] = $item;
}
}
public static function from (iterable $items) : static {
return new self($items);
}
///
/// Specialized constructors
///
/**
* Constructs a new instance of a vector by exploding a string
* according a specified delimiter.
*
* @param string $delimiter The substring to find for explosion
* @param string $string The string to explode
* @param int $limit If specified, the maximum count of vector elements
* @return static
*/
public static function explode (string $delimiter, string $string,
int $limit = PHP_INT_MAX) : self {
// There is some discussion to know if this method belongs
// to Vector or OmniString.
//
// The advantage to keep it here is we can have constructs like:
// Vector::explode(",", "1,1,2,3,5,8,13")
// ->toIntegers()
// >map(function($n) { return $n * $n; })
// ->toArray();
//
// In this chaining, it is clear we manipulate Vector methods.
return (new OmniString($string))
->explode($delimiter, $limit);
}
///
/// Interact with collection content at key level
///
public function get (int $key) : mixed {
if (!array_key_exists($key, $this->items)) {
throw new InvalidArgumentException("Key not found.");
}
return $this->items[$key];
}
public function getOr (int $key, mixed $defaultValue) : mixed {
return $this->items[$key] ?? $defaultValue;
}
public function set (int $key, mixed $value) : static {
$this->items[$key] = $value;
return $this;
}
+ public function unset (int $key) : static {
+ unset($this->items[$key]);
+
+ return $this;
+ }
+
public function contains (mixed $value) : bool {
return in_array($value, $this->items);
}
-
///
/// Interact with collection content at collection level
///
public function count () : int {
return count($this->items);
}
public function isEmpty () : bool {
return $this->count() === 0;
}
public function clear () : self {
$this->items = [];
return $this;
}
+ public function push (mixed $item) : self {
+ $this->items[] = $item;
+
+ return $this;
+ }
+
/**
* Append all elements of the specified iterable
* to the current vector.
*
* If a value already exists, the value is still added
* as a duplicate.
*
* @see update() when you need to only add unique values.
*/
public function append (iterable $iterable) : self {
foreach ($iterable as $value) {
$this->items[] = $value;
}
return $this;
}
/**
* Append all elements of the specified iterable
* to the current vector.
*
* If a value already exists, it is skipped.
*
* @see append() when you need to always add everything.
*/
public function update (iterable $iterable) : self {
foreach ($iterable as $value) {
if (!$this->contains($value)) {
$this->items[] = $value;
}
}
return $this;
}
/**
* Gets a copy of the internal vector.
*
* Scalar values (int, strings) are cloned.
* Objects are references to a specific objet, not a clone.
*
* @return array
*/
public function toArray () : array {
return $this->items;
}
///
/// HOF :: generic
///
public function map (callable $callable) : self {
return new self(array_map($callable, $this->items));
}
public function filter (callable $callable) : self {
$argc = (new CallableElement($callable))->countArguments();
if ($argc === 0) {
throw new InvalidArgumentException(
"Callback should have at least one argument"
);
}
$mode = (int)($argc > 1);
return new self(array_filter($this->items, $callable, $mode));
}
public function mapKeys (callable $callable) : self {
$mappedVector = [];
foreach ($this->items as $key => $value) {
$mappedVector[$callable($key)] = $value;
}
return new self($mappedVector);
}
public function filterKeys (callable $callable) : self {
return new self(
array_filter($this->items, $callable, ARRAY_FILTER_USE_KEY)
);
}
///
/// HOF :: specialized
///
public function toIntegers () : self {
array_walk($this->items, ArrayUtilities::toIntegerCallback());
return $this;
}
public function implode(string $delimiter) : OmniString {
return new OmniString(implode($delimiter, $this->items));
}
+ ///
+ /// ArrayAccess
+ /// Interface to provide accessing objects as arrays.
+ ///
+
+ private static function ensureOffsetIsInteger (mixed $offset) {
+ if (is_int($offset)) {
+ return;
+ }
+
+ throw new InvalidArgumentException(
+ "Offset of a vector must be an integer."
+ );
+ }
+
+ public function offsetExists (mixed $offset) : bool {
+ self::ensureOffsetIsInteger($offset);
+
+ return array_key_exists($offset, $this->items);
+ }
+
+ public function offsetGet (mixed $offset) : mixed {
+ self::ensureOffsetIsInteger($offset);
+
+ return $this->get($offset);
+ }
+
+ public function offsetSet (mixed $offset, mixed $value) : void {
+ if ($offset === null) {
+ $this->push($value);
+ return;
+ }
+
+ self::ensureOffsetIsInteger($offset);
+
+ $this->set($offset, $value);
+ }
+
+ public function offsetUnset (mixed $offset) : void {
+ self::ensureOffsetIsInteger($offset);
+
+ $this->unset($offset);
+ }
+
}
diff --git a/tests/Collections/HashMapTest.php b/tests/Collections/HashMapTest.php
index d815ad4..b2f9d35 100644
--- a/tests/Collections/HashMapTest.php
+++ b/tests/Collections/HashMapTest.php
@@ -1,279 +1,321 @@
<?php
namespace Keruald\OmniTools\Tests\Collections;
use Keruald\OmniTools\Collections\HashMap;
use PHPUnit\Framework\TestCase;
use InvalidArgumentException;
use IteratorAggregate;
use Traversable;
class HashMapTest extends TestCase {
///
/// Test set up
///
private HashMap $map;
const MAP_CONTENT = [
// Some sci-fi civilizations and author
"The Culture" => "Iain Banks",
"Radchaai Empire" => "Ann Leckie",
"Barrayar" => "Lois McMaster Bujold",
"Hainish" => "Ursula K. Le Guin",
];
protected function setUp () : void {
$this->map = new HashMap(self::MAP_CONTENT);
}
///
/// Constructors
///
public function testConstructorWithArray () {
$this->assertSame(self::MAP_CONTENT, $this->map->toArray());
}
public function testConstructorWithTraversable () {
$expected = [
"color" => "blue",
"material" => "glass",
"shape" => "sphere",
];
$iterable = new class implements IteratorAggregate {
function getIterator () : Traversable {
yield "color" => "blue";
yield "material" => "glass";
yield "shape" => "sphere";
}
};
$map = new HashMap($iterable);
$this->assertSame($expected, $map->toArray());
}
public function testFrom () {
$map = HashMap::from(self::MAP_CONTENT);
$this->assertSame(self::MAP_CONTENT, $map->toArray());
}
///
/// Getters and setters
///
public function testGet () {
$this->assertSame("Iain Banks", $this->map->get("The Culture"));
}
public function testGetWhenKeyIsNotFound () {
$this->expectException(InvalidArgumentException::class);
$this->map->get("Quuxians");
}
public function testGetOr () {
$actual = $this->map
->getOr("The Culture", "Another author");
$this->assertSame("Iain Banks", $actual);
}
public function testGetOrWhenKeyIsNotFound () {
$actual = $this->map
->getOr("Quuxians", "Another author");
$this->assertSame("Another author", $actual);
}
public function testSetWithNewKey () {
$this->map->set("Thélème", "François Rabelais");
$this->assertSame("François Rabelais",
$this->map->get("Thélème"));
}
public function testSetWithExistingKey () {
$this->map->set("The Culture", "Iain M. Banks");
$this->assertSame("Iain M. Banks",
$this->map->get("The Culture"));
}
+ public function testUnset() {
+ $this->map->unset("The Culture");
+ $this->assertFalse($this->map->contains("Iain Banks"));
+ }
+
+ public function testUnsetNotExistingKey() {
+ $this->map->unset("Not existing");
+ $this->assertEquals(4, $this->map->count());
+ }
+
public function testHas () {
$this->assertTrue($this->map->has("The Culture"));
$this->assertFalse($this->map->has("Not existing key"));
}
public function testContains () {
$this->assertTrue($this->map->contains("Iain Banks"));
$this->assertFalse($this->map->contains("Not existing value"));
}
///
/// Collection method
///
public function testCount () {
$this->assertSame(4, $this->map->count());
}
public function testClear () {
$this->map->clear();
$this->assertSame(0, $this->map->count());
}
public function testIsEmpty () : void {
$this->map->clear();
$this->assertTrue($this->map->isEmpty());
}
public function testMerge () {
$iterable = [
"The Culture" => "Iain M. Banks", // existing key
"Thélème" => "François Rabelais", // new key
];
$expected = [
// The original map
"The Culture" => "Iain Banks", // Old value should be kept
"Radchaai Empire" => "Ann Leckie",
"Barrayar" => "Lois McMaster Bujold",
"Hainish" => "Ursula K. Le Guin",
// The entries with a new key
"Thélème" => "François Rabelais",
];
$this->map->merge($iterable);
$this->assertSame($expected, $this->map->toArray());
}
public function testUpdate () {
$iterable = [
"The Culture" => "Iain M. Banks", // existing key
"Thélème" => "François Rabelais", // new key
];
$expected = [
// The original map
"The Culture" => "Iain M. Banks", // Old value should be updated
"Radchaai Empire" => "Ann Leckie",
"Barrayar" => "Lois McMaster Bujold",
"Hainish" => "Ursula K. Le Guin",
// The entries with a new key
"Thélème" => "François Rabelais",
];
$this->map->update($iterable);
$this->assertSame($expected, $this->map->toArray());
}
public function testToArray () {
$this->assertEquals(self::MAP_CONTENT, $this->map->toArray());
}
///
/// High order functions
///
public function testMap () {
$callback = function ($value) {
return "author='" . $value . "'";
};
$expected = [
"The Culture" => "author='Iain Banks'",
"Radchaai Empire" => "author='Ann Leckie'",
"Barrayar" => "author='Lois McMaster Bujold'",
"Hainish" => "author='Ursula K. Le Guin'",
];
$actual = $this->map->map($callback)->toArray();
$this->assertEquals($expected, $actual);
}
public function testMapKeys () {
$callback = function ($key) {
return "civ::" . $key;
};
$expected = [
// Some sci-fi civilizations and author
"civ::The Culture" => "Iain Banks",
"civ::Radchaai Empire" => "Ann Leckie",
"civ::Barrayar" => "Lois McMaster Bujold",
"civ::Hainish" => "Ursula K. Le Guin",
];
$actual = $this->map->mapKeys($callback)->toArray();
$this->assertEquals($expected, $actual);
}
public function testFilter () {
// Let's filter to keep names with 3 parts or more
$callback = function ($value) : bool {
return str_word_count($value) > 2;
};
$expected = [
// Some sci-fi civilizations and author
"Barrayar" => "Lois McMaster Bujold",
"Hainish" => "Ursula K. Le Guin",
];
$actual = $this->map->filter($callback)->toArray();
$this->assertEquals($expected, $actual);
}
public function testFilterWithKeyValueCallback () {
// Let's find civilization AND author with e inside
$expected = [
// Some sci-fi civilizations and author
"Radchaai Empire" => "Ann Leckie",
];
$callback = function ($key, $value) : bool {
return str_contains($key, "e") && str_contains($value, "e");
};
$actual = $this->map->filter($callback)->toArray();
$this->assertEquals($expected, $actual);
}
public function testFilterWithCallbackWithoutArgument() {
$this->expectException(InvalidArgumentException::class);
$callback = function () : bool { // No argument
return true;
};
$this->map->filter($callback);
}
public function testFilterKeys () {
// Let's filter to keep short civilization names
$callback = function ($key) : bool {
return str_word_count($key) == 1;
};
$expected = [
// Some sci-fi civilizations and author
"Barrayar" => "Lois McMaster Bujold",
"Hainish" => "Ursula K. Le Guin",
];
$actual = $this->map->filterKeys($callback)->toArray();
$this->assertEquals($expected, $actual);
}
+ ///
+ /// ArrayAccess
+ ///
+
+ public function testOffsetExists () : void {
+ $this->assertTrue(isset($this->map["The Culture"]));
+ $this->assertFalse(isset($this->map["Not existing"]));
+ }
+
+ public function testOffsetSetWithoutOffset () : void {
+ $this->expectException(InvalidArgumentException::class);
+ $this->map[] = "Another Author";
+ }
+
+ public function testOffsetSet () : void {
+ $this->map["The Culture"] = "Iain M. Banks";
+ $this->assertEquals("Iain M. Banks", $this->map["The Culture"]);
+ }
+
+ public function testOffsetUnset () : void {
+ unset($this->map["Barrayar"]);
+
+ $expected = [
+ "The Culture" => "Iain Banks",
+ "Radchaai Empire" => "Ann Leckie",
+ // "Barrayar" => "Lois McMaster Bujold", UNSET ENTRY
+ "Hainish" => "Ursula K. Le Guin",
+ ];
+
+ $this->assertEquals($expected, $this->map->toArray());
+ }
+
}
diff --git a/tests/Collections/VectorTest.php b/tests/Collections/VectorTest.php
index e3b3ea4..1c24f92 100644
--- a/tests/Collections/VectorTest.php
+++ b/tests/Collections/VectorTest.php
@@ -1,177 +1,221 @@
<?php
declare(strict_types=1);
namespace Keruald\OmniTools\Tests\Collections;
use Keruald\OmniTools\Collections\Vector;
use PHPUnit\Framework\TestCase;
use InvalidArgumentException;
use IteratorAggregate;
use Traversable;
class VectorTest extends TestCase {
private Vector $vector;
protected function setUp () : void {
$this->vector = new Vector([1, 2, 3, 4, 5]);
}
public function testConstructorWithIterable () : void {
$iterable = new class implements IteratorAggregate {
public function getIterator () : Traversable {
yield 42;
yield 100;
}
};
$vector = new Vector($iterable);
$this->assertEquals([42, 100], $vector->toArray());
}
public function testFrom () : void {
$this->assertEquals([42, 100], Vector::from([42, 100])->toArray());
}
public function testGet () : void {
$vector = new Vector(["a", "b", "c"]);
$this->assertEquals("b", $vector->get(1));
}
public function testGetOverflow () : void {
$this->expectException(InvalidArgumentException::class);
$this->vector->get(800);
}
public function testGetOr () : void {
$vector = new Vector(["a", "b", "c"]);
$this->assertEquals("X", $vector->getOr(800, "X"));
}
public function testSet () : void {
$vector = new Vector(["a", "b", "c"]);
$vector->set(1, "x"); // should replace "b"
$this->assertEquals(["a", "x", "c"], $vector->toArray());
}
public function testContains () : void {
$this->assertTrue($this->vector->contains(2));
$this->assertFalse($this->vector->contains(666));
}
public function testCount () : void {
$this->assertEquals(5, $this->vector->count());
$this->assertEquals(0, (new Vector)->count());
}
public function testClear () : void {
$this->vector->clear();
$this->assertEquals(0, $this->vector->count());
}
public function testIsEmpty () : void {
$this->vector->clear();
$this->assertTrue($this->vector->isEmpty());
}
+ public function testPush () : void {
+ $this->vector->push(6);
+
+ $this->assertEquals([1, 2, 3, 4, 5, 6], $this->vector->toArray());
+ }
+
public function testAppend () : void {
$this->vector->append([6, 7, 8]);
$this->assertEquals([1, 2, 3, 4, 5, 6, 7 ,8], $this->vector->toArray());
}
public function testUpdate () : void {
$this->vector->update([5, 5, 5, 6, 7, 8]); // 5 already exists
$this->assertEquals([1, 2, 3, 4, 5, 6, 7 ,8], $this->vector->toArray());
}
-
public function testMap () : void {
$actual = $this->vector
->map(function ($x) { return $x * $x; })
->toArray();
$this->assertEquals([1, 4, 9, 16, 25], $actual);
}
public function testMapKeys () : void {
$vector = new Vector(["foo", "bar", "quux", "xizzy"]);
$filter = function ($key) {
return 0; // Let's collapse our array
};
$actual = $vector->mapKeys($filter)->toArray();
$this->assertEquals(["xizzy"], $actual);
}
public function testFilter () : void {
$vector = new Vector(["foo", "bar", "quux", "xizzy"]);
$filter = function ($item) {
return strlen($item) === 3; // Let's keep 3-letters words
};
$actual = $vector->filter($filter)->toArray();
$this->assertEquals(["foo", "bar"], $actual);
}
public function testFilterWithBadCallback () : void {
$this->expectException(InvalidArgumentException::class);
$badFilter = function () {};
$this->vector->filter($badFilter);
}
public function testFilterKeys () : void {
$filter = function ($key) {
return $key % 2 === 0; // Let's keep even indices
};
$actual = $this->vector
->filterKeys($filter)
->toArray();
$this->assertEquals([0, 2, 4], array_keys($actual));
}
public function testImplode() : void {
$actual = (new Vector(["a", "b", "c"]))
->implode(".")
->__toString();
$this->assertEquals("a.b.c", $actual);
}
public function testImplodeWithoutDelimiter() : void {
$actual = (new Vector(["a", "b", "c"]))
->implode("")
->__toString();
$this->assertEquals("abc", $actual);
}
public function testExplode() : void {
$actual = Vector::explode(".", "a.b.c");
$this->assertEquals(["a", "b", "c"], $actual->toArray());
}
public function testExplodeWithoutDelimiter() : void {
$actual = Vector::explode("", "a.b.c");
$this->assertEquals(["a.b.c"], $actual->toArray());
}
+ ///
+ /// ArrayAccess
+ ///
+
+ public function testArrayAccessFailsWithStringKey () : void {
+ $this->expectException(InvalidArgumentException::class);
+
+ $this->vector["foo"];
+ }
+
+ public function testOffsetExists () : void {
+ $this->assertTrue(isset($this->vector[0]));
+ $this->assertFalse(isset($this->vector[8]));
+ }
+
+ public function testOffsetSetWithoutOffset () : void {
+ $this->vector[] = 6;
+ $this->assertEquals(6, $this->vector[5]);
+ }
+
+ public function testOffsetSet () : void {
+ $this->vector[0] = 9;
+ $this->assertEquals(9, $this->vector[0]);
+ }
+
+ public function testOffsetUnset () : void {
+ unset($this->vector[2]);
+
+ $expected = [
+ 0 => 1,
+ 1 => 2,
+ // vector[2] has been unset
+ 3 => 4,
+ 4 => 5,
+ ];
+
+ $this->assertEquals($expected, $this->vector->toArray());
+ }
+
}

File Metadata

Mime Type
text/x-diff
Expires
Mon, Nov 25, 14:50 (1 d, 3 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2260426
Default Alt Text
(29 KB)

Event Timeline