245 lines
10 KiB
PHP
245 lines
10 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;
|
|
|
|
$adSizesSource = 'ad_sizes';
|
|
$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'],
|
|
$adSizesSource => ['SizeName', 'Width', 'Height'],
|
|
'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);
|
|
}
|
|
}
|
|
|
|
$adSizesDsl = 'ads and ad_sizes where issue(125)';
|
|
$adSizesAst = $parser->parse($adSizesDsl);
|
|
$adSizesValidation = $validator->validate($adSizesAst);
|
|
if (($adSizesValidation['errors'] ?? []) !== []) {
|
|
fwrite(STDERR, "Expected valid DSL {$adSizesDsl}: " . json_encode($adSizesValidation['errors']) . "\n");
|
|
exit(1);
|
|
}
|
|
$adSizesSql = (string) ($compiler->compile($adSizesAst)['sql'] ?? '');
|
|
foreach ([
|
|
'INNER JOIN `ad_sizes` AS s_ad_sizes ON s_ads.`AdSize` = s_ad_sizes.`SizeName`',
|
|
's_ad_sizes.`Width` AS `ad_sizes.Width`',
|
|
] as $needle) {
|
|
if (strpos($adSizesSql, $needle) === false) {
|
|
fwrite(STDERR, "Expected Ad_Sizes SQL to contain {$needle}\n{$adSizesSql}\n");
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
$reverseAdSizesDsl = 'ad_sizes and ads where ad_sizes.SizeName = ads.AdSize';
|
|
$reverseAdSizesAst = $parser->parse($reverseAdSizesDsl);
|
|
$reverseAdSizesValidation = $validator->validate($reverseAdSizesAst);
|
|
if (($reverseAdSizesValidation['errors'] ?? []) !== []) {
|
|
fwrite(STDERR, "Expected valid reverse DSL {$reverseAdSizesDsl}: " . json_encode($reverseAdSizesValidation['errors']) . "\n");
|
|
exit(1);
|
|
}
|
|
$reverseAdSizesSql = (string) ($compiler->compile($reverseAdSizesAst)['sql'] ?? '');
|
|
if (strpos($reverseAdSizesSql, 'ON s_ad_sizes.`SizeName` = s_ads.`AdSize`') === false) {
|
|
fwrite(STDERR, "Expected reverse Ad_Sizes implied join\n{$reverseAdSizesSql}\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, "COALESCE(s_advertisers.`IsLapsed?`, '') = ?") === false) {
|
|
fwrite(STDERR, "Expected quoted physical field to compile directly\n{$quotedFieldSql}\n");
|
|
exit(1);
|
|
}
|
|
|
|
$notContainsDsl = "advertisers where not (advertisers.AdvertiserName contains 'discount')";
|
|
$notContainsSql = (string) ($compiler->compile($parser->parse($notContainsDsl))['sql'] ?? '');
|
|
if (strpos($notContainsSql, "NOT (COALESCE(s_advertisers.`AdvertiserName`, '') LIKE ?)") === false) {
|
|
fwrite(STDERR, "Expected negated contains to include NULL values via empty-string normalization\n{$notContainsSql}\n");
|
|
exit(1);
|
|
}
|
|
|
|
$fieldComparisonDsl = 'contacts and accounts where contacts.account_id = accounts.id';
|
|
$fieldComparisonSql = (string) ($compiler->compile($parser->parse($fieldComparisonDsl))['sql'] ?? '');
|
|
if (strpos($fieldComparisonSql, "COALESCE(s_contacts.`account_id`, '') = COALESCE(s_accounts.`id`, '')") === false) {
|
|
fwrite(STDERR, "Expected field comparison to normalize NULL values to empty strings\n{$fieldComparisonSql}\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";
|