update_checker.php 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675
  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.6-terra'; // '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. if ($error) {
  207. $this->error("CURL error: $error");
  208. }
  209. if ($httpCode !== 200) {
  210. $this->error("OpenAI API error (HTTP $httpCode): $response");
  211. }
  212. // echo "LOG RAW RESPONSE: \n--------\n" . $response . "\n--------\n";
  213. $result = json_decode($response, true);
  214. if (!isset($result['choices'][0]['message']['content'])) {
  215. $this->error("Invalid OpenAI API response: $response");
  216. }
  217. if (empty($result['choices'][0]['message']['content'])) {
  218. $this->error("Content in empty. " . (isset($result['choices'][0]['finish_reason']) ? "finish_reason: " . $result['choices'][0]['finish_reason'] : ''));
  219. }
  220. return $result['choices'][0]['message']['content'];
  221. }
  222. /*
  223. private function checkInstallSQL($oldContent, $newContent, $updateScript) {
  224. echo "\n🔍 Checking install.sql update script...\n";
  225. $prompt = <<<PROMPT
  226. 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.
  227. OLD install.sql:
  228. ```sql
  229. $oldContent
  230. ```
  231. NEW install.sql:
  232. ```sql
  233. $newContent
  234. ```
  235. UPDATE SCRIPT (install.sql in updates folder):
  236. ```sql
  237. $updateScript
  238. ```
  239. Evaluate these criteria and respond with ONLY a JSON object:
  240. {
  241. "syntax_correct": {"pass": true/false, "details": "explanation"},
  242. "idempotent": {"pass": true/false, "details": "explanation"},
  243. "all_changes_included": {"pass": true/false, "details": "explanation"},
  244. "no_extra_changes": {"pass": true/false, "details": "explanation"},
  245. "old_records_deleted": {"pass": true/false, "details": "explanation"},
  246. "sys_objects_storage_warning": {"has_changes": true/false, "details": "explanation"},
  247. "inconsistencies": {"pass": true/false, "details": "explanation"}
  248. }
  249. Criteria:
  250. 1. syntax_correct: Check for SQL syntax errors
  251. 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)
  252. 3. all_changes_included: All inserts, updates, and schema changes from new file are in update script
  253. 4. no_extra_changes: No changes beyond what's in the new file
  254. 5. old_records_deleted: Records in old but not in new are properly deleted
  255. 6. sys_objects_storage_warning: Check if sys_objects_storage table is modified
  256. 7. inconsistencies: Any logical problems, data integrity issues, or potential errors
  257. 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.
  258. 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.
  259. Don't report inconsistent order in sys_form_display_inputs table - it's acceptable behaviour.
  260. Be thorough and critical.
  261. PROMPT;
  262. $response = $this->callOpenAI([
  263. ['role' => 'system', 'content' => 'You are a SQL expert validator. Respond only with valid JSON.'],
  264. ['role' => 'user', 'content' => $prompt]
  265. ]);
  266. return $this->parseJsonResponse($response, 'install.sql');
  267. }
  268. */
  269. private function checkEnableSQL($oldContent, $newContent, $updateScript) {
  270. echo "\n🔍 Checking enable.sql update script...\n";
  271. if (!$this->validateContentSize($oldContent, $newContent, $updateScript, 'enable.sql')) {
  272. return $this->getSkippedResults();
  273. }
  274. $prompt = <<<PROMPT
  275. 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.
  276. OLD enable.sql:
  277. ```sql
  278. $oldContent
  279. ```
  280. NEW enable.sql:
  281. ```sql
  282. $newContent
  283. ```
  284. UPDATE SCRIPT (enable.sql in updates folder):
  285. ```sql
  286. $updateScript
  287. ```
  288. Evaluate these criteria and respond with ONLY a JSON object:
  289. {
  290. "syntax_correct": {"pass": true/false, "details": "explanation"},
  291. "idempotent": {"pass": true/false, "details": "explanation"},
  292. "all_changes_included": {"pass": true/false, "details": "explanation"},
  293. "no_extra_changes": {"pass": true/false, "details": "explanation"},
  294. "old_records_deleted": {"pass": true/false, "details": "explanation"},
  295. "sys_objects_storage_warning": {"has_changes": true/false, "details": "explanation"},
  296. "inconsistencies": {"pass": true/false, "details": "explanation"}
  297. }
  298. Criteria:
  299. 1. syntax_correct: Check for SQL syntax errors
  300. 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)
  301. 3. all_changes_included: All changes from new file are in update script
  302. 4. no_extra_changes: No changes beyond what's in the new file
  303. 5. old_records_deleted: Records in old but not in new are properly deleted, if no such records then pass
  304. 6. sys_objects_storage_warning: Check if sys_objects_storage table is modified
  305. 7. inconsistencies: Any logical problems or potential errors
  306. 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.
  307. 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.
  308. Don't report inconsistent order in sys_form_display_inputs table - it's acceptable behaviour.
  309. Be thorough and critical.
  310. PROMPT;
  311. $response = $this->callOpenAI([
  312. ['role' => 'system', 'content' => 'You are a SQL expert validator. Respond only with valid JSON.'],
  313. ['role' => 'user', 'content' => $prompt]
  314. ]);
  315. return $this->parseJsonResponse($response, 'enable.sql');
  316. }
  317. private function checkInstallerPHPandInstallSQL($oldContent, $newContent, $installerScript, $installSQL) {
  318. echo "\n🔍 Checking installer.php and install.sql update script...\n";
  319. if (!$this->validateContentSize($oldContent, $newContent, $installerScript.$installSQL, 'install.php and enable.sql')) {
  320. return $this->getSkippedResults();
  321. }
  322. $prompt = <<<PROMPT
  323. 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.
  324. OLD install.sql:
  325. ```sql
  326. $oldContent
  327. ```
  328. NEW install.sql:
  329. ```sql
  330. $newContent
  331. ```
  332. PHP UPDATE SCRIPT (installer.php):
  333. ```php
  334. $installerScript
  335. ```
  336. SQL UPDATE SCRIPT (install.sql):
  337. ```php
  338. $installSQL
  339. ```
  340. Evaluate these criteria and respond with ONLY a JSON object:
  341. {
  342. "syntax_correct": {"pass": true/false, "details": "explanation"},
  343. "idempotent": {"pass": true/false, "details": "explanation"},
  344. "all_changes_included": {"pass": true/false, "details": "explanation"},
  345. "no_extra_changes": {"pass": true/false, "details": "explanation"},
  346. "old_records_deleted": {"pass": true/false, "details": "explanation"},
  347. "sys_objects_storage_warning": {"has_changes": true/false, "details": "explanation"},
  348. "inconsistencies": {"pass": true/false, "details": "explanation"}
  349. }
  350. Criteria:
  351. 1. syntax_correct: Check for SQL syntax errors
  352. 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)
  353. 3. all_changes_included: All changes from new file are in update script
  354. 4. no_extra_changes: No changes beyond what's in the new file
  355. 5. old_records_deleted: Records in old but not in new are properly deleted, if no such records then pass
  356. 6. sys_objects_storage_warning: Check if sys_objects_storage table is modified
  357. 7. inconsistencies: Any logical problems or potential errors
  358. 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.
  359. install.sql contains all other changes.
  360. Threat these two files as a single SQL file.
  361. Please note that installer.php has php logic for old/new fields checks and some other.
  362. 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.
  363. 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.
  364. Don't report inconsistent order in sys_form_display_inputs table - it's acceptable behaviour.
  365. Be thorough and critical.
  366. PROMPT;
  367. $response = $this->callOpenAI([
  368. ['role' => 'system', 'content' => 'You are a PHP and SQL expert validator. Respond only with valid JSON.'],
  369. ['role' => 'user', 'content' => $prompt]
  370. ]);
  371. return $this->parseJsonResponse($response, 'installer.php');
  372. }
  373. private function parseJsonResponse($response, $filename) {
  374. // Try to extract JSON from response
  375. $response = trim($response);
  376. // Remove markdown code blocks if present
  377. $response = preg_replace('/```json\s*/', '', $response);
  378. $response = preg_replace('/```\s*$/', '', $response);
  379. $response = trim($response);
  380. $data = json_decode($response, true);
  381. if (json_last_error() !== JSON_ERROR_NONE) {
  382. $this->error("Failed to parse JSON response for $filename: " . json_last_error_msg() . "\nResponse: $response");
  383. }
  384. return $data;
  385. }
  386. private function getSkippedResults() {
  387. return [
  388. 'syntax_correct' => ['pass' => null, 'details' => 'Skipped due to size constraints'],
  389. 'idempotent' => ['pass' => null, 'details' => 'Skipped due to size constraints'],
  390. 'all_changes_included' => ['pass' => null, 'details' => 'Skipped due to size constraints'],
  391. 'no_extra_changes' => ['pass' => null, 'details' => 'Skipped due to size constraints'],
  392. 'old_records_deleted' => ['pass' => null, 'details' => 'Skipped due to size constraints'],
  393. 'sys_objects_storage_warning' => ['has_changes' => false, 'details' => 'Skipped due to size constraints'],
  394. 'inconsistencies' => ['pass' => null, 'details' => 'Skipped due to size constraints']
  395. ];
  396. }
  397. private function displayResults($results, $title) {
  398. echo "\n" . str_repeat("=", 80) . "\n";
  399. echo " $title\n";
  400. echo str_repeat("=", 80) . "\n\n";
  401. $checks = [
  402. 'syntax_correct' => 'Syntax Correct',
  403. 'idempotent' => 'Idempotent (Can run multiple times)',
  404. 'all_changes_included' => 'All changes included',
  405. 'no_extra_changes' => 'No extra changes',
  406. 'old_records_deleted' => 'Old records deleted',
  407. 'inconsistencies' => 'No inconsistencies'
  408. ];
  409. foreach ($checks as $key => $label) {
  410. if (isset($results[$key])) {
  411. if ($results[$key]['pass'] === null) {
  412. $status = '⊘';
  413. $color = "\033[90m"; // Gray for skipped
  414. } else {
  415. $status = $results[$key]['pass'] ? '✓' : '✗';
  416. $color = $results[$key]['pass'] ? "\033[32m" : "\033[31m";
  417. }
  418. $reset = "\033[0m";
  419. echo sprintf("%-45s %s%s%s\n", $label, $color, $status, $reset);
  420. if (!empty($results[$key]['details'])) {
  421. echo " └─ " . $results[$key]['details'] . "\n";
  422. }
  423. }
  424. }
  425. // Special handling for sys_objects_storage warning
  426. if (isset($results['sys_objects_storage_warning'])) {
  427. $warning = $results['sys_objects_storage_warning'];
  428. if ($warning['has_changes']) {
  429. echo sprintf("%-45s %s⚠%s\n", 'sys_objects_storage changes', "\033[33m", "\033[0m");
  430. echo " └─ WARNING: " . $warning['details'] . "\n";
  431. } else {
  432. echo sprintf("%-45s %s✓%s\n", 'sys_objects_storage changes', "\033[32m", "\033[0m");
  433. echo " └─ " . $warning['details'] . "\n";
  434. }
  435. }
  436. echo "\n";
  437. }
  438. public function run() {
  439. echo "SQL Update Script Checker\n";
  440. echo "Module: {$this->moduleName}\n";
  441. echo "Version: {$this->version}\n";
  442. echo str_repeat("-", 80) . "\n";
  443. // Read update scripts
  444. $installerPHP = $this->readFile('install/installer.php');
  445. $installSQL = $this->readFile('install/sql/install.sql');
  446. $enableSQL = $this->readFile('install/sql/enable.sql');
  447. if ('system' == $this->moduleName) {
  448. $installerPHP = $this->readFile('script.php');
  449. $installSQL = $this->readFile('sql.sql');
  450. $enableSQL = null;
  451. }
  452. if ($installerPHP === null && $installSQL === null && $enableSQL === null) {
  453. $this->error("No update scripts found in {$this->updatePath}/install/");
  454. }
  455. // Check installer.php(script.php) and install.sql(sql.sql) together
  456. if ($installSQL !== null) {
  457. echo "\n📄 Found install.sql\n";
  458. $oldInstallSQL = $this->getContent("Enter OLD install.sql content:", 'old_install');
  459. $newInstallSQL = $this->getContent("Enter NEW install.sql content:", 'new_install');
  460. if ($oldInstallSQL === null && $newInstallSQL === null) {
  461. echo "\033[90m⊘ Skipping install.sql check (empty input)\033[0m\n";
  462. }
  463. else {
  464. $results = $this->checkInstallerPHPandInstallSQL($oldInstallSQL, $newInstallSQL, $installerPHP, $installSQL);
  465. $this->displayResults($results, "installer.php(script.php) and install.sql(sql.sql) Results");
  466. $this->results['installer.php'] = $results;
  467. }
  468. }
  469. // Check enable.sql
  470. if ($enableSQL !== null) {
  471. echo "\n📄 Found enable.sql\n";
  472. $oldEnableSQL = $this->getContent("Enter OLD enable.sql content:", 'old_enable');
  473. $newEnableSQL = $this->getContent("Enter NEW enable.sql content:", 'new_enable');
  474. if ($oldEnableSQL === null && $newEnableSQL === null) {
  475. echo "\033[90m⊘ Skipping enable.sql check (empty input)\033[0m\n";
  476. }
  477. else {
  478. $results = $this->checkEnableSQL($oldEnableSQL, $newEnableSQL, $enableSQL);
  479. $this->displayResults($results, "enable.sql Results");
  480. $this->results['enable.sql'] = $results;
  481. }
  482. }
  483. // Summary
  484. $this->displaySummary();
  485. }
  486. private function displaySummary() {
  487. echo str_repeat("=", 80) . "\n";
  488. echo " OVERALL SUMMARY\n";
  489. echo str_repeat("=", 80) . "\n\n";
  490. $totalPassed = 0;
  491. $totalFailed = 0;
  492. $totalSkipped = 0;
  493. $hasWarnings = false;
  494. foreach ($this->results as $file => $results) {
  495. foreach ($results as $key => $result) {
  496. if ($key === 'sys_objects_storage_warning') {
  497. if ($result['has_changes']) {
  498. $hasWarnings = true;
  499. }
  500. continue;
  501. }
  502. if (isset($result['pass'])) {
  503. if ($result['pass'] === null) {
  504. $totalSkipped++;
  505. } elseif ($result['pass']) {
  506. $totalPassed++;
  507. } else {
  508. $totalFailed++;
  509. }
  510. }
  511. }
  512. }
  513. echo "Total Checks Passed: \033[32m$totalPassed\033[0m\n";
  514. echo "Total Checks Failed: \033[31m$totalFailed\033[0m\n";
  515. if ($totalSkipped > 0) {
  516. echo "Total Checks Skipped: \033[90m$totalSkipped\033[0m\n";
  517. }
  518. if ($hasWarnings) {
  519. echo "Warnings: \033[33mYes (sys_objects_storage modified)\033[0m\n";
  520. }
  521. if ($totalFailed === 0 && $totalSkipped === 0 && !$hasWarnings) {
  522. echo "\n\033[32m✓ All checks passed!\033[0m\n";
  523. } elseif ($totalFailed === 0 && $totalSkipped === 0 && $hasWarnings) {
  524. echo "\n\033[33m⚠ All checks passed but review warnings!\033[0m\n";
  525. } elseif ($totalSkipped > 0 && $totalFailed === 0) {
  526. echo "\n\033[33m⊘ Some checks were skipped. Manual review recommended.\033[0m\n";
  527. } else {
  528. echo "\n\033[31m✗ Some checks failed. Review the results above.\033[0m\n";
  529. }
  530. echo "\n";
  531. }
  532. }
  533. // Main execution
  534. if ($argc < 3) {
  535. echo "Usage: php {$argv[0]} MODULE_DIR VERSION [--old-install=file] [--new-install=file] [--old-enable=file] [--new-enable=file]\n";
  536. echo "\nExamples:\n";
  537. echo " Interactive mode:\n";
  538. echo " php {$argv[0]} timeline 15.0.0-A2\n";
  539. echo " php {$argv[0]} system 15.0.0-A2\n\n";
  540. echo " File input mode:\n";
  541. echo " php {$argv[0]} timeline 14.0.0-RC1 \\\n";
  542. echo " --old-install=/path/to/old_install.sql \\\n";
  543. echo " --new-install=/path/to/new_install.sql \\\n";
  544. echo " --old-enable=/path/to/old_enable.sql \\\n";
  545. echo " --new-enable=/path/to/new_enable.sql\n\n";
  546. echo "Note: All four file options must be provided together, or none at all.\n";
  547. exit(1);
  548. }
  549. try {
  550. $checker = new SQLUpdateChecker($argv[1], $argv[2], array_slice($argv, 3));
  551. $checker->run();
  552. } catch (Exception $e) {
  553. echo "\033[31mERROR: " . $e->getMessage() . "\033[0m\n";
  554. exit(1);
  555. }