Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
3 changes: 3 additions & 0 deletions app/Config/Database.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class Database extends Config
'datetime' => 'Y-m-d H:i:s',
'time' => 'H:i:s',
],
'timezone' => false,
];

// /**
Expand Down Expand Up @@ -98,6 +99,7 @@ class Database extends Config
// 'datetime' => 'Y-m-d H:i:s',
// 'time' => 'H:i:s',
// ],
// 'timezone' => false,
// ];

// /**
Expand Down Expand Up @@ -155,6 +157,7 @@ class Database extends Config
// 'datetime' => 'Y-m-d H:i:s',
// 'time' => 'H:i:s',
// ],
// 'timezone' => false,
// ];

/**
Expand Down
78 changes: 78 additions & 0 deletions system/Database/BaseConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
use Closure;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Events\Events;
use CodeIgniter\I18n\Time;
use Exception;
use stdClass;
use Stringable;
use Throwable;
Expand Down Expand Up @@ -156,6 +158,20 @@ abstract class BaseConnection implements ConnectionInterface
*/
protected $DBCollat = '';

/**
* Database session timezone
*
* false = Don't set timezone (default, backward compatible)
* true = Automatically sync with app timezone
* string = Specific timezone (offset or named timezone)
*
* Named timezones (e.g., 'America/New_York') will be automatically
* converted to offsets (e.g., '-05:00') for database compatibility.
*
* @var bool|string
*/
protected $timezone = false;

/**
* Swap Prefix
*
Expand Down Expand Up @@ -1915,6 +1931,68 @@ protected function _enableForeignKeyChecks()
return '';
}

/**
* Converts a named timezone to an offset string.
*
* Converts timezone identifiers (e.g., 'America/New_York') to offset strings
* (e.g., '-05:00' or '-04:00' depending on DST). This is useful because not all
* databases have timezone tables loaded, but all support offset notation.
*
* @param string $timezone Named timezone (e.g., 'America/New_York', 'UTC', 'Europe/Paris')
*
* @return string Offset string (e.g., '+00:00', '-05:00', '+01:00')
*/
protected function convertTimezoneToOffset(string $timezone): string
{
// If it's already an offset, return as-is
if (preg_match('/^[+-]\d{2}:\d{2}$/', $timezone)) {
return $timezone;
}

try {
$offset = Time::now($timezone)->getOffset();

// Convert offset seconds to +-HH:MM format
$hours = (int) ($offset / 3600);
$minutes = abs((int) (($offset % 3600) / 60));

return sprintf('%+03d:%02d', $hours, $minutes);
} catch (Exception $e) {
// If timezone conversion fails, log and return UTC
log_message('error', "Invalid timezone '{$timezone}': {$e->getMessage()}. Falling back to UTC.");

return '+00:00';
}
}

/**
* Gets the timezone string to use for database session.
*
* Handles the timezone configuration logic:
* - false: Don't set timezone (returns null)
* - true: Auto-sync with app timezone from config
* - string: Use specific timezone (converts named timezones to offsets)
*
* @return string|null The timezone offset string, or null if timezone should not be set
*/
protected function getSessionTimezone(): ?string
{
if ($this->timezone === false) {
return null;
}

// Auto-sync with app timezone
if ($this->timezone === true) {
$appConfig = config('App');
$timezone = $appConfig->appTimezone ?? 'UTC';
} else {
// Use specific timezone from config
$timezone = $this->timezone;
}

return $this->convertTimezoneToOffset($timezone);
}

