Skip to content
Merged
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
31 changes: 27 additions & 4 deletions docs/Cassandra/Timestamp.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,37 @@
final class Timestamp implements Value {

/**
* Creates a new timestamp from either unix timestamp and microseconds or
* from the current time by default.
* Creates a new timestamp from a unix timestamp and microseconds, from a
* DateTimeInterface, or from the current time by default.
*
* @param int $seconds The number of seconds
* @param int $microseconds The number of microseconds
* @param int|\DateTimeInterface $seconds The number of seconds, or a date and time object
* @param int $microseconds The number of microseconds. Not allowed with a DateTimeInterface
*/
public function __construct($seconds, $microseconds) { }

/**
* Creates a timestamp for the current time.
*
* @return static
*/
public static function now() { }

/**
* Alias of now(). A timestamp holds a UTC epoch value, so it carries no zone.
*
* @return static
*/
public static function nowUtc() { }

/**
* Creates a timestamp from a PHP date and time object.
*
* @param \DateTimeInterface $datetime The date and time to convert
*
* @return static
*/
public static function fromDateTime($datetime) { }

/**
* The type of this timestamp.
*
Expand Down
85 changes: 69 additions & 16 deletions src/DateTime/Timestamp.c
Original file line number Diff line number Diff line change
Expand Up @@ -84,21 +84,70 @@ PHP_SCYLLADB_API zend_result php_scylladb_timestamp_initialize(php_scylladb_time
return SUCCESS;
}

static zend_result php_scylladb_timestamp_from_datetime(zend_object *datetime, cass_int64_t *out) {
zval formatted = {};
zval format;
ZVAL_STR(&format, zend_string_init_existing_interned(ZEND_STRL("Uv"), false));

zval *ret = zend_call_method_with_1_params(datetime, datetime->ce, nullptr, "format", &formatted,
&format);
zval_ptr_dtor(&format);

if (ret == nullptr || Z_TYPE(formatted) != IS_STRING) {
zval_ptr_dtor(&formatted);
return FAILURE;
}

*out = strtoll(Z_STRVAL(formatted), nullptr, 10);
zval_ptr_dtor(&formatted);
return SUCCESS;
}

static bool php_scylladb_timestamp_create_now(zval *dst) {
php_scylladb_timestamp *self = php_scylladb_timestamp_instantiate(dst);

if (self == nullptr) {
zend_throw_exception(php_scylladb_runtime_exception_ce, "Failed to create Cassandra\\Timestamp",
0);
return false;
}

php_scylladb_timestamp_initialize(self, -1, -1);
return true;
}

ZEND_METHOD(Cassandra_Timestamp, __construct) {
zend_object *datetime = nullptr;
zend_long seconds = -1;
zend_long microseconds = -1;

// clang-format off
ZEND_PARSE_PARAMETERS_START(0, 2)
Z_PARAM_OPTIONAL
Z_PARAM_LONG(seconds)
Z_PARAM_OBJ_OF_CLASS_OR_LONG(datetime, php_date_get_interface_ce(), seconds)
Z_PARAM_LONG(microseconds)
ZEND_PARSE_PARAMETERS_END();
// clang-format on

php_scylladb_timestamp *self =
PHP_SCYLLADB_OBJ_FETCH(php_scylladb_timestamp, Z_OBJ_P(getThis()));

if (datetime != nullptr) {
if (ZEND_NUM_ARGS() > 1) {
zend_throw_exception(php_scylladb_invalid_argument_exception_ce,
"microseconds cannot be given together with a DateTimeInterface", 0);
RETURN_THROWS();
}

if (php_scylladb_timestamp_from_datetime(datetime, &self->timestamp) != SUCCESS) {
zend_throw_exception(php_scylladb_runtime_exception_ce,
"Failed to get Timestamp from DateTime", 0);
RETURN_THROWS();
}

return;
}

if (php_scylladb_timestamp_initialize(self, seconds, microseconds) != SUCCESS) {
zend_throw_exception_ex(php_scylladb_invalid_argument_exception_ce, 0,
"Failed to create Timestamp: seconds(%ld) microseconds(%ld)",
Expand Down Expand Up @@ -190,17 +239,9 @@ ZEND_METHOD(Cassandra_Timestamp, fromDateTime) {
ZEND_PARSE_PARAMETERS_END();
// clang-format on

zval getTimeStampResult = {};
zval format;
zend_string *val = zend_string_init_existing_interned(ZEND_STRL("Uv"), false);
ZVAL_STR(&format, val);

zval *ret = zend_call_method_with_1_params(Z_OBJ_P(datetime), Z_OBJCE_P(datetime), nullptr,
"format", &getTimeStampResult, &format);
cass_int64_t timestamp = 0;

if (ret == nullptr) {
zval_ptr_dtor(&getTimeStampResult);
zval_ptr_dtor(&format);
if (php_scylladb_timestamp_from_datetime(Z_OBJ_P(datetime), &timestamp) != SUCCESS) {
zend_throw_exception(php_scylladb_runtime_exception_ce, "Failed to get Timestamp from DateTime",
0);
RETURN_THROWS();
Expand All @@ -209,16 +250,28 @@ ZEND_METHOD(Cassandra_Timestamp, fromDateTime) {
php_scylladb_timestamp *self = php_scylladb_timestamp_instantiate(return_value);

if (self == nullptr) {
zval_ptr_dtor(&getTimeStampResult);
zval_ptr_dtor(&format);
zend_throw_exception(php_scylladb_runtime_exception_ce, "Failed to create Cassandra\\Timestamp",
0);
RETURN_THROWS();
}

self->timestamp = strtoll(Z_STRVAL(getTimeStampResult), nullptr, 10);
zval_ptr_dtor(&getTimeStampResult);
zval_ptr_dtor(&format);
self->timestamp = timestamp;
}

ZEND_METHOD(Cassandra_Timestamp, now) {
ZEND_PARSE_PARAMETERS_NONE();

if (!php_scylladb_timestamp_create_now(return_value)) {
RETURN_THROWS();
}
}

ZEND_METHOD(Cassandra_Timestamp, nowUtc) {
ZEND_PARSE_PARAMETERS_NONE();

if (!php_scylladb_timestamp_create_now(return_value)) {
RETURN_THROWS();
}
}

ZEND_METHOD(Cassandra_Timestamp, __toString) {
Expand Down
5 changes: 4 additions & 1 deletion src/DateTime/Timestamp.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@
* @scylladb-value-handlers
* @scylladb-struct php_scylladb_timestamp
*/ final class Timestamp implements Value {
public function __construct(int $seconds = UNKNOWN, int $microseconds = UNKNOWN) {}
public function __construct(int|\DateTimeInterface $seconds = UNKNOWN, int $microseconds = UNKNOWN) {}

public function type(): Type {}
public function time(): int {}
public function microtime(bool $get_as_float = false): float|string {}
public function toDateTime(): \DateTime {}
public static function fromDateTime(\DateTimeInterface $datetime): static {}
public static function now(): static {}
/** Alias of now(). A timestamp holds a UTC epoch value, so it carries no zone. */
public static function nowUtc(): static {}

public function __toString(): string {}
}
Expand Down
51 changes: 51 additions & 0 deletions tests/Unit/DateTime/TimestampTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,55 @@
expect($ts->time())->toBe(1700000000);
});
});

