├── README.md ├── composer.json ├── phpunit.xml ├── setup_cassandra_unittest_keyspace.cql ├── src ├── CassandraServiceProvider.php ├── Collection.php ├── Connection.php ├── Eloquent │ ├── Builder.php │ ├── Model.php │ ├── SoftDeletes.php │ └── SoftDeletingScope.php └── Query │ ├── Builder.php │ ├── Grammar.php │ └── Processor.php └── travis_bootstrap.sh /README.md: -------------------------------------------------------------------------------- 1 | THIS PACKAGE IS DEPRECATED AND NO LONGER SUPPORTED. 2 | ============== 3 | 4 | 5 | Laravel Cassandra 6 | =============== 7 | 8 | A Cassandra based Eloquent model and Query builder for Laravel (Casloquent) 9 | 10 | **WARNING**: This is a work in progress... not ready for usage yet. 11 | 12 | Installation 13 | ------------ 14 | 15 | ### Laravel version Compatibility 16 | 17 | Laravel | Package 18 | :---------|:---------- 19 | 5.4.x - 5.5.x | dev-master 20 | 21 | Make sure you have the Cassandra PHP driver installed (version 1.2+). You can find more information at http://datastax.github.io/php-driver/. 22 | 23 | Installation using composer: 24 | 25 | ``` 26 | composer require lroman242/laravel-cassandra 27 | ``` 28 | #### Laravel 29 | And add the service provider in `config/app.php`: 30 | 31 | ```php 32 | lroman242\LaravelCassandra\CassandraServiceProvider::class, 33 | ``` 34 | 35 | The service provider will register a cassandra database extension with the original database manager. There is no need to register additional facades or objects. When using cassandra connections, Laravel will automatically provide you with the corresponding cassandra objects. 36 | 37 | For usage outside Laravel, check out the [Capsule manager](https://github.com/illuminate/database/blob/master/README.md) and add: 38 | 39 | ```php 40 | $capsule->getDatabaseManager()->extend('cassandra', function($config) 41 | { 42 | return new lroman242\LaravelCassandra\Connection($config); 43 | }); 44 | ``` 45 | #### Lumen 46 | 47 | Add next lines to your `bootstrap.php` 48 | 49 | ```php 50 | $app->configure('database'); 51 | ``` 52 | 53 | ```php 54 | $app->register(lroman242\LaravelCassandra\CassandraServiceProvider::class); 55 | ``` 56 | 57 | Configuration 58 | ------------- 59 | 60 | Change your default database connection name in `config/database.php`: 61 | 62 | ```php 63 | 'default' => env('DB_CONNECTION', 'cassandra'), 64 | ``` 65 | 66 | And add a new cassandra connection: 67 | 68 | ```php 69 | 'cassandra' => [ 70 | 'driver' => 'cassandra', 71 | 'host' => env('DB_HOST', 'localhost'), 72 | 'port' => env('DB_PORT', 9042), 73 | 'keyspace' => env('DB_DATABASE'), 74 | 'username' => env('DB_USERNAME'), 75 | 'password' => env('DB_PASSWORD'), 76 | 'page_size' => env('DB_PAGE_SIZE', 5000), 77 | 'consistency' => Cassandra::CONSISTENCY_LOCAL_ONE, 78 | 'timeout' => null, 79 | 'connect_timeout' => 5.0, 80 | 'request_timeout' => 12.0, 81 | ], 82 | ``` 83 | 84 | You can connect to multiple servers with the following configuration: 85 | 86 | ```php 87 | 'cassandra' => [ 88 | 'driver' => 'cassandra', 89 | 'host' => ['192.168.0.1', '192.168.0.2'], //or '192.168.0.1,192.168.0.2' 90 | 'port' => env('DB_PORT', 9042), 91 | 'keyspace' => env('DB_DATABASE'), 92 | 'username' => env('DB_USERNAME'), 93 | 'password' => env('DB_PASSWORD'), 94 | 'page_size' => env('DB_PAGE_SIZE', 5000), 95 | 'consistency' => Cassandra::CONSISTENCY_LOCAL_ONE, 96 | 'timeout' => null, 97 | 'connect_timeout' => 5.0, 98 | 'request_timeout' => 12.0, 99 | ], 100 | ``` 101 | Note: you can enter all of your nodes in .env like : 102 | 103 | 104 | # .env 105 | DB_HOST=192.168.0.1,192.168.0.2,192.168.0.3 106 | 107 | Note: list of available consistency levels (php constants): 108 | 109 | Cassandra::CONSISTENCY_ANY 110 | Cassandra::CONSISTENCY_ONE 111 | Cassandra::CONSISTENCY_TWO 112 | Cassandra::CONSISTENCY_THREE 113 | Cassandra::CONSISTENCY_QUORUM 114 | Cassandra::CONSISTENCY_ALL 115 | Cassandra::CONSISTENCY_SERIAL 116 | Cassandra::CONSISTENCY_QUORUM 117 | Cassandra::CONSISTENCY_LOCAL_QUORUM 118 | Cassandra::CONSISTENCY_EACH_QUORUM 119 | Cassandra::CONSISTENCY_LOCAL_SERIAL 120 | Cassandra::CONSISTENCY_LOCAL_ONE 121 | 122 | Note: you can set specific consistency level according to the query using options 123 | 124 | Eloquent 125 | -------- 126 | Supported most of eloquent query build features, events, fields access. 127 | 128 | ```php 129 | $users = User::all(); 130 | 131 | $user = User::where('email', 'tester@test.com')->first(); 132 | 133 | $user = User::find(new \Cassandra\Uuid("7e4c27e2-1991-11e8-accf-0ed5f89f718b")) 134 | ``` 135 | 136 | Relations - NOT SUPPORTED 137 | 138 | Query Builder 139 | ------------- 140 | 141 | The database driver plugs right into the original query builder. When using cassandra connections, you will be able to build fluent queries to perform database operations. 142 | 143 | ```php 144 | $users = DB::table('users')->get(); 145 | 146 | $user = DB::table('users')->where('name', 'John')->first(); 147 | ``` 148 | 149 | If you did not change your default database connection, you will need to specify it when querying. 150 | 151 | ```php 152 | $user = DB::connection('cassandra')->table('users')->get(); 153 | ``` 154 | 155 | Default use of `get` method of query builder will call chunked fetch from database. 156 | Chunk size can be configured on config file (` 'page_size' => env('DB_PAGE_SIZE', 5000)`) or with additional query builder\`s method `setPageSize`. 157 | 158 | ```php 159 | $comments = Comments::setPageSize(500)->get(); // will return all comments, not 500 160 | ``` 161 | 162 | **WARNING**: Not recomended to use `get` if there are a lot of data in table. Use `getPage` instead. 163 | 164 | Get single page of resuts 165 | ```php 166 | $comments = Comments::setPageSize(500)->getPage(); // will return collection with 500 results 167 | ``` 168 | 169 | There is an ability to set next page token what allows to get next chunk of results 170 | ```php 171 | $comments = Comments::setPaginationStateToken($token)->getPage(); 172 | ``` 173 | 174 | Get next page: 175 | ```php 176 | $comments = $comments->nextPage(); 177 | ``` 178 | 179 | Get next page token: 180 | ```php 181 | $comments = $comments->getNextPageToken(); 182 | ``` 183 | 184 | Append collection with next page`s result: 185 | ```php 186 | $comments->appendNextPage(); 187 | ``` 188 | 189 | Check if it is last page: 190 | ```php 191 | $comments->isLastPage(); 192 | ``` 193 | 194 | Get raw cassandra response for current page (\Cassandra\Rows): 195 | ```php 196 | $rows = $commants->getRows(); 197 | ``` 198 | 199 | Read more about the query builder on http://laravel.com/docs/queries 200 | 201 | Examples 202 | --------- 203 | 204 | - store users data to csv 205 | 206 | ```php 207 | $users = User::setPageSize(1000)->getPage(); 208 | while(!$users->isLastPage()) { 209 | foreach($users as $user) { 210 | // here you can write a lines to csv file 211 | } 212 | 213 | $users = $users->nextPage(); 214 | } 215 | ``` 216 | 217 | - Simple api to make `Load more` as paggination on page 218 | 219 | ```php 220 | public function getComments(Request $request) { 221 | ... 222 | 223 | $comments = Comment::setPageSize(50) 224 | ->setPaginationStateToken($request->get('nextPageToken', null) 225 | ->getPage(); 226 | 227 | ... 228 | 229 | return response()->json([ 230 | ... 231 | 'comments' => $comments, 232 | 'nextPageToken' => !$comments->isLastPage() ? $comments->getNextPageToken() : null, 233 | ... 234 | ]); 235 | } 236 | ``` 237 | 238 | - If you use cassandra materialized views you can easily use it with eloquent models 239 | ```php 240 | $users = User::from('users_by_country_view')->where('country', 'USA')->get(); 241 | ``` 242 | 243 | 244 | TODO: 245 | ----- 246 | - full support of composite primary key 247 | - improved model update 248 | - full test coverage 249 | - fix diff between \Cassandra\Date with Carbon 250 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lroman242/laravel-cassandra", 3 | "description": "A Cassandra based Eloquent model and Query builder for Laravel (Casloquent)", 4 | "keywords": ["laravel","eloquent","cassandra","csql","database","model","casloquent"], 5 | "homepage": "https://github.com/lroman242/laravel-cassandra", 6 | "version": "1.0.0", 7 | "authors": [ 8 | { 9 | "name": "Pierre-Luc Brunet", 10 | "homepage": "https://github.com/lroman242" 11 | } 12 | ], 13 | "license" : "MIT", 14 | "require": { 15 | "php": ">=7.1.0", 16 | "illuminate/support": "^5.1", 17 | "illuminate/container": "^5.1", 18 | "illuminate/database": "^5.1", 19 | "illuminate/events": "^5.1" 20 | }, 21 | "require-dev": { 22 | "phpunit/phpunit": "^5.0|^6.0", 23 | "orchestra/testbench": "^3.1", 24 | "mockery/mockery": "^0.9", 25 | "satooshi/php-coveralls": "^1.0" 26 | }, 27 | "autoload": { 28 | "psr-4": { "lroman242\\LaravelCassandra\\" : "src/" } 29 | }, 30 | "autoload-dev": { 31 | "classmap": [ 32 | "tests/TestCase.php", 33 | "tests/models" 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | tests/ 17 | tests/MysqlRelationsTest.php 18 | 19 | 20 | tests/QueryBuilderTest.php 21 | tests/QueryTest.php 22 | 23 | 24 | 25 | 26 | tests/config 27 | tests/models 28 | ./fake 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /setup_cassandra_unittest_keyspace.cql: -------------------------------------------------------------------------------- 1 | DROP KEYSPACE IF EXISTS unittest; 2 | 3 | CREATE KEYSPACE unittest WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}; 4 | 5 | USE unittest; 6 | 7 | CREATE TABLE testtable ( 8 | id INT, 9 | name TEXT, 10 | PRIMARY KEY ((id))); 11 | 12 | INSERT INTO unittest.testtable (id, name) VALUES (1,'foo'); 13 | INSERT INTO unittest.testtable (id, name) VALUES (2,'bar'); 14 | INSERT INTO unittest.testtable (id, name) VALUES (3,'moo'); 15 | INSERT INTO unittest.testtable (id, name) VALUES (4,'cow'); 16 | 17 | CREATE TABLE users ( 18 | id INT, 19 | name TEXT, 20 | title TEXT, 21 | age INT, 22 | note1 TEXT, 23 | note2 TEXT, 24 | birthday TIMESTAMP, 25 | created_at TIMESTAMP, 26 | updated_at TIMESTAMP, 27 | PRIMARY KEY ((id))); 28 | 29 | 30 | CREATE TABLE soft ( 31 | id INT, 32 | name TEXT, 33 | created_at TIMESTAMP, 34 | updated_at TIMESTAMP, 35 | deleted_at TIMESTAMP, 36 | PRIMARY KEY ((id), deleted_at)); 37 | 38 | 39 | CREATE TABLE books ( 40 | title TEXT, 41 | author TEXT, 42 | created_at TIMESTAMP, 43 | updated_at TIMESTAMP, 44 | PRIMARY KEY ((title))); 45 | 46 | 47 | CREATE TYPE stats ( 48 | name TEXT, 49 | value INT 50 | ); 51 | 52 | 53 | CREATE TABLE items ( 54 | id INT, 55 | name TEXT, 56 | type TEXT, 57 | stats FROZEN, 58 | created_at TIMESTAMP, 59 | updated_at TIMESTAMP, 60 | PRIMARY KEY ((id), name)); 61 | 62 | -------------------------------------------------------------------------------- /src/CassandraServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->resolving('db', function ($db) { 24 | $db->extend('cassandra', function ($config, $name) { 25 | $config['name'] = $name; 26 | return new Connection($config); 27 | }); 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Collection.php: -------------------------------------------------------------------------------- 1 | rows = $items; 37 | } 38 | $this->model = $model; 39 | 40 | parent::__construct($this->prepareItems($items)); 41 | } 42 | 43 | /** 44 | * Prepare items for collection 45 | * 46 | * @return array|Model[] 47 | */ 48 | protected function prepareItems($items) 49 | { 50 | if ($this->model !== null && $items instanceof Rows) { 51 | $models = []; 52 | 53 | foreach ($items as $row) { 54 | $models[] = $this->model->newFromBuilder($row); 55 | } 56 | 57 | return $models; 58 | } else { 59 | return $items; 60 | } 61 | } 62 | 63 | /** 64 | * Next page token 65 | * 66 | * @return mixed 67 | */ 68 | public function getNextPageToken() 69 | { 70 | return $this->rows->pagingStateToken(); 71 | } 72 | 73 | /** 74 | * Last page indicator 75 | * @return bool 76 | */ 77 | public function isLastPage() 78 | { 79 | return $this->rows->isLastPage(); 80 | } 81 | 82 | /** 83 | * Get next page 84 | * 85 | * @return Collection 86 | */ 87 | public function nextPage() 88 | { 89 | if (!$this->isLastPage()) { 90 | return new self($this->rows->nextPage(), $this->model); 91 | } 92 | } 93 | 94 | /** 95 | * Get rows instance 96 | * 97 | * @return \Cassandra\Rows 98 | */ 99 | public function getRows() 100 | { 101 | return $this->rows; 102 | } 103 | 104 | /** 105 | * Update current collection with results from 106 | * the next page 107 | * 108 | * @return Collection 109 | */ 110 | public function appendNextPage() 111 | { 112 | $nextPage = $this->nextPage(); 113 | 114 | if ($nextPage) { 115 | $this->items = array_merge($this->items, $nextPage->toArray()); 116 | $this->rows = $nextPage->getRows(); 117 | } 118 | 119 | return $this; 120 | } 121 | 122 | /** 123 | * Find a model in the collection by key. 124 | * 125 | * @param mixed $key 126 | * @param mixed $default 127 | * @return \Illuminate\Database\Eloquent\Model|static 128 | */ 129 | public function find($key, $default = null) 130 | { 131 | if ($key instanceof Model) { 132 | $key = $key->getKey(); 133 | } 134 | 135 | if ($key instanceof Arrayable) { 136 | $key = $key->toArray(); 137 | } 138 | 139 | if (is_array($key)) { 140 | if ($this->isEmpty()) { 141 | return new static([], $this->model); 142 | } 143 | 144 | return $this->whereIn($this->first()->getKeyName(), $key); 145 | } 146 | 147 | return Arr::first($this->items, function ($model) use ($key) { 148 | return $model->getKey() == $key; 149 | }, $default); 150 | } 151 | 152 | /** 153 | * Merge the collection with the given items. 154 | * 155 | * @param \ArrayAccess|array $items 156 | * @return static 157 | */ 158 | public function merge($items) 159 | { 160 | $dictionary = $this->getDictionary(); 161 | 162 | foreach ($items as $item) { 163 | $dictionary[(string) $item->getKey()] = $item; 164 | } 165 | 166 | return new static(array_values($dictionary), $this->model); 167 | } 168 | 169 | /** 170 | * Reload a fresh model instance from the database for all the entities. 171 | * 172 | * @param array|string $with 173 | * @return static 174 | */ 175 | public function fresh($with = []) 176 | { 177 | if ($this->isEmpty()) { 178 | return new static([], $this->model); 179 | } 180 | 181 | $model = $this->first(); 182 | 183 | $freshModels = $model->newQueryWithoutScopes() 184 | ->with(is_string($with) ? func_get_args() : $with) 185 | ->whereIn($model->getKeyName(), $this->modelKeys()) 186 | ->get() 187 | ->getDictionary(); 188 | 189 | return $this->map(function ($model) use ($freshModels) { 190 | return $model->exists && isset($freshModels[(string) $model->getKey()]) 191 | ? $freshModels[(string) $model->getKey()] : null; 192 | }); 193 | } 194 | 195 | /** 196 | * Diff the collection with the given items. 197 | * 198 | * @param \ArrayAccess|array $items 199 | * @return static 200 | */ 201 | public function diff($items) 202 | { 203 | $diff = new static([], $this->model); 204 | 205 | $dictionary = $this->getDictionary($items); 206 | 207 | foreach ($this->items as $item) { 208 | if (!isset($dictionary[(string) $item->getKey()])) { 209 | $diff->add($item); 210 | } 211 | } 212 | 213 | return $diff; 214 | } 215 | 216 | /** 217 | * Intersect the collection with the given items. 218 | * 219 | * @param \ArrayAccess|array $items 220 | * @return static 221 | */ 222 | public function intersect($items) 223 | { 224 | $intersect = new static([], $this->model); 225 | 226 | $dictionary = $this->getDictionary($items); 227 | 228 | foreach ($this->items as $item) { 229 | if (isset($dictionary[(string) $item->getKey()])) { 230 | $intersect->add($item); 231 | } 232 | } 233 | 234 | return $intersect; 235 | } 236 | 237 | /** 238 | * Return only unique items from the collection. 239 | * 240 | * @param string|callable|null $key 241 | * @param bool $strict 242 | * @return static|\Illuminate\Support\Collection 243 | */ 244 | public function unique($key = null, $strict = false) 245 | { 246 | if (!is_null($key)) { 247 | return parent::unique($key, $strict); 248 | } 249 | 250 | return new static(array_values($this->getDictionary()), $this->model); 251 | } 252 | 253 | /** 254 | * Returns only the models from the collection with the specified keys. 255 | * 256 | * @param mixed $keys 257 | * @return static 258 | */ 259 | public function only($keys) 260 | { 261 | if (is_null($keys)) { 262 | return new static($this->items, $this->model); 263 | } 264 | 265 | $dictionary = Arr::only($this->getDictionary(), $keys); 266 | 267 | return new static(array_values($dictionary), $this->model); 268 | } 269 | 270 | /** 271 | * Returns all models in the collection except the models with specified keys. 272 | * 273 | * @param mixed $keys 274 | * @return static 275 | */ 276 | public function except($keys) 277 | { 278 | $dictionary = Arr::except($this->getDictionary(), $keys); 279 | 280 | return new static(array_values($dictionary), $this->model); 281 | } 282 | 283 | 284 | /** 285 | * Get a dictionary keyed by primary keys. 286 | * 287 | * @param \ArrayAccess|array|null $items 288 | * @return array 289 | */ 290 | public function getDictionary($items = null) 291 | { 292 | $items = is_null($items) ? $this->items : $items; 293 | 294 | $dictionary = []; 295 | 296 | foreach ($items as $value) { 297 | $dictionary[(string) $value->getKey()] = $value; 298 | } 299 | 300 | return $dictionary; 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /src/Connection.php: -------------------------------------------------------------------------------- 1 | config = $config; 48 | if (empty($this->config['page_size'])) { 49 | $this->config['page_size'] = self::DEFAULT_PAGE_SIZE; 50 | } 51 | 52 | // You can pass options directly to the Cassandra constructor 53 | $options = array_get($config, 'options', []); 54 | 55 | // Create the connection 56 | $this->cluster = $this->createCluster($config, $options); 57 | 58 | if (isset($options['database']) || isset($config['keyspace'])) { 59 | $keyspaceName = isset($options['database']) ? $options['database'] : $config['keyspace']; 60 | 61 | $this->keyspace = $keyspaceName; 62 | $this->session = $this->cluster->connect($keyspaceName); 63 | } 64 | 65 | $this->useDefaultPostProcessor(); 66 | 67 | $this->useDefaultSchemaGrammar(); 68 | 69 | $this->setQueryGrammar($this->getDefaultQueryGrammar()); 70 | } 71 | 72 | /** 73 | * Begin a fluent query against a database table. 74 | * 75 | * @param string $table 76 | * @return Query\Builder 77 | */ 78 | public function table($table) 79 | { 80 | $processor = $this->getPostProcessor(); 81 | 82 | $query = new Query\Builder($this, null, $processor); 83 | 84 | return $query->from($table); 85 | } 86 | 87 | /** 88 | * return Cassandra cluster. 89 | * 90 | * @return \Cassandra\Cluster 91 | */ 92 | public function getCassandraCluster() 93 | { 94 | return $this->cluster; 95 | } 96 | 97 | /** 98 | * return Cassandra Session. 99 | * 100 | * @return \Cassandra\Session 101 | */ 102 | public function getCassandraSession() 103 | { 104 | return $this->session; 105 | } 106 | 107 | /** 108 | * Return the Cassandra keyspace 109 | * 110 | * @return string 111 | */ 112 | public function getKeyspace() 113 | { 114 | return $this->keyspace; 115 | } 116 | 117 | /** 118 | * Create a new Cassandra cluster object. 119 | * 120 | * @param array $config 121 | * @param array $options 122 | * @return \Cassandra\Cluster 123 | */ 124 | protected function createCluster(array $config, array $options) 125 | { 126 | $cluster = Cassandra::cluster(); 127 | 128 | // Check if the credentials are not already set in the options 129 | if (!isset($options['username']) && !empty($config['username'])) { 130 | $options['username'] = $config['username']; 131 | } 132 | if (!isset($options['password']) && !empty($config['password'])) { 133 | $options['password'] = $config['password']; 134 | } 135 | 136 | // Authentication 137 | if (isset($options['username']) && isset($options['password'])) { 138 | $cluster->withCredentials($options['username'], $options['password']); 139 | } 140 | 141 | // Contact Points/Host 142 | if (isset($options['contactpoints']) || (isset($config['host']) && !empty($config['host']))) { 143 | $contactPoints = $config['host']; 144 | 145 | if (isset($options['contactpoints'])) { 146 | $contactPoints = $options['contactpoints']; 147 | } 148 | 149 | if (is_array($contactPoints)) { 150 | $contactPoints = implode(',', $contactPoints); 151 | } 152 | 153 | $contactPoints = !empty($contactPoints) ? $contactPoints : '127.0.0.1'; 154 | 155 | $cluster->withContactPoints($contactPoints); 156 | } 157 | 158 | if (!isset($options['port']) && !empty($config['port'])) { 159 | $cluster->withPort((int) $config['port']); 160 | } 161 | 162 | if (array_key_exists('page_size', $config) && !empty($config['page_size'])) { 163 | $cluster->withDefaultPageSize(intval($config['page_size'] ?? self::DEFAULT_PAGE_SIZE)); 164 | } 165 | 166 | if (array_key_exists('consistency', $config) && in_array(strtoupper($config['consistency']), [ 167 | Cassandra::CONSISTENCY_ANY, Cassandra::CONSISTENCY_ONE, Cassandra::CONSISTENCY_TWO, 168 | Cassandra::CONSISTENCY_THREE, Cassandra::CONSISTENCY_QUORUM, Cassandra::CONSISTENCY_ALL, 169 | Cassandra::CONSISTENCY_SERIAL, Cassandra::CONSISTENCY_QUORUM, Cassandra::CONSISTENCY_LOCAL_QUORUM, 170 | Cassandra::CONSISTENCY_EACH_QUORUM, Cassandra::CONSISTENCY_LOCAL_SERIAL, Cassandra::CONSISTENCY_LOCAL_ONE, 171 | ])) { 172 | $cluster->withDefaultConsistency($config['consistency']); 173 | } 174 | 175 | if (array_key_exists('timeout', $config) && !empty($config['timeout'])) { 176 | $cluster->withDefaultTimeout(intval($config['timeout'])); 177 | } 178 | 179 | if (array_key_exists('connect_timeout', $config) && !empty($config['connect_timeout'])) { 180 | $cluster->withConnectTimeout(floatval($config['connect_timeout'])); 181 | } 182 | 183 | if (array_key_exists('request_timeout', $config) && !empty($config['request_timeout'])) { 184 | $cluster->withRequestTimeout(floatval($config['request_timeout'])); 185 | } 186 | 187 | return $cluster->build(); 188 | } 189 | 190 | /** 191 | * Disconnect from the underlying Cassandra connection. 192 | */ 193 | public function disconnect() 194 | { 195 | unset($this->connection); 196 | } 197 | 198 | /** 199 | * Get the PDO driver name. 200 | * 201 | * @return string 202 | */ 203 | public function getDriverName() 204 | { 205 | return 'cassandra'; 206 | } 207 | 208 | /** 209 | * Run a select statement against the database. 210 | * 211 | * @param string $query 212 | * @param array $bindings 213 | * @param bool $useReadPdo 214 | * @param array $customOptions 215 | * 216 | * @return array 217 | */ 218 | public function select($query, $bindings = [], $useReadPdo = true, array $customOptions = []) 219 | { 220 | return $this->run($query, $bindings, function ($query, $bindings) use ($useReadPdo, $customOptions) { 221 | if ($this->pretending()) { 222 | return []; 223 | } 224 | 225 | $preparedStatement = $this->session->prepare($query); 226 | 227 | //Add bindings 228 | $customOptions['arguments'] = $bindings; 229 | 230 | return $this->session->execute($preparedStatement, $customOptions); 231 | }); 232 | } 233 | 234 | /** 235 | * Run an bulk insert statement against the database. 236 | * 237 | * @param array $queries 238 | * @param array $bindings 239 | * @param int $type 240 | * @param array $customOptions 241 | * 242 | * @return bool 243 | */ 244 | public function insertBulk($queries = [], $bindings = [], $type = Cassandra::BATCH_LOGGED, array $customOptions = []) 245 | { 246 | return $this->batchStatement($queries, $bindings, $type, $customOptions); 247 | } 248 | 249 | /** 250 | * Execute a group of queries inside a batch statement against the database. 251 | * 252 | * @param array $queries 253 | * @param array $bindings 254 | * @param int $type 255 | * @param array $customOptions 256 | * 257 | * @return bool 258 | */ 259 | public function batchStatement($queries = [], $bindings = [], $type = Cassandra::BATCH_LOGGED, array $customOptions = []) 260 | { 261 | return $this->run($queries, $bindings, function ($queries, $bindings) use ($type, $customOptions) { 262 | if ($this->pretending()) { 263 | return []; 264 | } 265 | 266 | $batch = new BatchStatement($type); 267 | 268 | foreach ($queries as $k => $query) { 269 | $preparedStatement = $this->session->prepare($query); 270 | $batch->add($preparedStatement, $bindings[$k]); 271 | } 272 | 273 | return $this->session->execute($batch, $customOptions); 274 | }); 275 | } 276 | 277 | /** 278 | * Execute an SQL statement and return the boolean result. 279 | * 280 | * @param string $query 281 | * @param array $bindings 282 | * @param array $customOptions 283 | * 284 | * @return bool 285 | */ 286 | public function statement($query, $bindings = [], array $customOptions = []) 287 | { 288 | return $this->run($query, $bindings, function ($query, $bindings) use ($customOptions) { 289 | if ($this->pretending()) { 290 | return []; 291 | } 292 | 293 | $preparedStatement = $this->session->prepare($query); 294 | //$this->recordsHaveBeenModified(); 295 | 296 | //Add bindings 297 | $customOptions['arguments'] = $bindings; 298 | 299 | return $this->session->execute($preparedStatement, $customOptions); 300 | }); 301 | } 302 | 303 | /** 304 | * Because Cassandra is an eventually consistent database, it's not possible to obtain 305 | * the affected count for statements so we're just going to return 0, based on the idea 306 | * that if the query fails somehow, an exception will be thrown 307 | * 308 | * @param string $query 309 | * @param array $bindings 310 | * @param array $customOptions 311 | * 312 | * @return int 313 | */ 314 | public function affectingStatement($query, $bindings = [], array $customOptions = []) 315 | { 316 | return $this->run($query, $bindings, function ($query, $bindings) { 317 | if ($this->pretending()) { 318 | return 0; 319 | } 320 | 321 | $preparedStatement = $this->session->prepare($query); 322 | //$this->recordsHaveBeenModified(); 323 | 324 | //Add bindings 325 | $customOptions['arguments'] = $bindings; 326 | 327 | $this->session->execute($preparedStatement, $customOptions); 328 | 329 | return 1; 330 | }); 331 | } 332 | 333 | /** 334 | * @inheritdoc 335 | */ 336 | protected function getDefaultPostProcessor() 337 | { 338 | return new Query\Processor(); 339 | } 340 | 341 | /** 342 | * @inheritdoc 343 | */ 344 | protected function getDefaultQueryGrammar() 345 | { 346 | return new Query\Grammar(); 347 | } 348 | 349 | /** 350 | * @inheritdoc 351 | */ 352 | protected function getDefaultSchemaGrammar() 353 | { 354 | //return new Schema\Grammar(); 355 | } 356 | 357 | /** 358 | * Reconnect to the database if connection is missing. 359 | * 360 | * @return void 361 | */ 362 | protected function reconnectIfMissingConnection() 363 | { 364 | if (is_null($this->session)) { 365 | $this->session = $this->createCluster($this->config, [])->connect($this->keyspace); 366 | } 367 | } 368 | 369 | /** 370 | * Dynamically pass methods to the connection. 371 | * 372 | * @param string $method 373 | * @param array $parameters 374 | * @return mixed 375 | */ 376 | public function __call($method, $parameters) 377 | { 378 | return call_user_func_array([$this->cluster, $method], $parameters); 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /src/Eloquent/Builder.php: -------------------------------------------------------------------------------- 1 | newModelInstance(); 21 | 22 | return $instance->newCassandraCollection($rows); 23 | } 24 | 25 | /** 26 | * Execute the query as a "select" statement. 27 | * 28 | * @param array $columns 29 | * 30 | * @return Collection 31 | * 32 | * @throws \Exception 33 | */ 34 | public function getPage($columns = ['*']) 35 | { 36 | $builder = $this->applyScopes(); 37 | 38 | return $builder->getModelsPage($columns); 39 | } 40 | 41 | /** 42 | * Get the hydrated models without eager loading. 43 | * 44 | * @param array $columns 45 | * 46 | * @return Collection 47 | * 48 | * @throws \Exception 49 | */ 50 | public function getModelsPage($columns = ['*']) 51 | { 52 | $results = $this->query->getPage($columns); 53 | 54 | if ($results instanceof Collection) { 55 | $results = $results->getRows(); 56 | } elseif (!$results instanceof Rows) { 57 | throw new \Exception('Invalid type of getPage response. Expected lroman242\LaravelCassandra\Collection or Cassandra\Rows'); 58 | } 59 | 60 | return $this->model->hydrateRows($results); 61 | } 62 | 63 | /** 64 | * Execute the query as a "select" statement. 65 | * 66 | * @param array $columns 67 | * 68 | * @return \Illuminate\Database\Eloquent\Collection|static[] 69 | * 70 | * @throws \Exception 71 | */ 72 | public function get($columns = ['*']) 73 | { 74 | $builder = $this->applyScopes(); 75 | 76 | return $builder->getModels($columns); 77 | } 78 | 79 | /** 80 | * Get the hydrated models without eager loading. 81 | * 82 | * @param array $columns 83 | * 84 | * @return \Illuminate\Database\Eloquent\Collection|static[] 85 | * 86 | * @throws \Exception 87 | */ 88 | public function getModels($columns = ['*']) 89 | { 90 | $results = $this->query->get($columns); 91 | 92 | if ($results instanceof Collection) { 93 | $results = $results->getRows(); 94 | } elseif (!$results instanceof Rows) { 95 | throw new \Exception('Invalid type of getPage response. Expected lroman242\LaravelCassandra\Collection or Cassandra\Rows'); 96 | } 97 | 98 | return $this->model->hydrateRows($results); 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /src/Eloquent/Model.php: -------------------------------------------------------------------------------- 1 | getConnection(); 37 | 38 | return new QueryBuilder($connection, null, $connection->getPostProcessor()); 39 | } 40 | 41 | /** 42 | * @inheritdoc 43 | */ 44 | public function freshTimestamp() 45 | { 46 | return new Timestamp(); 47 | } 48 | 49 | /** 50 | * @inheritdoc 51 | */ 52 | public function fromDateTime($value) 53 | { 54 | // If the value is already a Timestamp instance, we don't need to parse it. 55 | if ($value instanceof Timestamp) { 56 | return $value; 57 | } 58 | 59 | // Let Eloquent convert the value to a DateTime instance. 60 | if (!$value instanceof \DateTime) { 61 | $value = parent::asDateTime($value); 62 | } 63 | 64 | return new Timestamp($value->getTimestamp() * 1000); 65 | } 66 | 67 | /** 68 | * @inheritdoc 69 | */ 70 | protected function asDateTime($value) 71 | { 72 | // Convert UTCDateTime instances. 73 | if ($value instanceof Timestamp) { 74 | return Carbon::instance($value->toDateTime()); 75 | } 76 | 77 | return parent::asDateTime($value); 78 | } 79 | 80 | /** 81 | * @inheritdoc 82 | */ 83 | protected function originalIsNumericallyEquivalent($key) 84 | { 85 | $current = $this->attributes[$key]; 86 | $original = $this->original[$key]; 87 | 88 | // Date comparison. 89 | if (in_array($key, $this->getDates())) { 90 | $current = $current instanceof Timestamp ? $this->asDateTime($current) : $current; 91 | $original = $original instanceof Timestamp ? $this->asDateTime($original) : $original; 92 | 93 | return $current == $original; 94 | } 95 | 96 | return parent::originalIsNumericallyEquivalent($key); 97 | } 98 | 99 | /** 100 | * Get the table qualified key name. 101 | * Cassandra does not support the table.column annotation so 102 | * we override this 103 | * 104 | * @return string 105 | */ 106 | public function getQualifiedKeyName() 107 | { 108 | return $this->getKeyName(); 109 | } 110 | 111 | /** 112 | * Set a given attribute on the model. 113 | * 114 | * @param string $key 115 | * @param mixed $value 116 | * @return $this 117 | */ 118 | public function setAttribute($key, $value) 119 | { 120 | // First we will check for the presence of a mutator for the set operation 121 | // which simply lets the developers tweak the attribute as it is set on 122 | // the model, such as "json_encoding" an listing of data for storage. 123 | if ($this->hasSetMutator($key)) { 124 | $method = 'set' . Str::studly($key) . 'Attribute'; 125 | 126 | return $this->{$method}($value); 127 | } 128 | 129 | // If an attribute is listed as a "date", we'll convert it from a DateTime 130 | // instance into a form proper for storage on the database tables using 131 | // the connection grammar's date format. We will auto set the values. 132 | elseif ($value !== null && $this->isDateAttribute($key)) { 133 | $value = $this->fromDateTime($value); 134 | } 135 | 136 | if ($this->isJsonCastable($key) && !is_null($value)) { 137 | $value = $this->castAttributeAsJson($key, $value); 138 | } 139 | 140 | // If this attribute contains a JSON ->, we'll set the proper value in the 141 | // attribute's underlying array. This takes care of properly nesting an 142 | // attribute in the array's value in the case of deeply nested items. 143 | if (Str::contains($key, '->')) { 144 | return $this->fillJsonAttribute($key, $value); 145 | } 146 | 147 | $this->attributes[$key] = $value; 148 | 149 | return $this; 150 | } 151 | 152 | /** 153 | * @inheritdoc 154 | */ 155 | public function __call($method, $parameters) 156 | { 157 | // Unset method 158 | if ($method == 'unset') { 159 | return call_user_func_array([$this, 'drop'], $parameters); 160 | } 161 | 162 | return parent::__call($method, $parameters); 163 | } 164 | 165 | /** 166 | * Create a new Eloquent Collection instance. 167 | * 168 | * @param Rows $rows 169 | * 170 | * @return Collection 171 | */ 172 | public function newCassandraCollection(Rows $rows) 173 | { 174 | return new Collection($rows, $this); 175 | } 176 | 177 | /** 178 | * Determine if the new and old values for a given key are equivalent. 179 | * 180 | * @param string $key 181 | * @param mixed $current 182 | * @return bool 183 | */ 184 | public function originalIsEquivalent($key, $current) 185 | { 186 | if (!array_key_exists($key, $this->original)) { 187 | return false; 188 | } 189 | 190 | $original = $this->getOriginal($key); 191 | 192 | if ($current === $original) { 193 | return true; 194 | } elseif (is_null($current)) { 195 | return false; 196 | } elseif ($this->isDateAttribute($key)) { 197 | return $this->fromDateTime($current) === 198 | $this->fromDateTime($original); 199 | } elseif ($this->hasCast($key)) { 200 | return $this->castAttribute($key, $current) === 201 | $this->castAttribute($key, $original); 202 | } elseif ($this->isCassandraObject($current)) { 203 | return $this->valueFromCassandraObject($current) === 204 | $this->valueFromCassandraObject($original); 205 | } 206 | 207 | return is_numeric($current) && is_numeric($original) 208 | && strcmp((string) $current, (string) $original) === 0; 209 | } 210 | 211 | /** 212 | * Check if object is instance of any cassandra object types 213 | * 214 | * @param $obj 215 | * @return bool 216 | */ 217 | protected function isCassandraObject($obj) 218 | { 219 | if ($obj instanceof \Cassandra\Uuid || 220 | $obj instanceof \Cassandra\Date || 221 | $obj instanceof \Cassandra\Float || 222 | $obj instanceof \Cassandra\Decimal || 223 | $obj instanceof \Cassandra\Timestamp || 224 | $obj instanceof \Cassandra\Inet || 225 | $obj instanceof \Cassandra\Time 226 | ) { 227 | return true; 228 | } else { 229 | return false; 230 | } 231 | } 232 | 233 | /** 234 | * Check if object is instance of any cassandra object types 235 | * 236 | * @param $obj 237 | * @return bool 238 | */ 239 | protected function isCompareableCassandraObject($obj) 240 | { 241 | if ($obj instanceof \Cassandra\Uuid || 242 | $obj instanceof \Cassandra\Inet 243 | ) { 244 | return true; 245 | } else { 246 | return false; 247 | } 248 | } 249 | 250 | /** 251 | * Returns comparable value from cassandra object type 252 | * 253 | * @param $obj 254 | * @return mixed 255 | */ 256 | protected function valueFromCassandraObject($obj) 257 | { 258 | $class = get_class($obj); 259 | $value = ''; 260 | switch ($class) { 261 | case 'Cassandra\Date': 262 | $value = $obj->seconds(); 263 | break; 264 | case 'Cassandra\Time': 265 | $value = $obj->__toString(); 266 | break; 267 | case 'Cassandra\Timestamp': 268 | $value = $obj->time(); 269 | break; 270 | case 'Cassandra\Float': 271 | $value = $obj->value(); 272 | break; 273 | case 'Cassandra\Decimal': 274 | $value = $obj->value(); 275 | break; 276 | case 'Cassandra\Inet': 277 | $value = $obj->address(); 278 | break; 279 | case 'Cassandra\Uuid': 280 | $value = $obj->uuid(); 281 | break; 282 | } 283 | 284 | return $value; 285 | } 286 | 287 | } 288 | -------------------------------------------------------------------------------- /src/Eloquent/SoftDeletes.php: -------------------------------------------------------------------------------- 1 | getDeletedAtColumn(); 30 | } 31 | 32 | /** 33 | * Determine if the model instance has been soft-deleted. 34 | * 35 | * @return bool 36 | */ 37 | public function trashed() 38 | { 39 | return $this->{$this->getDeletedAtColumn()} != ''; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/Eloquent/SoftDeletingScope.php: -------------------------------------------------------------------------------- 1 | where($model->getQualifiedDeletedAtColumn(), '>=', Carbon::createFromTimestamp(-1)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Query/Builder.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 47 | $this->grammar = $grammar ?: $connection->getQueryGrammar(); 48 | $this->processor = $processor ?: $connection->getPostProcessor(); 49 | } 50 | 51 | /** 52 | * Support "allow filtering" 53 | */ 54 | public function allowFiltering($bool = true) { 55 | $this->allowFiltering = (bool) $bool; 56 | 57 | return $this; 58 | } 59 | 60 | /** 61 | * Insert a new record into the database. 62 | * 63 | * @param array $values 64 | * @return bool 65 | */ 66 | public function insert(array $values) 67 | { 68 | // Since every insert gets treated like a batch insert, we will make sure the 69 | // bindings are structured in a way that is convenient when building these 70 | // inserts statements by verifying these elements are actually an array. 71 | if (empty($values)) { 72 | return true; 73 | } 74 | 75 | if (!is_array(reset($values))) { 76 | $values = [$values]; 77 | 78 | return $this->connection->insert( 79 | $this->grammar->compileInsert($this, $values), 80 | $this->cleanBindings(Arr::flatten($values, 1)) 81 | ); 82 | } 83 | 84 | // Here, we'll generate the insert queries for every record and send those 85 | // for a batch query 86 | else { 87 | $queries = []; 88 | $bindings = []; 89 | 90 | foreach ($values as $key => $value) { 91 | ksort($value); 92 | 93 | $queries[] = $this->grammar->compileInsert($this, $value); 94 | $bindings[] = $this->cleanBindings(Arr::flatten($value, 1)); 95 | } 96 | 97 | return $this->connection->insertBulk($queries, $bindings); 98 | } 99 | } 100 | 101 | /** 102 | * Execute the query as a "select" statement. 103 | * 104 | * @param array $columns 105 | * 106 | * @return \Illuminate\Support\Collection 107 | */ 108 | public function get($columns = ['*']) 109 | { 110 | $original = $this->columns; 111 | 112 | if (is_null($original)) { 113 | $this->columns = $columns; 114 | } 115 | 116 | //Set up custom options 117 | $options = []; 118 | if ($this->pageSize !== null && (int) $this->pageSize > 0) { 119 | $options['page_size'] = (int) $this->pageSize; 120 | } 121 | if ($this->paginationStateToken !== null) { 122 | $options['paging_state_token'] = $this->paginationStateToken; 123 | } 124 | 125 | // Process select with custom options 126 | $results = $this->processor->processSelect($this, $this->runSelect($options)); 127 | 128 | // Get results from all pages 129 | $collection = new Collection($results); 130 | 131 | if ($this->fetchAllResults) { 132 | while (!$collection->isLastPage()) { 133 | $collection = $collection->appendNextPage(); 134 | } 135 | } 136 | 137 | $this->columns = $original; 138 | 139 | return $collection; 140 | } 141 | 142 | /** 143 | * Run the query as a "select" statement against the connection. 144 | * 145 | * @param array $options 146 | * 147 | * @return array 148 | */ 149 | protected function runSelect(array $options = []) 150 | { 151 | return $this->connection->select( 152 | $this->toSql(), $this->getBindings(), !$this->useWritePdo, $options 153 | ); 154 | } 155 | 156 | /** 157 | * Set pagination state token to fetch 158 | * next page 159 | * 160 | * @param string $token 161 | * 162 | * @return Builder 163 | */ 164 | public function setPaginationStateToken($token = null) 165 | { 166 | $this->paginationStateToken = $token; 167 | 168 | return $this; 169 | } 170 | 171 | /** 172 | * Set page size 173 | * 174 | * @param int $pageSize 175 | * 176 | * @return Builder 177 | */ 178 | public function setPageSize($pageSize = null) 179 | { 180 | if ($pageSize !== null) { 181 | $this->pageSize = (int) $pageSize; 182 | } else { 183 | $this->pageSize = $pageSize; 184 | } 185 | 186 | return $this; 187 | } 188 | 189 | /** 190 | * Get collection with single page results 191 | * 192 | * @param $columns array 193 | * 194 | * @return \Illuminate\Support\Collection 195 | */ 196 | public function getPage($columns = ['*']) 197 | { 198 | $this->fetchAllResults = false; 199 | 200 | $result = $this->get($columns); 201 | 202 | $this->fetchAllResults = true; 203 | 204 | return $result; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/Query/Grammar.php: -------------------------------------------------------------------------------- 1 |