From cf70880f48f3c348ba761e52a9d2e71ef4be7916 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Mon, 31 Aug 2026 10:24:59 +0800 Subject: [PATCH] fix: add the report execution statistics columns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every saved report fails to execute, in core and in every extension. `Report::updateExecutionStats()` sets `execution_count`, `average_execution_time` and `last_result_count` and then saves. The reports table has none of the three, so the save throws: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'execution_count' in 'field list' The query itself has already succeeded by that point — the failure happens while recording that it ran — so a working report reports an error, and the report builder's preview renders an empty result with nothing in the console to explain it. `getPerformanceMetrics()` returns all three and `cloneWithConfig()` resets all three, so this is a feature that was written and never migrated rather than a naming slip. The columns are distinct from `execution_time` and `row_count`, which 2025_09_25_084135_report_enhancements added and the API resource exposes: those describe the most recent run, these accumulate across runs. `average_execution_time` is a float because it holds a mean of integer millisecond timings. They are deliberately not added to `$fillable`. The model maintains them itself, and making server-owned statistics mass-assignable would let a client set its own execution count. ReportModelTest covers the rolling-average arithmetic well and could not catch this: it drives the model through a spy whose `save()` is a counter, so nothing it does reaches a schema. The new test closes that specific gap by reading the migrations, so it needs no database and stays in the pure-unit suite. It fails without the migration and passes with it. Verified against a running instance: before, all six of a company's saved reports failed with the missing-column error; after, all six execute and return rows, `execution_count` increments, and the rolling average updates. --- ...dd_report_execution_statistics_columns.php | 60 ++++++++++++++++++ .../Models/ReportExecutionColumnsTest.php | 63 +++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 migrations/2026_08_31_000000_add_report_execution_statistics_columns.php create mode 100644 tests/Unit/Models/ReportExecutionColumnsTest.php diff --git a/migrations/2026_08_31_000000_add_report_execution_statistics_columns.php b/migrations/2026_08_31_000000_add_report_execution_statistics_columns.php new file mode 100644 index 00000000..ad43896b --- /dev/null +++ b/migrations/2026_08_31_000000_add_report_execution_statistics_columns.php @@ -0,0 +1,60 @@ +unsignedInteger('execution_count')->default(0)->after('row_count'); + + // A mean of integer millisecond timings, so it needs somewhere for the + // fraction to go — `execution_time` is an integer because it holds one + // measurement rather than an average of several. + $table->float('average_execution_time')->nullable()->comment('Mean execution time in milliseconds across all runs')->after('execution_count'); + + $table->integer('last_result_count')->nullable()->after('average_execution_time'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('reports', function (Blueprint $table) { + $table->dropColumn([ + 'execution_count', + 'average_execution_time', + 'last_result_count', + ]); + }); + } +}; diff --git a/tests/Unit/Models/ReportExecutionColumnsTest.php b/tests/Unit/Models/ReportExecutionColumnsTest.php new file mode 100644 index 00000000..cad064e5 --- /dev/null +++ b/tests/Unit/Models/ReportExecutionColumnsTest.php @@ -0,0 +1,63 @@ +[A-Za-z]+\(\s*'([a-z0-9_]+)'/", $block, $found); + $columns = array_merge($columns, $found[1]); + } + } + + return array_unique($columns); +} + +it('has a column for every execution statistic the report model writes', function () { + $columns = reportsTableColumns(); + + expect($columns)->not->toBeEmpty('no reports blueprints were found to read'); + + foreach (['execution_count', 'average_execution_time', 'last_result_count', 'last_executed_at'] as $column) { + expect($columns)->toContain($column); + } +}); + +it('still has the per-run columns the api resource exposes', function () { + // execution_time and row_count describe the last run and are returned by the Report + // resource; the statistics above accumulate across runs. Both sets must exist — + // adding one must not be mistaken for replacing the other. + $columns = reportsTableColumns(); + + expect($columns)->toContain('execution_time') + ->and($columns)->toContain('row_count'); +});