describe('constructor with a DateTimeInterface', function () {
it('accepts a native DateTime', function () {
expect(new Timestamp(new DateTime('@1700000000')))
->toBeInstanceOf(Timestamp::class)
->and((new Timestamp(new DateTime('@1700000000')))->time())->toBe(1700000000);
});

it('keeps the millisecond part', function () {
$dt = new DateTimeImmutable('2020-01-02 03:04:05.678 UTC');

expect((string) new Timestamp($dt))->toBe('1577934245678');
});

it('accepts a Carbon instance', function () {
expect(new Timestamp(CarbonImmutable::parse('@1700000000')))
->toBeInstanceOf(Timestamp::class);
});

it('matches the fromDateTime factory', function () {
$dt = new DateTimeImmutable('2021-06-07 08:09:10.123 UTC');

expect((string) new Timestamp($dt))->toBe((string) Timestamp::fromDateTime($dt));
});

it('rejects microseconds together with a DateTimeInterface', function () {
expect(fn () => new Timestamp(new DateTime(), 5))
->toThrow(Cassandra\Exception\InvalidArgumentException::class);
});

it('still accepts an integer first argument', function () {
expect((new Timestamp(1700000000, 500000))->microtime(true))->toBe(1700000000.5);
});
});

describe('now factories', function () {
it('returns a Timestamp for the current time', function () {
$before = (int) (microtime(true) * 1000);
$value = (int) (string) Timestamp::now();
$after = (int) (microtime(true) * 1000);

expect(Timestamp::now())->toBeInstanceOf(Timestamp::class)
->and($value)->toBeGreaterThanOrEqual($before)->toBeLessThanOrEqual($after);
});

it('makes nowUtc an alias of now', function () {
expect(Timestamp::nowUtc())->toBeInstanceOf(Timestamp::class)
->and((int) (string) Timestamp::nowUtc() - (int) (string) Timestamp::now())
->toBeLessThanOrEqual(1000);
});
});
});
3 changes: 3 additions & 0 deletions website/guide/data-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,10 @@ CQL splits date and time. The driver mirrors that split.
| `Cassandra\Duration` | `duration` | Months, days, and nanoseconds |

```php
$ts = Cassandra\Timestamp::now(); // current time
$ts = Cassandra\Timestamp::nowUtc(); // alias of now()
$ts = new Cassandra\Timestamp(time()); // from a Unix timestamp
$ts = new Cassandra\Timestamp(new DateTime()); // from a date and time object
$ts = Cassandra\Timestamp::fromDateTime(new DateTime());

$ts->time(); // seconds
Expand Down
5 changes: 4 additions & 1 deletion website/reference/values.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,11 @@ final class Timeuuid implements Value, UuidInterface
final class Timestamp implements Value
{
// Omit both arguments to get the current time.
public function __construct(int $seconds = <now>, int $microseconds = <now>) {}
// A DateTimeInterface cannot be combined with $microseconds.
public function __construct(int|\DateTimeInterface $seconds = <now>, int $microseconds = <now>) {}

public static function now(): static;
public static function nowUtc(): static; // alias of now()
public static function fromDateTime(\DateTimeInterface $datetime): static;
public function toDateTime(): \DateTime;
public function time(): int; // seconds
Expand Down
Loading