├── 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 |