Skip to content

Enhance SQLite week interval handling and add tests #88

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
49 changes: 47 additions & 2 deletions src/Adapters/SqliteAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
89 changes: 89 additions & 0 deletions tests/Feature/SqliteWeekFormatTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

use Carbon\Carbon;
use Flowframe\Trend\Adapters\SqliteAdapter;
use Flowframe\Trend\Trend;
use Flowframe\Trend\TrendValue;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Mockery;

beforeEach(function () {
$this->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");
});