feat(snowflake): allows to generate Snowflake IDs matching a timestamp

Signed-off-by: Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
This commit is contained in:
Benjamin Gaussorgues 2026-06-04 14:53:36 +02:00
parent 3956e292b4
commit 28d32d8fff
No known key found for this signature in database
3 changed files with 50 additions and 0 deletions

View file

@ -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;

View file

@ -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;
}

View file

@ -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);