['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";