diff --git a/core/core.services.yml b/core/core.services.yml index 5b0de75..5f0493e 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -19,6 +19,8 @@ parameters: http.response.debug_cacheability_headers: false factory.keyvalue.expirable: default: keyvalue.expirable.database + factory.keyvalue.sorted_set: + default: keyvalue.sorted_set.database filter_protocols: - http - https @@ -384,6 +386,12 @@ services: keyvalue.expirable.database: class: Drupal\Core\KeyValueStore\KeyValueDatabaseExpirableFactory arguments: ['@serialization.phpserialize', '@database'] + keyvalue.sorted_set: + class: Drupal\Core\KeyValueStore\KeyValueSortedSetFactory + arguments: ['@service_container', '%factory.keyvalue.sorted_set%'] + keyvalue.sorted_set.database: + class: Drupal\Core\KeyValueStore\KeyValueDatabaseSortedSetFactory + arguments: ['@serialization.phpserialize', '@database'] logger.factory: class: Drupal\Core\Logger\LoggerChannelFactory parent: container.trait diff --git a/core/lib/Drupal/Core/KeyValueStore/DatabaseStorageSortedSet.php b/core/lib/Drupal/Core/KeyValueStore/DatabaseStorageSortedSet.php new file mode 100644 index 0000000..bbd56bd --- /dev/null +++ b/core/lib/Drupal/Core/KeyValueStore/DatabaseStorageSortedSet.php @@ -0,0 +1,131 @@ +collection = $collection; + $this->serializer = $serializer; + $this->connection = $connection; + $this->table = $table; + } + + /** + * {@inheritdoc} + */ + public function add($score, $member) { + $this->addMultiple(array(array($score => $member))); + } + + /** + * {@inheritdoc} + */ + public function addMultiple(array $pairs) { + foreach ($pairs as $pair) { + foreach ($pair as $score => $member) { + $encoded_member = $this->serializer->encode($member); + $this->connection->merge($this->table) + ->fields(array( + 'collection' => $this->collection, + 'name' => $score, + 'value' => $encoded_member, + )) + ->condition('collection', $this->collection) + ->condition('value', $encoded_member) + ->execute(); + } + } + } + + /** + * {@inheritdoc} + */ + public function getCount() { + return $this->connection->select($this->table, 't') + ->condition('collection', $this->collection) + ->countQuery() + ->execute() + ->fetchField(); + } + + /** + * {@inheritdoc} + */ + public function getRange($start, $stop = NULL) { + $query = $this->connection->select($this->table, 't') + ->fields('t', array('value')) + ->condition('collection', $this->collection) + ->condition('name', $start, '>='); + + if ($stop !== NULL) { + $query->condition('name', $stop, '<='); + } + $result = $query->orderBy('name', 'ASC')->execute(); + + $values = array(); + foreach ($result as $item) { + $values[] = $this->serializer->decode($item->value); + } + return $values; + } + + /** + * {@inheritdoc} + */ + public function getMaxScore() { + $query = $this->connection->select($this->table); + $query->condition('collection', $this->collection, '='); + $query->addExpression('MAX(name)'); + return $query->execute()->fetchField(); + } + + /** + * {@inheritdoc} + */ + public function getMinScore() { + $query = $this->connection->select($this->table); + $query->condition('collection', $this->collection, '='); + $query->addExpression('MIN(name)'); + return $query->execute()->fetchField(); + } +} diff --git a/core/lib/Drupal/Core/KeyValueStore/KeyValueDatabaseSortedSetFactory.php b/core/lib/Drupal/Core/KeyValueStore/KeyValueDatabaseSortedSetFactory.php new file mode 100644 index 0000000..071a45d --- /dev/null +++ b/core/lib/Drupal/Core/KeyValueStore/KeyValueDatabaseSortedSetFactory.php @@ -0,0 +1,46 @@ +serializer = $serializer; + $this->connection = $connection; + } + + /** + * {@inheritdoc} + */ + public function get($collection) { + return new DatabaseStorageSortedSet($collection, $this->serializer, $this->connection); + } +} diff --git a/core/lib/Drupal/Core/KeyValueStore/KeyValueSortedSetFactory.php b/core/lib/Drupal/Core/KeyValueStore/KeyValueSortedSetFactory.php new file mode 100644 index 0000000..70ef0bd --- /dev/null +++ b/core/lib/Drupal/Core/KeyValueStore/KeyValueSortedSetFactory.php @@ -0,0 +1,16 @@ + 'Key-value list storage table.', + 'fields' => array( + 'collection' => array( + 'description' => 'A named collection of key and value pairs.', + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'default' => '', + ), + // KEY is an SQL reserved word, so use 'name' as the key's field name. + 'name' => array( + 'description' => 'The index or score key for the value.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'size' => 'big', + ), + 'value' => array( + 'description' => 'The value.', + 'type' => 'blob', + 'not null' => TRUE, + 'size' => 'big', + ), + ), + 'indexes' => array( + 'collection_name' => array('collection', 'name'), + 'name' => array('name'), + ), + ); + $schema['sequences'] = array( 'description' => 'Stores IDs.', 'fields' => array( @@ -1794,3 +1825,49 @@ function system_update_8203() { ->set('logging', 1) ->save(TRUE); } + +/** + * @addtogroup updates-8.3.0 + * @{ + */ + +/** + * Install key_value_sorted schema. + */ +function system_update_8300() { + $table = array( + 'description' => 'Key-value list storage table.', + 'fields' => array( + 'collection' => array( + 'description' => 'A named collection of key and value pairs.', + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'default' => '', + ), + // KEY is an SQL reserved word, so use 'name' as the key's field name. + 'name' => array( + 'description' => 'The index or score key for the value.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'size' => 'big', + ), + 'value' => array( + 'description' => 'The value.', + 'type' => 'blob', + 'not null' => TRUE, + 'size' => 'big', + ), + ), + 'indexes' => array( + 'collection_name' => array('collection', 'name'), + 'name' => array('name'), + ), + ); + \Drupal::database()->schema()->createTable('key_value_sorted', $table); +} + +/** + * @} End of "addtogroup updates-8.3.0". + */ diff --git a/core/tests/Drupal/KernelTests/Core/KeyValueStore/DatabaseStorageSortedSetTest.php b/core/tests/Drupal/KernelTests/Core/KeyValueStore/DatabaseStorageSortedSetTest.php new file mode 100644 index 0000000..d06d2eb --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/KeyValueStore/DatabaseStorageSortedSetTest.php @@ -0,0 +1,120 @@ +installSchema('system', array('key_value_sorted')); + + $this->collection = $this->randomMachineName(); + $this->serializer = \Drupal::service('serialization.phpserialize'); + $this->connection = \Drupal::service('database'); + $this->store = \Drupal::service('keyvalue.sorted_set')->get($this->collection); + } + + public function assertPairs($expected_pairs) { + $result = $this->connection->select('key_value_sorted', 't') + ->fields('t', array('name', 'value')) + ->condition('collection', $this->collection) + ->condition('name', array_keys($expected_pairs), 'IN') + ->execute() + ->fetchAllAssoc('name'); + + $expected_count = count($expected_pairs); + $this->assertSame(count($result), $expected_count, "Query affected $expected_count records."); + foreach ($expected_pairs as $key => $value) { + $this->assertSame($this->serializer->decode($result[$key]->value), $value, "Key $key have value $value"); + } + } + + public function assertRecords($expected, $message = NULL) { + $count = $this->connection->select('key_value_sorted', 't') + ->fields('t') + ->condition('collection', $this->collection) + ->countQuery() + ->execute() + ->fetchField(); + $this->assertEquals($expected, $count, $message ? $message : "There are $expected records."); + } + + public function newKey() { + return (int) (microtime(TRUE) * 1000000); + } + + public function testCalls() { + $key0 = $this->newKey(); + $value0 = $this->randomMachineName(); + $this->store->add($key0, $value0); + $this->assertPairs(array($key0 => $value0)); + + $key1 = $this->newKey(); + $value1 = $this->randomMachineName(); + $this->store->add($key1, $value1); + $this->assertPairs(array($key1 => $value1)); + + // Ensure it works to add sets with the same score. + $key2 = $this->newKey(); + $value2 = $this->randomMachineName(); + $value3 = $this->randomMachineName(); + $value4 = $this->randomMachineName(); + $this->store->addMultiple(array( + array($key2 => $value2), + array($key2 => $value3), + array($key2 => $value4), + )); + + $count = $this->store->getCount(); + $this->assertEquals(5, $count, 'The count method returned correct count.'); + + $value = $this->store->getRange($key1, $key2); + $this->assertSame($value, array($value1, $value2, $value3, $value4)); + + $new1 = $this->newKey(); + $this->store->add($new1, $value1); + + $value = $this->store->getRange($new1, $new1); + $this->assertSame($value, array($value1), 'Member was successfully updated.'); + $this->assertRecords(5, 'Correct number of record in the collection after member update.'); + + $value = $this->store->getRange($key1, $key1); + $this->assertSame($value, array(), 'Non-existing range returned empty array.'); + + $max_score = $this->store->getMaxScore(); + $this->assertEquals($max_score, $new1, 'The getMaxScore method returned correct score.'); + + $min_score = $this->store->getMinScore(); + $this->assertEquals($min_score, $key0, 'The getMinScore method returned correct score.'); + } +}