/**
* Accessor for properties if they exist.
*
Expand Down
41 changes: 26 additions & 15 deletions system/Database/MySQLi/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,27 +123,38 @@ public function connect(bool $persistent = false)
$this->mysqli->options(MYSQLI_OPT_INT_AND_FLOAT_NATIVE, 1);
}

// Build init command for strictOn and timezone
$initCommands = [];

if ($this->strictOn !== null) {
if ($this->strictOn) {
$this->mysqli->options(
MYSQLI_INIT_COMMAND,
"SET SESSION sql_mode = CONCAT(@@sql_mode, ',', 'STRICT_ALL_TABLES')",
);
$initCommands[] = "sql_mode = CONCAT(@@sql_mode, ',', 'STRICT_ALL_TABLES')";
} else {
$this->mysqli->options(
MYSQLI_INIT_COMMAND,
"SET SESSION sql_mode = REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
@@sql_mode,
'STRICT_ALL_TABLES,', ''),
',STRICT_ALL_TABLES', ''),
'STRICT_ALL_TABLES', ''),
'STRICT_TRANS_TABLES,', ''),
',STRICT_TRANS_TABLES', ''),
'STRICT_TRANS_TABLES', '')",
);
$initCommands[] = "sql_mode = REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
@@sql_mode,
'STRICT_ALL_TABLES,', ''),
',STRICT_ALL_TABLES', ''),
'STRICT_ALL_TABLES', ''),
'STRICT_TRANS_TABLES,', ''),
',STRICT_TRANS_TABLES', ''),
'STRICT_TRANS_TABLES', '')";
}
}

// Set session timezone if configured
$timezoneOffset = $this->getSessionTimezone();
if ($timezoneOffset !== null) {
$initCommands[] = "time_zone = '{$timezoneOffset}'";
}

// Set init command if we have any commands
if ($initCommands !== []) {
$this->mysqli->options(
MYSQLI_INIT_COMMAND,
'SET SESSION ' . implode(', ', $initCommands),
);
}

if (is_array($this->encrypt)) {
$ssl = [];

Expand Down
12 changes: 11 additions & 1 deletion system/Database/OCI8/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,19 @@ public function connect(bool $persistent = false)

$func = $persistent ? 'oci_pconnect' : 'oci_connect';

return ($this->charset === '')
$this->connID = ($this->charset === '')
? $func($this->username, $this->password, $this->DSN)
: $func($this->username, $this->password, $this->DSN, $this->charset);

// Set session timezone if configured and connection is successful
if ($this->connID !== false) {
$timezoneOffset = $this->getSessionTimezone();
if ($timezoneOffset !== null) {
$this->simpleQuery("ALTER SESSION SET TIME_ZONE = '{$timezoneOffset}'");
}
}

return $this->connID;
}

/**
Expand Down
6 changes: 6 additions & 0 deletions system/Database/Postgre/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ public function connect(bool $persistent = false)

throw new DatabaseException($error);
}

// Set session timezone if configured
$timezoneOffset = $this->getSessionTimezone();
if ($timezoneOffset !== null) {
$this->simpleQuery("SET TIME ZONE '{$timezoneOffset}'");
}
}

return $this->connID;
Expand Down
89 changes: 89 additions & 0 deletions tests/system/Database/BaseConnectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -345,4 +345,93 @@ public static function provideEscapeIdentifier(): iterable
'with dots' => ['com.sitedb.web', '"com.sitedb.web"'],
];
}

public function testConvertTimezoneToOffsetWithOffset(): void
{
$db = new MockConnection($this->options);

// Offset strings should be returned as-is
$result = $this->getPrivateMethodInvoker($db, 'convertTimezoneToOffset')('+05:30');
$this->assertSame('+05:30', $result);

$result = $this->getPrivateMethodInvoker($db, 'convertTimezoneToOffset')('-08:00');
$this->assertSame('-08:00', $result);

$result = $this->getPrivateMethodInvoker($db, 'convertTimezoneToOffset')('+00:00');
$this->assertSame('+00:00', $result);
}

public function testConvertTimezoneToOffsetWithNamedTimezone(): void
{
$db = new MockConnection($this->options);

// UTC should always be +00:00
$result = $this->getPrivateMethodInvoker($db, 'convertTimezoneToOffset')('UTC');
$this->assertSame('+00:00', $result);

$result = $this->getPrivateMethodInvoker($db, 'convertTimezoneToOffset')('America/New_York');
$this->assertContains($result, ['-05:00', '-04:00']); // EST/EDT

$result = $this->getPrivateMethodInvoker($db, 'convertTimezoneToOffset')('Europe/Paris');
$this->assertContains($result, ['+01:00', '+02:00']); // CET/CEST

$result = $this->getPrivateMethodInvoker($db, 'convertTimezoneToOffset')('Asia/Tokyo');
$this->assertSame('+09:00', $result); // JST (no DST)
}

public function testConvertTimezoneToOffsetWithInvalidTimezone(): void
{
$db = new MockConnection($this->options);

$result = $this->getPrivateMethodInvoker($db, 'convertTimezoneToOffset')('Invalid/Timezone');
$this->assertSame('+00:00', $result);
}
Comment thread
paulbalandan marked this conversation as resolved.

public function testGetSessionTimezoneWithFalse(): void
{
$options = $this->options;
$options['timezone'] = false;
$db = new MockConnection($options);

$result = $this->getPrivateMethodInvoker($db, 'getSessionTimezone')();
$this->assertNull($result);
}

public function testGetSessionTimezoneWithTrue(): void
{
$options = $this->options;
$options['timezone'] = true;
$db = new MockConnection($options);

$result = $this->getPrivateMethodInvoker($db, 'getSessionTimezone')();
$this->assertSame('+00:00', $result); // UTC = +00:00
}

public function testGetSessionTimezoneWithSpecificOffset(): void
{
$options = $this->options;
$options['timezone'] = '+05:30';
$db = new MockConnection($options);

$result = $this->getPrivateMethodInvoker($db, 'getSessionTimezone')();
$this->assertSame('+05:30', $result);
}

public function testGetSessionTimezoneWithSpecificNamedTimezone(): void
{
$options = $this->options;
$options['timezone'] = 'America/Chicago';
$db = new MockConnection($options);

$result = $this->getPrivateMethodInvoker($db, 'getSessionTimezone')();
$this->assertContains($result, ['-06:00', '-05:00']);
}

public function testGetSessionTimezoneWithoutTimezoneKey(): void
{
$db = new MockConnection($this->options);

$result = $this->getPrivateMethodInvoker($db, 'getSessionTimezone')();
$this->assertNull($result);
}
}
Loading
Loading