efabrica / phoenix-scylla
Cassandra / ScyllaDB adapter for Phoenix database migrations
Requires
- php: ^8.4
- ext-cassandra: *
- efabrica/phoenix-core: ^4.0
Requires (Dev)
- phpunit/phpunit: ^9.5.11
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-26 10:36:05 UTC
README
Cassandra / ScyllaDB (CQL) adapter for Phoenix database migrations. It registers the cassandra adapter.
Requirements
- PHP 8.4+
- the
cassandraPHP extension (DataStax PHP driver), it is not available through PECL and has to be built for your PHP version - Cassandra 3+ or ScyllaDB (ScyllaDB 5.4 is covered by the test suite, see
.github/workflows/phpunit.yml)
Installation
composer require efabrica/phoenix-core efabrica/phoenix-scylla
The package registers the cassandra adapter in Phoenix\Database\Adapter\AdapterFactory from its composer bootstrap, nothing else has to be wired up. Composer refuses to install it without the extension, on a machine where the extension is missing (e.g. CI without a cluster) use --ignore-platform-req=ext-cassandra.
Setup in a project
- Create the keyspace. Phoenix does not create it and the adapter refuses to connect without one:
CREATE KEYSPACE my_app WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};
- Create a configuration file
phoenix.php(orphoenix.yml,phoenix.neon,phoenix.json) in the project root:<?php return [ 'migration_dirs' => [ 'app' => __DIR__ . '/migrations', ], 'environments' => [ 'local' => [ 'adapter' => 'cassandra', 'host' => '127.0.0.1', 'keyspace' => 'my_app', ], 'production' => [ 'adapter' => 'cassandra', 'host' => 'node1.internal,node2.internal,node3.internal', 'port' => 9042, 'keyspace' => 'my_app', 'username' => 'migrator', 'password' => getenv('DB_PASSWORD'), 'consistency' => 'local_quorum', 'datacenter' => 'dc1', 'ssl' => true, 'ssl_trusted_certs' => '/etc/ssl/certs/cluster-ca.pem', ], ], 'default_environment' => 'local', 'log_table_name' => 'phoenix_log', ];
ymlandneonconfig files needsymfony/yamlresp.nette/neoninstalled. All configuration keys are described in the configuration docs. - Create and run migrations with the command runner
vendor/bin/phoenix:
Usevendor/bin/phoenix create "App\CreateEvents" vendor/bin/phoenix migrate vendor/bin/phoenix status vendor/bin/phoenix rollback-e productionto pick another environment and-c path/to/config.phpfor a config file elsewhere. The log table (log_table_name) is created automatically on the first run. All commands are described in the commands docs, how to write migrations in the migrations docs. - The role used for migrations needs
CREATE,ALTERandDROPon the keyspace plusMODIFYandSELECTon the log table and on the tables you fill with data.
Configuration options
| key | required | description |
|---|---|---|
adapter |
yes | cassandra |
keyspace |
yes | keyspace, it has to exist. db_name is accepted as an alias, so a configuration can have the same shape for every adapter |
host |
no | one or more contact points separated by comma, default localhost |
port |
no | CQL port, default 9042 |
username, password |
no | credentials, both have to be set to enable authentication |
consistency |
no | default consistency level, one of the Cassandra::CONSISTENCY_* names in lower or upper case, e.g. local_quorum, quorum, one, all |
page_size |
no | default page size of selects |
connect_timeout, request_timeout |
no | seconds (float) |
datacenter |
no | local datacenter, enables datacenter aware round robin load balancing |
ssl |
no | true enables TLS |
ssl_trusted_certs |
no | path to the CA certificate used to verify the nodes (with ssl) |
version |
no | server version, read from system.local when not set. Currently informational only |
charset, collation |
no | accepted for compatibility with the other adapters, CQL has no charsets |
There is no dsn.
What the adapter supports
- tables with comment, a required primary key made of a partition key and clustering columns with their order, dropping and truncating tables, copying a table (structure is rebuilt from the loaded one, data is read and inserted back row by row)
- adding and dropping columns, changing the type of a column (where CQL allows it), renaming primary key columns
- secondary indexes over exactly one column (
CREATE INDEX) - materialized views through
view(),replaceView()anddropView(). The sql you pass is everything afterAS, including thePRIMARY KEYpart.replaceView()runs a drop followed by a create - CQL specific column types on top of the common ones:
ascii,varint,counter,inet,timeuuid,duration,list,map,tuple, andsetas a real CQL set. Element types of collections come from thevaluessetting dumpanddiffread the structure back fromsystem_schema- the migration log table is keyed by
(migration_datetime, classname)instead of an autoincrement id (LogTablePrimaryKeyInterface)
Primary keys
Every CQL table needs a primary key. The first primary column is the partition key, the rest are clustering columns:
$this->table('events', ['tenant_id', 'created_at'])
A composite partition key is written as one primary column wrapped in brackets, and a clustering column may carry its order:
$this->table('events', ['(tenant_id, day)', 'created_at DESC', 'id'])
This produces PRIMARY KEY ((tenant_id, day), created_at, id) WITH CLUSTERING ORDER BY (created_at DESC, id ASC).
Column type mapping
| Phoenix type | CQL type | note |
|---|---|---|
string, char, tinytext, text, mediumtext, longtext, json, enum |
text |
enum values are not enforced |
ascii |
ascii |
|
tinyinteger, smallinteger, mediuminteger, integer, biginteger |
tinyint, smallint, int, int, bigint |
|
year |
smallint |
|
bit, varint |
varint |
|
counter |
counter |
counter tables have their own rules in CQL |
numeric, decimal |
decimal |
pass a Cassandra\Decimal instance as value |
float, double |
float, double |
|
boolean |
boolean |
|
binary, varbinary, tinyblob, blob, mediumblob, longblob |
blob |
|
date, time |
date, time |
pass a Cassandra\Time instance for time |
datetime, timestamp, timestamptz |
timestamp |
|
duration |
duration |
pass a Cassandra\Duration instance |
uuid, timeuuid |
uuid, timeuuid |
|
inet |
inet |
|
set, list, map, tuple |
set<...>, list<...>, map<...,...>, tuple<...> |
element types from values, e.g. ['values' => ['text', 'int']] for a map |
point, line, polygon |
text |
no geometry in CQL |
$table->addColumn('tags', Column::TYPE_SET, ['values' => ['text']]); // set<text> $table->addColumn('scores', Column::TYPE_LIST, ['values' => ['int']]); // list<int> $table->addColumn('meta', Column::TYPE_MAP, ['values' => ['text', 'int']]); // map<text,int>
Limitations and things to watch
These are rejected with an explanatory exception instead of producing a query which would fail on the server:
- autoincrement columns (use
uuidortimeuuid), column default values - foreign keys, unique constraints,
UNIQUEandFULLTEXTindexes - a secondary index over more than one column
- renaming a table, renaming a non primary key column, changing name and type of a column in one step
- changing the primary key of an existing table (
addPrimary()/addPrimaryColumns()inalter), a table without a primary key LIMITwithOFFSET,OFFSETdoes not exist in CQL (this also stopscopyTable()from copying data in chunks)
Further differences worth knowing:
- There are no transactions. A migration on Cassandra is not atomic, a failing migration leaves the already executed statements applied. Keep migrations small and make them safe to re-run.
- Schema changes are eventually consistent across the cluster. After
CREATE TABLEthe other nodes may need a moment before they see it. The driver waits for schema agreement, but on a struggling cluster a following statement can still fail, re-running the migration usually helps. fetchAll()ordering. The server can only order by clustering columns, ordering by any other column is done in PHP after loading all matching rows, andLIMITis applied after that sorting.WHEREconditions follow CQL rules: equality on partition key columns, ranges only on clustering columns, no comparison toNULL, no!=.- Values with no lossless PHP representation.
decimal,varint,timeanddurationcolumns needCassandra\Decimal,Cassandra\Varint,Cassandra\Timeresp.Cassandra\Durationinstances when you insert or update through the adapter. dumpanddiffdo not write the primary key into the generated migration (a pre-existing limitation ofTable::toMigrationTable()shared by all adapters). On Cassandra a table cannot be created without one, so add the primary key to the generated migration by hand.- Materialized views are experimental in Cassandra (disabled by default since 4.0,
enable_materialized_views) and have restrictions in ScyllaDB. The adapter only generates the statements. consistencyapplies to every statement the adapter runs, including the log table writes.ALLon a cluster with a node down blocks migrations.
Tests
Tests need a running Cassandra or ScyllaDB, connection is taken from the PHOENIX_CASSANDRA_* environment variables (see phpunit.xml.dist). The keyspace is dropped and recreated by the tests.