update_checker.php 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676
  1. #!/usr/bin/env php
  2. <?php
  3. /**
  4. * SQL Update Script Checker
  5. * Validates SQL update scripts for correctness and completeness
  6. *
  7. * Usage: php script.php MODULE_DIR VERSION [--old-install=file] [--new-install=file] [--old-enable=file] [--new-enable=file]
  8. */
  9. class SQLUpdateChecker {
  10. private $moduleName;
  11. private $version;
  12. private $updatePath;
  13. private $apiKey;
  14. private $results = [];
  15. private $inputFiles = [];
  16. const API_ENDPOINT = 'https://api.openai.com/v1/chat/completions';
  17. const MODEL_VER = 5;
  18. // const MODEL = 'gpt-4-turbo';
  19. const MODEL = 'gpt-5'; // 'gpt-5-mini';
  20. const MAX_TOKENS_PER_REQUEST = 140000; // Conservative limit for model context // 120000 for gpt4
  21. const MAX_TOKENS_GPT5 = 9000; // can be increased if "finish_reason: length" error occured
  22. const MAX_TOKENS = 4000;
  23. const TOKENS_PER_CHAR = 0.325; // Approximate tokens per character, to evaluate potential silent truncation
  24. public function __construct($moduleName, $version, $args) {
  25. $this->moduleName = $moduleName;
  26. $this->version = $version;
  27. $this->parseInputFiles($args);
  28. $this->loadApiKey();
  29. $this->findUpdatePath();
  30. }
  31. private function parseInputFiles($args) {
  32. $fileOptions = [
  33. '--old-install' => 'old_install',
  34. '--new-install' => 'new_install',
  35. '--old-enable' => 'old_enable',
  36. '--new-enable' => 'new_enable'
  37. ];
  38. foreach ($args as $arg) {
  39. foreach ($fileOptions as $option => $key) {
  40. if (strpos($arg, $option . '=') === 0) {
  41. $file = substr($arg, strlen($option) + 1);
  42. if (!file_exists($file)) {
  43. $this->error("File not found: $file");
  44. }
  45. $this->inputFiles[$key] = $file;
  46. }
  47. }
  48. }
  49. // Validate: either all files provided or none
  50. $providedCount = count($this->inputFiles);
  51. if ($providedCount > 0 && $providedCount < 4) {
  52. $this->error("Input files must be provided for all or none. Required: --old-install, --new-install, --old-enable, --new-enable");
  53. }
  54. if ($providedCount === 4) {
  55. echo "✓ Using input files:\n";
  56. echo " - Old install.sql: {$this->inputFiles['old_install']}\n";
  57. echo " - New install.sql: {$this->inputFiles['new_install']}\n";
  58. echo " - Old enable.sql: {$this->inputFiles['old_enable']}\n";
  59. echo " - New enable.sql: {$this->inputFiles['new_enable']}\n";
  60. }
  61. }
  62. private function loadApiKey() {
  63. $keyFile = $_SERVER['HOME'] . '/.una_update_script_checker';
  64. if (!file_exists($keyFile)) {
  65. $this->error("API key file not found: $keyFile");
  66. }
  67. $this->apiKey = trim(file_get_contents($keyFile));
  68. if (empty($this->apiKey)) {
  69. $this->error("API key is empty in $keyFile");
  70. }
  71. }
  72. private function findUpdatePath() {
  73. $basePattern = "modules/boonex/{$this->moduleName}/updates/*_{$this->version}";
  74. if ('system' == $this->moduleName)
  75. $basePattern = "upgrade/files/*-{$this->version}";
  76. $paths = glob($basePattern);
  77. if (empty($paths)) {
  78. $this->error("No update folder found matching pattern: $basePattern");
  79. }
  80. if (count($paths) > 1) {
  81. $this->error("Multiple update folders found: " . implode(', ', $paths));
  82. }
  83. $this->updatePath = $paths[0];
  84. echo "✓ Found update path: {$this->updatePath}\n\n";
  85. }
  86. private function error($message) {
  87. echo "ERROR: $message\n";
  88. exit(1);
  89. }
  90. private function readFile($relativePath) {
  91. $fullPath = $this->updatePath . '/' . $relativePath;
  92. if (!file_exists($fullPath)) {
  93. return null;
  94. }
  95. return file_get_contents($fullPath);
  96. }
  97. private function getContentFromFile($fileKey) {
  98. if (!isset($this->inputFiles[$fileKey])) {
  99. return null;
  100. }
  101. $content = file_get_contents($this->inputFiles[$fileKey]);
  102. if ($content === false) {
  103. $this->error("Failed to read file: {$this->inputFiles[$fileKey]}");
  104. }
  105. $content = trim($content);
  106. // Show file stats
  107. $charCount = strlen($content);
  108. $estimatedTokens = (int)($charCount * self::TOKENS_PER_CHAR);
  109. echo " → File: {$this->inputFiles[$fileKey]}\n";
  110. echo " → Size: " . number_format($charCount) . " characters (~" . number_format($estimatedTokens) . " tokens)\n";
  111. return $content;
  112. }
  113. private function getUserInput($prompt) {
  114. echo "\n$prompt\n";
  115. echo "Enter content (end with a line containing only '~~~!'):\n";
  116. echo "(Leave empty to skip this check)\n";
  117. $content = '';
  118. $lineCount = 0;
  119. while (true) {
  120. $line = fgets(STDIN);
  121. if ($line === false) {
  122. $this->error("Failed to read input");
  123. }
  124. if (trim($line) === '~~~!') {
  125. break;
  126. }
  127. $content .= $line;
  128. $lineCount++;
  129. }
  130. $content = trim($content);
  131. if (empty($content)) {
  132. return null; // Signal to skip this check
  133. }
  134. // Check content size
  135. $charCount = strlen($content);
  136. $estimatedTokens = (int)($charCount * self::TOKENS_PER_CHAR);
  137. echo " → Input size: " . number_format($charCount) . " characters (~" . number_format($estimatedTokens) . " tokens)\n";
  138. return $content;
  139. }
  140. private function getContent($prompt, $fileKey) {
  141. // If using file input mode
  142. if (!empty($this->inputFiles)) {
  143. return $this->getContentFromFile($fileKey);
  144. }
  145. // Otherwise use manual input
  146. return $this->getUserInput($prompt);
  147. }
  148. private function estimatePromptTokens($oldContent, $newContent, $updateScript) {
  149. $totalChars = strlen($oldContent) + strlen($newContent) + strlen($updateScript) + 1000; // +1000 for prompt text
  150. return (int)($totalChars * self::TOKENS_PER_CHAR);
  151. }
  152. private function validateContentSize($oldContent, $newContent, $updateScript, $scriptType) {
  153. $estimatedTokens = $this->estimatePromptTokens($oldContent, $newContent, $updateScript);
  154. echo " → Estimated total tokens for analysis: ~" . number_format($estimatedTokens) . "\n";
  155. if ($estimatedTokens > self::MAX_TOKENS_PER_REQUEST) {
  156. echo "\n\033[31m";
  157. echo "ERROR: Content is too large for analysis!\n";
  158. echo " Estimated tokens: " . number_format($estimatedTokens) . "\n";
  159. echo " Maximum allowed: " . number_format(self::MAX_TOKENS_PER_REQUEST) . "\n";
  160. echo " \n";
  161. echo " This would result in silent truncation or API errors.\n";
  162. echo " Please consider:\n";
  163. echo " - Breaking the update into smaller parts\n";
  164. echo " - Analyzing only the most critical sections\n";
  165. echo " - Using a different validation approach for large files\n";
  166. echo "\033[0m\n";
  167. echo "\nDo you want to continue anyway? (y/N): ";
  168. $response = trim(fgets(STDIN));
  169. if (strtolower($response) !== 'y') {
  170. echo "Skipping $scriptType analysis.\n";
  171. return false;
  172. }
  173. echo "\033[33m⚠ Proceeding with analysis - results may be incomplete!\033[0m\n\n";
  174. } elseif ($estimatedTokens > self::MAX_TOKENS_PER_REQUEST * 0.8) {
  175. echo "\033[33m ⚠ Warning: Content is approaching token limits (" .
  176. number_format((int)($estimatedTokens / self::MAX_TOKENS_PER_REQUEST * 100)) . "% of max)\033[0m\n";
  177. }
  178. return true;
  179. }
  180. private function callOpenAI($messages) {
  181. $data = [
  182. 'model' => self::MODEL,
  183. 'messages' => $messages,
  184. ];
  185. if (self::MODEL_VER == 5) {
  186. $data['max_completion_tokens'] = self::MAX_TOKENS_GPT5; // 8000
  187. }
  188. else {
  189. $data['temperature'] = 0.1;
  190. $data['max_tokens'] = self::MAX_TOKENS;
  191. }
  192. $ch = curl_init(self::API_ENDPOINT);
  193. curl_setopt_array($ch, [
  194. CURLOPT_RETURNTRANSFER => true,
  195. CURLOPT_POST => true,
  196. CURLOPT_HTTPHEADER => [
  197. 'Content-Type: application/json',
  198. 'Authorization: Bearer ' . $this->apiKey
  199. ],
  200. CURLOPT_POSTFIELDS => json_encode($data),
  201. CURLOPT_TIMEOUT => 360
  202. ]);
  203. $response = curl_exec($ch);
  204. $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  205. $error = curl_error($ch);
  206. curl_close($ch);
  207. if ($error) {
  208. $this->error("CURL error: $error");
  209. }
  210. if ($httpCode !== 200) {
  211. $this->error("OpenAI API error (HTTP $httpCode): $response");
  212. }
  213. // echo "LOG RAW RESPONSE: \n--------\n" . $response . "\n--------\n";
  214. $result = json_decode($response, true);
  215. if (!isset($result['choices'][0]['message']['content'])) {
  216. $this->error("Invalid OpenAI API response: $response");
  217. }
  218. if (empty($result['choices'][0]['message']['content'])) {
  219. $this->error("Content in empty. " . (isset($result['choices'][0]['finish_reason']) ? "finish_reason: " . $result['choices'][0]['finish_reason'] : ''));
  220. }
  221. return $result['choices'][0]['message']['content'];
  222. }
  223. /*
  224. private function checkInstallSQL($oldContent, $newContent, $updateScript) {
  225. echo "\n🔍 Checking install.sql update script...\n";
  226. $prompt = <<<PROMPT
  227. You are an expert SQL validator. Analyze the SQL update script and check if it properly transforms the old install.sql to the new install.sql.
  228. OLD install.sql:
  229. ```sql
  230. $oldContent
  231. ```
  232. NEW install.sql:
  233. ```sql
  234. $newContent
  235. ```
  236. UPDATE SCRIPT (install.sql in updates folder):
  237. ```sql
  238. $updateScript
  239. ```
  240. Evaluate these criteria and respond with ONLY a JSON object:
  241. {
  242. "syntax_correct": {"pass": true/false, "details": "explanation"},
  243. "idempotent": {"pass": true/false, "details": "explanation"},
  244. "all_changes_included": {"pass": true/false, "details": "explanation"},
  245. "no_extra_changes": {"pass": true/false, "details": "explanation"},
  246. "old_records_deleted": {"pass": true/false, "details": "explanation"},
  247. "sys_objects_storage_warning": {"has_changes": true/false, "details": "explanation"},
  248. "inconsistencies": {"pass": true/false, "details": "explanation"}
  249. }
  250. Criteria:
  251. 1. syntax_correct: Check for SQL syntax errors
  252. 2. idempotent: Can be run multiple times without errors/duplicates, it uses IF EXISTS, IF NOT EXISTS, INSERT IGNORE and also deleting the same records before inserting - it's most used technique to avoid duplicates)
  253. 3. all_changes_included: All inserts, updates, and schema changes from new file are in update script
  254. 4. no_extra_changes: No changes beyond what's in the new file
  255. 5. old_records_deleted: Records in old but not in new are properly deleted
  256. 6. sys_objects_storage_warning: Check if sys_objects_storage table is modified
  257. 7. inconsistencies: Any logical problems, data integrity issues, or potential errors
  258. Very important - evaluate each block of code in sql file (which starts with comment and until next comment) and provide some short summary if everything is ok there.
  259. Most important is that there is a special technique for handling duplicates - before inserting a records it deletes the previous record, so there should be no dulicates upon repeated execution. If potencial duplicates still found - then print snippet with explanation.
  260. Don't report inconsistent order in sys_form_display_inputs table - it's acceptable behaviour.
  261. Be thorough and critical.
  262. PROMPT;
  263. $response = $this->callOpenAI([
  264. ['role' => 'system', 'content' => 'You are a SQL expert validator. Respond only with valid JSON.'],
  265. ['role' => 'user', 'content' => $prompt]
  266. ]);
  267. return $this->parseJsonResponse($response, 'install.sql');
  268. }
  269. */
  270. private function checkEnableSQL($oldContent, $newContent, $updateScript) {
  271. echo "\n🔍 Checking enable.sql update script...\n";
  272. if (!$this->validateContentSize($oldContent, $newContent, $updateScript, 'enable.sql')) {
  273. return $this->getSkippedResults();
  274. }
  275. $prompt = <<<PROMPT
  276. You are an expert SQL validator. Analyze the SQL update script and check if it properly transforms the old enable.sql to the new enable.sql.
  277. OLD enable.sql:
  278. ```sql
  279. $oldContent
  280. ```
  281. NEW enable.sql:
  282. ```sql
  283. $newContent
  284. ```
  285. UPDATE SCRIPT (enable.sql in updates folder):
  286. ```sql
  287. $updateScript
  288. ```
  289. Evaluate these criteria and respond with ONLY a JSON object:
  290. {
  291. "syntax_correct": {"pass": true/false, "details": "explanation"},
  292. "idempotent": {"pass": true/false, "details": "explanation"},
  293. "all_changes_included": {"pass": true/false, "details": "explanation"},
  294. "no_extra_changes": {"pass": true/false, "details": "explanation"},
  295. "old_records_deleted": {"pass": true/false, "details": "explanation"},
  296. "sys_objects_storage_warning": {"has_changes": true/false, "details": "explanation"},
  297. "inconsistencies": {"pass": true/false, "details": "explanation"}
  298. }
  299. Criteria:
  300. 1. syntax_correct: Check for SQL syntax errors
  301. 2. idempotent: Can be run multiple times without errors/duplicates, it uses IF EXISTS, IF NOT EXISTS, INSERT IGNORE and also deleting the same records before inserting - it's most used technique to avoid duplicates)
  302. 3. all_changes_included: All changes from new file are in update script
  303. 4. no_extra_changes: No changes beyond what's in the new file
  304. 5. old_records_deleted: Records in old but not in new are properly deleted, if no such records then pass
  305. 6. sys_objects_storage_warning: Check if sys_objects_storage table is modified
  306. 7. inconsistencies: Any logical problems or potential errors
  307. Very important - evaluate each block of code in sql file (which starts with comment and until next comment) and provide some short summary if everything is ok there.
  308. Most important is that there is a special technique for handling duplicates - before inserting a records it deletes the previous record, so there should be no dulicates upon repeated execution. If potencial duplicates still found - then print snippet with explanation.
  309. Don't report inconsistent order in sys_form_display_inputs table - it's acceptable behaviour.
  310. Be thorough and critical.
  311. PROMPT;
  312. $response = $this->callOpenAI([
  313. ['role' => 'system', 'content' => 'You are a SQL expert validator. Respond only with valid JSON.'],
  314. ['role' => 'user', 'content' => $prompt]
  315. ]);
  316. return $this->parseJsonResponse($response, 'enable.sql');
  317. }
  318. private function checkInstallerPHPandInstallSQL($oldContent, $newContent, $installerScript, $installSQL) {
  319. echo "\n🔍 Checking installer.php and install.sql update script...\n";
  320. if (!$this->validateContentSize($oldContent, $newContent, $installerScript.$installSQL, 'install.php and enable.sql')) {
  321. return $this->getSkippedResults();
  322. }
  323. $prompt = <<<PROMPT
  324. You are an expert PHP and SQL validator. Analyze the installer.php and install.sql update script and check if these files properly handles the transformation from old to new install.sql.
  325. OLD install.sql:
  326. ```sql
  327. $oldContent
  328. ```
  329. NEW install.sql:
  330. ```sql
  331. $newContent
  332. ```
  333. PHP UPDATE SCRIPT (installer.php):
  334. ```php
  335. $installerScript
  336. ```
  337. SQL UPDATE SCRIPT (install.sql):
  338. ```php
  339. $installSQL
  340. ```
  341. Evaluate these criteria and respond with ONLY a JSON object:
  342. {
  343. "syntax_correct": {"pass": true/false, "details": "explanation"},
  344. "idempotent": {"pass": true/false, "details": "explanation"},
  345. "all_changes_included": {"pass": true/false, "details": "explanation"},
  346. "no_extra_changes": {"pass": true/false, "details": "explanation"},
  347. "old_records_deleted": {"pass": true/false, "details": "explanation"},
  348. "sys_objects_storage_warning": {"has_changes": true/false, "details": "explanation"},
  349. "inconsistencies": {"pass": true/false, "details": "explanation"}
  350. }
  351. Criteria:
  352. 1. syntax_correct: Check for SQL syntax errors
  353. 2. idempotent: Can be run multiple times without errors/duplicates, it uses IF EXISTS, IF NOT EXISTS, INSERT IGNORE and also deleting the same records before inserting - it's most used technique to avoid duplicates)
  354. 3. all_changes_included: All changes from new file are in update script
  355. 4. no_extra_changes: No changes beyond what's in the new file
  356. 5. old_records_deleted: Records in old but not in new are properly deleted, if no such records then pass
  357. 6. sys_objects_storage_warning: Check if sys_objects_storage table is modified
  358. 7. inconsistencies: Any logical problems or potential errors
  359. installer.php must contain the database schema changes, if no changes in table fields and other structural changes, then it will not have any SQL queries.
  360. install.sql contains all other changes.
  361. Threat these two files as a single SQL file.
  362. Please note that installer.php has php logic for old/new fields checks and some other.
  363. Very important - evaluate each block of code in sql file (which starts with comment and until next comment) and provide some short summary if everything is ok there.
  364. Most important is that there is a special technique for handling duplicates - before inserting a records it deletes the previous record, so there should be no dulicates upon repeated execution. If potencial duplicates still found - then print snippet with explanation.
  365. Don't report inconsistent order in sys_form_display_inputs table - it's acceptable behaviour.
  366. Be thorough and critical.
  367. PROMPT;
  368. $response = $this->callOpenAI([
  369. ['role' => 'system', 'content' => 'You are a PHP and SQL expert validator. Respond only with valid JSON.'],
  370. ['role' => 'user', 'content' => $prompt]
  371. ]);
  372. return $this->parseJsonResponse($response, 'installer.php');
  373. }
  374. private function parseJsonResponse($response, $filename) {
  375. // Try to extract JSON from response
  376. $response = trim($response);
  377. // Remove markdown code blocks if present
  378. $response = preg_replace('/```json\s*/', '', $response);
  379. $response = preg_replace('/```\s*$/', '', $response);
  380. $response = trim($response);
  381. $data = json_decode($response, true);
  382. if (json_last_error() !== JSON_ERROR_NONE) {
  383. $this->error("Failed to parse JSON response for $filename: " . json_last_error_msg() . "\nResponse: $response");
  384. }
  385. return $data;
  386. }
  387. private function getSkippedResults() {
  388. return [
  389. 'syntax_correct' => ['pass' => null, 'details' => 'Skipped due to size constraints'],
  390. 'idempotent' => ['pass' => null, 'details' => 'Skipped due to size constraints'],
  391. 'all_changes_included' => ['pass' => null, 'details' => 'Skipped due to size constraints'],
  392. 'no_extra_changes' => ['pass' => null, 'details' => 'Skipped due to size constraints'],
  393. 'old_records_deleted' => ['pass' => null, 'details' => 'Skipped due to size constraints'],
  394. 'sys_objects_storage_warning' => ['has_changes' => false, 'details' => 'Skipped due to size constraints'],
  395. 'inconsistencies' => ['pass' => null, 'details' => 'Skipped due to size constraints']
  396. ];
  397. }
  398. private function displayResults($results, $title) {
  399. echo "\n" . str_repeat("=", 80) . "\n";
  400. echo " $title\n";
  401. echo str_repeat("=", 80) . "\n\n";
  402. $checks = [
  403. 'syntax_correct' => 'Syntax Correct',
  404. 'idempotent' => 'Idempotent (Can run multiple times)',
  405. 'all_changes_included' => 'All changes included',
  406. 'no_extra_changes' => 'No extra changes',
  407. 'old_records_deleted' => 'Old records deleted',
  408. 'inconsistencies' => 'No inconsistencies'
  409. ];
  410. foreach ($checks as $key => $label) {
  411. if (isset($results[$key])) {
  412. if ($results[$key]['pass'] === null) {
  413. $status = '⊘';
  414. $color = "\033[90m"; // Gray for skipped
  415. } else {
  416. $status = $results[$key]['pass'] ? '✓' : '✗';
  417. $color = $results[$key]['pass'] ? "\033[32m" : "\033[31m";
  418. }
  419. $reset = "\033[0m";
  420. echo sprintf("%-45s %s%s%s\n", $label, $color, $status, $reset);
  421. if (!empty($results[$key]['details'])) {
  422. echo " └─ " . $results[$key]['details'] . "\n";
  423. }
  424. }
  425. }
  426. // Special handling for sys_objects_storage warning
  427. if (isset($results['sys_objects_storage_warning'])) {
  428. $warning = $results['sys_objects_storage_warning'];
  429. if ($warning['has_changes']) {
  430. echo sprintf("%-45s %s⚠%s\n", 'sys_objects_storage changes', "\033[33m", "\033[0m");
  431. echo " └─ WARNING: " . $warning['details'] . "\n";
  432. } else {
  433. echo sprintf("%-45s %s✓%s\n", 'sys_objects_storage changes', "\033[32m", "\033[0m");
  434. echo " └─ " . $warning['details'] . "\n";
  435. }
  436. }
  437. echo "\n";
  438. }
  439. public function run() {
  440. echo "SQL Update Script Checker\n";
  441. echo "Module: {$this->moduleName}\n";
  442. echo "Version: {$this->version}\n";
  443. echo str_repeat("-", 80) . "\n";
  444. // Read update scripts
  445. $installerPHP = $this->readFile('install/installer.php');
  446. $installSQL = $this->readFile('install/sql/install.sql');
  447. $enableSQL = $this->readFile('install/sql/enable.sql');
  448. if ('system' == $this->moduleName) {
  449. $installerPHP = $this->readFile('script.php');
  450. $installSQL = $this->readFile('sql.sql');
  451. $enableSQL = null;
  452. }
  453. if ($installerPHP === null && $installSQL === null && $enableSQL === null) {
  454. $this->error("No update scripts found in {$this->updatePath}/install/");
  455. }
  456. // Check installer.php(script.php) and install.sql(sql.sql) together
  457. if ($installSQL !== null) {
  458. echo "\n📄 Found install.sql\n";
  459. $oldInstallSQL = $this->getContent("Enter OLD install.sql content:", 'old_install');
  460. $newInstallSQL = $this->getContent("Enter NEW install.sql content:", 'new_install');
  461. if ($oldInstallSQL === null && $newInstallSQL === null) {
  462. echo "\033[90m⊘ Skipping install.sql check (empty input)\033[0m\n";
  463. }
  464. else {
  465. $results = $this->checkInstallerPHPandInstallSQL($oldInstallSQL, $newInstallSQL, $installerPHP, $installSQL);
  466. $this->displayResults($results, "installer.php(script.php) and install.sql(sql.sql) Results");
  467. $this->results['installer.php'] = $results;
  468. }
  469. }
  470. // Check enable.sql
  471. if ($enableSQL !== null) {
  472. echo "\n📄 Found enable.sql\n";
  473. $oldEnableSQL = $this->getContent("Enter OLD enable.sql content:", 'old_enable');
  474. $newEnableSQL = $this->getContent("Enter NEW enable.sql content:", 'new_enable');
  475. if ($oldEnableSQL === null && $newEnableSQL === null) {
  476. echo "\033[90m⊘ Skipping enable.sql check (empty input)\033[0m\n";
  477. }
  478. else {
  479. $results = $this->checkEnableSQL($oldEnableSQL, $newEnableSQL, $enableSQL);
  480. $this->displayResults($results, "enable.sql Results");
  481. $this->results['enable.sql'] = $results;
  482. }
  483. }
  484. // Summary
  485. $this->displaySummary();
  486. }
  487. private function displaySummary() {
  488. echo str_repeat("=", 80) . "\n";
  489. echo " OVERALL SUMMARY\n";
  490. echo str_repeat("=", 80) . "\n\n";
  491. $totalPassed = 0;
  492. $totalFailed = 0;
  493. $totalSkipped = 0;
  494. $hasWarnings = false;
  495. foreach ($this->results as $file => $results) {
  496. foreach ($results as $key => $result) {
  497. if ($key === 'sys_objects_storage_warning') {
  498. if ($result['has_changes']) {
  499. $hasWarnings = true;
  500. }
  501. continue;
  502. }
  503. if (isset($result['pass'])) {
  504. if ($result['pass'] === null) {
  505. $totalSkipped++;
  506. } elseif ($result['pass']) {
  507. $totalPassed++;
  508. } else {
  509. $totalFailed++;
  510. }
  511. }
  512. }
  513. }
  514. echo "Total Checks Passed: \033[32m$totalPassed\033[0m\n";
  515. echo "Total Checks Failed: \033[31m$totalFailed\033[0m\n";
  516. if ($totalSkipped > 0) {
  517. echo "Total Checks Skipped: \033[90m$totalSkipped\033[0m\n";
  518. }
  519. if ($hasWarnings) {
  520. echo "Warnings: \033[33mYes (sys_objects_storage modified)\033[0m\n";
  521. }
  522. if ($totalFailed === 0 && $totalSkipped === 0 && !$hasWarnings) {
  523. echo "\n\033[32m✓ All checks passed!\033[0m\n";
  524. } elseif ($totalFailed === 0 && $totalSkipped === 0 && $hasWarnings) {
  525. echo "\n\033[33m⚠ All checks passed but review warnings!\033[0m\n";
  526. } elseif ($totalSkipped > 0 && $totalFailed === 0) {
  527. echo "\n\033[33m⊘ Some checks were skipped. Manual review recommended.\033[0m\n";
  528. } else {
  529. echo "\n\033[31m✗ Some checks failed. Review the results above.\033[0m\n";
  530. }
  531. echo "\n";
  532. }
  533. }
  534. // Main execution
  535. if ($argc < 3) {
  536. echo "Usage: php {$argv[0]} MODULE_DIR VERSION [--old-install=file] [--new-install=file] [--old-enable=file] [--new-enable=file]\n";
  537. echo "\nExamples:\n";
  538. echo " Interactive mode:\n";
  539. echo " php {$argv[0]} timeline 15.0.0-A2\n";
  540. echo " php {$argv[0]} system 15.0.0-A2\n\n";
  541. echo " File input mode:\n";
  542. echo " php {$argv[0]} timeline 14.0.0-RC1 \\\n";
  543. echo " --old-install=/path/to/old_install.sql \\\n";
  544. echo " --new-install=/path/to/new_install.sql \\\n";
  545. echo " --old-enable=/path/to/old_enable.sql \\\n";
  546. echo " --new-enable=/path/to/new_enable.sql\n\n";
  547. echo "Note: All four file options must be provided together, or none at all.\n";
  548. exit(1);
  549. }
  550. try {
  551. $checker = new SQLUpdateChecker($argv[1], $argv[2], array_slice($argv, 3));
  552. $checker->run();
  553. } catch (Exception $e) {
  554. echo "\033[31mERROR: " . $e->getMessage() . "\033[0m\n";
  555. exit(1);
  556. }