248 lines
7.9 KiB
PHP
248 lines
7.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
require_once dirname(__DIR__, 2) . '/feca_mailshots_plugin/src/autoload.php';
|
|
|
|
use FecaMailshots\Application\DslCompiler;
|
|
use FecaMailshots\Application\DslValidator;
|
|
use FecaMailshots\Application\SourceMetadataProvider;
|
|
use FecaMailshots\Domain\DslParser;
|
|
use FecaMailshots\Infrastructure\DatabaseRouter;
|
|
use FecaMailshots\Infrastructure\DatabaseSourceMetadataProvider;
|
|
|
|
/** @param mixed $actual @param mixed $expected */
|
|
function feca_assert_same($actual, $expected, string $message): void
|
|
{
|
|
if ($actual !== $expected) {
|
|
fwrite(
|
|
STDERR,
|
|
$message . "\nExpected: " . json_encode($expected) . "\nActual: " . json_encode($actual) . "\n"
|
|
);
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
function feca_assert_contains(string $haystack, string $needle, string $message): void
|
|
{
|
|
if (strpos($haystack, $needle) === false) {
|
|
fwrite(STDERR, $message . "\nMissing: {$needle}\nSQL: {$haystack}\n");
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
$parser = new DslParser();
|
|
|
|
$customMetadata = new class implements SourceMetadataProvider {
|
|
/** @var array<string, list<string>> */
|
|
private array $fields = [
|
|
'MixedSchema.MixedTable' => ['MixedId', 'Label'],
|
|
'OtherSchema.OtherTable' => ['ParentId', 'Description'],
|
|
];
|
|
|
|
public function sourceExists(string $source): bool
|
|
{
|
|
return isset($this->fields[$source]);
|
|
}
|
|
|
|
public function isBuiltInSource(string $source): bool
|
|
{
|
|
return false;
|
|
}
|
|
|
|
public function sourceFields(string $source): array
|
|
{
|
|
return $this->fields[$source] ?? [];
|
|
}
|
|
|
|
public function hasEmailField(string $source): bool
|
|
{
|
|
return false;
|
|
}
|
|
|
|
public function joinPath(string $left, string $right): ?array
|
|
{
|
|
if ($left === 'MixedSchema.MixedTable' && $right === 'OtherSchema.OtherTable') {
|
|
return [
|
|
'left' => 'MixedSchema.MixedTable.MixedId',
|
|
'right' => 'OtherSchema.OtherTable.ParentId',
|
|
];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public function allKnownSources(): array
|
|
{
|
|
return array_keys($this->fields);
|
|
}
|
|
|
|
public function sourceTable(string $source): ?string
|
|
{
|
|
return $this->sourceExists($source) ? $source : null;
|
|
}
|
|
|
|
public function fieldSql(string $source, string $field, string $alias): ?string
|
|
{
|
|
if (!in_array($field, $this->fields[$source] ?? [], true)) {
|
|
return null;
|
|
}
|
|
return $alias . '.`' . str_replace('`', '``', $field) . '`';
|
|
}
|
|
};
|
|
|
|
$customDsl = 'MixedSchema.MixedTable and OtherSchema.OtherTable '
|
|
. 'where MixedSchema.MixedTable.MixedId = OtherSchema.OtherTable.ParentId';
|
|
$customAst = $parser->parse($customDsl);
|
|
feca_assert_same(
|
|
$customAst['sources'],
|
|
['MixedSchema.MixedTable', 'OtherSchema.OtherTable'],
|
|
'Expected custom schema and table case to be preserved'
|
|
);
|
|
feca_assert_same(
|
|
$customAst['where'][0]['lhs']['source'] ?? null,
|
|
'MixedSchema.MixedTable',
|
|
'Expected left field source case to be preserved'
|
|
);
|
|
feca_assert_same(
|
|
$customAst['where'][0]['rhs']['source'] ?? null,
|
|
'OtherSchema.OtherTable',
|
|
'Expected right field source case to be preserved'
|
|
);
|
|
|
|
$customValidator = new DslValidator($customMetadata);
|
|
$customValidation = $customValidator->validate($customAst);
|
|
feca_assert_same($customValidation['errors'], [], 'Expected two custom sources to validate');
|
|
|
|
$customSql = (string) ((new DslCompiler($customMetadata))->compile($customAst)['sql'] ?? '');
|
|
feca_assert_contains(
|
|
$customSql,
|
|
'INNER JOIN `OtherSchema`.`OtherTable` AS s_otherschema_othertable '
|
|
. 'ON s_mixedschema_mixedtable.`MixedId` = s_otherschema_othertable.`ParentId`',
|
|
'Expected schema-qualified join references to split at their final dot'
|
|
);
|
|
feca_assert_contains(
|
|
$customSql,
|
|
"COALESCE(s_mixedschema_mixedtable.`MixedId`, '') = "
|
|
. "COALESCE(s_otherschema_othertable.`ParentId`, '')",
|
|
'Expected schema-qualified field comparison to use NULL-as-empty semantics'
|
|
);
|
|
|
|
$comparisonMetadata = new class implements SourceMetadataProvider {
|
|
public function sourceExists(string $source): bool
|
|
{
|
|
return $source === 'records';
|
|
}
|
|
|
|
public function isBuiltInSource(string $source): bool
|
|
{
|
|
return $source === 'records';
|
|
}
|
|
|
|
public function sourceFields(string $source): array
|
|
{
|
|
return $source === 'records' ? ['left_value', 'right_value'] : [];
|
|
}
|
|
|
|
public function hasEmailField(string $source): bool
|
|
{
|
|
return false;
|
|
}
|
|
|
|
public function joinPath(string $left, string $right): ?array
|
|
{
|
|
return null;
|
|
}
|
|
|
|
public function allKnownSources(): array
|
|
{
|
|
return ['records'];
|
|
}
|
|
|
|
public function sourceTable(string $source): ?string
|
|
{
|
|
return $source === 'records' ? 'records' : null;
|
|
}
|
|
|
|
public function fieldSql(string $source, string $field, string $alias): ?string
|
|
{
|
|
if ($source !== 'records' || !in_array($field, $this->sourceFields($source), true)) {
|
|
return null;
|
|
}
|
|
return $alias . '.`' . $field . '`';
|
|
}
|
|
};
|
|
|
|
$comparisonCompiler = new DslCompiler($comparisonMetadata);
|
|
$literalCases = [
|
|
"records.left_value = 'text'" => ["COALESCE(s_records.`left_value`, '') = ?", 'text'],
|
|
"records.left_value != 'text'" => ["COALESCE(s_records.`left_value`, '') != ?", 'text'],
|
|
"records.left_value contains 'text'" => ["COALESCE(s_records.`left_value`, '') LIKE ?", '%text%'],
|
|
"records.left_value starts-with 'text'" => ["COALESCE(s_records.`left_value`, '') LIKE ?", 'text%'],
|
|
"records.left_value ends-with 'text'" => ["COALESCE(s_records.`left_value`, '') LIKE ?", '%text'],
|
|
];
|
|
foreach ($literalCases as $predicate => [$sqlNeedle, $expectedParam]) {
|
|
$compiled = $comparisonCompiler->compile($parser->parse('records where ' . $predicate));
|
|
feca_assert_contains((string) $compiled['sql'], $sqlNeedle, 'Expected literal comparison to normalize NULL');
|
|
feca_assert_same($compiled['params'], [$expectedParam], 'Expected literal comparison parameter');
|
|
}
|
|
|
|
foreach (['=', '!='] as $operator) {
|
|
$compiled = $comparisonCompiler->compile(
|
|
$parser->parse("records where records.left_value {$operator} records.right_value")
|
|
);
|
|
feca_assert_contains(
|
|
(string) $compiled['sql'],
|
|
"COALESCE(s_records.`left_value`, '') {$operator} COALESCE(s_records.`right_value`, '')",
|
|
'Expected field comparison to normalize NULL on both sides'
|
|
);
|
|
feca_assert_same($compiled['params'], [], 'Expected no field comparison parameters');
|
|
}
|
|
|
|
$router = new class implements DatabaseRouter {
|
|
public function mailshotsPdo(): PDO
|
|
{
|
|
throw new RuntimeException('Unexpected mailshots database access');
|
|
}
|
|
|
|
public function membersPdo(): PDO
|
|
{
|
|
throw new RuntimeException('Unexpected members database access');
|
|
}
|
|
|
|
public function mailshotsDbName(): string
|
|
{
|
|
return 'mailshots_test';
|
|
}
|
|
|
|
public function membersDbName(): string
|
|
{
|
|
return 'members_test';
|
|
}
|
|
|
|
public function fenDbName(): string
|
|
{
|
|
return 'FenCaseSensitive';
|
|
}
|
|
};
|
|
|
|
$databaseMetadata = new DatabaseSourceMetadataProvider($router);
|
|
feca_assert_same($databaseMetadata->sourceExists('ad_sizes'), true, 'Expected ad_sizes to be a known source');
|
|
feca_assert_same($databaseMetadata->isBuiltInSource('ad_sizes'), true, 'Expected ad_sizes to be built in');
|
|
feca_assert_same(
|
|
$databaseMetadata->sourceTable('ad_sizes'),
|
|
'FenCaseSensitive.Ad_Sizes',
|
|
'Expected ad_sizes to map to the configured FEN database and physical table'
|
|
);
|
|
feca_assert_same(
|
|
$databaseMetadata->joinPath('ads', 'ad_sizes'),
|
|
['left' => 'ads.AdSize', 'right' => 'ad_sizes.SizeName'],
|
|
'Expected forward ads/ad_sizes join metadata'
|
|
);
|
|
feca_assert_same(
|
|
$databaseMetadata->joinPath('ad_sizes', 'ads'),
|
|
['left' => 'ad_sizes.SizeName', 'right' => 'ads.AdSize'],
|
|
'Expected reverse ads/ad_sizes join metadata'
|
|
);
|
|
|
|
echo "Recent DSL change regression tests passed\n";
|