From 28d32d8fff88ff0c37938f2b7d4feb21a3e32b4e Mon Sep 17 00:00:00 2001 From: Benjamin Gaussorgues Date: Thu, 4 Jun 2026 14:53:36 +0200 Subject: [PATCH] feat(snowflake): allows to generate Snowflake IDs matching a timestamp Signed-off-by: Benjamin Gaussorgues --- lib/private/Snowflake/SnowflakeGenerator.php | 17 +++++++++++++++++ lib/public/Snowflake/ISnowflakeGenerator.php | 14 ++++++++++++++ tests/lib/Snowflake/GeneratorTest.php | 19 +++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/lib/private/Snowflake/SnowflakeGenerator.php b/lib/private/Snowflake/SnowflakeGenerator.php index bad8a2288da..a247700ebde 100644 --- a/lib/private/Snowflake/SnowflakeGenerator.php +++ b/lib/private/Snowflake/SnowflakeGenerator.php @@ -43,6 +43,23 @@ final readonly class SnowflakeGenerator implements ISnowflakeGenerator { return $this->nextId(); } + return $this->packSnowflakeId($seconds, $milliseconds, $serverId, $isCli, $sequenceId); + } + + /** + * Return minimal snowflake ID for a given timestamp + * + * Not a real snowflake ID! + * Only use it for comparisons. For example get all snowflake IDs generated before $timestamp + * + * @since 34.0.1 + */ + #[Override] + public function minForTimeId(int $timestamp): string { + return $this->packSnowflakeId($timestamp - self::TS_OFFSET, 0, 0, 0, 0); + } + + private function packSnowflakeId($seconds, $milliseconds, $serverId, $isCli, $sequenceId): string { if (PHP_INT_SIZE === 8) { $firstHalf = $seconds & 0x7FFFFFFF; $secondHalf = (($milliseconds & 0x3FF) << 22) | ($serverId << 13) | ($isCli << 12) | $sequenceId; diff --git a/lib/public/Snowflake/ISnowflakeGenerator.php b/lib/public/Snowflake/ISnowflakeGenerator.php index 276e5dc52fa..f3a8a4cabd8 100644 --- a/lib/public/Snowflake/ISnowflakeGenerator.php +++ b/lib/public/Snowflake/ISnowflakeGenerator.php @@ -42,4 +42,18 @@ interface ISnowflakeGenerator { * @since 33.0 */ public function nextId(): string; + + /** + * Return the smallest possible Snowflake ID for a given timestamp + * + * Not a real snowflake ID! + * Only use it for comparisons. Examples: + * - find all Snowflake IDs generated from a given $timestamp + * Look for `>= minForTimeId($timestamp)` + * - delete all Snowflake IDs generated before a given $timestamp + * Delete where `id < minForTimeId($timestamp)` + * + * @since 34.0.1 + */ + public function minForTimeId(int $timestamp): string; } diff --git a/tests/lib/Snowflake/GeneratorTest.php b/tests/lib/Snowflake/GeneratorTest.php index 6eac3a41078..3323773105a 100644 --- a/tests/lib/Snowflake/GeneratorTest.php +++ b/tests/lib/Snowflake/GeneratorTest.php @@ -66,6 +66,25 @@ class GeneratorTest extends TestCase { $this->assertEquals($this->serverInfo->getServerId(), $data->getServerId()); } + public function testMinForTime(): void { + $generator = new SnowflakeGenerator(new TimeFactory(), $this->sequence, $this->serverInfo); + $now = time(); + $snowflakeId = $generator->minForTimeId($now); + $data = $this->decoder->decode($snowflakeId); + + $this->assertIsString($snowflakeId); + + // Check timestamp + $this->assertEquals($now - ISnowflakeGenerator::TS_OFFSET, $data->getSeconds()); + + // Check all other fields are at zero + $this->assertEquals(0, $data->getMilliseconds()); + $this->assertEquals(0, $data->getServerId()); + $this->assertEquals(0, $data->getSequenceId()); + $this->assertFalse($data->isCli()); + $this->assertEquals(0, $data->getServerId()); + } + #[DataProvider('provideSnowflakeData')] public function testGeneratorWithFixedTime(string $date, int $expectedSeconds, int $expectedMilliseconds): void { $dt = new \DateTimeImmutable($date);