feca-mailshots-plugin/tests/unit/test_dsl_fen_builtins.php

198 lines
8.1 KiB
PHP

<?php
declare(strict_types=1);
require_once dirname(__DIR__, 2) . '/feca_mailshots_plugin/src/autoload.php';
require_once __DIR__ . '/FakeMetadataProvider.php';
use FecaMailshots\Application\DslCompiler;
use FecaMailshots\Application\DslValidator;
use FecaMailshots\Domain\DslParser;
use FecaMailshots\Tests\Unit\FakeMetadataProvider;
$metadata = new FakeMetadataProvider([
'advertisers' => ['AdvertiserName', 'Selected', 'IsLapsed?'],
'ads' => ['ID', 'Advertiser', 'AdSize', 'Price', 'PageID', 'State', 'Notes'],
'pages' => ['ID', 'Issue', 'Page'],
'articles' => ['ID', 'PageID', 'ArticleNumber', 'Content', 'MemberName', 'Author', 'DCN', 'ArticleWords', 'OtherWords', 'OtherContent'],
'issues' => ['ID', 'IssueMonths', 'Description'],
'invoices' => ['id', 'issue', 'issue_id', 'ad_id', 'invoice_number', 'status'],
'contacts' => ['id', 'account_id', 'last_name', 'contact_email_1', 'is_fen_1', 'is_deleted'],
'accounts' => ['id', 'name', 'is_deleted'],
]);
$parser = new DslParser();
$validator = new DslValidator($metadata);
$compiler = new DslCompiler($metadata);
$cases = [
'ads and pages where issue(202605)' => [
'needle' => 's_pages.`Issue` = ?',
'params' => [202605],
],
'articles and pages where issue(202605)' => [
'needle' => 's_pages.`Issue` = ?',
'params' => [202605],
],
'advertisers and ads where selected' => [
'needle' => 'COALESCE(s_advertisers.`Selected`, 0) <> 0',
'params' => [],
],
'invoices and ads where invoice-ids(10, 11)' => [
'needle' => 's_invoices.`id` IN (?,?)',
'params' => [10, 11],
],
];
foreach ($cases as $dsl => $expect) {
$ast = $parser->parse($dsl);
$validation = $validator->validate($ast);
if (($validation['errors'] ?? []) !== []) {
fwrite(STDERR, "Expected valid DSL {$dsl}: " . json_encode($validation['errors']) . "\n");
exit(1);
}
$compiled = $compiler->compile($ast);
$sql = (string) ($compiled['sql'] ?? '');
if (strpos($sql, $expect['needle']) === false) {
fwrite(STDERR, "Expected SQL for {$dsl} to contain {$expect['needle']}\n{$sql}\n");
exit(1);
}
if (($compiled['params'] ?? []) !== $expect['params']) {
fwrite(STDERR, "Unexpected params for {$dsl}: " . json_encode($compiled['params'] ?? []) . "\n");
exit(1);
}
}
$projectionDsl = 'advertisers and ads where issue(125)';
$projectionAst = $parser->parse($projectionDsl);
$projectionValidation = $validator->validate($projectionAst);
if (($projectionValidation['errors'] ?? []) !== []) {
fwrite(STDERR, "Expected valid DSL {$projectionDsl}: " . json_encode($projectionValidation['errors']) . "\n");
exit(1);
}
$projection = $compiler->compile($projectionAst);
$projectionSql = (string) ($projection['sql'] ?? '');
foreach ([
'advertisers.AdvertiserName',
'advertisers.Selected',
'advertisers.IsLapsed?',
'ads.ID',
'ads.Advertiser',
'ads.AdSize',
'ads.Price',
'ads.PageID',
'ads.State',
'ads.Notes',
] as $qualifiedField) {
if (strpos($projectionSql, ' AS `' . $qualifiedField . '`') === false) {
fwrite(STDERR, "Expected projection for {$projectionDsl} to include {$qualifiedField}\n{$projectionSql}\n");
exit(1);
}
}
$quotedFieldDsl = 'advertisers where advertisers.`IsLapsed?` = true';
$quotedFieldAst = $parser->parse($quotedFieldDsl);
$quotedFieldValidation = $validator->validate($quotedFieldAst);
if (($quotedFieldValidation['errors'] ?? []) !== []) {
fwrite(STDERR, "Expected valid DSL {$quotedFieldDsl}: " . json_encode($quotedFieldValidation['errors']) . "\n");
exit(1);
}
$quotedFieldSql = (string) ($compiler->compile($quotedFieldAst)['sql'] ?? '');
if (strpos($quotedFieldSql, 's_advertisers.`IsLapsed?` = ?') === false) {
fwrite(STDERR, "Expected quoted physical field to compile directly\n{$quotedFieldSql}\n");
exit(1);
}
$issueCases = [
'issues where issue(202605)' => 's_issues.`ID` = ?',
'articles where issue(202605)' => 'p_article_issue.`Issue` = ?',
'ads where issue(202605)' => 'p_ad_issue.`Issue` = ?',
'advertisers where issue(202605)' => 'p_adv_issue.`Issue` = ?',
'invoices where issue(202605)' => 's_invoices.`issue_id` = ?',
'contacts and accounts and issues where fen1-contact and issue(202605)' => 'CROSS JOIN `issues` AS s_issues',
];
foreach ($issueCases as $dsl => $needle) {
$ast = $parser->parse($dsl);
$validation = $validator->validate($ast);
if (($validation['errors'] ?? []) !== []) {
fwrite(STDERR, "Expected valid DSL {$dsl}: " . json_encode($validation['errors']) . "\n");
exit(1);
}
$sql = (string) ($compiler->compile($ast)['sql'] ?? '');
if (strpos($sql, $needle) === false) {
fwrite(STDERR, "Expected issue filter SQL for {$dsl} to contain {$needle}\n{$sql}\n");
exit(1);
}
}
$softDeleteDsl = 'contacts and accounts where fen1-contact';
$softDeleteAst = $parser->parse($softDeleteDsl);
$softDeleteValidation = $validator->validate($softDeleteAst);
if (($softDeleteValidation['errors'] ?? []) !== []) {
fwrite(STDERR, "Expected valid DSL {$softDeleteDsl}: " . json_encode($softDeleteValidation['errors']) . "\n");
exit(1);
}
$softDeleteSql = (string) ($compiler->compile($softDeleteAst)['sql'] ?? '');
foreach ([
'(s_contacts.`is_deleted` IS NULL OR s_contacts.`is_deleted` + 0 = 0)',
'(s_accounts.`is_deleted` IS NULL OR s_accounts.`is_deleted` + 0 = 0)',
] as $needle) {
if (strpos($softDeleteSql, $needle) === false) {
fwrite(STDERR, "Expected soft-delete predicate {$needle}\n{$softDeleteSql}\n");
exit(1);
}
}
$articleExclusionDsl = 'contacts and accounts and issues where fen1-contact and issue(125) and member-or-affiliate-or-parish-council and not account-has-article-in-issue(125)';
$articleExclusionAst = $parser->parse($articleExclusionDsl);
$articleExclusionValidation = $validator->validate($articleExclusionAst);
if (($articleExclusionValidation['errors'] ?? []) !== []) {
fwrite(STDERR, "Expected valid DSL {$articleExclusionDsl}: " . json_encode($articleExclusionValidation['errors']) . "\n");
exit(1);
}
$articleExclusionCompiled = $compiler->compile($articleExclusionAst);
$articleExclusionSql = (string) ($articleExclusionCompiled['sql'] ?? '');
foreach ([
'NOT (EXISTS (SELECT 1 FROM `articles` AS article_account_issue',
'article_account_issue.`MemberName`',
'COLLATE utf8mb4_unicode_ci',
'page_account_issue.`Issue` = ?',
] as $needle) {
if (strpos($articleExclusionSql, $needle) === false) {
fwrite(STDERR, "Expected article exclusion SQL to contain {$needle}\n{$articleExclusionSql}\n");
exit(1);
}
}
if (($articleExclusionCompiled['params'] ?? []) !== [125, 125]) {
fwrite(STDERR, "Unexpected article exclusion params: " . json_encode($articleExclusionCompiled['params'] ?? []) . "\n");
exit(1);
}
$blankDsl = 'advertisers where advertisers.AdvertiserName blank';
$blankAst = $parser->parse($blankDsl);
$blankValidation = $validator->validate($blankAst);
if (($blankValidation['errors'] ?? []) !== []) {
fwrite(STDERR, "Expected valid DSL {$blankDsl}: " . json_encode($blankValidation['errors']) . "\n");
exit(1);
}
$blankSql = (string) ($compiler->compile($blankAst)['sql'] ?? '');
if (strpos($blankSql, "(s_advertisers.`AdvertiserName` IS NULL OR s_advertisers.`AdvertiserName` = '')") === false) {
fwrite(STDERR, "Expected blank field test to compile as null-or-empty check\n{$blankSql}\n");
exit(1);
}
$notBlankDsl = 'advertisers where advertisers.AdvertiserName != blank';
$notBlankAst = $parser->parse($notBlankDsl);
$notBlankValidation = $validator->validate($notBlankAst);
if (($notBlankValidation['errors'] ?? []) !== []) {
fwrite(STDERR, "Expected valid DSL {$notBlankDsl}: " . json_encode($notBlankValidation['errors']) . "\n");
exit(1);
}
$notBlankSql = (string) ($compiler->compile($notBlankAst)['sql'] ?? '');
if (strpos($notBlankSql, "(s_advertisers.`AdvertiserName` IS NOT NULL AND s_advertisers.`AdvertiserName` <> '')") === false) {
fwrite(STDERR, "Expected != blank field test to compile as not-null-and-not-empty check\n{$notBlankSql}\n");
exit(1);
}
echo "DSL FEN built-in tests passed\n";