testing dsl changes

This commit is contained in:
Adrian Stephens 2026-06-24 20:17:05 +01:00
parent c5bfde12a6
commit 535a798d62
2 changed files with 255 additions and 2 deletions

View File

@ -1,3 +1,9 @@
Split advertiser name into forename and surname
Split advertiser name into forename and surname - done in twig in message
Add ad sizes into mailmerge
Add ad sizes into mailmerge - done
Why does filter ads and advertisers where issue(125) and ads.Price != 0 and not (advertisers.Categories contains 'discount') identify fewer records than expected. - donei
tig

View File

@ -0,0 +1,247 @@
<?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";