diff --git a/docs/Cassandra/Cluster/Builder.php b/docs/Cassandra/Cluster/Builder.php index 42facfd52..d111ed105 100644 --- a/docs/Cassandra/Cluster/Builder.php +++ b/docs/Cassandra/Cluster/Builder.php @@ -210,7 +210,7 @@ public function withPersistentSessions($enabled) { } * * NOTE: Apache Cassandra 3.x supports protocol version 3 and 4 only * - * @param int $version The protocol version + * @param \Cassandra\ProtocolVersion|int $version The protocol version * * @return \Cassandra\Cluster\Builder self */ diff --git a/include/php_scylladb_types.h b/include/php_scylladb_types.h index e2aeb8d01..f6ad3c43e 100644 --- a/include/php_scylladb_types.h +++ b/include/php_scylladb_types.h @@ -726,6 +726,7 @@ extern PHP_SCYLLADB_API zend_class_entry *php_scylladb_core_ce; extern PHP_SCYLLADB_API zend_class_entry *php_scylladb_cluster_ce; extern PHP_SCYLLADB_API zend_class_entry *php_scylladb_default_cluster_ce; extern PHP_SCYLLADB_API zend_class_entry *php_scylladb_cluster_builder_ce; +extern PHP_SCYLLADB_API zend_class_entry *php_scylladb_protocol_version_ce; extern PHP_SCYLLADB_API zend_class_entry *php_scylladb_ssl_options_builder_ce; extern PHP_SCYLLADB_API zend_class_entry *php_scylladb_future_ce; extern PHP_SCYLLADB_API zend_class_entry *php_scylladb_future_prepared_statement_ce; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5a391596a..53e7aa44f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -21,6 +21,7 @@ target_sources(src PRIVATE UuidGen.c DefaultSession.c PreparedStatement.c + ProtocolVersion.c Set.c SimpleStatement.c Tuple.c @@ -89,6 +90,7 @@ php_scylladb_generate_arginfo(src Blob.stub.php Map.stub.php Cassandra.stub.php + ProtocolVersion.stub.php PreparedStatement.stub.php Collection.stub.php Session.stub.php @@ -127,6 +129,7 @@ php_scylladb_generate_descriptor(src Tuple.stub.php UserTypeValue.stub.php Cassandra.stub.php + ProtocolVersion.stub.php Future.stub.php FuturePreparedStatement.stub.php FutureRows.stub.php diff --git a/src/Cluster/Builder.c b/src/Cluster/Builder.c index 7750f555b..fee46a628 100644 --- a/src/Cluster/Builder.c +++ b/src/Cluster/Builder.c @@ -15,6 +15,7 @@ */ #include +#include #include @@ -528,14 +529,21 @@ ZEND_METHOD(Cassandra_Cluster_Builder, withPersistentSessions) } ZEND_METHOD(Cassandra_Cluster_Builder, withProtocolVersion) { - zend_long version; + zend_object *versionCase = nullptr; + zend_long version = 0; ZEND_PARSE_PARAMETERS_START(1, 1) - Z_PARAM_LONG(version) + Z_PARAM_OBJ_OF_CLASS_OR_LONG(versionCase, php_scylladb_protocol_version_ce, version) ZEND_PARSE_PARAMETERS_END(); - if (version < 1) + if (versionCase != nullptr) { + version = Z_LVAL_P(zend_enum_fetch_case_value(versionCase)); + } + else if (version < 1) + { + /* An int still gets through for versions the enum does not name, such + * as the DSE ones — so it keeps its own range check. */ zval val; ZVAL_LONG(&val, version); throw_invalid_argument(&val, "version", "a positive number"); @@ -543,7 +551,7 @@ ZEND_METHOD(Cassandra_Cluster_Builder, withProtocolVersion) } auto self = PHP_SCYLLADB_GET_CLUSTER_BUILDER(getThis()); - self->protocol_version = version; + self->protocol_version = (uint32_t)version; RETURN_ZVAL(getThis(), 1, 0); } diff --git a/src/Cluster/Builder.stub.php b/src/Cluster/Builder.stub.php index 1e709ba3c..5dcd86ea9 100644 --- a/src/Cluster/Builder.stub.php +++ b/src/Cluster/Builder.stub.php @@ -82,7 +82,7 @@ public function withPersistentSessions(bool $enabled = true): static { } - public function withProtocolVersion(int $version): static + public function withProtocolVersion(\Cassandra\ProtocolVersion|int $version): static { } diff --git a/src/Cluster/BuilderHandlers.c b/src/Cluster/BuilderHandlers.c index 21982e7c6..5c5bf77fe 100644 --- a/src/Cluster/BuilderHandlers.c +++ b/src/Cluster/BuilderHandlers.c @@ -1,4 +1,5 @@ #include +#include #include #include @@ -127,7 +128,19 @@ HashTable *php_scylladb_cluster_builder_properties(zend_object *object) } ZVAL_BOOL(&usePersistentSessions, self->persist); - ZVAL_LONG(&protocolVersion, self->protocol_version); + + zend_object *protocolVersionCase = nullptr; + if (zend_enum_get_case_by_value(&protocolVersionCase, php_scylladb_protocol_version_ce, + (zend_long)self->protocol_version, nullptr, true) == SUCCESS + && protocolVersionCase != nullptr) + { + ZVAL_OBJ_COPY(&protocolVersion, protocolVersionCase); + } + else + { + ZVAL_LONG(&protocolVersion, self->protocol_version); + } + ZVAL_LONG(&ioThreads, self->io_threads); ZVAL_LONG(&coreConnectionPerHost, self->core_connections_per_host); ZVAL_LONG(&maxConnectionsPerHost, self->max_connections_per_host); diff --git a/src/ProtocolVersion.c b/src/ProtocolVersion.c new file mode 100644 index 000000000..32d0dd035 --- /dev/null +++ b/src/ProtocolVersion.c @@ -0,0 +1,13 @@ +#include + +#include +#include + +/* gen_stub ignores docblocks on enum cases, so `@cvalue` cannot bind a case to + * a CASS_* macro the way it does for class constants. The cases carry literal + * values and these assertions hold them to the driver header instead. */ +static_assert(CASS_PROTOCOL_VERSION_V1 == 1, "Cassandra\\ProtocolVersion::V1 no longer matches CASS_PROTOCOL_VERSION_V1"); +static_assert(CASS_PROTOCOL_VERSION_V2 == 2, "Cassandra\\ProtocolVersion::V2 no longer matches CASS_PROTOCOL_VERSION_V2"); +static_assert(CASS_PROTOCOL_VERSION_V3 == 3, "Cassandra\\ProtocolVersion::V3 no longer matches CASS_PROTOCOL_VERSION_V3"); +static_assert(CASS_PROTOCOL_VERSION_V4 == 4, "Cassandra\\ProtocolVersion::V4 no longer matches CASS_PROTOCOL_VERSION_V4"); +static_assert(CASS_PROTOCOL_VERSION_V5 == 5, "Cassandra\\ProtocolVersion::V5 no longer matches CASS_PROTOCOL_VERSION_V5"); diff --git a/src/ProtocolVersion.stub.php b/src/ProtocolVersion.stub.php new file mode 100644 index 000000000..c05da7b61 --- /dev/null +++ b/src/ProtocolVersion.stub.php @@ -0,0 +1,24 @@ +value)->toBe(1); + expect(ProtocolVersion::V2->value)->toBe(2); + expect(ProtocolVersion::V3->value)->toBe(3); + expect(ProtocolVersion::V4->value)->toBe(4); + expect(ProtocolVersion::V5->value)->toBe(5); + }); + + it('exposes exactly the five native protocol versions', function () { + expect(array_column(ProtocolVersion::cases(), 'name')) + ->toBe(['V1', 'V2', 'V3', 'V4', 'V5']); + }); + + it('resolves a case from its backing value', function () { + expect(ProtocolVersion::from(4))->toBe(ProtocolVersion::V4); + expect(ProtocolVersion::tryFrom(9))->toBeNull(); + }); +}); + +describe('Cassandra\Cluster\Builder protocol version', function () { + + it('defaults to v4', function () { + $props = (array) Cassandra::cluster(); + + expect($props['protocolVersion'])->toBe(ProtocolVersion::V4); + }); + + it('stores the version passed as an enum case', function () { + $props = (array) Cassandra::cluster()->withProtocolVersion(ProtocolVersion::V5); + + expect($props['protocolVersion'])->toBe(ProtocolVersion::V5); + }); + + it('returns the builder so calls chain', function () { + $builder = Cassandra::cluster(); + + expect($builder->withProtocolVersion(ProtocolVersion::V3))->toBe($builder); + }); + + it('still accepts a plain integer', function () { + $props = (array) Cassandra::cluster()->withProtocolVersion(3); + + expect($props['protocolVersion'])->toBe(ProtocolVersion::V3); + }); + + it('keeps an integer the enum does not name', function () { + $props = (array) Cassandra::cluster()->withProtocolVersion(0x41); + + expect($props['protocolVersion'])->toBe(0x41); + }); + + it('rejects a non-positive integer', function () { + Cassandra::cluster()->withProtocolVersion(0); + })->throws(\Cassandra\Exception\InvalidArgumentException::class); + + it('rejects a value that is neither an int nor a ProtocolVersion', function () { + Cassandra::cluster()->withProtocolVersion('v4'); + })->throws(\TypeError::class); +}); diff --git a/tools/gen_descriptor/gen_class_descriptor.php b/tools/gen_descriptor/gen_class_descriptor.php index 95d49ce20..0d2c003d8 100644 --- a/tools/gen_descriptor/gen_class_descriptor.php +++ b/tools/gen_descriptor/gen_class_descriptor.php @@ -63,7 +63,7 @@ * Returns array of class records. Each record: * [ * 'fqn' => "Cassandra\\Foo\\Bar", // backslashes single, will be doubled at emit - * 'kind' => 'class' | 'interface', + * 'kind' => 'class' | 'interface' | 'enum', * 'is_final' => bool, * 'is_abstract'=> bool, * 'parents' => array of ['fqn' => '...', 'kind' => 'extends'|'implements'], @@ -115,8 +115,12 @@ function parse_stub(string $src): array continue; } - if ($id === T_CLASS || $id === T_INTERFACE) { - $kind = ($id === T_INTERFACE) ? 'interface' : 'class'; + if ($id === T_CLASS || $id === T_INTERFACE || $id === T_ENUM) { + $kind = match ($id) { + T_INTERFACE => 'interface', + T_ENUM => 'enum', + default => 'class', + }; // Walk back to find modifiers (final / abstract) and the doc comment. $isFinal = false; @@ -342,9 +346,8 @@ function emit_descriptor_file(array $classes, string $arginfoHeader, string $stu * PHP_SCYLLADB_VERSION, …) is visible to the arginfo register fn. */ #include "php_scylladb.h" #include -#include "$arginfoHeader" +$extraIncBlock#include "$arginfoHeader" #include -$extraIncBlock EOF . implode("\n", $bodies); @@ -380,6 +383,13 @@ function emit_class_descriptor(array $cls): array $handlersVar = "php_scylladb_{$snake}_handlers"; $registerFn = "php_scylladb_register_{$snake}"; + // Enums own no instances the extension allocates: cases are created by + // zend_register_internal_enum() and are immutable. No create_object, no + // handlers table, no offset — only the post_register hook stays. + if ($cls['kind'] === 'enum') { + return emit_enum_descriptor($cls, $snake, $ceVar, $registerFn, $fqnLit, $registerClassFn, $registryDeps, $registerArgs, $extraIncludes); + } + // Value-typed classes use a richer handlers struct that wraps the // standard zend_object_handlers with an extra `hash_value` callback. // Field access for standard handlers goes via `.std` when this is on. @@ -478,48 +488,108 @@ function emit_class_descriptor(array $cls): array $createObjWire = " if (&php_scylladb_{$snake}_new) ce->create_object = php_scylladb_{$snake}_new;\n"; $postReg = " if (&php_scylladb_{$snake}_post_register) php_scylladb_{$snake}_post_register(ce);\n"; - // Descriptor macro selection: single parent shortcut vs deps array. + $macro = emit_registry_macro($snake, $fqnLit, $ceVar, $registerFn, $registryDeps); + + $body = <<