Skip to content
Merged
Show file tree
Hide file tree
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
11 changes: 6 additions & 5 deletions docs/Server/Database-Schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,20 @@ The schema ships in the package under `resources/schema`:
schema changes.

Point your migration tool at these files, or copy them into your project. If you would rather get the baseline as a
string in code, `SqliteStorage::schemaDdl()` and `MysqlStorage::schemaDdl()` return the same content.
string in code, `PdoStorage::schemaDdl(new SqliteDialect())` and `PdoStorage::schemaDdl(new MysqlDialect())` return the same content.

## Managing the Schema Yourself

For a managed database you usually want to apply schema changes with your own tooling rather than have the library issue
DDL on startup. Turn automatic setup off with the `initializeSchema` flag on the storage factory:

```php
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\SqliteStorage;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\Pdo\PdoConfig;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\Pdo\PdoStorageFactory;

$storage = SqliteStorage::forPcntl(
'/var/lib/freedsx/directory.sqlite',
initializeSchema: false,
$storage = PdoStorageFactory::forPcntl(
PdoConfig::forSqlite('/var/lib/freedsx/directory.sqlite')
->setInitializeSchema(false),
);
```

Expand Down
128 changes: 45 additions & 83 deletions docs/Server/General-Usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ General LDAP Server Usage
* [Built-In Storage Implementations](#built-in-storage-implementations)
* [InMemoryStorage](#inmemorystorage)
* [JsonFileStorage](#jsonfilestorage)
* [SqliteStorage](#sqlitestorage)
* [MysqlStorage](#mysqlstorage)
* [SQLite](#sqlite)
* [MySQL](#mysql)
* [Custom Filter Evaluation](#custom-filter-evaluation)
* [LDIF Data](#ldif-data)
* [Seeding Initial Entries](#seeding-initial-entries)
Expand Down Expand Up @@ -338,7 +338,7 @@ An in-memory, array-backed storage implementation. Suitable for:
> Under the PCNTL runner, `InMemoryStorage` is **not safe for multi-client write workloads**. Each forked child receives
> its own copy of the store at fork time. Written data is not propagated.
>
> Use one of `JsonFileStorage`, `SqliteStorage`, or `MysqlStorage` if you need write behavior and use PCNTL.
> Use `JsonFileStorage` or a `PdoStorage` (via `PdoStorageFactory` with a SQLite/MySQL `PdoConfig`) if you need write behavior and use PCNTL.

```php
use FreeDSx\Ldap\Entry\Attribute;
Expand Down Expand Up @@ -398,56 +398,64 @@ JSON format:
}
```

#### SqliteStorage
#### SQLite

A SQLite-backed storage implementation that persists the directory in a SQLite database file via PDO. Suitable for
A SQLite-backed `PdoStorage` that persists the directory in a SQLite database file via PDO. Suitable for
use cases that need durable persistence across restarts with support for concurrent access:

- **PCNTL**: a single shared PDO connection is inherited by all forked child processes. SQLite WAL mode handles concurrent access at the OS level.
- **Swoole**: each coroutine gets its own PDO connection to avoid blocking.

Use the named constructor that matches your server runner:
Build a `PdoConfig::forSqlite()` and pass it to the `PdoStorageFactory` method that matches your server runner:

```php
use FreeDSx\Ldap\LdapServer;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\SqliteStorage;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\Pdo\PdoConfig;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\Pdo\PdoStorageFactory;
use FreeDSx\Ldap\ServerOptions;

// PCNTL runner — WAL journal mode, shared connection
$server = new LdapServer((new ServerOptions())->setStorage(SqliteStorage::forPcntl('/var/lib/myapp/ldap.sqlite')));
$server = new LdapServer((new ServerOptions())->setStorage(
PdoStorageFactory::forPcntl(PdoConfig::forSqlite('/var/lib/myapp/ldap.sqlite'))
));
$server->run();

// Swoole runner — WAL journal mode, per-coroutine connection
$server = new LdapServer((new ServerOptions())->setStorage(SqliteStorage::forSwoole('/var/lib/myapp/ldap.sqlite')));
$server = new LdapServer((new ServerOptions())->setStorage(
PdoStorageFactory::forSwoole(PdoConfig::forSqlite('/var/lib/myapp/ldap.sqlite'))
));
$server->run();
```

Use `:memory:` as the path to run a non-persistent, in-process SQLite database (useful for testing):

```php
$server = new LdapServer((new ServerOptions())->setStorage(SqliteStorage::forPcntl(':memory:')));
$server = new LdapServer((new ServerOptions())->setStorage(
PdoStorageFactory::forPcntl(PdoConfig::forSqlite(':memory:'))
));
```

#### MysqlStorage
#### MySQL

A MySQL/MariaDB-backed storage implementation. Requires MySQL 8.0+ or MariaDB 10.6+.
A MySQL/MariaDB-backed `PdoStorage`. Requires MySQL 8.0+ or MariaDB 10.6+.

Use the named constructor that matches your server runner:
Build a `PdoConfig::forMysql()` and pass it to the `PdoStorageFactory` method that matches your server runner:

```php
use FreeDSx\Ldap\LdapServer;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\MysqlStorage;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\Pdo\PdoConfig;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\Pdo\PdoStorageFactory;
use FreeDSx\Ldap\ServerOptions;

// PCNTL runner — shared connection across forked processes
$server = new LdapServer((new ServerOptions())->setStorage(
MysqlStorage::forPcntl('mysql:host=localhost;dbname=ldap', 'user', 'secret')
PdoStorageFactory::forPcntl(PdoConfig::forMysql('mysql:host=localhost;dbname=ldap', 'user', 'secret'))
));
$server->run();

// Swoole runner — per-coroutine connection
$server = new LdapServer((new ServerOptions())->setStorage(
MysqlStorage::forSwoole('mysql:host=localhost;dbname=ldap', 'user', 'secret')
PdoStorageFactory::forSwoole(PdoConfig::forMysql('mysql:host=localhost;dbname=ldap', 'user', 'secret'))
));
$server->run();
```
Expand All @@ -457,73 +465,25 @@ and the time zone to UTC on each connection.

##### Custom PDO driver

`SqliteStorage` and `MysqlStorage` are both factories for `PdoStorage`, which is the generic PDO-backed
implementation. To support a different database engine, implement `PdoStorageFactoryInterface` with
`PdoStorageFactoryTrait` and supply the appropriate `PdoDialectInterface`, filter translator, and
connection opener:
`PdoConfig::forSqlite()` and `PdoConfig::forMysql()` are conveniences over the generic `PdoConfig::forDriver()`,
which builds a config for any PDO driver. Supply your `PdoDialectInterface`, a `FilterTranslatorInterface`, the DSN,
and the required PDO driver extension, then tune the rest with the setters:

```php
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\PdoStorage;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\Pdo\PdoStorageFactoryInterface;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\Pdo\PdoStorageFactoryTrait;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\Dialect\PdoDialectInterface;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\SqlFilter\FilterTranslatorInterface;
use PDO;

final class PostgresStorage implements PdoStorageFactoryInterface
{
use PdoStorageFactoryTrait;

public function __construct(
private readonly string $dsn,
private readonly string $username,
#[\SensitiveParameter]
private readonly string $password,
) {
}

public static function forPcntl(
string $dsn,
string $username,
#[\SensitiveParameter]
string $password,
): PdoStorage {
return (new self($dsn, $username, $password))->createShared();
}
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\Pdo\PdoConfig;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\Pdo\PdoStorageFactory;

public static function forSwoole(
string $dsn,
string $username,
#[\SensitiveParameter]
string $password,
): PdoStorage {
return (new self($dsn, $username, $password))->createPerCoroutine();
}

protected function dialect(): PdoDialectInterface
{
return new MyPostgresDialect();
}

protected function translator(): FilterTranslatorInterface
{
return new MyPostgresFilterTranslator();
}

protected function openConnection(PdoDialectInterface $dialect): PDO
{
$pdo = new PDO(
$this->dsn,
$this->username,
$this->password,
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION],
);
$config = PdoConfig::forDriver(
new MyPostgresDialect(),
new MyPostgresFilterTranslator(),
'pgsql:host=localhost;dbname=ldap',
'pdo_pgsql',
)
->setUsername('user')
->setPassword('secret')
->setSessionStatements(["SET client_encoding = 'UTF8'"]);

PdoStorage::initialize($pdo, $dialect);

return $pdo;
}
}
$storage = PdoStorageFactory::forPcntl($config);
```

### Custom Filter Evaluation
Expand Down Expand Up @@ -580,11 +540,12 @@ other source (database, remote URL, gzip stream, etc.). LDIF output uses the par
use FreeDSx\Ldap\Entry\Dn;
use FreeDSx\Ldap\LdapServer;
use FreeDSx\Ldap\Ldif\Loader\FileLdifLoader;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\SqliteStorage;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\Pdo\PdoConfig;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\Pdo\PdoStorageFactory;
use FreeDSx\Ldap\ServerOptions;

$server = new LdapServer(
(new ServerOptions())->setStorage(SqliteStorage::forPcntl('/var/lib/myapp/ldap.sqlite')),
(new ServerOptions())->setStorage(PdoStorageFactory::forPcntl(PdoConfig::forSqlite('/var/lib/myapp/ldap.sqlite'))),
);
$server->seed(
new FileLdifLoader('/etc/myapp/initial-data.ldif'),
Expand Down Expand Up @@ -650,11 +611,12 @@ entries exactly as they were.
```php
use FreeDSx\Ldap\LdapServer;
use FreeDSx\Ldap\Ldif\Output\FileLdifOutput;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\SqliteStorage;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\Pdo\PdoConfig;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\Pdo\PdoStorageFactory;
use FreeDSx\Ldap\ServerOptions;

$server = new LdapServer(
(new ServerOptions())->setStorage(SqliteStorage::forPcntl('/var/lib/myapp/ldap.sqlite')),
(new ServerOptions())->setStorage(PdoStorageFactory::forPcntl(PdoConfig::forSqlite('/var/lib/myapp/ldap.sqlite'))),
);
$server->dump(new FileLdifOutput('/var/backups/ldap-snapshot.ldif'));
```
Expand Down
10 changes: 6 additions & 4 deletions docs/Server/Replication.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ use FreeDSx\Ldap\LdapServer;
use FreeDSx\Ldap\Server\AccessControl\Rule\ControlRule;
use FreeDSx\Ldap\Server\AccessControl\Subject\Subject;
use FreeDSx\Ldap\Server\AccessControl\Target\Target;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\SqliteStorage;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\Pdo\PdoConfig;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\Pdo\PdoStorageFactory;
use FreeDSx\Ldap\ServerOptions;

// The one sync-specific grant: the privileged sync control, over the base to sync.
Expand All @@ -45,7 +46,7 @@ $syncGrant = ControlRule::allow(
$aclRules = $myAclRules->withControlRules($syncGrant);

// A storage that is visible across connections (see Storage and Runner Requirements below).
$storage = SqliteStorage::forPcntl('/var/lib/freedsx/directory.sqlite');
$storage = PdoStorageFactory::forPcntl(PdoConfig::forSqlite('/var/lib/freedsx/directory.sqlite'));

$options = (new ServerOptions())
->setSyncEnabled(true)
Expand Down Expand Up @@ -170,7 +171,8 @@ use FreeDSx\Ldap\ClientOptions;
use FreeDSx\Ldap\LdapServer;
use FreeDSx\Ldap\Operations;
use FreeDSx\Ldap\ReplicaConfig;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\SqliteStorage;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\Pdo\PdoConfig;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\Pdo\PdoStorageFactory;
use FreeDSx\Ldap\Sync\Consumer\Checkpoint\FileReplicationCheckpoint;
use FreeDSx\Ldap\ServerOptions;

Expand All @@ -190,7 +192,7 @@ $replicaConfig = new ReplicaConfig(
$replicaConfig->setBind(Operations::bind('cn=replica,dc=example,dc=com', 'secret'));

$options = ServerOptions::forReplica($replicaConfig)
->setStorage(SqliteStorage::forPcntl('/var/lib/freedsx/replica.sqlite'));
->setStorage(PdoStorageFactory::forPcntl(PdoConfig::forSqlite('/var/lib/freedsx/replica.sqlite')));

(new LdapServer($options))->run();
```
Expand Down
9 changes: 9 additions & 0 deletions resources/schema/mysql/trigram.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS entry_attribute_trigrams (
entry_lc_dn VARCHAR(768) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
attr_name_lower VARCHAR(255) NOT NULL,
trigram VARCHAR(3) NOT NULL,
INDEX idx_trgm_attr (attr_name_lower, trigram),
INDEX idx_trgm_entry (entry_lc_dn),
CONSTRAINT fk_trgm_entry FOREIGN KEY (entry_lc_dn)
REFERENCES entries(lc_dn) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
10 changes: 10 additions & 0 deletions resources/schema/sqlite/trigram.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
CREATE TABLE IF NOT EXISTS entry_attribute_trigrams (
entry_lc_dn TEXT NOT NULL,
attr_name_lower TEXT NOT NULL,
trigram TEXT NOT NULL,
FOREIGN KEY (entry_lc_dn) REFERENCES entries(lc_dn) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS idx_trgm_attr ON entry_attribute_trigrams (attr_name_lower, trigram);

CREATE INDEX IF NOT EXISTS idx_trgm_entry ON entry_attribute_trigrams (entry_lc_dn);
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,11 @@ public function schemaSql(): string;
* @return list<string>
*/
public function schemaStatements(): array;

/**
* The statements of a named auxiliary schema (e.g. an optional substring index), for strategies to apply their own DDL.
*
* @return list<string>
*/
public function schemaStatementsNamed(string $name): array;
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@
*/
trait PdoSchemaTrait
{
private const BASELINE_SCHEMA = 'baseline';

public function schemaSql(): string
{
return $this->schemaFile()
return $this->schemaFile(self::BASELINE_SCHEMA)
->sql();
}

Expand All @@ -33,7 +35,16 @@ public function schemaSql(): string
*/
public function schemaStatements(): array
{
return $this->schemaFile()
return $this->schemaFile(self::BASELINE_SCHEMA)
->statements();
}

/**
* @return list<string>
*/
public function schemaStatementsNamed(string $name): array
{
return $this->schemaFile($name)
->statements();
}

Expand All @@ -42,10 +53,10 @@ public function schemaStatements(): array
*/
abstract protected function schemaName(): string;

private function schemaFile(): SchemaFile
private function schemaFile(string $name): SchemaFile
{
return new SchemaFile(Resources::path(
'schema/' . $this->schemaName() . '/baseline.sql',
'schema/' . $this->schemaName() . '/' . $name . '.sql',
));
}
}
Loading
Loading