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
6 changes: 3 additions & 3 deletions docs/Server/General-Usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -466,16 +466,16 @@ and the time zone to UTC on each connection.
##### Custom PDO driver

`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:
which builds a config for any PDO driver. Supply your `PdoDialectInterface`, the DSN, and the required PDO driver
extension, then tune the rest with the setters. Your dialect's `createFilterTranslator()` supplies the paired
`FilterTranslatorInterface` (with the substring index injected):

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

$config = PdoConfig::forDriver(
new MyPostgresDialect(),
new MyPostgresFilterTranslator(),
'pgsql:host=localhost;dbname=ldap',
'pdo_pgsql',
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@

namespace FreeDSx\Ldap\Server\Backend\Storage\Adapter\Dialect;

use FreeDSx\Ldap\Server\Backend\Storage\Adapter\SqlFilter\FilterTranslatorInterface;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\SqlFilter\MysqlFilterTranslator;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\SubstringIndex\SubstringIndexInterface;

/**
* MySQL/MariaDB SQL for PdoStorage; requires MySQL 8.0+ or MariaDB 10.6+.
*
Expand All @@ -24,6 +28,11 @@ final class MysqlDialect implements PdoDialectInterface
use PdoJournalDialectTrait;
use PdoSchemaTrait;

public function createFilterTranslator(?SubstringIndexInterface $substringIndex): FilterTranslatorInterface
{
return new MysqlFilterTranslator($substringIndex);
}

/**
* @todo Replace VALUES() with row alias syntax once MariaDB supports it.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,21 @@

namespace FreeDSx\Ldap\Server\Backend\Storage\Adapter\Dialect;

use FreeDSx\Ldap\Server\Backend\Storage\Adapter\SqlFilter\FilterTranslatorInterface;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\SubstringIndex\SubstringIndexInterface;

/**
* The full database-specific SQL a PdoStorage needs.
*
* @author Chad Sikorra <Chad.Sikorra@gmail.com>
*/
interface PdoDialectInterface extends PdoEntryDialectInterface, PdoJournalDialectInterface
{
/**
* The filter translator paired with this dialect, with the (optional) substring index injected for query narrowing.
*/
public function createFilterTranslator(?SubstringIndexInterface $substringIndex): FilterTranslatorInterface;

/**
* The full schema (all tables) as a runnable SQL script.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@

namespace FreeDSx\Ldap\Server\Backend\Storage\Adapter\Dialect;

use FreeDSx\Ldap\Server\Backend\Storage\Adapter\SqlFilter\FilterTranslatorInterface;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\SqlFilter\SqliteFilterTranslator;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\SubstringIndex\SubstringIndexInterface;
use PDO;

/**
Expand All @@ -26,6 +29,11 @@ final class SqliteDialect implements PdoDialectInterface
use PdoJournalDialectTrait;
use PdoSchemaTrait;

public function createFilterTranslator(?SubstringIndexInterface $substringIndex): FilterTranslatorInterface
{
return new SqliteFilterTranslator($substringIndex);
}

/**
* `BEGIN IMMEDIATE` acquires the reserved lock up front so concurrent writers wait (honoring `busy_timeout`)
* instead of racing, which returns SQLITE_BUSY immediately to avoid deadlock.
Expand Down
20 changes: 0 additions & 20 deletions src/FreeDSx/Ldap/Server/Backend/Storage/Adapter/Pdo/PdoConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\Dialect\MysqlDialect;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\Dialect\PdoDialectInterface;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\Dialect\SqliteDialect;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\SqlFilter\FilterTranslatorInterface;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\SqlFilter\MysqlFilterTranslator;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\SqlFilter\SqliteFilterTranslator;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\SubstringIndex\SubstringIndexInterface;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\SubstringIndex\TrigramSubstringIndex;
use PDO;
Expand All @@ -39,7 +36,6 @@ final class PdoConfig
*/
private function __construct(
private PdoDialectInterface $dialect,
private FilterTranslatorInterface $translator,
private string $dsn,
private ?string $username,
private ?string $password,
Expand All @@ -55,7 +51,6 @@ public static function forSqlite(string $path): self
{
return new self(
dialect: new SqliteDialect(),
translator: new SqliteFilterTranslator(),
dsn: 'sqlite:' . $path,
username: null,
password: null,
Expand All @@ -80,7 +75,6 @@ public static function forMysql(
): self {
return new self(
dialect: new MysqlDialect(),
translator: new MysqlFilterTranslator(),
dsn: $dsn,
username: $username,
password: $password,
Expand All @@ -102,13 +96,11 @@ public static function forMysql(
*/
public static function forDriver(
PdoDialectInterface $dialect,
FilterTranslatorInterface $translator,
string $dsn,
string $driverExtension,
): self {
return new self(
dialect: $dialect,
translator: $translator,
dsn: $dsn,
username: null,
password: null,
Expand All @@ -131,18 +123,6 @@ public function setDialect(PdoDialectInterface $dialect): self
return $this;
}

public function getTranslator(): FilterTranslatorInterface
{
return $this->translator;
}

public function setTranslator(FilterTranslatorInterface $translator): self
{
$this->translator = $translator;

return $this;
}

public function getDsn(): string
{
return $this->dsn;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ private static function shared(PdoConfig $config): PdoStorage
$open(),
$open,
),
$config->getTranslator(),
$config->getDialect()->createFilterTranslator($config->getSubstringIndex()),
$config->getDialect(),
$config->getSubstringIndex(),
);
Expand All @@ -71,7 +71,7 @@ private static function perCoroutine(PdoConfig $config): PdoStorage
{
return new PdoStorage(
new CoroutinePdoConnectionProvider(static fn(): PDO => self::open($config)),
$config->getTranslator(),
$config->getDialect()->createFilterTranslator($config->getSubstringIndex()),
$config->getDialect(),
$config->getSubstringIndex(),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

namespace FreeDSx\Ldap\Server\Backend\Storage\Adapter\SqlFilter;

use FreeDSx\Ldap\Server\Backend\Storage\Adapter\SubstringIndex\SubstringIndexInterface;

/**
* MySQL/MariaDB SQL WHERE translator for LDAP filters; targets the `entry_attribute_values` sidecar index.
*
Expand All @@ -22,6 +24,11 @@ final class MysqlFilterTranslator implements FilterTranslatorInterface
{
use SqlFilterTranslatorTrait;

public function __construct(?SubstringIndexInterface $substringIndex = null)
{
$this->substringIndex = $substringIndex;
}

private function buildPresenceCheck(string $attribute): string
{
return <<<SQL
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
use FreeDSx\Ldap\Search\Filter\OrFilter;
use FreeDSx\Ldap\Search\Filter\PresentFilter;
use FreeDSx\Ldap\Search\Filter\SubstringFilter;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\SubstringIndex\SubstringIndexInterface;

/**
* Translates LDAP filters to SQL against the `entry_attribute_values` sidecar index.
Expand All @@ -33,6 +34,8 @@
*/
trait SqlFilterTranslatorTrait
{
private ?SubstringIndexInterface $substringIndex = null;

public function translate(FilterInterface $filter): ?SqlFilterResult
{
return match (true) {
Expand Down Expand Up @@ -150,6 +153,16 @@ private function translateSubstring(SubstringFilter $filter): ?SqlFilterResult
return null;
}

$indexed = $this->indexedSubstring(
$attribute,
$startsWith,
$contains,
$endsWith,
);
if ($indexed !== null) {
return $indexed;
}

// Prefix-anchored LIKE is the only valid superset under truncation; other fragments fall back to presence + PHP re-eval.
$alias = $this->valueAlias();

Expand Down Expand Up @@ -179,6 +192,48 @@ private function translateSubstring(SubstringFilter $filter): ?SqlFilterResult
);
}

/**
* The substring index's candidate-narrowing predicate for an infix/suffix filter, or null when it does not apply.
*
* @param array<string> $contains
*/
private function indexedSubstring(
string $attribute,
?string $startsWith,
array $contains,
?string $endsWith,
): ?SqlFilterResult {
if ($startsWith !== null || $this->substringIndex === null) {
return null;
}

return $this->substringIndex->buildSubstringPredicate(
$attribute,
$this->substringFragments(
$contains,
$endsWith,
),
);
}

/**
* @param array<string> $contains
*
* @return list<string>
*/
private function substringFragments(
array $contains,
?string $endsWith,
): array {
$fragments = array_values($contains);

if ($endsWith !== null) {
$fragments[] = $endsWith;
}

return $fragments;
}

private function isExactEquality(string $value): bool
{
return Text::isAscii($value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

namespace FreeDSx\Ldap\Server\Backend\Storage\Adapter\SqlFilter;

use FreeDSx\Ldap\Server\Backend\Storage\Adapter\SubstringIndex\SubstringIndexInterface;

/**
* SQLite SQL WHERE translator for LDAP filters; targets the `entry_attribute_values` sidecar index.
*
Expand All @@ -22,6 +24,11 @@ final class SqliteFilterTranslator implements FilterTranslatorInterface
{
use SqlFilterTranslatorTrait;

public function __construct(?SubstringIndexInterface $substringIndex = null)
{
$this->substringIndex = $substringIndex;
}

private function buildPresenceCheck(string $attribute): string
{
return <<<SQL
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use FreeDSx\Ldap\Entry\Entry;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\Dialect\PdoDialectInterface;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\SqlFilter\SqlFilterResult;

/**
* A pluggable substring-search index for the PDO backend.
Expand All @@ -40,4 +41,14 @@ public function maintain(
Entry $entry,
callable $execute,
): void;

/**
* A candidate-narrowing WHERE fragment for a substring filter on an indexed attribute; null to decline.
*
* @param list<string> $fragments
*/
public function buildSubstringPredicate(
string $attributeLower,
array $fragments,
): ?SqlFilterResult;
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use FreeDSx\Ldap\Entry\Entry;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\Dialect\PdoDialectInterface;
use FreeDSx\Ldap\Server\Backend\Storage\Adapter\SqlFilter\SqlFilterResult;

/**
* Portable substring index: a generic trigram table usable across every PDO dialect.
Expand Down Expand Up @@ -50,6 +51,15 @@ final class TrigramSubstringIndex implements SubstringIndexInterface
VALUES %s
SQL;

private const PREDICATE_SQL = <<<SQL
lc_dn IN (
SELECT entry_lc_dn FROM entry_attribute_trigrams
WHERE attr_name_lower = ? AND trigram IN (%s)
GROUP BY entry_lc_dn
HAVING COUNT(DISTINCT trigram) = %d
)
SQL;

/**
* @var array<string, true> Indexed attribute names, lowercased.
*/
Expand Down Expand Up @@ -98,6 +108,47 @@ public function maintain(
);
}

public function buildSubstringPredicate(
string $attributeLower,
array $fragments,
): ?SqlFilterResult {
if (!isset($this->attributes[$attributeLower])) {
return null;
}

$trigrams = [];
foreach ($fragments as $fragment) {
foreach (Trigrams::of($fragment) as $trigram) {
$trigrams[] = $trigram;
}
}
$trigrams = array_values(array_unique($trigrams));

if ($trigrams === []) {
return null;
}

$markers = implode(
', ',
array_fill(
0,
count($trigrams),
'?',
),
);

return new SqlFilterResult(
sprintf(
self::PREDICATE_SQL,
$markers,
count($trigrams),
),
[$attributeLower, ...$trigrams],
isExact: false,
referencedAttributes: [$attributeLower],
);
}

/**
* @return list<array{0: string, 1: string, 2: string}>
*/
Expand Down
Loading
Loading