diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b5665b..aec8b12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to `laravel-trend` will be documented in this file. +## 1.0.1 - 2025-04-18 + +- Fixed SQLite week interval date format compatibility issue +- Added support for SQLite 3.46+ ISO week format + ## 1.0.0 - 202X-XX-XX - initial release diff --git a/src/Adapters/SqliteAdapter.php b/src/Adapters/SqliteAdapter.php index 3e17022..a1431bb 100644 --- a/src/Adapters/SqliteAdapter.php +++ b/src/Adapters/SqliteAdapter.php @@ -3,21 +3,66 @@ namespace Flowframe\Trend\Adapters; use Error; +use Illuminate\Support\Facades\DB; class SqliteAdapter extends AbstractAdapter { public function format(string $column, string $interval): string { + // Get SQLite version + $sqliteVersion = null; + try { + $versionInfo = DB::select('SELECT sqlite_version() as version')[0] ?? null; + if ($versionInfo) { + $sqliteVersion = $versionInfo->version; + } + } catch (\Throwable $e) { + // Silently fail if version check fails + } + $format = match ($interval) { 'minute' => '%Y-%m-%d %H:%M:00', 'hour' => '%Y-%m-%d %H:00', 'day' => '%Y-%m-%d', - 'week' => '%Y-%W', + 'week' => $this->getWeekFormat($column, $sqliteVersion), 'month' => '%Y-%m', 'year' => '%Y', default => throw new Error('Invalid interval.'), }; - return "strftime('{$format}', {$column})"; + // For non-week intervals, use regular strftime formatting + if ($interval !== 'week' || $this->usesBuiltInISOWeek($sqliteVersion)) { + return "strftime('{$format}', {$column})"; + } + + // For week interval, return the concatenated expression + return $format; + } + + /** + * Get the appropriate SQL expression for week formatting based on SQLite version + */ + protected function getWeekFormat(string $column, ?string $sqliteVersion): string + { + // SQLite 3.46+ supports %G-%V format for ISO week numbers + if ($this->usesBuiltInISOWeek($sqliteVersion)) { + return '%G-%V'; + } + + // For older SQLite versions, use custom expression to match the ISO format + return "strftime('%Y', {$column}) || '-' || (strftime('%W', {$column}) + 1)"; + } + + /** + * Check if SQLite version supports ISO week format + */ + protected function usesBuiltInISOWeek(?string $version): bool + { + if (!$version) { + return false; + } + + $versionNumber = (float) $version; + return $versionNumber >= 3.46; } } diff --git a/tests/Feature/SqliteWeekFormatTest.php b/tests/Feature/SqliteWeekFormatTest.php new file mode 100644 index 0000000..1b30369 --- /dev/null +++ b/tests/Feature/SqliteWeekFormatTest.php @@ -0,0 +1,89 @@ +now = Carbon::parse('2023-01-15'); // middle of January 2023 + Carbon::setTestNow($this->now); + + // Mock the database connection and query builder + $this->builder = Mockery::mock(Builder::class); + $this->baseBuilder = Mockery::mock(); + + // Setup the builder to return our test values and SQLite driver + $this->builder->shouldReceive('getConnection->getDriverName')->andReturn('sqlite'); + $this->builder->shouldReceive('toBase')->andReturn($this->baseBuilder); + + // Create test data for two weeks (formatted as they would be from SQLite) + $week1 = '2023-2'; // Week 2 of 2023 + $week2 = '2023-3'; // Week 3 of 2023 + + $this->testData = collect([ + (object) ['date' => $week1, 'aggregate' => 10], + (object) ['date' => $week2, 'aggregate' => 20], + ]); +}); + +afterEach(function () { + Mockery::close(); + Carbon::setTestNow(); +}); + +it('correctly handles week format for SQLite', function () { + // Set up mock to return test data + $this->baseBuilder->shouldReceive('selectRaw')->andReturnSelf(); + $this->baseBuilder->shouldReceive('whereBetween')->andReturnSelf(); + $this->baseBuilder->shouldReceive('groupBy')->andReturnSelf(); + $this->baseBuilder->shouldReceive('orderBy')->andReturnSelf(); + $this->baseBuilder->shouldReceive('get')->andReturn($this->testData); + + // Create trend for weeks + $trend = Trend::query($this->builder) + ->between( + Carbon::parse('2023-01-01'), // Start of week 1 + Carbon::parse('2023-01-21') // End of week 3 + ) + ->perWeek() + ->count(); + + expect($trend)->toBeInstanceOf(Collection::class); + + // Should have 3 weeks (week 1, 2, and 3) + expect($trend)->toHaveCount(3); + + // Check that the weeks are correctly represented + $weeks = $trend->pluck('date')->toArray(); + expect($weeks)->toContain('2023-1'); + expect($weeks)->toContain('2023-2'); + expect($weeks)->toContain('2023-3'); + + // Week 1 should have 0 since we didn't provide data for it (falls back to placeholder) + $week1Value = $trend->firstWhere('date', '2023-1'); + expect($week1Value->aggregate)->toBe(0); + + // Week 2 should have 10 + $week2Value = $trend->firstWhere('date', '2023-2'); + expect($week2Value->aggregate)->toBe(10); + + // Week 3 should have 20 + $week3Value = $trend->firstWhere('date', '2023-3'); + expect($week3Value->aggregate)->toBe(20); +}); + +it('creates the right sqlite adapter format string for weeks', function () { + $adapter = new SqliteAdapter(); + + // Call the format method for the 'week' interval + $formatString = $adapter->format('created_at', 'week'); + + // Verify the format contains some expected text + expect($formatString)->toContain("strftime('%Y', created_at)"); + expect($formatString)->toContain("strftime('%W', created_at) + 1"); +}); \ No newline at end of file