Skip to content

Cleaning namespace in SQLite #53

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 23 additions & 11 deletions readme.md
Original file line number Diff line number Diff line change
@@ -7,19 +7,32 @@ Nette Caching
[![Latest Stable Version](https://poser.pugx.org/nette/caching/v/stable)](https://github.com/nette/caching/releases)
[![License](https://img.shields.io/badge/license-New%20BSD-blue.svg)](https://github.com/nette/caching/blob/master/license.md)


Introduction
------------

Cache accelerates your application by storing data - once hardly retrieved - for future use.

Install it using Composer:
Documentation can be found on the [website](https://doc.nette.org/caching).


Installation
------------

The recommended way to install Nette Caching is via Composer:

```
composer require nette/caching
```

The last stable release requires PHP version 5.6 or newer (is compatible with PHP 7.0 and 7.1). The dev-master version requires PHP 7.1.
It requires PHP version 5.6 and supports PHP up to 7.2. The dev-master version requires PHP 7.1.

Nette offers a very intuitive API for cache manipulation. After all, you wouldn't expect anything else, right? ;-)
Before we show you the first example, we need to think about place where to store data physically. We can use a database, //Memcached// server,
or the most available storage - hard drive:

Usage
-----

Nette Caching offers a very intuitive API for cache manipulation. Before we show you the first example, we need to think about place where
to store data physically. We can use a database, Memcached server, or the most available storage - hard drive:

```php
// the `temp` directory will be the storage
@@ -82,7 +95,7 @@ $cache = new Cache($storage, 'htmlOutput');


Caching Function Results
-------------------------
------------------------

Caching the result of a function or method call can be achieved using the `call()` method:

@@ -94,7 +107,7 @@ The `gethostbyaddr($ip)` will therefore be called only once and next time, only
different results are cached.

Output Caching
------------------
--------------

The output can be cached not only in templates:

@@ -217,9 +230,8 @@ $cache->clean(array(
```



Cache Storage
--------
-------------
In addition to already mentioned `FileStorage`, Nette Framework also provides `MemcachedStorage` which stores
data to the `Memcached` server, and also `MemoryStorage` for storing data in memory for duration of the request.
The special `DevNullStorage`, which does precisely nothing, can be used for testing, when we want to eliminate the influence of caching.
@@ -238,8 +250,8 @@ The solution is to modify application behaviour so that data are created only by
or use an anonymous function:

```php
$result = $cache->save($key, function() { // or callback(...)
return buildData(); // difficult operation
$result = $cache->save($key, function() {
return buildData(); // difficult operation
});
```

40 changes: 29 additions & 11 deletions src/Caching/Storages/SQLiteStorage.php
Original file line number Diff line number Diff line change
@@ -34,6 +34,8 @@ public function __construct($path)
$this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$this->pdo->exec('
PRAGMA foreign_keys = ON;
PRAGMA case_sensitive_like = ON;

CREATE TABLE IF NOT EXISTS cache (
key BLOB NOT NULL PRIMARY KEY,
data BLOB NOT NULL,
@@ -58,6 +60,7 @@ public function __construct($path)
*/
public function read(string $key)
{
$key = self::sanitize($key);
$stmt = $this->pdo->prepare('SELECT data, slide FROM cache WHERE key=? AND (expire IS NULL OR expire >= ?)');
$stmt->execute([$key, time()]);
if ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
@@ -75,6 +78,7 @@ public function read(string $key)
*/
public function bulkRead(array $keys): array
{
$keys = array_map([self::class, 'sanitize'], $keys);
$stmt = $this->pdo->prepare('SELECT key, data, slide FROM cache WHERE key IN (?' . str_repeat(',?', count($keys) - 1) . ') AND (expire IS NULL OR expire >= ?)');
$stmt->execute(array_merge($keys, [time()]));
$result = [];
@@ -83,7 +87,7 @@ public function bulkRead(array $keys): array
if ($row['slide'] !== null) {
$updateSlide[] = $row['key'];
}
$result[$row['key']] = unserialize($row['data']);
$result[str_replace("\x01", Cache::NAMESPACE_SEPARATOR, $row['key'])] = unserialize($row['data']);
}
if (!empty($updateSlide)) {
$stmt = $this->pdo->prepare('UPDATE cache SET expire = ? + slide WHERE key IN(?' . str_repeat(',?', count($updateSlide) - 1) . ')');
@@ -106,6 +110,7 @@ public function lock(string $key): void
*/
public function write(string $key, $data, array $dependencies): void
{
$key = self::sanitize($key);
$expire = isset($dependencies[Cache::EXPIRATION]) ? $dependencies[Cache::EXPIRATION] + time() : null;
$slide = isset($dependencies[Cache::SLIDING]) ? $dependencies[Cache::EXPIRATION] : null;

@@ -131,7 +136,7 @@ public function write(string $key, $data, array $dependencies): void
public function remove(string $key): void
{
$this->pdo->prepare('DELETE FROM cache WHERE key=?')
->execute([$key]);
->execute([self::sanitize($key)]);
}


@@ -142,18 +147,31 @@ public function clean(array $conditions): void
{
if (!empty($conditions[Cache::ALL])) {
$this->pdo->prepare('DELETE FROM cache')->execute();
return;
}

} else {
$sql = 'DELETE FROM cache WHERE expire < ?';
$args = [time()];
$sql = 'DELETE FROM cache WHERE expire < ?';
$args = [time()];

if (!empty($conditions[Cache::TAGS])) {
$tags = $conditions[Cache::TAGS];
$sql .= ' OR key IN (SELECT key FROM tags WHERE tag IN (?' . str_repeat(',?', count($tags) - 1) . '))';
$args = array_merge($args, $tags);
}
if (!empty($conditions[Cache::TAGS])) {
$tags = $conditions[Cache::TAGS];
$sql .= ' OR key IN (SELECT key FROM tags WHERE tag IN (?' . str_repeat(',?', count($tags) - 1) . '))';
$args = array_merge($args, $tags);
}

$this->pdo->prepare($sql)->execute($args);
if (!empty($conditions[Cache::NAMESPACES])) {
foreach ($conditions[Cache::NAMESPACES] as $namespace) {
$sql .= ' OR key LIKE ?';
$args[] = self::sanitize($namespace . Cache::NAMESPACE_SEPARATOR . '%');
}
}

$this->pdo->prepare($sql)->execute($args);
}


private function sanitize($key)
{
return str_replace(Cache::NAMESPACE_SEPARATOR, "\x01", $key);
}
}
94 changes: 94 additions & 0 deletions tests/Storages/SQLiteStorage.clean-namespace.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php

/**
* Test: Nette\Caching\Storages\SQLiteStorage clean with Cache::NAMESPACE
* @phpExtension pdo_sqlite
*/

declare(strict_types=1);

use Nette\Caching\Cache;
use Nette\Caching\Storages\SQLiteStorage;
use Tester\Assert;


require __DIR__ . '/../bootstrap.php';

$storage = new SQLiteStorage(':memory:');

/*
* Create SQLiteStorage cache without namespace and some with namespaces
*/
$cacheA = new Cache($storage);
$cacheB = new Cache($storage, 'C' . Cache::NAMESPACE_SEPARATOR . 'A');
$cacheC = new Cache($storage, 'C');
$cacheD = new Cache($storage, 'D');

/*
* Fill with data
*/
$cacheA->save('test1', 'David');
$cacheA->save('test2', 'Grudl');

$cacheB->save('test1', 'Barry');
$cacheB->save('test2', 'Allen');

$cacheC->save('test1', 'Oliver');
$cacheC->save('test2', 'Queen');

$cacheD->save('test1', 'Bruce');
$cacheD->save('test2', 'Wayne');


/*
* Check if fill wass successfull
*/
Assert::same('David', $cacheA->load('test1'));
Assert::same('Grudl', $cacheA->load('test2'));

Assert::same('Barry', $cacheB->load('test1'));
Assert::same('Allen', $cacheB->load('test2'));

Assert::same('Oliver', $cacheC->load('test1'));
Assert::same('Queen', $cacheC->load('test2'));

Assert::same('Bruce', $cacheD->load('test1'));
Assert::same('Wayne', $cacheD->load('test2'));


/*
* Clean one namespace
*/
$storage->clean([Cache::NAMESPACES => [$cacheB->getNamespace()]]);

Assert::same('David', $cacheA->load('test1'));
Assert::same('Grudl', $cacheA->load('test2'));

// Only these should be null now
Assert::null($cacheB->load('test1'));
Assert::null($cacheB->load('test2'));

Assert::same('Oliver', $cacheC->load('test1'));
Assert::same('Queen', $cacheC->load('test2'));

Assert::same('Bruce', $cacheD->load('test1'));
Assert::same('Wayne', $cacheD->load('test2'));


/*
* Test cleaning multiple namespaces
*/
$storage->clean([Cache::NAMESPACES => [$cacheC->getNamespace(), $cacheD->getNamespace()]]);

Assert::same('David', $cacheA->load('test1'));
Assert::same('Grudl', $cacheA->load('test2'));

// All other should be null
Assert::null($cacheB->load('test1'));
Assert::null($cacheB->load('test2'));

Assert::null($cacheC->load('test1'));
Assert::null($cacheC->load('test2'));

Assert::null($cacheD->load('test1'));
Assert::null($cacheD->load('test2'));