├── .gitignore ├── .mysql-dev-env ├── .postgres-dev-env ├── Makefile ├── README.md ├── Testing.php ├── composer.json ├── composer.lock ├── docker-compose.yaml ├── shard_matrix.yaml ├── src ├── Config.php ├── DB │ ├── Builder │ │ ├── BuilderException.php │ │ ├── DB.php │ │ ├── Model.php │ │ ├── QueryBuilder.php │ │ ├── Schema.php │ │ ├── SchemaBuilder.php │ │ ├── ShardMatrixConnection.php │ │ ├── UnassignedConnection.php │ │ └── UnassignedNode.php │ ├── Connections.php │ ├── DataRow.php │ ├── DataRowFactory.php │ ├── DataRows.php │ ├── DuplicateException.php │ ├── Exception.php │ ├── GroupSum.php │ ├── GroupSums.php │ ├── Interfaces │ │ ├── ConstructArrayInterface.php │ │ ├── ConstructObjectInterface.php │ │ ├── CreatorInterface.php │ │ ├── DBDataRowTransactionsInterface.php │ │ ├── PreSerialize.php │ │ ├── ResultsInterface.php │ │ └── ShardDataRowInterface.php │ ├── Models │ │ └── EloquentDataRowModel.php │ ├── NodeQueries.php │ ├── NodeQuery.php │ ├── NodeQueryModifier.php │ ├── NodeQueryModifiers.php │ ├── PaginationStatement.php │ ├── PreStatement.php │ ├── ShardCache.php │ ├── ShardDB.php │ ├── ShardMatrixStatement.php │ └── ShardMatrixStatements.php ├── DockerNetwork.php ├── Dsn.php ├── Exception.php ├── GoThreaded │ ├── Client.php │ ├── GoThreadedException.php │ ├── NodeResult.php │ └── Results.php ├── Node.php ├── NodeDistributor.php ├── NodeQueriesAsyncInterface.php ├── NodeQueriesGoThreaded.php ├── NodeQueriesPcntlFork.php ├── Nodes.php ├── PdoCache.php ├── PdoCacheInterface.php ├── PdoCacheMemcached.php ├── PdoCacheRedis.php ├── ShardMatrix.php ├── Table.php ├── TableGroup.php ├── TableGroups.php ├── Tables.php ├── UniqueColumns.php └── Uuid.php └── tests ├── .mysql-dev-env ├── .postgres-dev-env ├── docker-compose.yaml ├── shard_matrix.yaml └── src └── TestSchema.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | vendor/* 3 | shard_matrix_cache 4 | shard_matrix_cache/* 5 | dataOld/* 6 | dataOld 7 | data 8 | data/* -------------------------------------------------------------------------------- /.mysql-dev-env: -------------------------------------------------------------------------------- 1 | MYSQL_ROOT_PASSWORD=password 2 | MYSQL_DATABASE=shard -------------------------------------------------------------------------------- /.postgres-dev-env: -------------------------------------------------------------------------------- 1 | POSTGRES_PASSWORD=password 2 | POSTGRES_DB=shard 3 | POSTGRES_USER=postgres 4 | PGDATA=/data/postgres -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | pwd && vendor/bin/phpunit --bootstrap vendor/autoload.php tests/src/* 3 | 4 | test-up: 5 | make test-down && docker-compose -f ./tests/docker-compose.yaml up -d \ 6 | && sleep 25 && make test \ 7 | && make test-down 8 | 9 | test-down: 10 | docker-compose -f ./tests/docker-compose.yaml down 11 | # && make docker-clean && make docker-prune 12 | 13 | push: 14 | make test-up && git push 15 | 16 | docker-prune: 17 | docker image prune -a -f 18 | 19 | docker-clean: 20 | docker rmi -f $$(docker images -a -q) || docker ps 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![Latest Stable Version](https://poser.pugx.org/jrsaunders/shard-matrix/v/stable)](https://packagist.org/packages/jrsaunders/shard-matrix) 3 | [![Total Downloads](https://poser.pugx.org/jrsaunders/shard-matrix/downloads)](https://packagist.org/packages/jrsaunders/shard-matrix) 4 | [![Latest Unstable Version](https://poser.pugx.org/jrsaunders/shard-matrix/v/unstable)](https://packagist.org/packages/jrsaunders/shard-matrix) 5 | [![License](https://poser.pugx.org/jrsaunders/shard-matrix/license)](https://packagist.org/packages/jrsaunders/shard-matrix) 6 | # ShardMatrix for PHP 7 | 8 | ### Database Sharding system for MYSQL and Postgres 9 | 10 | * Requirements 11 | * PHP 7.4^ 12 | 13 | * Supports: 14 | * **A single Yaml configuration file** 15 | * **Multiple Nodes** (DB servers) 16 | * **Mysql** 17 | * **Postgres** 18 | * **Mysql & Postgres can be used together** and hot swapped 19 | * **Multiple Geo Locations** 20 | * **UUIDs** bakes in all relevant data for tables and to which node it belongs 21 | * **Docker** 22 | * **Kubernetes** 23 | * **Fast Asynchronous DB queries** (using a purpose built GoThreaded service https://github.com/jrsaunders/go-threaded | https://hub.docker.com/r/jrsaunders/gothreaded or PHP Forking for crons or dev work) 24 | * Caching to File or to **Redis** or **MemcacheD** 25 | * Unique table columns across nodes 26 | * Table Grouping to ensure data is kept in the right shards so joins can be done 27 | * Using popular ORM from Laravel ( though your project does **not** need be in Laravel ) https://laravel.com/docs/7.x 28 | * Query building being database agnostic 29 | * Efficient pagination system across Nodes using caching 30 | * Raw SQL Queries 31 | 32 | ## Quick Usage 33 | 34 | Once you have initiated it as outlined in the [INSTALLATION](#installation) section below - here are some quick examples of usage. 35 | 36 | _If you are familiar with the ORM in Laravel - this is just an extension of that._ 37 | 38 | ### Create Table 39 | * Creates Table across all appropriate Nodes (Mysql and Postgres simultaneously). This follows the guidance you have given in your Yaml Config file as to what tables belong on what nodes 40 | ```php 41 | use ShardMatrix\DB\Builder\Schema; 42 | 43 | # Creates Table across all appropriate Nodes (Mysql and Postgres simultaneously). 44 | # This follows the guidance you have given in your Yaml Config file as to what tables 45 | # belong on what nodes 46 | 47 | Schema::create( 'users', 48 | function ( \Illuminate\Database\Schema\Blueprint $table ) { 49 | 50 | $table->string( 'uuid', 50 )->primary(); 51 | $table->string('username',255)->unique(); 52 | $table->string('email',255)->unique(); 53 | $table->integer('something'); 54 | $table->dateTime( 'created' ); 55 | 56 | } 57 | ); 58 | ``` 59 | 60 | 61 | ### Insert Record 62 | * Insert Data - the system will choose an appropriate shard node and create a UUID for it that will be attributed to an appropriate node 63 | ```php 64 | use ShardMatrix\DB\Builder\DB; 65 | 66 | # Insert Data - the system will choose an appropriate shard node and create a UUID for it that will be attributed to an appropriate node 67 | 68 | $uuid = DB::table( 'users' )->insert( 69 | [ 70 | 'username' => 'jack-malone', 71 | 'password' => 'poootpooty', 72 | 'created' => (new \DateTime())->format('Y-m-d H:i:s'), 73 | 'something' => 5, 74 | 'email' => 'jack.malone@yatti.com', 75 | ] 76 | ); 77 | 78 | echo $uuid->toString(); 79 | # outputs 06a00233-1ea8af83-9b6f-6104-b465-444230303037 80 | 81 | echo $uuid->getNode()->getName(); 82 | # outputs DB0007 83 | 84 | echo $uuid->getTable()->getName(); 85 | # outputs users 86 | 87 | ``` 88 | **Inserted Data** 89 | ``` 90 | uuid 06a00233-1ea8af83-9b6f-6104-b465-444230303037 91 | username jack-malone 92 | password poootpooty 93 | email jack.malone@yatti.com 94 | created 2020-04-30 15:35:31.000000 95 | something 5 96 | ``` 97 | 98 | * Any further inserts done in this php process will be inserted into the same shard, if in the correct table group 99 | 100 | ### Get Record By UUID and Update Record 101 | * Get the record directly from the correct node (shard) 102 | * Manipulate the record 103 | * Update the record 104 | 105 | ```php 106 | use ShardMatrix\DB\Builder\DB; 107 | use ShardMatrix\DB\Interfaces\DBDataRowTransactionsInterface; 108 | 109 | # Get the record directly from the correct node (shard) 110 | $record = DB::getByUuid( '06a00233-1ea8af83-9b6f-6104-b465-444230303037' ); 111 | 112 | # Manipulate the record 113 | if ( $record && $record instanceof DBDataRowTransactionsInterface) { 114 | 115 | # As above you could run an additional check for the instance of the record returned, but it should always follow this interface through the query builder 116 | 117 | echo $record->username; 118 | # outputs jack-malone 119 | 120 | echo $record->email; 121 | # outputs jack.malone@yatti.com 122 | 123 | # overwrite the email attribute 124 | $record->email = 'anotheremail@yatti.com'; 125 | 126 | # Update the record 127 | $record->save(); 128 | } 129 | 130 | ``` 131 | 132 | ### Query Data and Conditionally Delete a Record 133 | * Query all relevant nodes for the data 134 | * Data returns as a Collection that can be iterated through 135 | * Use data conditionally 136 | * Manipulate the record and commit changes 137 | 138 | ```php 139 | use ShardMatrix\DB\Builder\DB; 140 | use ShardMatrix\DB\Interfaces\DBDataRowTransactionsInterface; 141 | 142 | # Query all relevant nodes for the data 143 | $collection = DB::allNodesTable( 'users')->where('email','like','%yatti%')->limit(50)->get(); 144 | 145 | # Data returns as a Collection that can be iterated through 146 | $collection->each( function(DBDataRowTransactionsInterface $record){ 147 | 148 | # Use data conditionally 149 | if($record->username == 'a-bad-user'){ 150 | 151 | # Manipulate the record and commit changes 152 | $record->delete(); 153 | } 154 | 155 | }); 156 | 157 | ``` 158 | 159 | ## Pagination 160 | 161 | ### Pagination of Data from all shards 162 | 163 | 164 | ```php 165 | use ShardMatrix\DB\Builder\DB; 166 | use ShardMatrix\DB\Interfaces\DBDataRowTransactionsInterface; 167 | 168 | $pagination = DB::allNodesTable( 'users' ) 169 | ->orderBy( 'created', 'desc' ) 170 | ->paginate(); 171 | 172 | $pagination->each( function ( DBDataRowTransactionsInterface $record) { 173 | 174 | echo $record->username; 175 | 176 | echo $record->getUuid(); 177 | }); 178 | 179 | echo $pagination->total(); 180 | 181 | echo $pagination->perPage(); 182 | 183 | echo $pagination->nextPageUrl(); 184 | 185 | echo $pagination->previousPageUrl(); 186 | ``` 187 | ### Pagination of Data from one shard defined by the UUID location 188 | 189 | ```php 190 | use ShardMatrix\DB\Builder\DB; 191 | use ShardMatrix\DB\Interfaces\DBDataRowTransactionsInterface; 192 | 193 | $uuidFromCurrentUser = "06a00233-1ea8af83-d514-6a76-83ae-444230303037"; 194 | 195 | $pagination = DB::table( 'users' ) 196 | ->uuidAsNodeReference($uuidFromCurrentUser) 197 | ->orderBy( 'created', 'desc' ) 198 | ->paginate(); 199 | 200 | $pagination->each( function ( DBDataRowTransactionsInterface $record) { 201 | 202 | echo $record->username; 203 | 204 | echo $record->getUuid(); 205 | }); 206 | 207 | echo $pagination->total(); 208 | 209 | echo $pagination->perPage(); 210 | 211 | echo $pagination->nextPageUrl(); 212 | 213 | echo $pagination->previousPageUrl(); 214 | ``` 215 | 216 | 217 | # Installation 218 | 219 | ## Installing ShardMatrix for PHP 220 | 221 | Use Composer to install ShardMatrix, or pull the repo from github. 222 | 223 | ``` 224 | composer require jrsaunders/shard-matrix 225 | ``` 226 | 227 | ## Preparing the YAML config file 228 | 229 | ShardMatrix needs to know how your tables and columns and databases interact, so this config file will define this in a simple yaml file. 230 | * You will need your credentials for your databases, and access privileges setup. 231 | [Reference Yaml file](shard_matrix.yaml) 232 | 233 | ### Example 234 | This is a full example of how a configuration file should look. 235 | ```yaml 236 | version: 1 237 | 238 | table_groups: 239 | user: 240 | - users 241 | - payments 242 | - offers 243 | tracking: 244 | - visitors 245 | - sign_ups 246 | published: 247 | - published_offers 248 | 249 | unique_columns: 250 | users: 251 | - email 252 | - username 253 | 254 | nodes: 255 | DB0001: 256 | dsn: mysql:dbname=shard;host=localhost:3301;user=root;password=password 257 | docker_network: DB0001:3306 258 | geo: UK 259 | table_groups: 260 | - user 261 | - published 262 | DB0002: 263 | dsn: mysql:dbname=shard;host=localhost:3302;user=root;password=password 264 | docker_network: DB0002:3306 265 | geo: UK 266 | table_groups: 267 | - user 268 | - published 269 | DB0003: 270 | dsn: mysql:dbname=shard;host=localhost:3303;user=root;password=password 271 | docker_network: DB0003:3306 272 | geo: UK 273 | table_groups: 274 | - user 275 | - published 276 | DB0004: 277 | dsn: mysql:dbname=shard;host=localhost:3304;user=root;password=password 278 | docker_network: DB0004:3306 279 | geo: UK 280 | table_groups: 281 | - published 282 | DB0005: 283 | dsn: mysql:dbname=shard;host=localhost:3305;user=root;password=password 284 | docker_network: DB0005:3306 285 | table_groups: 286 | - tracking 287 | DB0006: 288 | dsn: mysql:dbname=shard;host=localhost:3306;user=root;password=password 289 | docker_network: DB0006:3306 290 | geo: UK 291 | insert_data: false 292 | table_groups: 293 | - tracking 294 | DB0007: 295 | dsn: pgsql:dbname=shard;host=localhost:5407;user=postgres;password=password 296 | docker_network: DB0007:5432 297 | geo: UK 298 | table_groups: 299 | - user 300 | - tracking 301 | 302 | ``` 303 | ## Anatomy of the Configuration File 304 | 305 | ### Version 306 | Define the version. The most recent version is 1. 307 | ```yaml 308 | version: 1 309 | ``` 310 | ### Table Groups 311 | Define the table groups. As you add tables to your Application you will need to explicitly add them here to. 312 | 313 | The group name is only used in ShardMatrix. 314 | 315 | The table names are attributed to the groups. A table can only be in one group at a time and once you have written to the Databases, it is best not to change any table assigned to a group. 316 | * Denotes the table groups section on config 317 | * Denotes the name of a group of tables 318 | * Denotes the table name 319 | 320 | ```yaml 321 | # Denotes the table groups section on config 322 | 323 | table_groups: 324 | 325 | # Denotes the name of a group of tables 326 | 327 | user: 328 | 329 | # Denotes the table name 330 | 331 | - users 332 | ``` 333 | This section as it may appear. 334 | ```yaml 335 | table_groups: 336 | user: 337 | - users 338 | - payments 339 | - offers 340 | tracking: 341 | - visitors 342 | - sign_ups 343 | published: 344 | - published_offers 345 | ``` 346 | ### Unique Columns in Tables 347 | Unique Columns can be defined here. So in the `users` table `email` and `username` must be unique across all Nodes (shard databases). 348 | ```yaml 349 | unique_columns: 350 | users: 351 | - email 352 | - username 353 | facebook_users: 354 | - fb_id 355 | ``` 356 | ### Nodes 357 | 358 | This is where you define your database connections, credentials, and what table groups and geos the node maybe using. 359 | 360 | Nodes can be extended and added to as you go. 361 | 362 | Node names must remain the same though as must the table groups they correspond to. 363 | 364 | The anatomy of the node section. 365 | * Denotes the where the nodes are defined 366 | * Node Name 367 | * DSN for connection to DB 368 | * *optional Docker service name and port number 369 | * *optional Geo - if a geo is stated the application inserting data will use this to choose this node to write new inserts to it 370 | * *optional Stop new data being written here, unless connected to an existing UUID from this node 371 | * Table groups that use this node must be defined here 372 | * Table group user (that consists of the users, offers, payments tables) 373 | ```yaml 374 | # Denotes the where the nodes are defined 375 | 376 | nodes: 377 | 378 | # Node Name 379 | 380 | DBUK01: 381 | 382 | # DSN for connection to DB 383 | 384 | dsn: mysql:dbname=shard;host=localhost:3301;user=root;password=password 385 | 386 | # *optional docker service name and port number 387 | 388 | docker_network: DBUK:3306 389 | 390 | # *optional Geo - if a geo is stated the application inserting data will use this to choose this node to write new inserts to it 391 | 392 | geo: UK 393 | 394 | # *optional Stop new data being written here, unless connected to an existing UUID from this node 395 | 396 | insert_data: false 397 | 398 | # Table groups that use this node must be defined here 399 | 400 | table_groups: 401 | 402 | # Table group user (that consists of the users, offers, payments tables) 403 | 404 | - user 405 | 406 | - published 407 | ``` 408 | The Node Section as it may appear in the config yaml. 409 | ```yaml 410 | nodes: 411 | DBUK01: 412 | dsn: mysql:dbname=shard;host=localhost:3301;user=root;password=password 413 | docker_network: DBUK:3306 414 | geo: UK 415 | table_groups: 416 | - user 417 | - published 418 | postg1: 419 | dsn: pgsql:dbname=shard;host=localhost:5407;user=postgres;password=password 420 | docker_network: postg1_db:5432 421 | table_groups: 422 | - tracking 423 | DB0001: 424 | dsn: mysql:dbname=shard;host=localhost:3304;user=root;password=password 425 | docker_network: DB0001:3306 426 | insert_data: false 427 | table_groups: 428 | - user 429 | - published 430 | ``` 431 | 432 | ### Once Yaml Config File is Complete 433 | 434 | Save the **file** to where the application is, in either a protected directory or externally inaccessible directory. 435 | 436 | Alternatively it can be made into a **Kubernetes Secret** and given to your application that way. 437 | 438 | 439 | # Initiate in PHP 440 | 441 | In these examples we have saved our Config file as `shard_matrix.yaml` and placed it in the same directory as our applications index php. 442 | 443 | #### Basic Setup Using Only PHP and Webserver Resources 444 | * Our config file 445 | * Specifying a local directory to write db data to when it needs to 446 | ```php 447 | 448 | use ShardMatrix\ShardMatrix; 449 | 450 | 451 | # Our config file 452 | 453 | ShardMatrix::initFromYaml( __DIR__ . '/shard_matrix.yaml' ); 454 | 455 | 456 | # Specifying a local directory to write db data to when it needs to 457 | 458 | ShardMatrix::setPdoCachePath( __DIR__ . '/shard_matrix_cache' ); 459 | 460 | ``` 461 | #### Setup Using Only GoThreaded and Redis 462 | * Our config file 463 | * Changes the service from PHP forking for asynchronous queries to GoThreaded 464 | * Uses GoThreaded for asynchronous DB calls when we have to query all relevant shards 465 | * This overwrites the PdoCache Service that was using writing to file, and now instead uses Redis caching 466 | ```php 467 | 468 | use ShardMatrix\ShardMatrix; 469 | 470 | 471 | # Our config file 472 | 473 | ShardMatrix::initFromYaml( __DIR__ . '/shard_matrix.yaml' ); 474 | 475 | 476 | # Changes the service from PHP forking for asynchronous queries to GoThreaded 477 | 478 | ShardMatrix::useGoThreadedForAsyncQueries(); 479 | 480 | 481 | # Uses GoThreaded for asynchronous DB calls when we have to query all relevant shards 482 | 483 | ShardMatrix::setGoThreadedService( function () { 484 | return new \ShardMatrix\GoThreaded\Client( '127.0.0.1', 1534, 'gothreaded', 'password' ); 485 | } ); 486 | 487 | # This overwrites the PdoCache Service that was used to write to file, and now instead uses Redis caching 488 | 489 | ShardMatrix::setPdoCacheService( function () { 490 | return new \ShardMatrix\PdoCacheRedis( new \Predis\Client( 'tcp://127.0.0.1:6379' ) ); 491 | } ); 492 | 493 | ``` 494 | 495 | -------------------------------------------------------------------------------- /Testing.php: -------------------------------------------------------------------------------- 1 | addServer( 'localhost', 11211 ); 23 | // 24 | // return new \ShardMatrix\PdoCacheMemcached( $memcached ); 25 | //} ); 26 | //ShardMatrix::setGoThreadedService( function () { 27 | // return new \ShardMatrix\GoThreaded\Client( '127.0.0.1', 1534, 'gothreaded', 'password', 10 ); 28 | //} ); 29 | //ShardMatrix::setTableToDataRowClassMap( [ 'users' => \ShardMatrix\DB\DataRow::class ] ); 30 | //ShardMatrix::setGeo( 'UK' ); 31 | // 32 | //$pagination = DB::allNodesTable( 'users' )->getPagination( [ "*" ], 1, 15, 12 ); 33 | //$results = $pagination->countResults(); 34 | // 35 | // 36 | //$pages = $pagination->countPages(); 37 | // 38 | //$nodes = []; 39 | //foreach ( $pagination->getResults()->fetchDataRows() as $result ) { 40 | // $uuid = $result->getUuid(); 41 | // echo $uuid; 42 | // $nodes[ $uuid->getNode()->getName() ] = $uuid->getNode()->getName(); 43 | // $result->password = 'sillybilly69'; 44 | //// $result->save(); 45 | // $result->getUuid(); 46 | //} 47 | //echo $count = count( $nodes ); 48 | //ShardMatrix::getPdoCacheService()->write( 'poo-hoo-do2', 'hello1' ); 49 | //ShardMatrix::getPdoCacheService()->write( 'poo-hoo-do1', 'hello3' ); 50 | //var_dump( ShardMatrix::getPdoCacheService()->scan( 'poo' )); 51 | 52 | //$statement = DB::allNodesTable( 'users')->where('username','like','randy%')->getPagination(); 53 | //$statement = DB::allNodesTable( 'users')->limit('10')->getPagination()->getResults(); 54 | 55 | //var_dump( $statement->getResults()->fetchDataRows() ); 56 | 57 | 58 | // 59 | //$shardDb->nodeQuery( 60 | // ShardMatrix::getConfig()->getNodes()->getNodeByName( 'DB0001'), 61 | // "select uuid, username , ROW_NUMBER() OVER(ORDER BY uuid) as rowNum from users limit 10; " 62 | //); 63 | // 64 | //var_dump($shardDb->allNodesQuery( 'users', "select uuid from users limit 100;",null,'uuid','asc')); 65 | 66 | //DB::allNodesTable( 'users')->getPagination(); 67 | // 68 | 69 | 70 | //var_dump(DB::allNodesTable( 'users' )->uuidMarkerPageAbove('06a00233-1ea82fe3-6a4d-6398-ab7b-444230303032')->getStatement()->fetchAllObjects()); 71 | // 72 | 73 | 74 | //$f = ( new ShardDB() )->allNodesQuery( 'users', "ALTER TABLE users add created DATETIME null; " ); 75 | //$f = ( new ShardDB() )->allNodesQuery( 'users', "select * from users" ,null,'username','asc'); 76 | //var_dump($f->fetchRowArray()); 77 | //Schema::silent()->table( 'users', function(\Illuminate\Database\Schema\Blueprint $table){ 78 | // $table->integer( 'something')->after( 'uuid'); 79 | //}); 80 | //DB::updateByUuid( '06a00233-1ea82fe3-4937-626c-8f1e-444230303032', ['password' => 'onlypoo12']); 81 | 82 | //$shardDb = new ShardDB(); 83 | //echo DB::table( 'users')->sum('created'); die; 84 | //$shardDb->nodeQuery( ShardMatrix::getConfig()->getNodes()->getNodeByName( 'DB0007'), "create table users 85 | //( 86 | // uuid varchar(50) not null 87 | // primary key, 88 | // username varchar(100) null, 89 | // password varchar(100) null, 90 | // email varchar(200) null, 91 | // created timestamp without time zone null 92 | //);"); 93 | 94 | //Schema::create( 'visitors', 95 | // function ( \Illuminate\Database\Schema\Blueprint $table ) { 96 | // $table->string( 'uuid', 50 )->primary(); 97 | // $table->dateTime( 'created' ); 98 | // 99 | // } ); 100 | 101 | //$tableName = 'users'; 102 | //$con = new ShardMatrixConnection( 103 | // NodeDistributor::getNode( $tableName ) 104 | // ); 105 | // 106 | //$q = $con->query()->select('username')->from( 'users')->whereBetween( 'created',['2020-04-19','2020-04-21'])->limit(10); 107 | 108 | 109 | // 110 | //echo $q->toSql(); 111 | // 112 | //var_dump( $q->getBindings()); 113 | // 114 | //$x = $shardDb->allNodesQuery( 'users', 'select * from `users` where `created` between ? and ? and uuid = ? limit 10', ['2020-04-19','2020-04-21','06a00233-1ea82fe3-46ef-6464-8494-444230303031']); 115 | // 116 | //echo $x->rowCount(); 117 | // 118 | 119 | //$x = $shardDb->allNodesQuery( 'users', 'select * from users limit 10' ); 120 | 121 | //var_dump($x->fetchResultSet()); 122 | //var_dump( $shardDb->allNodesQuery( 'users', 'select count(*) as count , 123 | // HOUR( created ) as hours from users group by hours' )->sumColumnByGroup( 'count', 'hours' )); 124 | //$dsn = new Dsn('mysql:dbname=shard;host=localhost:3304;user=root;password=password'); 125 | //echo $dsn; 126 | //$x = $shardDb->allNodesQuery( 'users', "select * from users where created between '2020-04-20 11:50:10' and '2020-04-20 12:20:00' order by uuid desc limit 300;",null,'uuid','asc'); 127 | 128 | //$x = $shardDb->allNodesQuery( 'users', "select * from users where uuid > '06a00233-1ea830fb-874c-6cb4-ac3a-444230303033' order by uuid asc;",null,'uuid','asc'); 129 | 130 | 131 | //var_dump( $x->fetchAllObjects()); 132 | // 133 | //$i = 0; 134 | //while ( $i < 100 ) { 135 | // $username = 'randy' . rand( 5000, 10000000 ) . uniqid(); 136 | // $password = 'cool!!' . rand( 5000, 100000 ); 137 | // $email = 'timmy' . rand( 1, 10000000 ) . uniqid() . '@google.com'; 138 | // $created = ( new DateTime() )->format( 'Y-m-d H:i:s' ); 139 | // $i ++; 140 | // try { 141 | // \ShardMatrix\DB\Connections::closeConnections(); 142 | // $shardDb = new ShardDB(); 143 | // $shardDb->newNodeInsert( 'users', "insert into users (uuid,username,password,email,created,something) values (:uuid,:username,:password,:email,:created,:something);", [ 144 | // ':username' => $username, 145 | // ':password' => $password, 146 | // ':email' => $email, 147 | // ':created' => $created, 148 | // ':something' => 4 149 | // ] ); 150 | // } catch ( \Exception $exception ) { 151 | // echo $exception->getMessage() . PHP_EOL; 152 | // } 153 | //} 154 | //$shardDb->setCheckSuccessFunction( function ( \ShardMatrix\DB\ShardMatrixStatement $statement, string $calledMethod ) use ( $shardDb ) { 155 | // if ( $calledMethod == 'insert' && $statement->getUuid()->getTable()->getName() == 'users' ) { 156 | // $email = $shardDb->getByUuid( $statement->getUuid() )->email; 157 | // $checkDupes = $shardDb->nodesQuery( $statement->getAllTableNodes(), "select uuid from users where email = :email and uuid != :uuid", [ ':email' => $email ,':uuid' => $statement->getUuid()->toString()] ); 158 | // if($checkDupes->isSuccessful()){ 159 | // $shardDb->deleteByUuid( $statement->getUuid()); 160 | // throw new \Exception('Duplicate Record'); 161 | // } 162 | // } 163 | // 164 | // return true; 165 | //} ); 166 | //$shardDb->setDefaultRowReturnClass( \ShardMatrix\DB\TestRow::class); 167 | 168 | 169 | //$shardDb->insert( 'users', "insert into users (uuid,email,username,password) values (:uuid,'email50ss5@google.com','odeq234iwuow','qwug234ddugwq');"); 170 | // 171 | //$x = $shardDb->allNodesQuery( 'users', 'select * from users limit 23000;' ); 172 | //$i = 0; 173 | //foreach($x->fetchResultSet() as $row){ 174 | // echo $row->getUuid()->__toString().PHP_EOL.$i++; 175 | //} 176 | //echo PHP_EOL.$x->rowCount(); 177 | 178 | 179 | // 180 | //foreach ($x->getShardMatrixStatements() as $s){ 181 | // echo PHP_EOL.$s->getQueryString().PHP_EOL; 182 | // echo PHP_EOL.$s->getNode()->getName().':'.$s->rowCount().PHP_EOL; 183 | // //var_dump($s->fetchResultSet()->jsonSerialize()); 184 | //} 185 | 186 | //$shardDb->deleteByUuid( new \ShardMatrix\Uuid('06a00233-1ea82566-fa3d-6066-ac4d-444230303031')); 187 | 188 | //$shardDb->insert( 'users', "insert into users (uuid,username,password,email) values (:uuid,:username,:password,:email);", [ 189 | // ':username' => $username, 190 | // ':password' => $password, 191 | // ':email' => $email 192 | //] ); 193 | 194 | //ShardMatrix::getConfig()->getUniqueColumns(); 195 | 196 | 197 | //$stmt = ( new ShardQuery() )->test( ShardMatrix::getConfig()->getNodes()->getNodeByName( 'DB0001' ), 'select * from users' ); 198 | //var_dump( $stmt ); 199 | 200 | //(new ShardQuery())->allNodeQuery( 'users', "select * from users where username = :username",[':username'=>'bobbyB45'])->fetchRowObject(); 201 | 202 | //$node = ShardMatrix::getConfig()->getNodes()->getNodes()[4]; 203 | ////make NODE distributer 204 | //$table = new \ShardMatrix\Table( 'visitors'); 205 | // 206 | //$uuid = \ShardMatrix\Uuid::make( $node, $table); 207 | // 208 | //echo $uuid->getNode()->getName(); 209 | //echo $uuid->getTable()->getName(); 210 | //echo $uuid; 211 | 212 | //$uuid = new \ShardMatrix\Uuid('0fca0384-1ea80cba-e8a2-6a8c-bacd-444230303035'); 213 | // 214 | //echo $uuid->getTable()->getName().' '.$uuid->getNode()->getName(); 215 | // 216 | 217 | //echo \ShardMatrix\NodeDistributor::getNode( 'visitors')->getName().PHP_EOL; 218 | //echo \ShardMatrix\NodeDistributor::getNode( 'sign_ups')->getName().PHP_EOL; 219 | //echo \ShardMatrix\NodeDistributor::getNode( 'users')->getName().PHP_EOL; 220 | //echo \ShardMatrix\NodeDistributor::getNode( 'payments')->getName().PHP_EOL; 221 | //echo 'parent '.uniqid(getmypid().'-').PHP_EOL; 222 | //$results = []; 223 | //for($i=0;$i<10;$i++) { 224 | // $pid = pcntl_fork(); 225 | // if ( $pid == - 1 ) { 226 | // die( 'could not fork' ); 227 | // } else if ( $pid ) { 228 | // // we are the parent 229 | // pcntl_wait( $status ); //Protect against Zombie children 230 | // var_dump( $status ); 231 | // } else { 232 | // echo 'I am a child '.$i.' ' . getmypid() . PHP_EOL; 233 | // $results[] = 'child'; 234 | // exit; 235 | // } 236 | //} 237 | //echo 'back in parent '.getmypid().PHP_EOL; 238 | //var_dump($results); 239 | 240 | //$q = new QueryBuilder(); 241 | // 242 | //$con = new ShardMatrixConnection( NodeDistributor::getNode( 'users' ) ); 243 | //$con->prepareQuery( $q ); 244 | 245 | //$x = DB::table( 'users')->where('uuid' ,'>','06a00233-1ea82fe3-79a2-6b72-98eb-444230303033' )->limit(40)->getBindings(); 246 | // 247 | //var_dump($x); 248 | 249 | //DB::table( 'users')->whereUuid( '06a00233-1ea82fe3-79a2-6b72-98eb-444230303033')->get()->each(function(\ShardMatrix\DB\Illuminate\Model $model){ 250 | // $model->username = 'harry'; 251 | // var_dump($model->save() ); 252 | //}); 253 | 254 | //$user = DB::getByUuid( '06a00233-1ea82fe3-46ef-6464-8494-444230303031'); 255 | //$user->username = 'tim48135'; 256 | //var_dump($user->__toArray()); 257 | ////var_dump($user->save()); 258 | //// 259 | ////var_dump(DB::getByUuid( '06a00233-1ea82fe3-79a2-6b72-98eb-444230303033')); 260 | 261 | 262 | //$sdb = new ShardDB(); 263 | //// 264 | //$x = $sdb->insert( 'users', "insert into users ( uuid, username, email , password , created ) values (:uuid , :username, :email, :password, :created )", [ 265 | // ':username' => 'tim48135ff11', 266 | // ':email' => 'johfn@poo11.com', 267 | // ':password' => 'kjsffjksdds11', 268 | // ':created' => (new DateTime())->format('Y-m-d H:i:s') 269 | //] ); 270 | 271 | //$user = new \ShardMatrix\DB\Models\EloquentDataRowModel( []); 272 | //$user->setTable( 'users'); 273 | //$user->username = 'tank'; 274 | //$user->email = 'bill@fish.com'; 275 | //$user->created = (new DateTime())->format('Y-m-d H:i:s'); 276 | //$user->create(); 277 | 278 | // 279 | //$x = DB::allNodesTable( 'users')->where( 'email','=','bill@fish.com')->get()->first(); 280 | ////$x->username = 'tim48135'; 281 | ////$x->save(); 282 | //var_dump( $x->username); 283 | 284 | //$cache = new \ShardMatrix\PdoCache(); 285 | //$cache->runCleanPolicy( new ShardDB()); 286 | //$nowString = ( new DateTime() )->format( 'Y-m-d H:i:s' ); 287 | // 288 | //var_dump( DB::table( 'users' )->insert( [ 289 | // 'username' => 'jackmaolne', 290 | // 'password' => 'pooo', 291 | // 'created' => $nowString, 292 | // 'something' => 5, 293 | // 'email' => 'jack.malone@yatti.com' 294 | //] ) ); 295 | // 296 | //var_dump(DB::allNodesThisGeoTable('users',null)->where( 'email','=','jack.malone@yatti.com')->first()->username); 297 | 298 | //foreach ( DB::allNodesTable( 'users' )->orderBy( 'uuid', 'desc' )->getPagination()->getResults()->fetchDataRows() as $row ) { 299 | // echo $row->getUuid()->toString() . ' ' . $row->created . ' ' . PHP_EOL; 300 | // echo $row->getUuid()->getNode()->getName() . PHP_EOL; 301 | //} 302 | //$i = 0; 303 | //foreach ( DB::allNodesTable( 'users' )->orderBy( 'uuid', 'desc' )->getPagination( ["*"],2)->getResults()->fetchDataRows() as $object ) { 304 | // $i ++; 305 | // echo $object->getUuid() . ' ' . $object->username . PHP_EOL; 306 | // echo $object->getUuid()->getNode()->getName() . ' ' . $object->created . PHP_EOL; 307 | // echo $object->getUuid()->getNode()->getDsn()->getConnectionType() . PHP_EOL; 308 | // echo 'result: ' . $i . PHP_EOL; 309 | // $object->something = 6; 310 | // 311 | //} 312 | 313 | //Schema::table( 'users', function (\Illuminate\Database\Schema\Blueprint $table){ 314 | // $table->timestamp( 'modified')->useCurrent(); 315 | //}); 316 | // 317 | //$handle = fsockopen( "localhost", 1534 ); 318 | // 319 | //$i = 0; 320 | ////while ($c = fgets($handle)){ 321 | //// echo $c.$i++; 322 | ////} 323 | //try { 324 | // $client = new \ShardMatrix\GoThreaded\Client(); 325 | // $nodeQueries = new NodeQueries( [ 326 | // new NodeQuery( ShardMatrix::getConfig()->getNodes()->getNodeByName( 'DB0001' ), "select * from users where created > ? and created < ? limit 10000;", [ 327 | // "2020-04-10 11:58:10", 328 | // "2020-04-21 11:58:10" 329 | // ] ), 330 | // new NodeQuery( ShardMatrix::getConfig()->getNodes()->getNodeByName( 'DB0007' ), "select * from users where created > ? and created < ? limit 10000;", [ 331 | // "2020-04-10 11:58:10", 332 | // "2020-05-04 11:58:10" 333 | // ] ) 334 | // ] ); 335 | // 336 | // $client->execQueries( $nodeQueries )->getResults(); 337 | // var_dump( $client->execQueries( $nodeQueries )->getResults() ); 338 | //} catch ( \Exception $e ) { 339 | // echo $e->getCode() . PHP_EOL; 340 | //} 341 | 342 | 343 | //echo json_encode( [ 'node_queries' => $nQs ] ) ; 344 | // 345 | //$uuid = DB::table( 'users' )->insert( 346 | // [ 347 | // 'username' => 'jack-malone', 348 | // 'password' => 'poootpooty', 349 | // 'created' => ( new \DateTime() )->format( 'Y-m-d H:i:s' ), 350 | // 'something' => 5, 351 | // 'email' => 'jack.malone@yatti.com', 352 | // ] 353 | //); 354 | // 355 | //$uuidString = $uuid->toString(); 356 | //$nodeName = $uuid->getNode()->getName(); 357 | //$uuidTableName = $uuid->getTable()->getName(); 358 | // 359 | //$record = DB::getByUuid( '06a00233-1ea8af83-9b6f-6104-b465-444230303037' ); 360 | // 361 | //if ( $record && $record instanceof \ShardMatrix\DB\Interfaces\DBDataRowTransactionsInterface) { 362 | // 363 | // echo $record->username; 364 | // # outputs jack-malone 365 | // 366 | // echo $record->email; 367 | // # outputs jack.malone@yatti.com 368 | // 369 | // # overwrite the email attribute 370 | // $record->email = 'anotheremail@yatti.com'; 371 | // 372 | // # Save the record 373 | // $record->save(); 374 | //} 375 | // 376 | //$collection = DB::allNodesTable( 'users')->where('email','like','%yatti%')->limit(50)->get(); 377 | // 378 | //$collection->each( function(\ShardMatrix\DB\Interfaces\DBDataRowTransactionsInterface $record){ 379 | // echo $record->email; 380 | // echo $record->username; 381 | // if($record->username == 'a-bad-user'){ 382 | // $record->delete(); 383 | // } 384 | //}); 385 | 386 | //$c = new \ShardMatrix\GoThreaded\Client(); 387 | // 388 | //$c->execQueries( new NodeQueries( [])); 389 | 390 | //$m = new Memcached(); 391 | 392 | //echo strcmp( 'b','a'); 393 | echo substr(str_pad('2021-08-09 10:27:17.142179-0fca0384-1ebf8fc5-32e9-6b78-a44a-444230303033', 72, "0", STR_PAD_LEFT),0,72); 394 | echo substr(str_pad('00000000000000000000000000-0fca0384-1ebf8fc5-32e9-6b78-a44a-444230303033', 72, "0", STR_PAD_LEFT),0,72); -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jrsaunders/shard-matrix", 3 | "description": "A Complete Database Sharding system for MYSQL and/or Postgres. Using Laravels Query Builder easily scale up your application. Configure your whole solution in one Yaml Config file.", 4 | "license": "MIT", 5 | "keywords": [ 6 | "shards", 7 | "sharding", 8 | "node", 9 | "nodes", 10 | "uuid", 11 | "database", 12 | "mysql", 13 | "postgres", 14 | "pgsql", 15 | "gothreaded", 16 | "asynchronous", 17 | "laravel", 18 | "eloquent", 19 | "yaml config" 20 | ], 21 | "authors": [ 22 | { 23 | "name": "John Saunders", 24 | "email": "john@yettimedia.co.uk" 25 | } 26 | ], 27 | "require": { 28 | "php": "^7.4", 29 | "symfony/yaml": "^5.0", 30 | "ramsey/uuid": "^4.0", 31 | "laravel/framework": "7.13", 32 | "doctrine/dbal": "^2.10", 33 | "predis/predis": "^1.1", 34 | "ext-zlib": "*" 35 | }, 36 | "require-dev": { 37 | "phpunit/phpunit": "^7" 38 | }, 39 | "autoload": { 40 | "psr-4": { 41 | "ShardMatrix\\": "src/" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.5" 2 | 3 | services: 4 | 5 | DB0001: 6 | restart: always 7 | image: percona:5.7 8 | env_file: 9 | - .mysql-dev-env 10 | volumes: 11 | - ./data/db01/mysql:/var/lib/mysql:cached 12 | ports: 13 | - 3301:3306 14 | 15 | 16 | DB0002: 17 | restart: always 18 | image: percona:5.7 19 | env_file: 20 | - .mysql-dev-env 21 | volumes: 22 | - ./data/db02/mysql:/var/lib/mysql:cached 23 | ports: 24 | - 3302:3306 25 | 26 | 27 | DB0003: 28 | restart: always 29 | image: percona:5.7 30 | env_file: 31 | - .mysql-dev-env 32 | volumes: 33 | - ./data/db03/mysql:/var/lib/mysql:cached 34 | ports: 35 | - 3303:3306 36 | 37 | 38 | DB0004: 39 | restart: always 40 | image: percona:5.7 41 | env_file: 42 | - .mysql-dev-env 43 | volumes: 44 | - ./data/db04/mysql:/var/lib/mysql:cached 45 | ports: 46 | - 3304:3306 47 | 48 | 49 | DB0005: 50 | restart: always 51 | image: percona:5.7 52 | env_file: 53 | - .mysql-dev-env 54 | volumes: 55 | - ./data/db05/mysql:/var/lib/mysql:cached 56 | ports: 57 | - 3305:3306 58 | 59 | 60 | DB0006: 61 | restart: always 62 | image: percona:5.7 63 | env_file: 64 | - .mysql-dev-env 65 | volumes: 66 | - ./data/db06/mysql:/var/lib/mysql:cached 67 | ports: 68 | - 3306:3306 69 | 70 | 71 | DB0007: 72 | restart: always 73 | image: postgres 74 | env_file: 75 | - .postgres-dev-env 76 | volumes: 77 | - ./data/db07/postgres:/data/postgres 78 | ports: 79 | - 5407:5432 80 | 81 | gothreaded: 82 | image: jrsaunders/gothreaded 83 | restart: always 84 | environment: 85 | GOTHREADED_DEBUG: "true" 86 | ports: 87 | - 1534:1534 88 | 89 | redis: 90 | image: redis 91 | restart: always 92 | volumes: 93 | - ./data/redis:/data 94 | entrypoint: redis-server --appendonly yes 95 | ports: 96 | - 6379:6379 97 | 98 | memcached: 99 | image: memcached 100 | restart: always 101 | volumes: 102 | - ./data/memcached:/data 103 | ports: 104 | - 11211:11211 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /shard_matrix.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | 3 | table_groups: 4 | user: 5 | - users 6 | - payments 7 | - offers 8 | tracking: 9 | - visitors 10 | - sign_ups 11 | published: 12 | - published_offers 13 | 14 | unique_columns: 15 | users: 16 | - email 17 | - username 18 | 19 | nodes: 20 | DB0001: 21 | dsn: mysql:dbname=shard;host=localhost:3301;user=root;password=password 22 | docker_network: DB0001:3306 23 | geo: UK 24 | table_groups: 25 | - user 26 | - published 27 | DB0002: 28 | dsn: mysql:dbname=shard;host=localhost:3302;user=root;password=password 29 | docker_network: DB0002:3306 30 | geo: UK 31 | table_groups: 32 | - user 33 | - published 34 | DB0003: 35 | dsn: mysql:dbname=shard;host=localhost:3303;user=root;password=password 36 | docker_network: DB0003:3306 37 | geo: UK 38 | table_groups: 39 | - user 40 | - published 41 | DB0004: 42 | dsn: mysql:dbname=shard;host=localhost:3304;user=root;password=password 43 | docker_network: DB0004:3306 44 | geo: UK 45 | table_groups: 46 | - published 47 | DB0005: 48 | dsn: mysql:dbname=shard;host=localhost:3305;user=root;password=password 49 | docker_network: DB0005:3306 50 | table_groups: 51 | - tracking 52 | DB0006: 53 | dsn: mysql:dbname=shard;host=localhost:3306;user=root;password=password 54 | docker_network: DB0006:3306 55 | geo: UK 56 | insert_data: false 57 | table_groups: 58 | - tracking 59 | DB0007: 60 | dsn: pgsql:dbname=shard;host=localhost:5407;user=postgres;password=password 61 | docker_network: DB0007:5432 62 | geo: UK 63 | table_groups: 64 | - user 65 | - tracking 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/Config.php: -------------------------------------------------------------------------------- 1 | configArray = $configArray; 35 | } 36 | 37 | /** 38 | * @return TableGroups 39 | */ 40 | public function getTableGroups(): TableGroups { 41 | return $this->tableGroups ?? ( $this->tableGroups = new TableGroups( $this->configArray['table_groups'] ) ); 42 | } 43 | 44 | /** 45 | * @return Nodes 46 | */ 47 | public function getNodes(): Nodes { 48 | return $this->nodes ?? ( $this->nodes = new Nodes( $this->configArray['nodes'] ) ); 49 | } 50 | 51 | /** 52 | * @return UniqueColumns 53 | */ 54 | public function getUniqueColumns(): UniqueColumns { 55 | return $this->uniqueColumns ?? ( $this->uniqueColumns = new UniqueColumns( $this->configArray['unique_columns'] ) ); 56 | } 57 | 58 | } -------------------------------------------------------------------------------- /src/DB/Builder/BuilderException.php: -------------------------------------------------------------------------------- 1 | node = $node; 31 | parent::__construct( $message, $code, $previous ); 32 | } 33 | 34 | /** 35 | * @return Node|null 36 | */ 37 | public function getNode(): ?Node { 38 | return $this->node; 39 | } 40 | } -------------------------------------------------------------------------------- /src/DB/Builder/DB.php: -------------------------------------------------------------------------------- 1 | $method( ...$args ); 34 | } 35 | 36 | /** 37 | * node name 38 | * 39 | * @param string $name 40 | * 41 | * @return ShardMatrixConnection 42 | */ 43 | static public function connection( string $name ) { 44 | $node = ShardMatrix::getConfig()->getNodes()->getNodeByName( $name ); 45 | 46 | return ( new ShardMatrixConnection( $node ) ); 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /src/DB/Builder/Model.php: -------------------------------------------------------------------------------- 1 | getUuid(); 13 | if ( isset( $this->row->modified ) ) { 14 | $this->row->modified = ( new \DateTime() )->format( 'Y-m-d H:i:s' ); 15 | } 16 | 17 | return (bool) DB::table( $uuid->getTable()->getName() )->whereUuid( $uuid )->update( $this->saveArray() ); 18 | } 19 | 20 | public function delete(): bool { 21 | $uuid = $this->getUuid(); 22 | 23 | return (bool) DB::table( $uuid->getTable()->getName() )->delete( $uuid ); 24 | } 25 | 26 | protected function saveArray(): array { 27 | $array = $this->__toArray(); 28 | unset( $array['uuid'] ); 29 | 30 | return $array; 31 | } 32 | } -------------------------------------------------------------------------------- /src/DB/Builder/QueryBuilder.php: -------------------------------------------------------------------------------- 1 | connection; 67 | } 68 | 69 | /** 70 | * @param ShardMatrixConnection $connection 71 | * 72 | * @return $this 73 | * @throws Exception 74 | */ 75 | public function setShardMatrixConnection( ShardMatrixConnection $connection ): QueryBuilder { 76 | if ( $connection instanceof UnassignedConnection ) { 77 | throw new BuilderException( null, 'Connection has to be assigned' ); 78 | } 79 | $connection->prepareQuery( $this ); 80 | 81 | return $this; 82 | } 83 | 84 | /** 85 | * @return string|null 86 | */ 87 | public function getPrimaryOrderColumn(): ?string { 88 | if ( isset( $this->primaryOrderColumn ) ) { 89 | return $this->primaryOrderColumn; 90 | } 91 | if ( $this->orders ) { 92 | $order = $this->orders[0]; 93 | 94 | return $order['column'] ?? null; 95 | } 96 | 97 | return null; 98 | } 99 | 100 | /** 101 | * @return string|null 102 | */ 103 | public function getPrimaryOrderDirection(): ?string { 104 | if ( isset( $this->primaryOrderDirection ) ) { 105 | return $this->primaryOrderDirection; 106 | } 107 | if ( $this->orders ) { 108 | $order = $this->orders[0]; 109 | 110 | return $order['direction'] ?? null; 111 | } 112 | 113 | return null; 114 | } 115 | 116 | 117 | /** 118 | * @param Uuid| string $uuid 119 | * 120 | * @return $this 121 | * @throws Exception 122 | */ 123 | public function uuidAsNodeReference( $uuid ): QueryBuilder { 124 | if ( is_string( $uuid ) ) { 125 | $uuid = new Uuid( $uuid ); 126 | } 127 | if ( ! $uuid instanceof Uuid || ! $uuid->isValid() ) { 128 | throw new BuilderException( null, 'Uuid Object Required' ); 129 | } 130 | 131 | $this->setShardMatrixConnection( new ShardMatrixConnection( $uuid->getNode() ) ); 132 | $this->nodeReferenceUuid = $uuid; 133 | 134 | return $this; 135 | } 136 | 137 | /** 138 | * @param Uuid | string $uuid 139 | * 140 | * @return $this 141 | * @throws Exception 142 | */ 143 | public function whereUuid( $uuid ): QueryBuilder { 144 | if ( is_string( $uuid ) ) { 145 | $uuid = new Uuid( $uuid ); 146 | } 147 | if ( ! $uuid instanceof Uuid ) { 148 | throw new BuilderException( null, 'Uuid Object Required' ); 149 | } 150 | $this->setShardMatrixConnection( new ShardMatrixConnection( $uuid->getNode() ) ); 151 | parent::where( 'uuid', '=', $uuid->toString() )->limit( 1 ); 152 | $this->uuid = $uuid; 153 | 154 | return $this; 155 | } 156 | 157 | /** 158 | * @param array|\Closure|string $column 159 | * @param null $operator 160 | * @param null $value 161 | * @param string $boolean 162 | * 163 | * @return QueryBuilder 164 | * @throws Exception 165 | */ 166 | public function where( $column, $operator = null, $value = null, $boolean = 'and' ) { 167 | if ( $column == 'uuid' && $operator == '=' ) { 168 | $this->uuid = new Uuid( $value ); 169 | $this->setShardMatrixConnection( new ShardMatrixConnection( $this->uuid->getNode() ) ); 170 | } 171 | 172 | return parent::where( $column, $operator, $value, $boolean ); 173 | } 174 | 175 | 176 | /** 177 | * @return string 178 | */ 179 | public function toSqlHash(): string { 180 | return md5( str_replace( [ 181 | '"', 182 | '`', 183 | "'", 184 | "$", 185 | "?" 186 | ], '-', strtolower( $this->toSql() . ( join( '-', $this->getBindings() ) ) ) ) ); 187 | } 188 | 189 | /** 190 | * @param array $values 191 | * @param Uuid|null $uuid 192 | * 193 | * @return Uuid|null 194 | * @throws \ShardMatrix\Exception 195 | */ 196 | public function insert( array $values, ?Uuid $uuid = null ): ?Uuid { 197 | $uuid = $uuid ?? Uuid::make( $this->getConnection()->getNode(), new Table( $this->from ) ); 198 | $values = array_merge( [ 'uuid' => $uuid->toString() ], $values ); 199 | 200 | if ( empty( $values ) ) { 201 | return null; 202 | } 203 | if ( ! is_array( reset( $values ) ) ) { 204 | $values = [ $values ]; 205 | } else { 206 | foreach ( $values as $key => $value ) { 207 | ksort( $value ); 208 | 209 | $values[ $key ] = $value; 210 | } 211 | } 212 | 213 | $result = $this->getShardDB()->uuidInsert( $uuid, $this->grammar->compileInsert( $this, $values ), $this->cleanBindings( Arr::flatten( $values, 1 ) ) ); 214 | if ( $result ) { 215 | return $result->getLastInsertUuid(); 216 | } 217 | 218 | return null; 219 | } 220 | 221 | /** 222 | * @param array $values 223 | * 224 | * @return int|ShardMatrixStatement|ShardMatrixStatements|null 225 | * @throws Exception 226 | * @throws \ShardMatrix\DB\DuplicateException 227 | */ 228 | public function update( array $values ) { 229 | if ( $this->uuid ) { 230 | 231 | $rowData = (object) $values; 232 | $rowData->uuid = $this->uuid->toString(); 233 | $dataRow = new DataRow( $rowData ); 234 | 235 | return $this->getShardDB()->uuidUpdate( 236 | $this->uuid, 237 | $this->grammar->compileUpdate( $this, $values ), 238 | $this->cleanBindings( 239 | $this->grammar->prepareBindingsForUpdate( $this->bindings, $values ) 240 | ), 241 | $dataRow 242 | ); 243 | } 244 | 245 | 246 | if ( $this->getConnection()->hasNodes() && ( $nodes = $this->getConnection()->getNodes() ) ) { 247 | $nodeQueries = []; 248 | foreach ( $nodes as $node ) { 249 | $queryBuilder = clone( $this ); 250 | ( new ShardMatrixConnection( $node ) )->prepareQuery( $queryBuilder ); 251 | 252 | $nodeQueries[] = new NodeQuery( $node, $queryBuilder->getGrammar()->compileUpdate( $queryBuilder, $values ), $this->cleanBindings( 253 | $queryBuilder->getGrammar()->prepareBindingsForUpdate( $this->bindings, $values ) 254 | ), false ); 255 | } 256 | 257 | return $this->getShardDB()->nodeQueries( new NodeQueries( $nodeQueries ), null, null, 'update' ); 258 | } 259 | 260 | return parent::update( $values ); 261 | 262 | } 263 | 264 | /** 265 | * @param ShardDataRowInterface $dataRow 266 | * 267 | * @return bool|null 268 | * @throws Exception 269 | * @throws \ShardMatrix\DB\DuplicateException 270 | */ 271 | public function updateByDataRow( ShardDataRowInterface $dataRow ): ?bool { 272 | 273 | if ( ! ( $uuid = $dataRow->getUuid() ) ) { 274 | return null; 275 | } 276 | $values = $dataRow->__toArray(); 277 | unset( $values['uuid'] ); 278 | if ( isset( $values['modified'] ) ) { 279 | $array['modified'] = ( new \DateTime() )->format( 'Y-m-d H:i:s' ); 280 | } 281 | $this->from( $dataRow->getUuid()->getTable()->getName() )->whereUuid( $dataRow->getUuid() ); 282 | 283 | $update = $this->getShardDB()->execute( new PreStatement( 284 | $dataRow->getUuid()->getNode(), $this->grammar->compileUpdate( $this, $values ), 285 | $this->cleanBindings( 286 | $this->grammar->prepareBindingsForUpdate( $this->bindings, $values ) 287 | ), 288 | $dataRow->getUuid(), 289 | $dataRow, 290 | 'update', 291 | true 292 | ) ); 293 | if ( $update && $update->isSuccessful() ) { 294 | return true; 295 | } 296 | 297 | return false; 298 | } 299 | 300 | 301 | /** 302 | * @param NodeQueryModifiers|null $modifiers 303 | * 304 | * @return ShardMatrixStatements|null 305 | * @throws Exception 306 | * @throws \ShardMatrix\DB\DuplicateException 307 | * @throws \ShardMatrix\Exception 308 | */ 309 | protected function returnNodesResults( ?NodeQueryModifiers $modifiers = null ): ?ShardMatrixStatements { 310 | if ( $nodes = $this->getConnection()->getNodes() ) { 311 | $nodeQueries = []; 312 | foreach ( $nodes as $node ) { 313 | $queryBuilder = clone( $this ); 314 | if ( $modifiers ) { 315 | $queryBuilder = $modifiers->modifyQueryForNode( $node, $queryBuilder ); 316 | if ( ! $queryBuilder ) { 317 | continue; 318 | } 319 | } 320 | ( new ShardMatrixConnection( $node ) )->prepareQuery( $queryBuilder ); 321 | $nodeQueries[] = new NodeQuery( $node, $queryBuilder->toSql(), $queryBuilder->getBindings(), $this->useCache ); 322 | } 323 | 324 | return $this->getShardDB()->nodeQueries( new NodeQueries( $nodeQueries ), $this->getPrimaryOrderColumn(), $this->getPrimaryOrderDirection(), __METHOD__ ); 325 | } 326 | } 327 | 328 | /** 329 | * @return ShardMatrixStatement|null 330 | */ 331 | protected function returnNodeResult(): ?ShardMatrixStatement { 332 | return $this->getShardDB()->nodeQuery( $this->getConnection()->getNode(), $this->toSql(), $this->getBindings(), $this->uuid, ! $this->useCache ); 333 | } 334 | 335 | 336 | /** 337 | * @param bool $asShardMatrixStatement 338 | * 339 | * @return Collection|ShardMatrixStatement|ShardMatrixStatements|null 340 | * @throws Exception 341 | * @throws \ShardMatrix\DB\DuplicateException 342 | */ 343 | protected function returnResults( bool $asShardMatrixStatement = false ) { 344 | if ( $this->getConnection()->hasNodes() ) { 345 | $result = $this->returnNodesResults(); 346 | } else { 347 | $result = $this->returnNodeResult(); 348 | } 349 | if ( ! $asShardMatrixStatement ) { 350 | if ( $result ) { 351 | return new Collection( $result->fetchDataRows()->getDataRows() ); 352 | } 353 | 354 | return new Collection( [] ); 355 | } 356 | 357 | return $result; 358 | } 359 | 360 | /** 361 | * @param null $id 362 | * 363 | * @return int|ShardMatrixStatements|null 364 | * @throws Exception 365 | * @throws \ShardMatrix\DB\DuplicateException 366 | * @throws \ShardMatrix\Exception 367 | */ 368 | public function delete( $id = null ) { 369 | $this->setUseCache( false ); 370 | if ( isset( $id ) ) { 371 | $uuid = new Uuid( $id ); 372 | if ( $uuid->isValid() ) { 373 | $this->uuidAsNodeReference( $uuid ); 374 | $this->uuid = $uuid; 375 | $this->where( $this->from . '.uuid', '=', $uuid->toString() ); 376 | 377 | 378 | return $this->connection->delete( 379 | $this->grammar->compileDelete( $this ), $this->cleanBindings( 380 | $this->grammar->prepareBindingsForDelete( $this->bindings ) 381 | ) 382 | ); 383 | } 384 | 385 | return 0; 386 | } 387 | 388 | if ( $this->getConnection()->hasNodes() && ( $nodes = $this->getConnection()->getNodes() ) ) { 389 | $nodeQueries = []; 390 | foreach ( $nodes as $node ) { 391 | $queryBuilder = clone( $this ); 392 | ( new ShardMatrixConnection( $node ) )->prepareQuery( $queryBuilder ); 393 | 394 | $nodeQueries[] = new NodeQuery( $node, $queryBuilder->getGrammar()->compileDelete( $queryBuilder ), $this->cleanBindings( 395 | $queryBuilder->getGrammar()->prepareBindingsForDelete( $this->bindings ) 396 | ), false ); 397 | } 398 | 399 | return (int) $this->getShardDB()->nodeQueries( new NodeQueries( $nodeQueries ), null, null, 'delete' )->isSuccessful(); 400 | } 401 | 402 | return parent::delete( $id ); 403 | } 404 | 405 | /** 406 | * @param string[] $columns 407 | * 408 | * @return Collection 409 | */ 410 | public function get( $columns = [ '*' ] ) { 411 | $this->select( $columns ); 412 | 413 | return $this->returnResults(); 414 | } 415 | 416 | /** 417 | * @param Uuid|null $uuid 418 | * @param int $perPage 419 | * 420 | * @return QueryBuilder 421 | */ 422 | public function uuidMarkerPageAbove( $uuid = null, int $perPage = 15 ): QueryBuilder { 423 | if ( ! is_null( $uuid ) ) { 424 | $uuid = new Uuid( $uuid ); 425 | } 426 | $uuid ?? $this->uuid; 427 | if ( $uuid ) { 428 | return $this->where( 'uuid', '>', $uuid->toString() )->orderBy( 'uuid' )->limit( $perPage ); 429 | } 430 | 431 | return $this->limit( $perPage ); 432 | } 433 | 434 | /** 435 | * @param Uuid|null $uuid 436 | * @param int $perPage 437 | * 438 | * @return QueryBuilder 439 | */ 440 | public function uuidMarkerPageBelow( ?Uuid $uuid = null, int $perPage = 15 ): QueryBuilder { 441 | if ( ! is_null( $uuid ) ) { 442 | $uuid = new Uuid( $uuid ); 443 | } 444 | $uuid ?? $this->uuid; 445 | 446 | if ( $uuid ) { 447 | return $this->where( 'uuid', '<', $uuid->toString() )->orderBy( 'uuid' )->limit( $perPage ); 448 | } 449 | 450 | return $this->limit( $perPage ); 451 | } 452 | 453 | /** 454 | * @param string[] $columns 455 | * 456 | * @return array|ResultsInterface 457 | */ 458 | protected function runPaginationCountQuery( $columns = [ '*' ] ) { 459 | 460 | if ( $this->groups || $this->havings ) { 461 | $clone = $this->cloneForPaginationCount(); 462 | 463 | if ( is_null( $clone->columns ) && ! empty( $this->joins ) ) { 464 | $clone->select( $this->from . '.*' ); 465 | } 466 | 467 | return $this->newQuery() 468 | ->from( new Expression( '(' . $clone->toSql() . ') as ' . $this->grammar->wrap( 'aggregate_table' ) ) ) 469 | ->mergeBindings( $clone ) 470 | ->setAggregate( 'count', $this->withoutSelectAliases( $columns ) ) 471 | ->getStatement(); 472 | } 473 | 474 | $without = $this->unions ? [ 'orders', 'limit', 'offset' ] : [ 'columns', 'orders', 'limit', 'offset' ]; 475 | 476 | return $this->cloneWithout( $without ) 477 | ->cloneWithoutBindings( $this->unions ? [ 'order' ] : [ 'select', 'order' ] ) 478 | ->setAggregate( 'count', $this->withoutSelectAliases( $columns ) ) 479 | ->getStatement( $columns ); 480 | } 481 | 482 | /** 483 | * @param string[] $columns 484 | * 485 | * @return int 486 | */ 487 | public function getCountForPagination( $columns = [ '*' ] ) { 488 | $results = $this->runPaginationCountQuery( $columns ); 489 | // Once we have run the pagination count query, we will get the resulting count and 490 | // take into account what type of query it was. When there is a group by we will 491 | // just return the count of the entire results set since that will be correct. 492 | if ( isset( $this->groups ) ) { 493 | return count( $results->fetchAllArrays() ); 494 | } elseif ( ! $results->isSuccessful() ) { 495 | return 0; 496 | } 497 | 498 | return (int) $results->sumColumn( 'aggregate' ); 499 | } 500 | 501 | /** 502 | * @param array|string[] $columns 503 | * 504 | * @return ResultsInterface 505 | */ 506 | public function getStatement( array $columns = [ '*' ] ): ResultsInterface { 507 | $this->select( $columns ); 508 | 509 | return $this->returnResults( true ); 510 | } 511 | 512 | public function getNodeModifierStatement( array $columns = [ '*' ], NodeQueryModifiers $modifiers ): ResultsInterface { 513 | return $this->returnNodesResults( $modifiers ); 514 | } 515 | 516 | /** 517 | * @param int|string $uuid 518 | * @param string[] $columns 519 | * 520 | * @return \Illuminate\Database\Eloquent\Model|mixed|object|QueryBuilder|null 521 | * @throws Exception 522 | */ 523 | public function find( $uuid, $columns = [ '*' ] ) { 524 | if ( ! $uuid instanceof Uuid ) { 525 | $uuid = new Uuid( $uuid ); 526 | } 527 | 528 | return $this->whereUuid( $uuid )->first( $columns ); 529 | } 530 | 531 | /** 532 | * @param string $function 533 | * @param string[] $columns 534 | * 535 | * @return mixed 536 | */ 537 | public function aggregate( $function, $columns = [ '*' ] ) { 538 | $results = $this->cloneWithout( $this->unions ? [] : [ 'columns' ] ) 539 | ->cloneWithoutBindings( $this->unions ? [] : [ 'select' ] ) 540 | ->setAggregate( $function, $columns ) 541 | ->getStatement( $columns ); 542 | $aggregateKey = 'aggregate'; 543 | if ( $results->isSuccessful() ) { 544 | switch ( $function ) { 545 | case 'sum': 546 | case 'count': 547 | return $results->sumColumn( $aggregateKey ); 548 | break; 549 | case 'avg': 550 | return $results->avgColumn( $aggregateKey ); 551 | break; 552 | case 'min': 553 | return $results->minColumn( $aggregateKey ); 554 | break; 555 | case 'max': 556 | return $results->maxColumn( $aggregateKey ); 557 | break; 558 | } 559 | } 560 | } 561 | 562 | /** 563 | * @param array|string[] $columns 564 | * @param int $pageNumber 565 | * @param int $perPage 566 | * @param int|null $limitPages 567 | */ 568 | public function getPagination( array $columns = [ "*" ], int $pageNumber = 1, int $perPage = 15, ?int $limitPages = 10 ): PaginationStatement { 569 | $this->select( $columns ); 570 | 571 | return $this->getShardDB()->paginationByQueryBuilder( $this, $pageNumber, $perPage, $limitPages ); 572 | } 573 | 574 | /** 575 | * @param int $perPage 576 | * @param string[] $columns 577 | * @param string $pageName 578 | * @param null $page 579 | * 580 | * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator|\Illuminate\Pagination\LengthAwarePaginator 581 | */ 582 | public function paginate( $perPage = 15, $columns = [ '*' ], $pageName = 'page', $page = null ) { 583 | if ( $this->getConnection()->hasNodes() ) { 584 | $page = $page ?: Paginator::resolveCurrentPage( $pageName ); 585 | 586 | $paginationStatement = $this->getPagination( $columns, $page, $perPage ); 587 | 588 | return $this->paginator( new Collection( $paginationStatement->getResults()->fetchDataRows()->getDataRows() ), $paginationStatement->countResults(), $perPage, $page, [ 589 | 'path' => Paginator::resolveCurrentPath(), 590 | 'pageName' => $pageName, 591 | ] ); 592 | } 593 | 594 | return parent::paginate( $perPage, $columns, $pageName, $page ); 595 | } 596 | 597 | 598 | /** 599 | * @param string|null $primaryOrderDirection 600 | * 601 | * @return $this 602 | */ 603 | public function setPrimaryOrderDirection( ?string $primaryOrderDirection ): QueryBuilder { 604 | $this->primaryOrderDirection = $primaryOrderDirection; 605 | 606 | return $this; 607 | } 608 | 609 | /** 610 | * @param string|null $primaryOrderColumn 611 | * 612 | * @return QueryBuilder 613 | */ 614 | public function setPrimaryOrderColumn( ?string $primaryOrderColumn ): QueryBuilder { 615 | $this->primaryOrderColumn = $primaryOrderColumn; 616 | 617 | return $this; 618 | } 619 | 620 | /** 621 | * @param string $rowDataClass 622 | * 623 | * @return QueryBuilder 624 | */ 625 | public function setRowDataClass( string $rowDataClass ): QueryBuilder { 626 | $this->rowDataClass = $rowDataClass; 627 | 628 | return $this; 629 | } 630 | 631 | /** 632 | * @return string 633 | */ 634 | public function getRowDataClass(): string { 635 | return $this->rowDataClass; 636 | } 637 | 638 | /** 639 | * @return ShardDB 640 | * @throws \ShardMatrix\Exception 641 | */ 642 | protected function getShardDB(): ShardDB { 643 | if ( isset( $this->shardDB ) ) { 644 | return $this->shardDB->setDefaultDataRowClass( $this->getRowDataClass() ); 645 | } 646 | 647 | return $this->shardDB = ShardMatrix::db()->setDefaultDataRowClass( $this->getRowDataClass() ); 648 | } 649 | 650 | /** 651 | * @return ShardCache 652 | * @throws \ShardMatrix\Exception 653 | */ 654 | protected function getShardCache(): ShardCache { 655 | return new ShardCache( $this->getShardDB() ); 656 | } 657 | 658 | /** 659 | * @param bool $useCache 660 | * 661 | * @return $this 662 | */ 663 | public function setUseCache( bool $useCache = true ): QueryBuilder { 664 | $this->useCache = $useCache; 665 | 666 | return $this; 667 | } 668 | 669 | public function isUseCache(): bool { 670 | return $this->useCache; 671 | } 672 | 673 | 674 | } -------------------------------------------------------------------------------- /src/DB/Builder/Schema.php: -------------------------------------------------------------------------------- 1 | getNodes()->getNodeByName( $name ); 28 | 29 | return ( new ShardMatrixConnection( $node ) )->getSchemaBuilder()->node( $node ); 30 | } 31 | 32 | /** 33 | * @return SchemaBuilder 34 | */ 35 | protected static function getFacadeAccessor() { 36 | return ( new ShardMatrixConnection( Connections::getLastUsedNode() ) )->getSchemaBuilder(); 37 | } 38 | 39 | /** 40 | * @param string $method 41 | * @param array $args 42 | * 43 | * @return mixed 44 | */ 45 | public static function __callStatic( $method, $args ) { 46 | $instance = new UnassignedConnection(); 47 | 48 | return $instance->getSchemaBuilder()->$method( ...$args ); 49 | } 50 | } -------------------------------------------------------------------------------- /src/DB/Builder/SchemaBuilder.php: -------------------------------------------------------------------------------- 1 | nodes = $nodes; 44 | 45 | return $this; 46 | } 47 | 48 | /** 49 | * @param Node $node 50 | * 51 | * @return $this 52 | */ 53 | public function node( Node $node ): SchemaBuilder { 54 | $this->nodes = new Nodes( [ $node ] ); 55 | 56 | return $this; 57 | } 58 | 59 | /** 60 | * @return $this 61 | */ 62 | public function silent(): SchemaBuilder { 63 | $this->throwExceptions = false; 64 | 65 | return $this; 66 | } 67 | 68 | /** 69 | * @param $table 70 | * @param bool $useGeo 71 | * 72 | * @return Nodes 73 | */ 74 | protected function getNodes( $table, bool $useGeo = false ) { 75 | if ( $this->nodes ) { 76 | return $this->nodes->getNodesWithTableName( $table, $useGeo ); 77 | } 78 | 79 | return ShardMatrix::getConfig()->getNodes()->getNodesWithTableName( $table, $useGeo ); 80 | } 81 | 82 | /** 83 | * @param string $table 84 | * @param Closure $callback 85 | * 86 | * @throws BuilderException 87 | */ 88 | public function create( $table, Closure $callback ) { 89 | $nodes = $this->getNodes( $table, false ); 90 | if ( $nodes->countNodes() == 0 ) { 91 | throw new BuilderException( null, 'Table not specified in Shard Matrix Config' ); 92 | } 93 | foreach ( $nodes as $node ) { 94 | try { 95 | $this->connection = new ShardMatrixConnection( $node ); 96 | $this->grammar = $this->connection->getSchemaGrammar(); 97 | parent::create( $table, $callback ); 98 | } catch ( \Exception $exception ) { 99 | if ( $this->throwExceptions ) { 100 | throw new BuilderException( $node, $exception->getMessage(), (int) $exception->getCode(), $exception->getPrevious() ); 101 | } 102 | } 103 | } 104 | } 105 | 106 | /** 107 | * @param string $table 108 | * 109 | * @throws BuilderException 110 | */ 111 | public function drop( $table ) { 112 | $nodes = $this->getNodes( $table, false ); 113 | if ( $nodes->countNodes() == 0 ) { 114 | throw new BuilderException( null, 'Table not specified in Shard Matrix Config' ); 115 | } 116 | foreach ( $nodes as $node ) { 117 | try { 118 | $this->connection = new ShardMatrixConnection( $node ); 119 | $this->grammar = $this->connection->getSchemaGrammar(); 120 | parent::drop( $table ); 121 | } catch ( \Exception $exception ) { 122 | if ( $this->throwExceptions ) { 123 | throw new BuilderException( $node, $exception->getMessage(), (int) $exception->getCode(), $exception->getPrevious() ); 124 | } 125 | } 126 | } 127 | } 128 | 129 | /** 130 | * @param string $table 131 | * 132 | * @throws BuilderException 133 | */ 134 | public function dropIfExists( $table ) { 135 | $nodes = $this->getNodes( $table, false ); 136 | if ( $nodes->countNodes() == 0 ) { 137 | throw new BuilderException( null, 'Table not specified in Shard Matrix Config' ); 138 | } 139 | foreach ( $nodes as $node ) { 140 | try { 141 | $this->connection = new ShardMatrixConnection( $node ); 142 | $this->grammar = $this->connection->getSchemaGrammar(); 143 | parent::dropIfExists( $table ); 144 | } catch ( \Exception $exception ) { 145 | if ( $this->throwExceptions ) { 146 | throw new BuilderException( $node, $exception->getMessage(), (int) $exception->getCode(), $exception->getPrevious() ); 147 | } 148 | } 149 | } 150 | 151 | } 152 | 153 | /** 154 | * @param string $from 155 | * @param string $to 156 | * 157 | * @throws BuilderException 158 | */ 159 | public function rename( $from, $to ) { 160 | $nodes = $this->getNodes( $from, false ); 161 | if ( $nodes->countNodes() == 0 ) { 162 | throw new BuilderException( null, 'Table not specified in Shard Matrix Config' ); 163 | } 164 | foreach ( $nodes as $node ) { 165 | 166 | if ( $node->containsTableName( $to ) ) { 167 | try { 168 | $this->connection = new ShardMatrixConnection( $node ); 169 | $this->grammar = $this->connection->getSchemaGrammar(); 170 | parent::rename( $from, $to ); 171 | } catch ( \Exception $exception ) { 172 | if ( $this->throwExceptions ) { 173 | throw new BuilderException( $node, $exception->getMessage(), (int) $exception->getCode(), $exception->getPrevious() ); 174 | } 175 | } 176 | } else { 177 | if ( $this->throwExceptions ) { 178 | throw new BuilderException( $node, 'Renamed Table is required in same shard matrix config table group for this node ' . $node->getName() ); 179 | } 180 | } 181 | } 182 | 183 | } 184 | 185 | /** 186 | * @param string $table 187 | * @param Closure $callback 188 | * 189 | * @throws BuilderException 190 | */ 191 | public function table( $table, Closure $callback ) { 192 | $nodes = $this->getNodes( $table, false ); 193 | if ( $nodes->countNodes() == 0 ) { 194 | throw new BuilderException( null, 'Table not specified in Shard Matrix Config' ); 195 | } 196 | foreach ( $nodes as $node ) { 197 | try { 198 | $this->connection = new ShardMatrixConnection( $node ); 199 | $this->grammar = $this->connection->getSchemaGrammar(); 200 | parent::table( $table, $callback ); 201 | } catch ( \Exception $exception ) { 202 | if ( $this->throwExceptions ) { 203 | throw new BuilderException( $node, $exception->getMessage(), (int) $exception->getCode(), $exception->getPrevious() ); 204 | } 205 | } 206 | } 207 | } 208 | 209 | 210 | } -------------------------------------------------------------------------------- /src/DB/Builder/ShardMatrixConnection.php: -------------------------------------------------------------------------------- 1 | node = $node; 43 | parent::__construct( $this->getNodePdo(), $database, $tablePrefix, $config ); 44 | } 45 | 46 | /** 47 | * @return Node 48 | */ 49 | public function getNode(): Node { 50 | return $this->node; 51 | } 52 | 53 | /** 54 | * @return \PDO 55 | */ 56 | public function getNodePdo(): \PDO { 57 | return Connections::getNodeConnection( $this->getNode() ); 58 | } 59 | 60 | /** 61 | * @return QueryBuilder 62 | * @throws Exception 63 | */ 64 | public function query(): QueryBuilder { 65 | return new QueryBuilder( 66 | $this, $this->getQueryGrammar(), $this->getPostProcessor() 67 | ); 68 | } 69 | 70 | /** 71 | * @param QueryBuilder $queryBuilder 72 | * 73 | * @return $this 74 | * @throws Exception 75 | */ 76 | public function prepareQuery( QueryBuilder $queryBuilder ): ShardMatrixConnection { 77 | 78 | $queryBuilder->connection = $this; 79 | $queryBuilder->grammar = $this->getQueryGrammar(); 80 | $queryBuilder->processor = $this->getPostProcessor(); 81 | 82 | return $this; 83 | } 84 | 85 | /** 86 | * @return \Illuminate\Database\Query\Grammars\Grammar|MySqlGrammar 87 | * @throws Exception 88 | */ 89 | public function getQueryGrammar() { 90 | $returnGrammar = $this->getDefaultQueryGrammar(); 91 | switch ( $this->getNode()->getDsn()->getDriver() ) { 92 | case 'mysql': 93 | $returnGrammar = new MySqlGrammar(); 94 | break; 95 | case 'pgsql': 96 | $returnGrammar = new PostgresGrammar(); 97 | break; 98 | case 'sqlite': 99 | throw new Exception( 'SQL LITE IS NOT SUPPORTED' ); 100 | break; 101 | } 102 | 103 | return $returnGrammar; 104 | } 105 | 106 | /** 107 | * @return \Illuminate\Database\Schema\Grammars\Grammar|\Illuminate\Database\Schema\Grammars\MySqlGrammar|\Illuminate\Database\Schema\Grammars\PostgresGrammar 108 | * @throws Exception 109 | */ 110 | public function getSchemaGrammar() { 111 | $returnGrammar = $this->getDefaultSchemaGrammar(); 112 | switch ( $this->getNode()->getDsn()->getDriver() ) { 113 | case 'mysql': 114 | $returnGrammar = new \Illuminate\Database\Schema\Grammars\MySqlGrammar(); 115 | break; 116 | case 'pgsql': 117 | $returnGrammar = new \Illuminate\Database\Schema\Grammars\PostgresGrammar(); 118 | break; 119 | case 'sqlite': 120 | throw new Exception( 'SQL LITE IS NOT SUPPORTED' ); 121 | break; 122 | } 123 | 124 | return $returnGrammar; 125 | } 126 | 127 | /** 128 | * @return \Illuminate\Database\Query\Grammars\Grammar|MySqlGrammar 129 | */ 130 | public function getDefaultQueryGrammar() { 131 | return new MySqlGrammar(); 132 | } 133 | 134 | /** 135 | * @return MySqlProcessor|\Illuminate\Database\Query\Processors\Processor 136 | * @throws Exception 137 | */ 138 | public function getPostProcessor() { 139 | $returnProcessor = $this->getDefaultPostProcessor(); 140 | switch ( $this->getNode()->getDsn()->getDriver() ) { 141 | case 'mysql': 142 | $returnProcessor = new MySqlProcessor(); 143 | break; 144 | case 'pgsql': 145 | $returnProcessor = new PostgresProcessor(); 146 | break; 147 | case 'sqlite': 148 | throw new Exception( 'SQL LITE IS NOT SUPPORTED' ); 149 | break; 150 | } 151 | 152 | return $returnProcessor; 153 | } 154 | 155 | /** 156 | * @param $table 157 | * @param null $as 158 | * 159 | * @return ShardMatrixConnection 160 | * @throws \ShardMatrix\Exception 161 | */ 162 | public function shardTable( $table, $as = null ) { 163 | NodeDistributor::clearGroupNodes(); 164 | 165 | return $this->table( $table, $as ); 166 | } 167 | 168 | /** 169 | * @return MySqlProcessor|\Illuminate\Database\Query\Processors\Processor 170 | */ 171 | public function getDefaultPostProcessor() { 172 | return new MySqlProcessor(); 173 | } 174 | 175 | /** 176 | * @param \Closure|\Illuminate\Database\Query\Builder|string $table 177 | * @param null $as 178 | * 179 | * @return QueryBuilder 180 | * @throws \ShardMatrix\Exception 181 | */ 182 | public function table( $table, $as = null ) { 183 | if ( $this instanceof UnassignedConnection ) { 184 | $this->node = NodeDistributor::getNode( $table ); 185 | $this->pdo = $this->getNodePdo(); 186 | } 187 | if ( ! $this->getNode()->containsTableName( $table ) || ! isset( $this->pdo ) ) { 188 | $this->node = NodeDistributor::getNode( $table ); 189 | $this->pdo = $this->getNodePdo(); 190 | } 191 | 192 | return parent::table( $table, $as ); 193 | } 194 | 195 | /** 196 | * @param $table 197 | * @param null $as 198 | * @param bool $useGeo 199 | * 200 | * @return QueryBuilder 201 | * @throws \ShardMatrix\Exception 202 | */ 203 | public function allNodesTable( string $table, $as = null, bool $useGeo = false ): QueryBuilder { 204 | 205 | $this->nodes = ShardMatrix::getConfig()->getNodes()->getNodesWithTableName( $table, $useGeo ); 206 | 207 | return $this->table( $table, $as ); 208 | } 209 | 210 | /** 211 | * @param string $geo 212 | * @param string $table 213 | * @param null $as 214 | * 215 | * @return QueryBuilder 216 | * @throws \ShardMatrix\Exception 217 | */ 218 | public function allNodesGeoTable( string $geo, string $table, $as = null ): QueryBuilder { 219 | $this->nodes = ShardMatrix::getConfig()->getNodes()->getNodesWithTableNameAndGeo( $table, $geo ); 220 | 221 | return $this->table( $table, $as ); 222 | } 223 | 224 | /** 225 | * @param string $table 226 | * @param null $as 227 | * 228 | * @return QueryBuilder 229 | * @throws \ShardMatrix\Exception 230 | */ 231 | public function allNodesThisGeoTable( string $table, $as = null ) { 232 | if ( $geo = ShardMatrix::getGeo() ) { 233 | $this->nodes = ShardMatrix::getConfig()->getNodes()->getNodesWithTableNameAndGeo( $table, $geo ); 234 | } else { 235 | return $this->allNodesTable( $table, $as ); 236 | } 237 | 238 | return $this->table( $table, $as ); 239 | } 240 | 241 | /** 242 | * @return bool 243 | */ 244 | public function hasNodes(): bool { 245 | return isset( $this->nodes ); 246 | } 247 | 248 | /** 249 | * @return Nodes|null 250 | */ 251 | public function getNodes(): ?Nodes { 252 | $clonedNodes = null; 253 | if ( $this->nodes ) { 254 | $clonedNodes = clone( $this->nodes ); 255 | } 256 | 257 | return $clonedNodes; 258 | } 259 | 260 | /** 261 | * @param $uuid 262 | * 263 | * @return DBDataRowTransactionsInterface|null 264 | * @throws Exception 265 | * @throws \ShardMatrix\Exception 266 | */ 267 | public function getByUuid( $uuid ): ?DBDataRowTransactionsInterface { 268 | if ( ! $uuid instanceof Uuid ) { 269 | $uuid = new Uuid( $uuid ); 270 | } 271 | 272 | return $this->table( $uuid->getTable()->getName() )->whereUuid( $uuid )->first( [ '*' ] ); 273 | } 274 | 275 | /** 276 | * @param $uuid 277 | * @param array $values 278 | * 279 | * @return bool 280 | * @throws Exception 281 | * @throws \ShardMatrix\DB\DuplicateException 282 | * @throws \ShardMatrix\Exception 283 | */ 284 | public function updateByUuid( $uuid, array $values ): bool { 285 | if ( ! $uuid instanceof Uuid ) { 286 | $uuid = new Uuid( $uuid ); 287 | } 288 | 289 | $stmt = $this->table( $uuid->getTable()->getName() )->whereUuid( $uuid )->update( $values ); 290 | if ( $stmt && $stmt->isSuccessful() ) { 291 | return true; 292 | } 293 | 294 | return false; 295 | } 296 | 297 | /** 298 | * @return SchemaBuilder 299 | */ 300 | public function getSchemaBuilder() { 301 | if ( is_null( $this->schemaGrammar ) ) { 302 | $this->useDefaultSchemaGrammar(); 303 | } 304 | 305 | return new SchemaBuilder( $this ); 306 | } 307 | 308 | /** 309 | * @return \Illuminate\Database\Schema\Grammars\Grammar|\Illuminate\Database\Schema\Grammars\MySqlGrammar 310 | */ 311 | public function getDefaultSchemaGrammar() { 312 | return new \Illuminate\Database\Schema\Grammars\MySqlGrammar(); 313 | } 314 | 315 | /** 316 | * @return Driver|\Doctrine\DBAL\Driver\PDOPgSql\Driver 317 | * @throws Exception 318 | */ 319 | public function getDoctrineDriver() { 320 | 321 | $returnProcessor = $this->getDefaultDoctrineDriver(); 322 | switch ( $this->getNode()->getDsn()->getDriver() ) { 323 | case 'mysql': 324 | $returnProcessor = new Driver(); 325 | break; 326 | case 'pgsql': 327 | $returnProcessor = new \Doctrine\DBAL\Driver\PDOPgSql\Driver(); 328 | break; 329 | case 'sqlite': 330 | throw new Exception( 'SQL LITE IS NOT SUPPORTED' ); 331 | break; 332 | } 333 | 334 | return $returnProcessor; 335 | 336 | } 337 | 338 | public function getDefaultDoctrineDriver() { 339 | return new Driver(); 340 | } 341 | 342 | public function getDoctrineConnection() { 343 | if ( is_null( $this->doctrineConnection ) ) { 344 | $driver = $this->getDoctrineDriver(); 345 | 346 | $this->doctrineConnection = new DoctrineConnection( array_filter( [ 347 | 'pdo' => $this->getPdo(), 348 | 'dbname' => $this->getNode()->getDsn()->getDbname(), 349 | 'driver' => $driver->getName(), 350 | 'serverVersion' => $this->getConfig( 'server_version' ), 351 | ] ), $driver ); 352 | } 353 | 354 | return $this->doctrineConnection; 355 | } 356 | 357 | 358 | 359 | } -------------------------------------------------------------------------------- /src/DB/Builder/UnassignedConnection.php: -------------------------------------------------------------------------------- 1 | node = new UnassignedNode(); 19 | $this->reconnector = function () { 20 | $this->reconnector = function () { 21 | if ( $pdo = Connections::getLastUsedConnection() ) { 22 | $this->pdo = $pdo; 23 | $this->node = Connections::getLastUsedNode(); 24 | } 25 | }; 26 | }; 27 | } 28 | } -------------------------------------------------------------------------------- /src/DB/Builder/UnassignedNode.php: -------------------------------------------------------------------------------- 1 | \PDO::ERRMODE_EXCEPTION ]; 19 | protected static ? Node $lastNodeUsed = null; 20 | 21 | /** 22 | * @param array $dbAttributes 23 | */ 24 | public static function setDbAttributes( array $dbAttributes ): void { 25 | array_merge( static::$dbAttributes, $dbAttributes ); 26 | } 27 | 28 | /** 29 | * @param Node $node 30 | * @param bool $useNewConnection 31 | * 32 | * @return \PDO 33 | */ 34 | public static function getNodeConnection( Node $node, bool $useNewConnection = false ): \PDO { 35 | static::$lastNodeUsed = $node; 36 | if ( isset( static::$connections[ $node->getName() ] ) && ! $useNewConnection ) { 37 | return static::$connections[ $node->getName() ]; 38 | } 39 | 40 | $db = new \PDO( $node->getDsn()->__toString() ); 41 | 42 | foreach ( static::$dbAttributes as $attribute => $value ) { 43 | $db->setAttribute( $attribute, $value ); 44 | } 45 | if ( ! $useNewConnection ) { 46 | return static::$connections[ $node->getName() ] = $db; 47 | } 48 | 49 | return $db; 50 | } 51 | 52 | 53 | /** 54 | * @param string $nodeName 55 | * 56 | * @return \PDO 57 | * @throws Exception 58 | */ 59 | static public function getConnectionByNodeName( string $nodeName ) { 60 | $node = ShardMatrix::getConfig()->getNodes()->getNodeByName( $nodeName ); 61 | if ( $node ) { 62 | return static::getNodeConnection( $node ); 63 | } 64 | throw new Exception( 'No Node by name ' . $nodeName . ' Exists!' ); 65 | } 66 | 67 | static public function closeConnections(): void { 68 | foreach ( static::$connections as &$con ) { 69 | $con = null; 70 | } 71 | static::$connections = []; 72 | } 73 | 74 | /** 75 | * @param bool $useNewConnection 76 | * 77 | * @return \PDO|null 78 | */ 79 | static public function getLastUsedConnection( bool $useNewConnection = false ): ?\PDO { 80 | if ( static::$lastNodeUsed ) { 81 | return static::getNodeConnection( static::$lastNodeUsed, $useNewConnection ); 82 | } 83 | 84 | return null; 85 | } 86 | 87 | /** 88 | * @return Node|null 89 | */ 90 | static public function getLastUsedNode(): ?Node { 91 | return static::$lastNodeUsed; 92 | } 93 | } -------------------------------------------------------------------------------- /src/DB/DataRow.php: -------------------------------------------------------------------------------- 1 | row = $row; 23 | $this->__originalState = clone( $this ); 24 | } 25 | 26 | /** 27 | * @param $column 28 | * 29 | * @return bool 30 | */ 31 | final public function __columnIsset( $column ): bool { 32 | return isset( $this->row->$column ); 33 | } 34 | 35 | /** 36 | * @return Uuid|null 37 | */ 38 | final public function getUuid(): ?Uuid { 39 | if ( isset( $this->uuids['uuid'] ) ) { 40 | return $this->uuids['uuid']; 41 | } 42 | if ( isset( $this->row->uuid ) ) { 43 | return $this->uuids['uuid'] = new Uuid( $this->row->uuid ); 44 | } 45 | 46 | return null; 47 | } 48 | 49 | /** 50 | * @return Uuid[] 51 | */ 52 | final public function getJoinUuids(): array { 53 | $resultArray = []; 54 | foreach ( $this->row as $name => $value ) { 55 | if ( strpos( $name, '_uuid' ) !== false ) { 56 | if ( isset( $this->uuids[ $name ] ) ) { 57 | $resultArray[] = $this->uuids[ $name ]; 58 | } else { 59 | $resultArray[] = $this->uuids[ $name ] = new Uuid( $this->row->$name ); 60 | } 61 | } 62 | } 63 | 64 | return $resultArray; 65 | } 66 | 67 | /** 68 | * @param $name 69 | * 70 | * @return mixed|Uuid|null 71 | */ 72 | final public function __get( $name ) { 73 | if ( strpos( $name, 'uuid' ) !== false && isset( $this->row->$name ) ) { 74 | if ( isset( $this->uuids[ $name ] ) ) { 75 | return $this->uuids[ $name ]; 76 | } 77 | 78 | return $this->uuids[ $name ] = new Uuid( $this->row->$name ); 79 | } 80 | if ( isset( $this->row->$name ) ) { 81 | return $this->row->$name; 82 | } 83 | 84 | return null; 85 | } 86 | 87 | public function __set( $name, $value ) { 88 | if ( $name != 'uuid' ) { 89 | $this->row->$name = $value; 90 | } 91 | } 92 | 93 | public function __toObject(): \stdClass { 94 | return $this->row; 95 | } 96 | 97 | public function __toArray(): array { 98 | return (array) $this->row; 99 | } 100 | 101 | public function jsonSerialize() { 102 | return $this->__toObject(); 103 | } 104 | 105 | public function __setRowData( \stdClass $row ) { 106 | $this->row = $row; 107 | } 108 | 109 | public function __getOriginalState(): ShardDataRowInterface { 110 | return $this->__originalState; 111 | } 112 | } -------------------------------------------------------------------------------- /src/DB/DataRowFactory.php: -------------------------------------------------------------------------------- 1 | rowData = $rowData; 28 | $this->rowReturnClass = $rowReturnClass; 29 | } 30 | 31 | public function create() { 32 | $row = $this->rowData; 33 | $rowReturnClass = $this->rowReturnClass; 34 | if ( ! $row instanceof ShardDataRowInterface ) { 35 | if ( in_array( ConstructObjectInterface::class, class_implements( $rowReturnClass ) ) ) { 36 | $row = new $rowReturnClass( (object) $row ); 37 | } 38 | if ( in_array( ConstructArrayInterface::class, class_implements( $rowReturnClass ) ) ) { 39 | $row = new $rowReturnClass( (array) $row ); 40 | } 41 | } 42 | 43 | return $row; 44 | } 45 | } -------------------------------------------------------------------------------- /src/DB/DataRows.php: -------------------------------------------------------------------------------- 1 | setDataRows( $resultSet, $resultRowReturnClass ); 28 | } 29 | 30 | /** 31 | * @param array $resultSet 32 | * @param string $resultRowReturnClass 33 | */ 34 | public function setDataRows( array $resultSet, string $resultRowReturnClass = DataRow::class ) { 35 | foreach ( $resultSet as &$row ) { 36 | $row = ( new DataRowFactory( $row, $resultRowReturnClass ) )->create(); 37 | } 38 | $this->dataRows = $resultSet; 39 | } 40 | 41 | /** 42 | * @return ShardDataRowInterface[] 43 | */ 44 | public function getDataRows(): array { 45 | return $this->dataRows; 46 | } 47 | 48 | /** 49 | * @return ShardDataRowInterface 50 | */ 51 | public function current(): ShardDataRowInterface { 52 | return $this->dataRows[ $this->position ]; 53 | } 54 | 55 | public function next() { 56 | $this->position ++; 57 | } 58 | 59 | public function key() { 60 | return $this->position; 61 | } 62 | 63 | public function valid() { 64 | return isset( $this->dataRows[ $this->position ] ); 65 | } 66 | 67 | public function rewind() { 68 | $this->position = 0; 69 | } 70 | 71 | 72 | public function jsonSerialize() { 73 | $array = []; 74 | foreach ( $this->getDataRows() as $result ) { 75 | $array[] = $result->__toObject(); 76 | } 77 | 78 | return $array; 79 | } 80 | } -------------------------------------------------------------------------------- /src/DB/DuplicateException.php: -------------------------------------------------------------------------------- 1 | duplicateColumns = $duplicateColumns; 14 | parent::__construct( $message, $code, $previous ); 15 | } 16 | 17 | /** 18 | * @return array 19 | */ 20 | public function getDuplicateColumns(): array { 21 | return $this->duplicateColumns; 22 | } 23 | } -------------------------------------------------------------------------------- /src/DB/Exception.php: -------------------------------------------------------------------------------- 1 | row->sum ?? 0; 11 | } 12 | 13 | public function getColumn(): ?string { 14 | return $this->row->column ?? null; 15 | } 16 | } -------------------------------------------------------------------------------- /src/DB/GroupSums.php: -------------------------------------------------------------------------------- 1 | dataRows = $resultSet; 41 | } 42 | /** 43 | * @return GroupSum 44 | */ 45 | public function current() { 46 | return $this->dataRows[ $this->position ]; 47 | } 48 | 49 | /** 50 | * @return int 51 | */ 52 | public function getTotalSum(): int { 53 | $sum = 0; 54 | foreach ( $this->getDataRows() as $groupSum ) { 55 | $sum = $sum + $groupSum->getSum(); 56 | } 57 | 58 | return $sum; 59 | } 60 | } -------------------------------------------------------------------------------- /src/DB/Interfaces/ConstructArrayInterface.php: -------------------------------------------------------------------------------- 1 | __originalState = clone( $this ); 30 | } 31 | 32 | 33 | public function __columnIsset( $column ): bool { 34 | return isset( $this->attributes[ $column ] ); 35 | } 36 | 37 | public function getUuid(): ?\ShardMatrix\Uuid { 38 | if ( isset( $this->uuids['uuid'] ) ) { 39 | return $this->uuids['uuid']; 40 | } 41 | if ( isset( $this->attributes['uuid'] ) ) { 42 | return $this->uuids['uuid'] = new Uuid( $this->attributes['uuid'] ); 43 | } 44 | 45 | return null; 46 | } 47 | 48 | public function getJoinUuids(): array { 49 | $resultArray = []; 50 | foreach ( $this->attributes as $name => $value ) { 51 | if ( strpos( $name, '_uuid' ) !== false ) { 52 | if ( isset( $this->uuids[ $name ] ) ) { 53 | $resultArray[] = $this->uuids[ $name ]; 54 | } else { 55 | $resultArray[] = $this->uuids[ $name ] = new Uuid( $this->attributes[ $name ] ); 56 | } 57 | } 58 | } 59 | 60 | return $resultArray; 61 | } 62 | 63 | public function __toObject(): \stdClass { 64 | return (object) $this->attributes; 65 | } 66 | 67 | public function __toArray(): array { 68 | return $this->attributes; 69 | } 70 | 71 | public function __setRowData( \stdClass $row ) { 72 | $this->attributes = (array) $row; 73 | } 74 | 75 | /** 76 | * @param array $options 77 | * 78 | * @return bool|mixed 79 | * @throws Exception 80 | * @throws \ShardMatrix\DB\DuplicateException 81 | */ 82 | public function save( array $options = [] ) { 83 | if ( ! $this->getUuid() ) { 84 | return false; 85 | } 86 | 87 | return (bool) DB::table( $this->getUuid()->getTable()->getName() )->updateByDataRow( $this ); 88 | } 89 | 90 | public function delete() { 91 | $uuid = $this->getUuid(); 92 | 93 | return (bool) DB::table( $uuid->getTable()->getName() )->delete( $uuid ); 94 | } 95 | 96 | /** 97 | * @return Uuid|null 98 | * @throws Exception 99 | * @throws \ShardMatrix\Exception 100 | */ 101 | public function create(): ?Uuid { 102 | if ( ! isset( $this->table ) ) { 103 | throw new Exception( 'table needs to be set' ); 104 | } 105 | 106 | return DB::table( $this->table )->insert( $this->__toArray() ); 107 | } 108 | 109 | public function __getOriginalState(): ShardDataRowInterface { 110 | // TODO: Implement __getOriginalState() method. 111 | } 112 | } -------------------------------------------------------------------------------- /src/DB/NodeQueries.php: -------------------------------------------------------------------------------- 1 | nodeQueries = $nodeQueries; 25 | } 26 | 27 | /** 28 | * @return array|NodeQuery[] 29 | */ 30 | public function getNodeQueries(): array { 31 | return $this->nodeQueries; 32 | } 33 | 34 | /** 35 | * @return NodeQuery 36 | */ 37 | public function current() { 38 | return $this->getNodeQueries()[ $this->position ]; 39 | } 40 | 41 | public function next() { 42 | $this->position ++; 43 | } 44 | 45 | public function key() { 46 | return $this->position; 47 | } 48 | 49 | public function valid() { 50 | return isset( $this->nodeQueries[ $this->position ] ); 51 | } 52 | 53 | public function rewind() { 54 | $this->position = 0; 55 | } 56 | 57 | public function jsonSerialize() { 58 | return $this->nodeQueries; 59 | } 60 | } -------------------------------------------------------------------------------- /src/DB/NodeQuery.php: -------------------------------------------------------------------------------- 1 | node = $node; 29 | $this->sql = $sql; 30 | $this->binds = $binds; 31 | $this->useCache = $useCache; 32 | } 33 | 34 | public function getNode(): Node { 35 | return $this->node; 36 | } 37 | 38 | /** 39 | * @return string 40 | */ 41 | public function getSql(): string { 42 | return $this->sql; 43 | } 44 | 45 | public function getBinds(): ?array { 46 | return $this->binds; 47 | } 48 | 49 | public function jsonSerialize() { 50 | 51 | $bindsArray = []; 52 | if ( $binds = $this->getBinds() ) { 53 | foreach ( $binds as $key => $value ) { 54 | $bind = new \stdClass(); 55 | $bind->value = (string) $value; 56 | $bind->key = (string) $key; 57 | $bindsArray[] = $bind; 58 | } 59 | } 60 | 61 | return [ 62 | 'node' => $this->getNode(), 63 | 'sql' => $this->getSql(), 64 | 'binds' => $bindsArray 65 | ]; 66 | } 67 | 68 | /** 69 | * @return bool 70 | */ 71 | public function isUseCache(): bool { 72 | return $this->useCache; 73 | } 74 | } -------------------------------------------------------------------------------- /src/DB/NodeQueryModifier.php: -------------------------------------------------------------------------------- 1 | node = $node; 16 | $this->modifierQuery = $modifierQuery; 17 | } 18 | 19 | /** 20 | * @return Node 21 | */ 22 | public function getNode(): Node { 23 | return $this->node; 24 | } 25 | 26 | /** 27 | * @return QueryBuilder 28 | */ 29 | public function getModifierQuery(): QueryBuilder { 30 | return $this->modifierQuery; 31 | } 32 | } -------------------------------------------------------------------------------- /src/DB/NodeQueryModifiers.php: -------------------------------------------------------------------------------- 1 | setNodeQueryModifiers( $nodeQueryModifiers ); 23 | } 24 | 25 | /** 26 | * @param array $nodeQueryModifiers 27 | * 28 | * @return $this 29 | * @throws Exception 30 | */ 31 | public function setNodeQueryModifiers( array $nodeQueryModifiers ): NodeQueryModifiers { 32 | 33 | foreach ( $nodeQueryModifiers as $modifier ) { 34 | if ( ! $modifier instanceof NodeQueryModifier ) { 35 | throw new Exception( 'Modifiers need to be ' . NodeQueryModifier::class ); 36 | } 37 | } 38 | 39 | $this->nodeQueryModifiers = $nodeQueryModifiers; 40 | 41 | return $this; 42 | } 43 | 44 | /** 45 | * @param Node $node 46 | * @param QueryBuilder $query 47 | * 48 | * @return QueryBuilder 49 | */ 50 | public function modifyQueryForNode( Node $node, QueryBuilder $query ): ?QueryBuilder { 51 | 52 | foreach ( $this->nodeQueryModifiers as $modifier ) { 53 | if ( $node->getName() == $modifier->getNode()->getName() ) { 54 | return $modifier->getModifierQuery(); 55 | } 56 | } 57 | 58 | return null; 59 | } 60 | } -------------------------------------------------------------------------------- /src/DB/PaginationStatement.php: -------------------------------------------------------------------------------- 1 | markerData = $markerData; 32 | $this->currentPageNumber = $currentPageNumber; 33 | $this->resultsPerPage = $resultsPerPage; 34 | } 35 | 36 | /** 37 | * @param $pageNumber 38 | * 39 | * @return array 40 | */ 41 | public function getUuidsFromPageNumber( $pageNumber ): array { 42 | $returnUuids = []; 43 | $initIndex = ( $pageNumber - 1 ) * $this->resultsPerPage; 44 | if ( isset( $this->markerData[ $initIndex ] ) ) { 45 | for ( $i = 0; $i < $this->resultsPerPage; $i ++ ) { 46 | if ( isset( $this->markerData[ $initIndex + $i ] ) ) { 47 | $returnUuids[] = $this->markerData[ $initIndex + $i ]; 48 | } else { 49 | break; 50 | } 51 | } 52 | } 53 | 54 | return $returnUuids; 55 | 56 | } 57 | 58 | public function countResults(): int { 59 | return count( $this->markerData ); 60 | } 61 | 62 | public function countPages(): int { 63 | return ceil( count( $this->markerData ) / $this->resultsPerPage ); 64 | } 65 | 66 | /** 67 | * @return int 68 | */ 69 | public function getCurrentPageNumber(): int { 70 | return $this->currentPageNumber; 71 | } 72 | 73 | /** 74 | * @return ResultsInterface|null 75 | */ 76 | public function getResults(): ResultsInterface { 77 | return $this->results ?? new ShardMatrixStatement( null, null, null ); 78 | } 79 | 80 | /** 81 | * @param ResultsInterface|null $results 82 | * 83 | * @return PaginationStatement 84 | */ 85 | public function setResults( ?ResultsInterface $results ): PaginationStatement { 86 | $this->results = $results; 87 | 88 | return $this; 89 | } 90 | 91 | /** 92 | * @return int 93 | */ 94 | public function getResultsPerPage(): int { 95 | return $this->resultsPerPage; 96 | } 97 | 98 | 99 | public function __preSerialize(): void { 100 | if($this->results instanceof PreSerialize){ 101 | $this->results->__preSerialize(); 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /src/DB/PreStatement.php: -------------------------------------------------------------------------------- 1 | node = $node; 38 | $this->sql = $sql; 39 | $this->bind = $bind; 40 | $this->uuid = $uuid; 41 | $this->dataRow = $dataRow; 42 | $this->calledMethod = $calledMethod; 43 | $this->freshDataOnly = $freshDataOnly; 44 | } 45 | 46 | /** 47 | * @return Node|string 48 | */ 49 | public function getNode() { 50 | return $this->node; 51 | } 52 | 53 | /** 54 | * @return string 55 | */ 56 | public function getSql(): string { 57 | return $this->sql; 58 | } 59 | 60 | /** 61 | * @return array|null 62 | */ 63 | public function getBind(): ?array { 64 | return $this->bind; 65 | } 66 | 67 | /** 68 | * @return ShardDataRowInterface|null 69 | */ 70 | public function getDataRow(): ?ShardDataRowInterface { 71 | return $this->dataRow; 72 | } 73 | 74 | /** 75 | * @param ShardDataRowInterface|null $dataRow 76 | * 77 | * @return $this 78 | */ 79 | public function setDataRow( ?ShardDataRowInterface $dataRow ): PreStatement { 80 | $this->dataRow = $dataRow; 81 | 82 | return $this; 83 | } 84 | 85 | /** 86 | * @return string|null 87 | */ 88 | public function getCalledMethod(): ?string { 89 | return $this->calledMethod; 90 | } 91 | 92 | /** 93 | * @param string|null $calledMethod 94 | */ 95 | public function setCalledMethod( ?string $calledMethod ): PreStatement { 96 | $this->calledMethod = $calledMethod; 97 | 98 | return $this; 99 | } 100 | 101 | /** 102 | * @param Uuid|null $uuid 103 | * 104 | * @return PreStatement 105 | */ 106 | public function setUuid( ?Uuid $uuid ): PreStatement { 107 | $this->uuid = $uuid; 108 | 109 | return $this; 110 | } 111 | 112 | /** 113 | * @return Uuid|null 114 | */ 115 | public function getUuid(): ?Uuid { 116 | return $this->uuid; 117 | } 118 | 119 | public function getHashKey(): string { 120 | $hashParts = []; 121 | if ( $this->getUuid() ) { 122 | $hashParts[] = $this->getUuid(); 123 | } else { 124 | $hashParts[] = $this->getNode()->getName(); 125 | } 126 | $bind = $this->getBind() ?? []; 127 | $hashParts[] = md5( str_replace( [ 128 | '"', 129 | '`', 130 | "'", 131 | "$", 132 | "?", 133 | " " 134 | ], '', strtolower( $this->getSql() . ( join( '-', $bind ) ) ) ) ); 135 | 136 | if ( count( $hashParts ) == 1 ) { 137 | return $hashParts[0] . ':0'; 138 | } 139 | 140 | return join( ':', $hashParts ); 141 | } 142 | 143 | /** 144 | * @param bool $freshDataOnly 145 | * 146 | * @return PreStatement 147 | */ 148 | public function setFreshDataOnly( bool $freshDataOnly ): PreStatement { 149 | $this->freshDataOnly = $freshDataOnly; 150 | 151 | return $this; 152 | } 153 | 154 | /** 155 | * @return bool 156 | */ 157 | public function isFreshDataOnly(): bool { 158 | return $this->freshDataOnly; 159 | } 160 | 161 | public function isDeleteQuery(): bool { 162 | return strpos( strtolower( trim( $this->getSql() ) ), 'delete' ) === 0; 163 | } 164 | 165 | /** 166 | * @return bool 167 | */ 168 | public function isSelectQuery(): bool { 169 | return strpos( strtolower( trim( $this->getSql() ) ), 'select' ) === 0; 170 | } 171 | 172 | public function isShowQuery(): bool { 173 | return strpos( strtolower( trim( $this->getSql() ) ), 'show' ) === 0; 174 | } 175 | 176 | /** 177 | * @return bool 178 | */ 179 | public function isUpdateQuery(): bool { 180 | return strpos( strtolower( trim( $this->getSql() ) ), 'update' ) === 0; 181 | } 182 | 183 | /** 184 | * @return bool 185 | */ 186 | public function isInsertQuery(): bool { 187 | return strpos( strtolower( trim( $this->getSql() ) ), 'insert' ) === 0; 188 | } 189 | } -------------------------------------------------------------------------------- /src/DB/ShardCache.php: -------------------------------------------------------------------------------- 1 | getCalledMethod() ? strtolower( $preStatement->getCalledMethod() ) : ''; 30 | $remove = false; 31 | if ( $preStatement->isUpdateQuery() || $preStatement->isDeleteQuery() ) { 32 | $remove = true; 33 | } 34 | if ( strpos( $method, 'insert' ) !== false ) { 35 | return $shardDb->__execute( $preStatement, $useNewConnection, $rollbacks ); 36 | } 37 | $key = $preStatement->getHashKey(); 38 | if ( $remove ) { 39 | if ( $preStatement->getUuid() ) { 40 | $shardDb->getPdoCache()->clean( $preStatement->getUuid() ); 41 | if ( $preStatement->isFreshDataOnly() ) { 42 | $shardDb->getPdoCache()->cleanAllMatching( $preStatement->getUuid() ); 43 | } 44 | } else { 45 | $shardDb->getPdoCache()->clean( $key ); 46 | } 47 | } elseif ( ! $preStatement->isFreshDataOnly() && ! $useNewConnection ) { 48 | if ( $preStatement->getUuid() ) { 49 | $cacheRead = $shardDb->getPdoCache()->read( $preStatement->getUuid() ); 50 | if ( $cacheRead instanceof ResultsInterface && $cacheRead->isSuccessful() && $cacheRead->rowCount() == 1 ) { 51 | return $cacheRead; 52 | } 53 | } else { 54 | $cacheRead = $shardDb->getPdoCache()->read( $key ); 55 | if ( $cacheRead instanceof ResultsInterface && $cacheRead->isSuccessful() ) { 56 | return $cacheRead; 57 | } 58 | } 59 | } 60 | $returnValue = $shardDb->__execute( $preStatement, $useNewConnection, $rollbacks ); 61 | if ( $returnValue instanceof ResultsInterface && $preStatement->isSelectQuery() && ! $preStatement->getUuid() ) { 62 | $shardDb->getPdoCache()->write( $key, $returnValue ); 63 | } else if ( $returnValue instanceof ResultsInterface && $preStatement->isSelectQuery() && $preStatement->getUuid() && $returnValue->rowCount() == 1 ) { 64 | $shardDb->getPdoCache()->write( $preStatement->getUuid(), $returnValue ); 65 | } 66 | 67 | return $returnValue; 68 | 69 | } 70 | 71 | 72 | } -------------------------------------------------------------------------------- /src/DB/ShardMatrixStatement.php: -------------------------------------------------------------------------------- 1 | uuid = $uuid; 73 | $this->node = $node; 74 | $this->pdoStatement = $pdoStatement; 75 | $this->dataRowReturnClass = $dataRowReturnClass; 76 | } 77 | 78 | /** 79 | * @return \PDOStatement|null 80 | */ 81 | public function getPdoStatement(): ?\PDOStatement { 82 | return $this->pdoStatement; 83 | } 84 | 85 | /** 86 | * @return Node|null 87 | */ 88 | public function getNode(): ?Node { 89 | return $this->node; 90 | } 91 | 92 | /** 93 | * @return Uuid|null 94 | */ 95 | public function getUuid(): ?Uuid { 96 | return $this->uuid; 97 | } 98 | 99 | /** 100 | * @return array[] 101 | */ 102 | public function fetchAllArrays(): array { 103 | if ( $this->pdoStatement ) { 104 | if ( $this->pdoStatement->rowCount() > 0 ) { 105 | $this->dataSuccess = true; 106 | 107 | return $this->pdoStatement->fetchAll( \PDO::FETCH_ASSOC ); 108 | } 109 | } 110 | if ( $this->data ) { 111 | if ( ! $this->isDataInArrayFormat() ) { 112 | $returnArray = []; 113 | foreach ( $this->data as $data ) { 114 | $returnArray[] = (array) $data; 115 | } 116 | 117 | return $returnArray; 118 | } 119 | 120 | return $this->data; 121 | } 122 | 123 | return []; 124 | } 125 | 126 | /** 127 | * @return \stdClass[] 128 | */ 129 | public function fetchAllObjects(): array { 130 | if ( $this->pdoStatement ) { 131 | if ( $this->pdoStatement->rowCount() > 0 ) { 132 | if ( $this->isSelectQuery() ) { 133 | return $this->pdoStatement->fetchAll( \PDO::FETCH_OBJ ); 134 | } 135 | } 136 | } 137 | if ( $this->data ) { 138 | 139 | if ( $this->isDataInArrayFormat() ) { 140 | $returnArray = []; 141 | foreach ( $this->data as $data ) { 142 | $returnArray[] = (object) $data; 143 | } 144 | 145 | return $returnArray; 146 | } 147 | 148 | return $this->data; 149 | } 150 | 151 | return []; 152 | } 153 | 154 | private function isDataInArrayFormat(): bool { 155 | if ( isset( $this->data[0] ) && is_object( $this->data[0] ) ) { 156 | return false; 157 | } 158 | 159 | return true; 160 | } 161 | 162 | /** 163 | * @return array 164 | */ 165 | public function fetchRowArray(): array { 166 | if ( $this->pdoStatement ) { 167 | if ( $this->pdoStatement->rowCount() > 0 ) { 168 | if ( $this->isSelectQuery() ) { 169 | return $this->pdoStatement->fetch( \PDO::FETCH_ASSOC ); 170 | } 171 | } 172 | } 173 | if ( $this->data && isset( $this->data[0] ) ) { 174 | return (array) $this->data[0]; 175 | } 176 | 177 | return []; 178 | } 179 | 180 | /** 181 | * @return \stdClass|null 182 | */ 183 | public function fetchRowObject(): ?\stdClass { 184 | if ( $this->pdoStatement ) { 185 | if ( $this->pdoStatement->rowCount() > 0 ) { 186 | if ( $this->isSelectQuery() ) { 187 | return $this->pdoStatement->fetch( \PDO::FETCH_OBJ ); 188 | } 189 | 190 | } 191 | } 192 | if ( $this->data && isset( $this->data[0] ) ) { 193 | return (object) $this->data[0]; 194 | } 195 | 196 | return null; 197 | } 198 | 199 | /** 200 | * @return int 201 | */ 202 | public function rowCount(): int { 203 | if ( $this->pdoStatement ) { 204 | return $this->pdoStatement->rowCount(); 205 | } 206 | 207 | return count( $this->data ); 208 | } 209 | 210 | public function __preSerialize(): void { 211 | if ( $this->isSelectQuery() || $this->isShowQuery() ) { 212 | $this->data = $this->fetchAllArrays(); 213 | } else if ( $this->isInsertQuery() || $this->isUpdateQuery() ) { 214 | $this->dataSuccess = $this->isSuccessful(); 215 | } 216 | if ( ! $this->queryString ) { 217 | $this->queryString = $this->getPdoStatement()->queryString; 218 | } 219 | $this->pdoStatement = null; 220 | } 221 | 222 | /** 223 | * @return DataRows 224 | */ 225 | public function fetchDataRows(): DataRows { 226 | $resultSet = new DataRows( [], $this->dataRowReturnClass ); 227 | if ( $results = $this->fetchAllObjects() ) { 228 | $resultSet->setDataRows( $results, $this->getDataRowReturnClass() ); 229 | } 230 | 231 | return $resultSet; 232 | } 233 | 234 | /** 235 | * @return ShardDataRowInterface|null 236 | */ 237 | public function fetchDataRow(): ?ShardDataRowInterface { 238 | if ( $row = $this->fetchRowObject() ) { 239 | $returnClass = $this->dataRowReturnClass; 240 | 241 | return ( new DataRowFactory( $row, $returnClass ) )->create(); 242 | } 243 | 244 | return null; 245 | } 246 | 247 | /** 248 | * @param bool|null $successChecked 249 | */ 250 | public function setSuccessChecked( ?bool $successChecked ): ShardMatrixStatement { 251 | $this->successChecked = $successChecked; 252 | 253 | return $this; 254 | } 255 | 256 | /** 257 | * @return bool 258 | */ 259 | public function isSuccessful(): bool { 260 | $success = false; 261 | if ( $this->pdoStatement ) { 262 | if ( $this->pdoStatement->rowCount() > 0 ) { 263 | $success = true; 264 | } 265 | } else if ( $this->data ) { 266 | $success = true; 267 | } else { 268 | $success = $this->dataSuccess; 269 | } 270 | 271 | if ( is_bool( $this->successChecked ) && $success ) { 272 | return $this->successChecked; 273 | } 274 | 275 | return $success; 276 | 277 | } 278 | 279 | /** 280 | * @return Uuid|null 281 | */ 282 | public function getLastInsertUuid(): ?Uuid { 283 | 284 | if ( $this->lastInsertUuid && $this->lastInsertUuid->isValid() ) { 285 | return $this->lastInsertUuid; 286 | } 287 | 288 | return null; 289 | } 290 | 291 | /** 292 | * @param Uuid|null $lastInsertUuid 293 | * 294 | * @return $this 295 | */ 296 | public function setLastInsertUuid( Uuid $lastInsertUuid ): ShardMatrixStatement { 297 | $this->lastInsertUuid = $lastInsertUuid; 298 | 299 | return $this; 300 | } 301 | 302 | /** 303 | * @return Nodes 304 | */ 305 | public function getOtherTableNodes(): Nodes { 306 | $nodes = []; 307 | if ( $this->getUuid() ) { 308 | 309 | foreach ( $this->getAllTableNodes() as $node ) { 310 | if ( $node->getName() != $this->getNode()->getName() ) { 311 | $nodes[] = $node; 312 | } 313 | } 314 | 315 | } 316 | 317 | return new Nodes( $nodes ); 318 | } 319 | 320 | /** 321 | * @return Nodes 322 | */ 323 | public function getAllTableNodes(): Nodes { 324 | return ShardMatrix::getConfig()->getNodes()->getNodesWithTableName( $this->getUuid()->getTable()->getName(), false ); 325 | } 326 | 327 | /** 328 | * @return string 329 | */ 330 | public function getDataRowReturnClass(): string { 331 | return $this->dataRowReturnClass; 332 | } 333 | 334 | /** 335 | * @return string|null 336 | */ 337 | public function getQueryString(): ?string { 338 | if ( $this->getPdoStatement() ) { 339 | return $this->getPdoStatement()->queryString; 340 | } 341 | 342 | return $this->queryString; 343 | } 344 | 345 | /** 346 | * @return bool 347 | */ 348 | public function isDeleteQuery(): bool { 349 | return strpos( strtolower( trim( $this->getQueryString() ) ), 'delete' ) === 0; 350 | } 351 | 352 | /** 353 | * @return bool 354 | */ 355 | public function isSelectQuery(): bool { 356 | return strpos( strtolower( trim( $this->getQueryString() ) ), 'select' ) === 0; 357 | } 358 | 359 | public function isShowQuery(): bool { 360 | return strpos( strtolower( trim( $this->getQueryString() ) ), 'show' ) === 0; 361 | } 362 | 363 | /** 364 | * @return bool 365 | */ 366 | public function isUpdateQuery(): bool { 367 | return strpos( strtolower( trim( $this->getQueryString() ) ), 'update' ) === 0; 368 | } 369 | 370 | /** 371 | * @return bool 372 | */ 373 | public function isInsertQuery(): bool { 374 | return strpos( strtolower( trim( $this->getQueryString() ) ), 'insert' ) === 0; 375 | } 376 | 377 | 378 | /** 379 | * @param string $column 380 | * @param string|null $groupByColumn 381 | * 382 | * @return int 383 | */ 384 | public function sumColumn( string $column ): float { 385 | $sum = 0; 386 | foreach ( $this->fetchDataRows() as $row ) { 387 | if ( isset( $row->__toObject()->$column ) && is_numeric( $row->__toObject()->$column ) ) { 388 | $sum = $sum + $row->__toObject()->$column; 389 | } 390 | } 391 | 392 | return $sum; 393 | } 394 | 395 | /** 396 | * @param string $column 397 | * 398 | * @return float 399 | */ 400 | public function avgColumn( string $column ): float { 401 | $sum = 0; 402 | $i = 0; 403 | foreach ( $this->fetchDataRows() as $row ) { 404 | if ( isset( $row->__toObject()->$column ) && is_numeric( $row->__toObject()->$column ) ) { 405 | $i ++; 406 | $sum = $sum + $row->__toObject()->$column; 407 | } 408 | } 409 | if ( $i == 0 ) { 410 | return 0; 411 | } 412 | 413 | return $sum / $i; 414 | } 415 | 416 | /** 417 | * @param string $column 418 | * 419 | * @return float|null 420 | */ 421 | public function minColumn( string $column ): ?float { 422 | $result = null; 423 | 424 | foreach ( $this->fetchDataRows() as $row ) { 425 | if ( isset( $row->__toObject()->$column ) && is_numeric( $row->__toObject()->$column ) ) { 426 | if ( ! isset( $result ) ) { 427 | $result = $row->__toObject()->$column; 428 | } 429 | if ( $result > $row->__toObject()->$column ) { 430 | $result = $row->__toObject()->$column; 431 | } 432 | } 433 | } 434 | 435 | return $result; 436 | } 437 | 438 | /** 439 | * @param string $column 440 | * 441 | * @return float|null 442 | */ 443 | public function maxColumn( string $column ): ?float { 444 | $result = null; 445 | 446 | foreach ( $this->fetchDataRows() as $row ) { 447 | if ( isset( $row->__toObject()->$column ) && is_numeric( $row->__toObject()->$column ) ) { 448 | if ( ! isset( $result ) ) { 449 | $result = $row->__toObject()->$column; 450 | } 451 | if ( $result < $row->__toObject()->$column ) { 452 | $result = $row->__toObject()->$column; 453 | } 454 | } 455 | } 456 | 457 | return $result; 458 | } 459 | 460 | /** 461 | * @param string $column 462 | * @param string $groupByColumn 463 | * 464 | * @return GroupSums 465 | */ 466 | public function sumColumnByGroup( string $column, string $groupByColumn ): GroupSums { 467 | $sum = []; 468 | foreach ( $this->fetchDataRows() as $row ) { 469 | if ( isset( $row->__toObject()->$groupByColumn ) ) { 470 | if ( isset( $row->__toObject()->$column ) && is_numeric( $row->__toObject()->$column ) ) { 471 | if ( ! isset( $sum[ $row->__toObject()->$groupByColumn ] ) ) { 472 | $sum[ $row->__toObject()->$groupByColumn ] = 0; 473 | } 474 | $sum[ $row->__toObject()->$groupByColumn ] = $sum[ $row->__toObject()->$groupByColumn ] + $row->__toObject()->$column; 475 | } 476 | } 477 | } 478 | 479 | 480 | $results = []; 481 | foreach ( $sum as $group => $result ) { 482 | $results[] = new GroupSum( (object) [ 'column' => $group, 'sum' => $result ] ); 483 | } 484 | 485 | return new GroupSums( $results ); 486 | 487 | } 488 | 489 | /** 490 | * @param NodeResult $nodeResult 491 | */ 492 | public function setDataFromGoThreadedResult( NodeResult $nodeResult ): ShardMatrixStatement { 493 | $this->data = $nodeResult->getData(); 494 | 495 | return $this; 496 | } 497 | 498 | /** 499 | * @return bool 500 | */ 501 | public function isFromCache(): bool { 502 | return $this->fromCache; 503 | } 504 | 505 | /** 506 | * @param bool $fromCache 507 | */ 508 | public function setFromCache( bool $fromCache = true ): void { 509 | $this->fromCache = $fromCache; 510 | } 511 | } -------------------------------------------------------------------------------- /src/DB/ShardMatrixStatements.php: -------------------------------------------------------------------------------- 1 | shardMatrixStatements = $shardMatrixStatements; 47 | $this->orderByColumn = $orderByColumn; 48 | $this->orderByDirection = $orderByDirection; 49 | } 50 | 51 | /** 52 | * @return int 53 | */ 54 | public function countShardMatrixStatements(): int { 55 | return count( $this->getShardMatrixStatements() ); 56 | } 57 | 58 | 59 | /** 60 | * Return the current element 61 | * @link https://php.net/manual/en/iterator.current.php 62 | * @return ShardMatrixStatement Can return any type. 63 | * @since 5.0.0 64 | */ 65 | public function current() { 66 | return $this->getShardMatrixStatements()[ $this->position ]; 67 | } 68 | 69 | /** 70 | * Move forward to next element 71 | * @link https://php.net/manual/en/iterator.next.php 72 | * @return void Any returned value is ignored. 73 | * @since 5.0.0 74 | */ 75 | public function next() { 76 | $this->position ++; 77 | } 78 | 79 | /** 80 | * Return the key of the current element 81 | * @link https://php.net/manual/en/iterator.key.php 82 | * @return mixed scalar on success, or null on failure. 83 | * @since 5.0.0 84 | */ 85 | public function key() { 86 | return $this->position; 87 | } 88 | 89 | /** 90 | * Rewind the Iterator to the first element 91 | * @link https://php.net/manual/en/iterator.rewind.php 92 | * @return void Any returned value is ignored. 93 | * @since 5.0.0 94 | */ 95 | public function rewind() { 96 | $this->position = 0; 97 | } 98 | 99 | /** 100 | * @return bool 101 | */ 102 | public function valid() { 103 | return isset( $this->shardMatrixStatements[ $this->position ] ); 104 | } 105 | 106 | /** 107 | * @param $results 108 | * @param bool $row 109 | */ 110 | private function orderResults( &$results, bool $row = false ) { 111 | 112 | if ( $this->orderByColumn && count( $results ) > 1 ) { 113 | usort( $results, function ( $a, $b ) { 114 | $orderByColumn = $this->orderByColumn; 115 | if ( ! $a instanceof \stdClass ) { 116 | $a = (object) $a; 117 | } 118 | if ( ! $b instanceof \stdClass ) { 119 | $b = (object) $b; 120 | } 121 | $aComp = $a->$orderByColumn; 122 | $bComp = $b->$orderByColumn; 123 | $asc = is_string( $this->orderByDirection ) && strtolower( $this->orderByDirection ) == 'asc'; 124 | $desc = is_string( $this->orderByDirection ) && strtolower( $this->orderByDirection ) == 'desc'; 125 | $int = $aComp == (string) ( (int) $aComp ); 126 | 127 | if ( $int ) { 128 | $aInt = (int) $aComp; 129 | $bInt = (int) $bComp; 130 | if ( $asc ) { 131 | return ( $aInt < $bInt ) ? - 1 : ( ( $aInt > $bInt ) ? 1 : 0 ); 132 | } 133 | if ( $desc ) { 134 | return ( $bInt < $aInt ) ? - 1 : ( ( $bInt > $aInt ) ? 1 : 0 ); 135 | } 136 | } 137 | $aComp = substr( $aComp, 0, 26 ); 138 | $bComp = substr( $bComp, 0, 26 ); 139 | if ( ! ctype_digit( $aComp ) ) { 140 | $aComp = ltrim( $aComp, '0' ); 141 | } 142 | if ( ! ctype_digit( $bComp ) ) { 143 | $bComp = ltrim( $bComp, '0' ); 144 | } 145 | if ( $desc ) { 146 | return strcmp( strtolower( $bComp ), strtolower( $aComp ) ); 147 | } 148 | if ( $asc ) { 149 | return strcmp( strtolower( $aComp ), strtolower( $bComp ) ); 150 | } 151 | 152 | } ); 153 | } 154 | if ( $row && isset( $results[0] ) ) { 155 | $results = $results[0]; 156 | } 157 | 158 | } 159 | 160 | 161 | /** 162 | * @return array 163 | */ 164 | public function fetchAllArrays(): array { 165 | $results = []; 166 | foreach ( $this->getShardMatrixStatements() as $statement ) { 167 | 168 | $results = array_merge( $results, $statement->fetchAllArrays() ); 169 | } 170 | $this->orderResults( $results ); 171 | 172 | return $results; 173 | } 174 | 175 | /** 176 | * @return array 177 | */ 178 | public function fetchAllObjects(): array { 179 | $results = []; 180 | foreach ( $this->getShardMatrixStatements() as $statement ) { 181 | $results = array_merge( $results, $statement->fetchAllObjects() ); 182 | } 183 | $this->orderResults( $results ); 184 | 185 | return $results; 186 | } 187 | 188 | /** 189 | * @return array 190 | */ 191 | public function fetchRowArray(): array { 192 | $results = []; 193 | foreach ( $this->getShardMatrixStatements() as $statement ) { 194 | if ( $row = $statement->fetchRowArray() ) { 195 | $results[] = $row; 196 | } 197 | 198 | } 199 | $this->orderResults( $results, true ); 200 | 201 | return $results; 202 | } 203 | 204 | /** 205 | * @return \stdClass|null 206 | */ 207 | public function fetchRowObject(): ?\stdClass { 208 | $results = []; 209 | foreach ( $this->getShardMatrixStatements() as $statement ) { 210 | if ( $row = $statement->fetchRowObject() ) { 211 | $results[] = $row; 212 | } 213 | } 214 | $this->orderResults( $results, true ); 215 | if ( ! $results ) { 216 | return null; 217 | } 218 | 219 | return $results; 220 | } 221 | 222 | /** 223 | * @return ShardMatrixStatement[] 224 | */ 225 | public function getShardMatrixStatements(): array { 226 | return $this->shardMatrixStatements; 227 | } 228 | 229 | /** 230 | * @return DataRows 231 | */ 232 | public function fetchDataRows(): DataRows { 233 | $class = DataRow::class; 234 | if ( isset( $this->getShardMatrixStatements()[0] ) ) { 235 | $class = $this->getShardMatrixStatements()[0]->getDataRowReturnClass(); 236 | } 237 | $resultSet = new DataRows( [], $class ); 238 | if ( $results = $this->fetchAllObjects() ) { 239 | $resultSet->setDataRows( $results, $class ); 240 | } 241 | 242 | return $resultSet; 243 | } 244 | 245 | /** 246 | * @return ShardDataRowInterface|null 247 | */ 248 | public function fetchDataRow(): ?ShardDataRowInterface { 249 | if ( $row = $this->fetchRowObject() ) { 250 | $class = DataRow::class; 251 | if ( isset( $this->getShardMatrixStatements()[0] ) ) { 252 | $class = $this->getShardMatrixStatements()[0]->getDataRowReturnClass(); 253 | } 254 | 255 | return ( new DataRowFactory( $row, $class ) )->create(); 256 | } 257 | 258 | return null; 259 | } 260 | 261 | /** 262 | * @return bool 263 | */ 264 | public function isSuccessful(): bool { 265 | foreach ( $this->getShardMatrixStatements() as $statement ) { 266 | if ( $statement->isSuccessful() ) { 267 | return true; 268 | } 269 | } 270 | 271 | return false; 272 | } 273 | 274 | 275 | /** 276 | * @return int 277 | */ 278 | public function rowCount(): int { 279 | $count = 0; 280 | foreach ( $this->getShardMatrixStatements() as $statement ) { 281 | $count = $count + $statement->rowCount(); 282 | } 283 | 284 | return $count; 285 | } 286 | 287 | /** 288 | * @return Uuid[] 289 | */ 290 | public function getLastInsertUuids(): array { 291 | $results = []; 292 | foreach ( $this->getShardMatrixStatements() as $statement ) { 293 | if ( $uuid = $statement->getLastInsertUuid() ) { 294 | $results[] = $uuid; 295 | } 296 | } 297 | 298 | return $results; 299 | } 300 | 301 | 302 | /** 303 | * @param string $column 304 | * 305 | * @return int 306 | */ 307 | public function sumColumn( string $column ): float { 308 | $sum = 0; 309 | foreach ( $this->fetchDataRows() as $row ) { 310 | if ( isset( $row->__toObject()->$column ) && is_numeric( $row->__toObject()->$column ) ) { 311 | $sum = $sum + $row->$column; 312 | } 313 | } 314 | 315 | return $sum; 316 | } 317 | 318 | /** 319 | * @param string $column 320 | * 321 | * @return float 322 | */ 323 | public function avgColumn( string $column ): float { 324 | $sum = 0; 325 | $i = 0; 326 | foreach ( $this->fetchDataRows() as $row ) { 327 | if ( isset( $row->__toObject()->$column ) && is_numeric( $row->__toObject()->$column ) ) { 328 | $i ++; 329 | $sum = $sum + $row->$column; 330 | } 331 | } 332 | 333 | if ( $i == 0 ) { 334 | return 0; 335 | } 336 | 337 | return $sum / $i; 338 | } 339 | 340 | /** 341 | * @param string $column 342 | * 343 | * @return float|null 344 | */ 345 | public function minColumn( string $column ): ?float { 346 | $result = null; 347 | foreach ( $this->fetchDataRows() as $row ) { 348 | if ( isset( $row->__toObject()->$column ) && is_numeric( $row->__toObject()->$column ) ) { 349 | if ( ! isset( $result ) ) { 350 | $result = $row->__toObject()->$column; 351 | } 352 | if ( $result > $row->__toObject()->$column ) { 353 | $result = $row->__toObject()->$column; 354 | } 355 | } 356 | } 357 | 358 | return $result; 359 | } 360 | 361 | /** 362 | * @param string $column 363 | * 364 | * @return float|null 365 | */ 366 | public function maxColumn( string $column ): ?float { 367 | $result = null; 368 | 369 | foreach ( $this->fetchDataRows() as $row ) { 370 | if ( isset( $row->__toObject()->$column ) && is_numeric( $row->__toObject()->$column ) ) { 371 | if ( ! isset( $result ) ) { 372 | $result = $row->__toObject()->$column; 373 | } 374 | if ( $result < $row->__toObject()->$column ) { 375 | $result = $row->__toObject()->$column; 376 | } 377 | } 378 | } 379 | 380 | return $result; 381 | } 382 | 383 | /** 384 | * @param string $column 385 | * @param string $groupByColumn 386 | * 387 | * @return GroupSums 388 | */ 389 | public function sumColumnByGroup( string $column, string $groupByColumn ): GroupSums { 390 | $sum = []; 391 | foreach ( $this->fetchDataRows() as $row ) { 392 | if ( isset( $row->__toObject()->$groupByColumn ) ) { 393 | if ( isset( $row->__toObject()->$column ) && is_numeric( $row->__toObject()->$column ) ) { 394 | if ( ! isset( $sum[ $row->__toObject()->$groupByColumn ] ) ) { 395 | $sum[ $row->__toObject()->$groupByColumn ] = 0; 396 | } 397 | $sum[ $row->__toObject()->$groupByColumn ] = $sum[ $row->__toObject()->$groupByColumn ] + $row->__toObject()->$column; 398 | } 399 | } 400 | } 401 | 402 | $results = []; 403 | foreach ( $sum as $group => $result ) { 404 | $results[] = new GroupSum( (object) [ 'column' => $group, 'sum' => $result ] ); 405 | } 406 | 407 | return new GroupSums( $results ); 408 | 409 | } 410 | 411 | /** 412 | * @return Uuid|null 413 | */ 414 | public function getLastInsertUuid(): ?Uuid { 415 | if ( $results = $this->getLastInsertUuids() ) { 416 | return end( $results ); 417 | } 418 | 419 | return null; 420 | } 421 | 422 | /** 423 | * @param string|null $orderByColumn 424 | * 425 | * @return ShardMatrixStatements 426 | */ 427 | public function setOrderByColumn( ?string $orderByColumn ): ShardMatrixStatements { 428 | $this->orderByColumn = $orderByColumn; 429 | 430 | return $this; 431 | } 432 | 433 | /** 434 | * @param string|null $orderByDirection 435 | * 436 | * @return ShardMatrixStatements 437 | */ 438 | public function setOrderByDirection( ?string $orderByDirection ): ShardMatrixStatements { 439 | $this->orderByDirection = $orderByDirection; 440 | 441 | return $this; 442 | } 443 | 444 | /** 445 | * @return bool 446 | */ 447 | public function isFromCache(): bool { 448 | return $this->fromCache; 449 | } 450 | 451 | /** 452 | * @param bool $fromCache 453 | */ 454 | public function setFromCache( bool $fromCache = true ): void { 455 | foreach ( $this->getShardMatrixStatements() as $statement ) { 456 | $statement->setFromCache( $fromCache ); 457 | } 458 | $this->fromCache = $fromCache; 459 | } 460 | } -------------------------------------------------------------------------------- /src/DockerNetwork.php: -------------------------------------------------------------------------------- 1 | dockerNetwork = $dockerNetwork; 22 | if ( $dockerNetwork ) { 23 | $parts = explode( ':', $dockerNetwork ); 24 | if ( isset( $parts[1] ) ) { 25 | $this->port = $parts[1]; 26 | } 27 | if ( isset( $parts[0] ) ) { 28 | $this->host = $parts[0]; 29 | } 30 | } 31 | } 32 | 33 | /** 34 | * @return string|null 35 | */ 36 | public function getHost(): ?string { 37 | return $this->host; 38 | } 39 | 40 | /** 41 | * @return string|null 42 | */ 43 | public function getPort(): ?string { 44 | return $this->port; 45 | } 46 | 47 | public function jsonSerialize() { 48 | return [ 49 | 'port' => $this->port, 50 | 'host' => $this->host 51 | ]; 52 | } 53 | } -------------------------------------------------------------------------------- /src/Dsn.php: -------------------------------------------------------------------------------- 1 | dsn = $dsn ?? ''; 23 | $this->dockerNetwork = $dockerNetwork; 24 | 25 | } 26 | 27 | /** 28 | * @return string|null 29 | */ 30 | public function getDriver(): ?string { 31 | return ( $value = explode( ':', $this->dsn )[0] ) ? $value : null; 32 | } 33 | 34 | /** 35 | * @param $key 36 | * 37 | * @return string|null 38 | */ 39 | public function getAttribute( $key ): ?string { 40 | if ( strpos( $this->dsn, $key . '=' ) === false ) { 41 | return null; 42 | } 43 | $internalValueParts = explode( $key . '=', $this->dsn ); 44 | if ( isset( $internalValueParts[1] ) ) { 45 | $internalValue = $internalValueParts[1]; 46 | } else { 47 | $internalValue = $internalValueParts[0]; 48 | } 49 | if ( $value = explode( ';', $internalValue )[0] ) { 50 | return $value; 51 | } 52 | 53 | return null; 54 | } 55 | 56 | /** 57 | * @return string|null 58 | */ 59 | public function getDbname(): ?string { 60 | return $this->getAttribute( 'dbname' ); 61 | } 62 | 63 | /** 64 | * @return string|null 65 | */ 66 | public function getUsername(): ?string { 67 | return $this->getAttribute( 'user' ); 68 | } 69 | 70 | /** 71 | * @return string|null 72 | */ 73 | public function getPassword(): ?string { 74 | return $this->getAttribute( 'password' ); 75 | } 76 | 77 | /** 78 | * @param bool $removePort 79 | * 80 | * @return string|null 81 | */ 82 | public function getHost( bool $removePort = true ): ?string { 83 | $value = $this->getAttribute( 'host' ); 84 | if ( $removePort && $value ) { 85 | if ( strpos( $value, ':' ) !== false ) { 86 | $value = explode( ':', $value )[0]; 87 | } 88 | } 89 | if ( $value ) { 90 | $value = str_replace( 'localhost', '127.0.0.1', $value ); 91 | } 92 | 93 | return $value; 94 | } 95 | 96 | /** 97 | * @return string|null 98 | */ 99 | public function getPort(): ?string { 100 | 101 | $port = $this->getAttribute( 'port' ); 102 | $host = $this->getHost( false ); 103 | if ( ! $port && strpos( $host, ':' ) !== false ) { 104 | return explode( ':', $host )[1]; 105 | } 106 | 107 | return $port; 108 | } 109 | 110 | public function getCharacterSet( bool $returnDefault = true ): ?string { 111 | if ( $this->getDriver() == 'pgsql' ) { 112 | return ( $this->getAttribute( 'charset' ) ?? 'utf8' ); 113 | } else { 114 | return ( $this->getAttribute( 'charset' ) ?? 'utf8mb4' ); 115 | } 116 | } 117 | 118 | public function getCharacterSetString(): ?string { 119 | if ( $this->getDriver() == 'pgsql' ) { 120 | return "options='--client_encoding=" . $this->getCharacterSet() . "'"; 121 | } else { 122 | return "charset=" . $this->getCharacterSet(); 123 | } 124 | 125 | } 126 | 127 | /** 128 | * @return string 129 | */ 130 | public function __toString() { 131 | $host = $this->getHost( true ); 132 | $port = $this->getPort(); 133 | if ( ShardMatrix::isDocker() ) { 134 | if ( $this->dockerNetwork && $this->dockerNetwork->getHost() ) { 135 | $host = $this->dockerNetwork->getHost(); 136 | } 137 | if ( $this->dockerNetwork && $this->dockerNetwork->getHost() ) { 138 | $host = $this->dockerNetwork->getHost(); 139 | } 140 | } 141 | 142 | return join( ';', [ 143 | $this->getDriver() . ':host=' . $host, 144 | 'port=' . $port, 145 | 'dbname=' . $this->getDbname(), 146 | 'user=' . $this->getUsername(), 147 | 'password=' . $this->getPassword(), 148 | $this->getCharacterSetString() 149 | ] ); 150 | } 151 | 152 | 153 | public function jsonSerialize() { 154 | return [ 155 | 'driver' => $this->getDriver(), 156 | 'host' => $this->getHost(), 157 | 'port' => $this->getPort(), 158 | 'dbname' => $this->getDbname(), 159 | 'user' => $this->getUsername(), 160 | 'password' => $this->getPassword(), 161 | 'charset' => $this->getCharacterSet() 162 | ]; 163 | } 164 | } -------------------------------------------------------------------------------- /src/Exception.php: -------------------------------------------------------------------------------- 1 | host = $hostname; 58 | $this->port = $port; 59 | $this->timeout = $timeout; 60 | $this->username = $username; 61 | $this->password = $password; 62 | } 63 | 64 | /** 65 | * @throws GoThreadedException 66 | */ 67 | private function connect() { 68 | if ( ! is_resource( $this->resource ) ) { 69 | $this->resource = @fsockopen( $this->host, $this->port, $this->errorNumber, $this->errorString, $this->timeout ); 70 | 71 | if ( ! $this->resource ) { 72 | throw new GoThreadedException( $this->errorString, $this->errorNumber ); 73 | } 74 | } 75 | } 76 | 77 | /** 78 | * @return $this 79 | * @throws GoThreadedException 80 | */ 81 | public function __killClient(): Client { 82 | $this->close(); 83 | $this->connect(); 84 | fwrite( $this->resource, json_encode( [ 85 | 'auth' => [ 86 | 'username' => $this->username, 87 | 'password' => $this->password 88 | ], 89 | 'kill' => 1 90 | ] ) ); 91 | $this->close(); 92 | return $this; 93 | } 94 | 95 | public function execQueries( NodeQueries $nodeQueries ): Client { 96 | $this->connect(); 97 | fwrite( $this->resource, json_encode( [ 98 | 'auth' => [ 99 | 'username' => $this->username, 100 | 'password' => $this->password 101 | ], 102 | 'node_queries' => $nodeQueries 103 | ] ) ); 104 | 105 | return $this; 106 | } 107 | 108 | /** 109 | * @return Results 110 | * @throws GoThreadedException 111 | */ 112 | public function getResults(): Results { 113 | try { 114 | $results = new Results( json_decode( fgets( $this->resource ) ) ); 115 | $this->close(); 116 | 117 | return $results; 118 | } catch ( \Error | \Exception $e ) { 119 | throw new GoThreadedException( $e->getMessage(), $e->getCode() ); 120 | } 121 | } 122 | 123 | public function close(): bool { 124 | if ( is_resource( $this->resource ) ) { 125 | return (bool) fclose( $this->resource ); 126 | } 127 | 128 | return true; 129 | } 130 | 131 | public function __destruct() { 132 | $this->close(); 133 | } 134 | 135 | /** 136 | * @return int|null 137 | */ 138 | public function getErrorNumber(): ?int { 139 | return $this->errorNumber; 140 | } 141 | 142 | /** 143 | * @return string|null 144 | */ 145 | public function getErrorString(): ?string { 146 | return $this->errorString; 147 | } 148 | } -------------------------------------------------------------------------------- /src/GoThreaded/GoThreadedException.php: -------------------------------------------------------------------------------- 1 | nodeResult = $nodeResult; 13 | } 14 | 15 | public function getNodeName(): ?string { 16 | return $this->nodeResult->node_name ?? null; 17 | 18 | } 19 | 20 | public function getData(): array { 21 | return $this->nodeResult->data ?? []; 22 | } 23 | 24 | /** 25 | * @return string|null 26 | */ 27 | public function getError(): ?string { 28 | if ( strlen( $this->nodeResult->error ?? '' ) ) { 29 | return $this->nodeResult->error; 30 | } 31 | 32 | return null; 33 | } 34 | } -------------------------------------------------------------------------------- /src/GoThreaded/Results.php: -------------------------------------------------------------------------------- 1 | results = $results; 17 | } 18 | 19 | /** 20 | * @return NodeResult[] 21 | */ 22 | public function getNodes(): array { 23 | if ( isset( $this->results->nodes ) ) { 24 | foreach ( $this->results->nodes as &$result ) { 25 | if ( ! $result instanceof NodeResult ) { 26 | $result = new NodeResult( $result ); 27 | } 28 | } 29 | 30 | return $this->results->nodes; 31 | } 32 | 33 | return []; 34 | } 35 | } -------------------------------------------------------------------------------- /src/Node.php: -------------------------------------------------------------------------------- 1 | name = $name; 34 | $this->nodeData = $nodeData; 35 | } 36 | 37 | /** 38 | * @return Dsn 39 | */ 40 | public function getDsn(): Dsn { 41 | return new Dsn( $this->nodeData['dsn'] ?? null, $this->getDockerNetwork() ); 42 | } 43 | 44 | /** 45 | * @return DockerNetwork 46 | */ 47 | public function getDockerNetwork(): DockerNetwork { 48 | return new DockerNetwork( $this->nodeData['docker_network'] ?? null ); 49 | } 50 | 51 | /** 52 | * @return string|null 53 | */ 54 | public function getGeo(): ?string { 55 | return $this->nodeData['geo'] ?? null; 56 | } 57 | 58 | /** 59 | * @return bool 60 | */ 61 | public function isInsertData(): bool { 62 | if ( isset( $this->nodeData['insert_data'] ) && $this->nodeData['insert_data'] == 'false' ) { 63 | return false; 64 | } 65 | 66 | return true; 67 | } 68 | 69 | /** 70 | * @return TableGroups 71 | */ 72 | public function getTableGroups(): TableGroups { 73 | if ( isset( $this->tableGroups ) ) { 74 | return $this->tableGroups; 75 | } 76 | $allGroups = ShardMatrix::getConfig()->getTableGroups(); 77 | $nodeGroups = []; 78 | foreach ( $this->nodeData['table_groups'] as $name ) { 79 | $nodeGroups[] = $allGroups->getTableGroupByName( $name ); 80 | } 81 | 82 | return $this->tableGroups ?? ( $this->tableGroups = new TableGroups( $nodeGroups ) ); 83 | } 84 | 85 | /** 86 | * @param $tableName 87 | * 88 | * @return bool 89 | */ 90 | public function containsTableName( $tableName ): bool { 91 | 92 | foreach ( $this->getTableGroups() as $group ) { 93 | if ( $group->containsTableName( $tableName ) ) { 94 | return true; 95 | } 96 | } 97 | 98 | 99 | return false; 100 | } 101 | 102 | /** 103 | * @return string 104 | */ 105 | public function getName(): string { 106 | return $this->name; 107 | } 108 | 109 | /** 110 | * @param string|null $lastUsedTableName 111 | * 112 | * @return Node 113 | */ 114 | public function setLastUsedTableName( ?string $lastUsedTableName ): Node { 115 | $this->lastUsedTableName = $lastUsedTableName; 116 | 117 | return $this; 118 | } 119 | 120 | /** 121 | * @return string|null 122 | */ 123 | public function getLastUsedTableName(): ?string { 124 | return $this->lastUsedTableName; 125 | } 126 | 127 | 128 | public function jsonSerialize() { 129 | return [ 130 | 'name' => $this->getName(), 131 | 'dsn' => $this->getDsn(), 132 | 'geo' => $this->getGeo(), 133 | 'docker_network' => $this->getDockerNetwork() 134 | ]; 135 | } 136 | } -------------------------------------------------------------------------------- /src/NodeDistributor.php: -------------------------------------------------------------------------------- 1 | getNode() 18 | ->getTableGroups() 19 | ->getTableGroupByTableName( $uuid->getTable()->getName() ) 20 | ->getName() ] = $uuid->getNode(); 21 | } 22 | 23 | /** 24 | * @param string $tableName 25 | * 26 | * @return Node 27 | * @throws Exception 28 | */ 29 | static public function getNode( string $tableName ): Node { 30 | $group = ShardMatrix::getConfig()->getTableGroups()->getTableGroupByTableName( $tableName ); 31 | if ( isset( static::$groupNodes[ $group->getName() ] ) ) { 32 | return static::$groupNodes[ $group->getName() ]->setLastUsedTableName( $tableName ); 33 | } 34 | 35 | $nodes = ShardMatrix::getConfig()->getNodes()->getNodesWithTableName( $tableName ); 36 | $count = $nodes->countNodes(); 37 | $randomKey = rand( 1, $count ) - 1; 38 | 39 | $node = $nodes->getInsertNodes()[ $randomKey ]; 40 | if ( $node && $group ) { 41 | static::$groupNodes[ $group->getName() ] = $node; 42 | 43 | return $node; 44 | } 45 | 46 | throw new Exception( 'No Node Found for ' . $tableName, 1 ); 47 | } 48 | 49 | static public function clearGroupNodes() { 50 | static::$groupNodes = []; 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /src/NodeQueriesAsyncInterface.php: -------------------------------------------------------------------------------- 1 | shardDb = $shardDb; 36 | $this->nodeQueries = $nodeQueries; 37 | $this->orderByColumn = $orderByColumn; 38 | $this->orderByDirection = $orderByDirection; 39 | $this->calledMethod = $calledMethod; 40 | 41 | } 42 | 43 | /** 44 | * @return ShardMatrixStatements 45 | */ 46 | public function getResults(): ShardMatrixStatements { 47 | $client = ShardMatrix::getGoThreadedService(); 48 | 49 | $results = $client->execQueries( $this->nodeQueries )->getResults(); 50 | $client->close(); 51 | $statementResult = []; 52 | if ( $resultNodes = $results->getNodes() ) { 53 | 54 | foreach ( $resultNodes as $nodeResult ) { 55 | foreach ( $this->nodeQueries->getNodeQueries() as $query ) { 56 | if ( $query->getNode()->getName() == $nodeResult->getNodeName() ) { 57 | $statementResult[] = ( new ShardMatrixStatement( null, $query->getNode(), null, $this->shardDb->getDataRowClassByNode( $query->getNode() ) ) )->setDataFromGoThreadedResult( $nodeResult ); 58 | } 59 | } 60 | 61 | } 62 | 63 | } 64 | 65 | return new ShardMatrixStatements( $statementResult, $this->orderByColumn, $this->orderByDirection ); 66 | 67 | 68 | } 69 | } -------------------------------------------------------------------------------- /src/NodeQueriesPcntlFork.php: -------------------------------------------------------------------------------- 1 | shardDb = $shardDb; 37 | $this->nodeQueries = $nodeQueries; 38 | $this->orderByColumn = $orderByColumn; 39 | $this->orderByDirection = $orderByDirection; 40 | $this->calledMethod = $calledMethod; 41 | 42 | } 43 | 44 | /** 45 | * @return ShardMatrixStatements 46 | * @throws DB\DuplicateException 47 | * @throws DB\Exception 48 | */ 49 | public function getResults(): ShardMatrixStatements { 50 | $queryPidUuid = uniqid( getmypid() . '-' ); 51 | $pids = []; 52 | 53 | foreach ( $this->nodeQueries as $nodeQuery ) { 54 | $pid = pcntl_fork(); 55 | Connections::closeConnections(); 56 | ShardMatrix::clearServiceInstances(); 57 | if ( $pid == - 1 ) { 58 | die( 'could not fork' ); 59 | } else if ( $pid ) { 60 | 61 | $pids[] = $pid; 62 | 63 | } else { 64 | $stmt = $this->shardDb->execute( new PreStatement( $nodeQuery->getNode(), $nodeQuery->getSql(), $nodeQuery->getBinds(), null, null, $this->calledMethod , ! $nodeQuery->isUseCache()) ); 65 | 66 | $this->shardDb->getPdoCache()->write( $queryPidUuid . '-' . getmypid(), $stmt ); 67 | exit; 68 | } 69 | } 70 | 71 | 72 | while ( count( $pids ) > 0 ) { 73 | foreach ( $pids as $key => $pid ) { 74 | $res = pcntl_waitpid( $pid, $status, WNOHANG ); 75 | // If the process has already exited 76 | if ( $res == - 1 || $res > 0 ) { 77 | unset( $pids[ $key ] ); 78 | } 79 | } 80 | usleep( 100000 ); 81 | } 82 | 83 | $results = $this->shardDb->getPdoCache()->scanAndClean( $queryPidUuid . '-' ); 84 | 85 | if ( $results ) { 86 | return new ShardMatrixStatements( $results, $this->orderByColumn, $this->orderByDirection ); 87 | } 88 | 89 | return new ShardMatrixStatements( [], $this->orderByColumn, $this->orderByDirection ); 90 | } 91 | } -------------------------------------------------------------------------------- /src/Nodes.php: -------------------------------------------------------------------------------- 1 | setNodes( $nodes ); 28 | } 29 | 30 | /** 31 | * @return Node[] 32 | */ 33 | public function getNodes(): array { 34 | 35 | return $this->nodes; 36 | } 37 | 38 | /** 39 | * @return Node[] 40 | */ 41 | public function getInsertNodes(): array { 42 | $returnArray = []; 43 | foreach ( $this->getNodes() as $node ) { 44 | if ( $node->isInsertData() ) { 45 | $returnArray[] = $node; 46 | } 47 | } 48 | 49 | return $returnArray; 50 | } 51 | 52 | /** 53 | * @param array $nodes 54 | * 55 | * @return Nodes 56 | */ 57 | public function setNodes( array $nodes ): Nodes { 58 | foreach ( $nodes as $name => $data ) { 59 | if ( $data instanceof Node ) { 60 | $this->nodes[] = $data; 61 | } else { 62 | $this->nodes[] = new Node( $name, $data ); 63 | } 64 | } 65 | 66 | return $this; 67 | } 68 | 69 | /** 70 | * @return int 71 | */ 72 | public function countNodes(): int { 73 | return count( $this->getNodes() ); 74 | } 75 | 76 | /** 77 | * @return int 78 | */ 79 | public function countInsertNodes(): int { 80 | return count( $this->getInsertNodes() ); 81 | } 82 | 83 | 84 | /** 85 | * Return the current element 86 | * @link https://php.net/manual/en/iterator.current.php 87 | * @return Node Can return any type. 88 | * @since 5.0.0 89 | */ 90 | public function current() { 91 | return $this->getNodes()[ $this->position ]; 92 | } 93 | 94 | /** 95 | * Move forward to next element 96 | * @link https://php.net/manual/en/iterator.next.php 97 | * @return void Any returned value is ignored. 98 | * @since 5.0.0 99 | */ 100 | public function next() { 101 | $this->position ++; 102 | } 103 | 104 | /** 105 | * Return the key of the current element 106 | * @link https://php.net/manual/en/iterator.key.php 107 | * @return mixed scalar on success, or null on failure. 108 | * @since 5.0.0 109 | */ 110 | public function key() { 111 | return $this->position; 112 | } 113 | 114 | /** 115 | * Rewind the Iterator to the first element 116 | * @link https://php.net/manual/en/iterator.rewind.php 117 | * @return void Any returned value is ignored. 118 | * @since 5.0.0 119 | */ 120 | public function rewind() { 121 | $this->position = 0; 122 | } 123 | 124 | /** 125 | * @return bool 126 | */ 127 | public function valid() { 128 | return isset( $this->nodes[ $this->position ] ); 129 | } 130 | 131 | /** 132 | * @param $name 133 | * 134 | * @return Node|null 135 | */ 136 | public function getNodeByName( $name ): ?Node { 137 | foreach ( $this->getNodes() as $node ) { 138 | if ( $node->getName() == $name ) { 139 | return $node; 140 | } 141 | } 142 | 143 | return null; 144 | } 145 | 146 | /** 147 | * @param string $tableName 148 | * @param bool $useGeo 149 | * 150 | * @return Nodes 151 | */ 152 | public function getNodesWithTableName( string $tableName, bool $useGeo = true ): Nodes { 153 | $nodes = []; 154 | foreach ( $this->getNodes() as $node ) { 155 | if ( $node->containsTableName( $tableName ) ) { 156 | if ( ( $useGeo && ! is_null( $node->getGeo() ) ) && $node->getGeo() != ShardMatrix::getGeo() && ! is_null( ShardMatrix::getGeo() ) ) { 157 | continue; 158 | } 159 | $nodes[] = $node->setLastUsedTableName( $tableName ); 160 | } 161 | } 162 | return new Nodes( $nodes ); 163 | } 164 | 165 | /** 166 | * @param string $tableName 167 | * @param string $geo 168 | * 169 | * @return Nodes 170 | */ 171 | public function getNodesWithTableNameAndGeo( string $tableName, string $geo ): Nodes { 172 | $nodes = []; 173 | foreach ( $this->getNodes() as $node ) { 174 | if ( $node->containsTableName( $tableName ) && $node->getGeo() == $geo ) { 175 | 176 | $nodes[] = $node->setLastUsedTableName( $tableName ); 177 | } 178 | } 179 | 180 | return new Nodes( $nodes ); 181 | 182 | 183 | } 184 | 185 | 186 | } -------------------------------------------------------------------------------- /src/PdoCache.php: -------------------------------------------------------------------------------- 1 | setFromCache( true ); 31 | } 32 | 33 | return $data; 34 | } 35 | 36 | return null; 37 | } 38 | 39 | /** 40 | * @param string $key 41 | * @param string $data 42 | */ 43 | public function write( string $key, $data ): bool { 44 | $this->hasWritten = true; 45 | if ( $data instanceof PreSerialize ) { 46 | $data->__preSerialize(); 47 | } 48 | 49 | return (bool) file_put_contents( ShardMatrix::getPdoCachePath() . '/' . $key, gzdeflate( serialize( $data ) ) ); 50 | } 51 | 52 | /** 53 | * @param string $key 54 | * 55 | * @return bool 56 | */ 57 | public function clean( string $key ): bool { 58 | $filename = ShardMatrix::getPdoCachePath() . '/' . $key; 59 | if ( file_exists( $filename ) ) { 60 | return unlink( $filename ); 61 | } 62 | return false; 63 | } 64 | 65 | /** 66 | * @param string $key 67 | * 68 | * @return array 69 | */ 70 | public function scan( string $key ): array { 71 | $key = rtrim( $key, "*" ); 72 | $results = []; 73 | foreach ( glob( ShardMatrix::getPdoCachePath() . '/' . $key . '*' ) as $filename ) { 74 | $result = unserialize( gzinflate( file_get_contents( $filename ) ) ); 75 | if ( $result ) { 76 | if ( $result instanceof ResultsInterface ) { 77 | $result->setFromCache( true ); 78 | } 79 | $results[] = $result; 80 | 81 | } 82 | } 83 | 84 | return $results; 85 | } 86 | 87 | /** 88 | * @param string $key 89 | * 90 | * @return array 91 | */ 92 | public function scanAndClean( string $key ): array { 93 | $key = rtrim( $key, "*" ); 94 | $results = []; 95 | foreach ( glob( ShardMatrix::getPdoCachePath() . '/' . $key . '*' ) as $filename ) { 96 | $result = unserialize( gzinflate( file_get_contents( $filename ) ) ); 97 | if ( $result ) { 98 | $results[] = $result; 99 | } 100 | unlink( $filename ); 101 | } 102 | 103 | return $results; 104 | } 105 | 106 | /** 107 | * @param ShardDB $shardDb 108 | */ 109 | public function runCleanPolicy( ShardDB $shardDb ): void { 110 | if ( $this->hasWritten ) { 111 | $cutoff = new \DateTime( 'now - 10 minute' ); 112 | foreach ( glob( ShardMatrix::getPdoCachePath() . '/*' ) as $filename ) { 113 | if ( ( new \DateTime() )->setTimestamp( filemtime( $filename ) ) < $cutoff ) { 114 | @unlink( $filename ); 115 | } 116 | } 117 | } 118 | } 119 | 120 | /** 121 | * @param string $key 122 | * 123 | * @return bool 124 | */ 125 | public function cleanAllMatching( string $key ): bool { 126 | $key = rtrim( $key, "*" ); 127 | $cleaned = false; 128 | foreach ( glob( ShardMatrix::getPdoCachePath() . '/' . $key . '*' ) as $filename ) { 129 | $cleaned = true; 130 | unlink( $filename ); 131 | } 132 | 133 | return $cleaned; 134 | } 135 | } -------------------------------------------------------------------------------- /src/PdoCacheInterface.php: -------------------------------------------------------------------------------- 1 | cacheTime = $cacheTime; 24 | $this->memcached = $memcached; 25 | } 26 | 27 | protected function prefixKey( $key ): string { 28 | return static::PREFIX . ltrim( $key, static::PREFIX ); 29 | } 30 | 31 | public function read( string $key ) { 32 | $raw = $this->memcached->get( $this->prefixKey( $key ) ); 33 | if ( strlen( $raw ) ) { 34 | $data = unserialize( gzinflate( $raw ) ); 35 | if ( $data instanceof ResultsInterface ) { 36 | $data->setFromCache( true ); 37 | } 38 | 39 | return $data; 40 | } 41 | 42 | return null; 43 | } 44 | 45 | public function scan( string $key ): array { 46 | $results = []; 47 | $keys = $this->memcached->get( $this->prefixKey( $key ) ); 48 | if ( is_array( $keys ) ) { 49 | foreach ( $keys as $key ) { 50 | $results[] = $this->read( $key ); 51 | } 52 | } 53 | 54 | return $results; 55 | } 56 | 57 | public function scanAndClean( string $key ): array { 58 | $results = []; 59 | $keys = $this->memcached->get( $this->prefixKey( $key ) ); 60 | if ( is_array( $keys ) ) { 61 | foreach ( $keys as $key ) { 62 | $results[] = $this->read( $key ); 63 | } 64 | if ( $keys ) { 65 | $this->memcached->deleteMulti( $keys ); 66 | } 67 | } 68 | 69 | return $results; 70 | } 71 | 72 | public function write( string $key, $data ): bool { 73 | if ( $data instanceof PreSerialize ) { 74 | $data->__preSerialize(); 75 | } 76 | $this->setKeysArray( $key ); 77 | 78 | return (bool) $this->memcached->set( $this->prefixKey( $key ), gzdeflate( serialize( $data ) ), $this->cacheTime ); 79 | } 80 | 81 | /** 82 | * @param $key 83 | */ 84 | protected function setKeysArray( $key ) { 85 | $splitKey = function ( $keySplit, $delimiter, $key ) { 86 | $partKey = ''; 87 | $i = 0; 88 | foreach ( $keySplit as $part ) { 89 | $i ++; 90 | $partKey .= $part; 91 | if ( $i == count( $keySplit ) ) { 92 | $partKey .= $delimiter; 93 | } 94 | $existingKeys = $this->memcached->get( $this->prefixKey( $partKey ) ); 95 | if ( ! is_array( $existingKeys ) ) { 96 | $existingKeys = []; 97 | } 98 | 99 | $existingKeys[] = $key; 100 | 101 | $this->memcached->set( $this->prefixKey( $partKey ), array_unique( $existingKeys ), $this->cacheTime ); 102 | } 103 | }; 104 | $splitKey( explode( '-', $key ), '-', $key ); 105 | $splitKey( explode( ':', $key ), ':', $key ); 106 | 107 | } 108 | 109 | public function clean( string $key ): bool { 110 | return (bool) $this->memcached->delete( $this->prefixKey( $key ) ); 111 | } 112 | 113 | public function runCleanPolicy( ShardDB $shardDb ): void { 114 | //do nothing 115 | } 116 | 117 | /** 118 | * @param string $key 119 | * 120 | * @return bool 121 | */ 122 | public function cleanAllMatching( string $key ): bool { 123 | $keys = $this->memcached->get( $this->prefixKey( $key ) ); 124 | if ( is_array( $keys ) ) { 125 | if ( $keys ) { 126 | $this->memcached->deleteMulti( $keys ); 127 | return true; 128 | } 129 | } 130 | 131 | return false; 132 | } 133 | } -------------------------------------------------------------------------------- /src/PdoCacheRedis.php: -------------------------------------------------------------------------------- 1 | cacheTime = $cacheTime; 38 | $this->redis = $redis; 39 | $this->errorHandler = $errorHandler; 40 | } 41 | 42 | /** 43 | * @param \Error $error 44 | */ 45 | private function __handleError( \Error $error ) { 46 | if ( $this->errorHandler ) { 47 | call_user_func_array( $this->errorHandler, [ $error ] ); 48 | } else { 49 | throw $error; 50 | } 51 | } 52 | 53 | /** 54 | * @param string $key 55 | * 56 | * @return mixed|ResultsInterface|null 57 | */ 58 | public function read( string $key ) { 59 | $raw = $this->redis->get( $this->prefixKey( $key ) ); 60 | if ( strlen( $raw ) ) { 61 | try { 62 | $raw = gzinflate( $raw ); 63 | $data = unserialize( $raw ); 64 | unset( $raw ); 65 | if ( $data instanceof ResultsInterface ) { 66 | $data->setFromCache( true ); 67 | } 68 | } catch ( \Error $error ) { 69 | $this->__handleError( $error ); 70 | } 71 | 72 | return $data; 73 | } 74 | 75 | return null; 76 | } 77 | 78 | public function scan( string $key ): array { 79 | $key = rtrim( $key, "*" ); 80 | $results = []; 81 | foreach ( new Keyspace( $this->redis, $this->prefixKey( $key ) . '*', 1000 ) as $matchKey ) { 82 | $results[] = $this->read( $matchKey ); 83 | } 84 | 85 | return $results; 86 | } 87 | 88 | protected function prefixKey( $key ): string { 89 | return static::PREFIX . ltrim( $key, static::PREFIX ); 90 | } 91 | 92 | public function scanAndClean( string $key ): array { 93 | $key = rtrim( $key, "*" ); 94 | $matches = []; 95 | $results = []; 96 | foreach ( new Keyspace( $this->redis, $this->prefixKey( $key ) . '*', 10000 ) as $matchKey ) { 97 | $matches[] = $this->prefixKey( $matchKey ); 98 | $results[] = $this->read( $matchKey ); 99 | } 100 | if ( $matches ) { 101 | $this->redis->del( $matches ); 102 | } 103 | 104 | return $results; 105 | } 106 | 107 | public function write( string $key, $data ): bool { 108 | if ( $data instanceof PreSerialize ) { 109 | $data->__preSerialize(); 110 | } 111 | 112 | return (bool) $this->redis->setex( $this->prefixKey( $key ), $this->cacheTime, gzdeflate( serialize( $data ) ) ); 113 | } 114 | 115 | public function clean( string $key ): bool { 116 | return (bool) $this->redis->del( [ $this->prefixKey( $key ) ] ); 117 | } 118 | 119 | public function runCleanPolicy( ShardDB $shardDb ): void { 120 | //do nothing 121 | } 122 | 123 | /** 124 | * @param string $key 125 | * 126 | * @return bool 127 | */ 128 | public function cleanAllMatching( string $key ): bool { 129 | $key = rtrim( $key, "*" ); 130 | $matches = []; 131 | foreach ( new Keyspace( $this->redis, $this->prefixKey( $key ) . '*', null ) as $matchKey ) { 132 | $matches[] = $this->prefixKey( $matchKey ); 133 | } 134 | if ( $matches ) { 135 | $this->redis->del( $matches ); 136 | 137 | return true; 138 | } 139 | 140 | return false; 141 | } 142 | } -------------------------------------------------------------------------------- /src/ShardMatrix.php: -------------------------------------------------------------------------------- 1 | setDataRowClasses( static::$tableToDataRowMap ); 237 | } 238 | 239 | 240 | } -------------------------------------------------------------------------------- /src/Table.php: -------------------------------------------------------------------------------- 1 | table = $table; 27 | } 28 | 29 | /** 30 | * @return string 31 | */ 32 | public function getName(): string { 33 | return $this->table; 34 | } 35 | 36 | /** 37 | * @return string 38 | */ 39 | public function getHash(): string { 40 | return $this->hash ?? ( $this->hash = hash( ShardMatrix::TABLE_ALGO, $this->table ) ); 41 | } 42 | 43 | /** 44 | * @return string[] 45 | */ 46 | public function getUniqueColumns(): array { 47 | return ShardMatrix::getConfig()->getUniqueColumns()->getUniqueColumnByTableName( $this->getName() ); 48 | } 49 | 50 | 51 | } -------------------------------------------------------------------------------- /src/TableGroup.php: -------------------------------------------------------------------------------- 1 | name = $name; 25 | $this->setTables( $tables ); 26 | } 27 | 28 | /** 29 | * @param array $tables 30 | * 31 | * @return $this 32 | */ 33 | public function setTables( array $tables ): TableGroup { 34 | $this->tables = new Tables( $tables ); 35 | return $this; 36 | } 37 | 38 | /** 39 | * @param $tableName 40 | * 41 | * @return bool 42 | */ 43 | public function containsTableName( $tableName ): bool { 44 | foreach ( $this->getTables() as $table ) { 45 | if ( $table->getName() == $tableName ) { 46 | return true; 47 | } 48 | } 49 | 50 | return false; 51 | } 52 | 53 | /** 54 | * @param $hash 55 | * 56 | * @return Table|null 57 | */ 58 | public function getTableByTableHash( $hash ): ?Table { 59 | foreach ( $this->getTables() as $table ) { 60 | if ( $table->getHash() == $hash ) { 61 | return $table; 62 | } 63 | } 64 | 65 | return null; 66 | } 67 | 68 | /** 69 | * @return string 70 | */ 71 | public function getName() { 72 | return $this->name; 73 | } 74 | 75 | /** 76 | * @return Tables 77 | */ 78 | public function getTables(): Tables { 79 | return $this->tables; 80 | } 81 | } -------------------------------------------------------------------------------- /src/TableGroups.php: -------------------------------------------------------------------------------- 1 | setTableGroups( $TableGroups ); 28 | } 29 | 30 | /** 31 | * @return TableGroup[] 32 | */ 33 | public function getTableGroups(): array { 34 | 35 | return $this->tableGroups; 36 | } 37 | 38 | /** 39 | * @param string $tableName 40 | * 41 | * @return TableGroup|null 42 | */ 43 | public function getTableGroupByTableName( string $tableName ): ?TableGroup { 44 | 45 | foreach ( $this->getTableGroups() as $group ) { 46 | if ( $group->containsTableName( $tableName ) ) { 47 | return $group; 48 | } 49 | } 50 | 51 | return null; 52 | } 53 | 54 | /** 55 | * @param string $name 56 | * 57 | * @return TableGroup|null 58 | */ 59 | public function getTableGroupByName( string $name ): ?TableGroup { 60 | 61 | foreach ( $this->getTableGroups() as $group ) { 62 | if ( $group->getName() == $name ) { 63 | return $group; 64 | } 65 | } 66 | 67 | return null; 68 | } 69 | 70 | 71 | /** 72 | * @param array $tableGroups 73 | * 74 | * @return TableGroups 75 | */ 76 | public function setTableGroups( array $tableGroups ): TableGroups { 77 | foreach ( $tableGroups as $name => $tableGroup ) { 78 | if ( $tableGroup instanceof TableGroup ) { 79 | $this->tableGroups[] = $tableGroup; 80 | } else { 81 | $this->tableGroups[] = new TableGroup( $name, $tableGroup ); 82 | } 83 | } 84 | 85 | return $this; 86 | } 87 | 88 | /** 89 | * @return int 90 | */ 91 | public function countTableGroups(): int { 92 | return count( $this->getTableGroups() ); 93 | } 94 | 95 | 96 | /** 97 | * Return the current element 98 | * @link https://php.net/manual/en/iterator.current.php 99 | * @return TableGroup Can return any type. 100 | * @since 5.0.0 101 | */ 102 | public function current() { 103 | return $this->getTableGroups()[ $this->position ]; 104 | } 105 | 106 | /** 107 | * Move forward to next element 108 | * @link https://php.net/manual/en/iterator.next.php 109 | * @return void Any returned value is ignored. 110 | * @since 5.0.0 111 | */ 112 | public function next() { 113 | $this->position ++; 114 | } 115 | 116 | /** 117 | * Return the key of the current element 118 | * @link https://php.net/manual/en/iterator.key.php 119 | * @return mixed scalar on success, or null on failure. 120 | * @since 5.0.0 121 | */ 122 | public function key() { 123 | return $this->position; 124 | } 125 | 126 | /** 127 | * Rewind the Iterator to the first element 128 | * @link https://php.net/manual/en/iterator.rewind.php 129 | * @return void Any returned value is ignored. 130 | * @since 5.0.0 131 | */ 132 | public function rewind() { 133 | $this->position = 0; 134 | } 135 | 136 | /** 137 | * @return bool 138 | */ 139 | public function valid() { 140 | return isset( $this->tableGroups[ $this->position ] ); 141 | } 142 | 143 | /** 144 | * @param string $hash 145 | * 146 | * @return Table|null 147 | */ 148 | public function getTableByHash( string $hash ): ?Table { 149 | foreach ( $this->getTableGroups() as $group ) { 150 | foreach ( $group->getTables() as $table ) { 151 | if ( $table->getHash() == $hash ) { 152 | return $table; 153 | } 154 | } 155 | } 156 | 157 | return null; 158 | } 159 | } -------------------------------------------------------------------------------- /src/Tables.php: -------------------------------------------------------------------------------- 1 | setTables( $tables ); 27 | } 28 | 29 | /** 30 | * @return Table[] 31 | */ 32 | public function getTables(): array { 33 | 34 | return $this->tables; 35 | } 36 | 37 | /** 38 | * @param array $tables 39 | * 40 | * @return Tables 41 | */ 42 | public function setTables( array $tables ): Tables { 43 | foreach ( $tables as $name ) { 44 | if ( $name instanceof Table ) { 45 | $this->tables[] = $name; 46 | } else { 47 | $this->tables[] = new Table( $name ); 48 | } 49 | } 50 | 51 | return $this; 52 | } 53 | 54 | /** 55 | * @return int 56 | */ 57 | public function countTables(): int { 58 | return count( $this->getTables() ); 59 | } 60 | 61 | 62 | /** 63 | * Return the current element 64 | * @link https://php.net/manual/en/iterator.current.php 65 | * @return Table Can return any type. 66 | * @since 5.0.0 67 | */ 68 | public function current() { 69 | return $this->getTables()[ $this->position ]; 70 | } 71 | 72 | /** 73 | * Move forward to next element 74 | * @link https://php.net/manual/en/iterator.next.php 75 | * @return void Any returned value is ignored. 76 | * @since 5.0.0 77 | */ 78 | public function next() { 79 | $this->position ++; 80 | } 81 | 82 | /** 83 | * Return the key of the current element 84 | * @link https://php.net/manual/en/iterator.key.php 85 | * @return mixed scalar on success, or null on failure. 86 | * @since 5.0.0 87 | */ 88 | public function key() { 89 | return $this->position; 90 | } 91 | 92 | /** 93 | * Rewind the Iterator to the first element 94 | * @link https://php.net/manual/en/iterator.rewind.php 95 | * @return void Any returned value is ignored. 96 | * @since 5.0.0 97 | */ 98 | public function rewind() { 99 | $this->position = 0; 100 | } 101 | 102 | /** 103 | * @return bool 104 | */ 105 | public function valid() { 106 | return isset( $this->tables[ $this->position ] ); 107 | } 108 | } -------------------------------------------------------------------------------- /src/UniqueColumns.php: -------------------------------------------------------------------------------- 1 | uniqueColumns = $uniqueColumns; 27 | } 28 | 29 | /** 30 | * @return UniqueColumn[] 31 | */ 32 | public function getUniqueColumns(): array { 33 | 34 | return $this->uniqueColumns; 35 | } 36 | 37 | /** 38 | * @param string $tableName 39 | * 40 | * @return array 41 | */ 42 | public function getUniqueColumnByTableName( string $tableName ): array { 43 | 44 | foreach ( $this->getUniqueColumns() as $tableNameKey => $uniqueColumns ) { 45 | if ( $tableName == $tableNameKey ) { 46 | return $uniqueColumns; 47 | } 48 | } 49 | 50 | return []; 51 | } 52 | 53 | /** 54 | * @return int 55 | */ 56 | public function countUniqueColumns(): int { 57 | return count( $this->getUniqueColumns() ); 58 | } 59 | 60 | 61 | /** 62 | * Return the current element 63 | * @link https://php.net/manual/en/iterator.current.php 64 | * @return UniqueColumn Can return any type. 65 | * @since 5.0.0 66 | */ 67 | public function current() { 68 | return $this->getUniqueColumns()[ $this->position ]; 69 | } 70 | 71 | /** 72 | * Move forward to next element 73 | * @link https://php.net/manual/en/iterator.next.php 74 | * @return void Any returned value is ignored. 75 | * @since 5.0.0 76 | */ 77 | public function next() { 78 | $this->position ++; 79 | } 80 | 81 | /** 82 | * Return the key of the current element 83 | * @link https://php.net/manual/en/iterator.key.php 84 | * @return mixed scalar on success, or null on failure. 85 | * @since 5.0.0 86 | */ 87 | public function key() { 88 | return $this->position; 89 | } 90 | 91 | /** 92 | * Rewind the Iterator to the first element 93 | * @link https://php.net/manual/en/iterator.rewind.php 94 | * @return void Any returned value is ignored. 95 | * @since 5.0.0 96 | */ 97 | public function rewind() { 98 | $this->position = 0; 99 | } 100 | 101 | /** 102 | * @return bool 103 | */ 104 | public function valid() { 105 | return isset( $this->uniqueColumns[ $this->position ] ); 106 | } 107 | 108 | } -------------------------------------------------------------------------------- /src/Uuid.php: -------------------------------------------------------------------------------- 1 | uuid = $uuid; 26 | } 27 | 28 | /** 29 | * @param Node $node 30 | * @param Table $table 31 | * 32 | * @return Uuid 33 | * @throws Exception 34 | */ 35 | public static function make( Node $node, Table $table ): Uuid { 36 | 37 | if ( ! $node->containsTableName( $table->getName() ) ) { 38 | throw new Exception( 'Table is not in the table groups for this Node!' ); 39 | } 40 | 41 | $ramseyUuid = Ruuid::uuid6( new Hexadecimal( bin2hex( $node->getName() ) ) ); 42 | 43 | return new static( $table->getHash() . '-' . $ramseyUuid->toString() ); 44 | } 45 | 46 | /** 47 | * @return string 48 | */ 49 | protected function stripToRamseyUuidString(): string { 50 | $parts = $this->getParts(); 51 | array_shift( $parts ); 52 | 53 | return join( '-', $parts ); 54 | } 55 | 56 | /** 57 | * @return array 58 | */ 59 | protected function getParts(): array { 60 | return explode( '-', $this->uuid ); 61 | } 62 | 63 | /** 64 | * @return string 65 | */ 66 | protected function getTableHash(): string { 67 | return $this->getParts()[0]; 68 | } 69 | 70 | /** 71 | * @return UuidInterface|null 72 | */ 73 | protected function getRamseyUuid(): ?UuidInterface { 74 | if ( is_string( $this->uuid ) ) { 75 | return $this->ramseyUuid ?? ( $this->ramseyUuid = Ruuid::fromString( $this->stripToRamseyUuidString() ) ); 76 | } 77 | 78 | return null; 79 | } 80 | 81 | /** 82 | * @return Node|null 83 | */ 84 | public function getNode(): ?Node { 85 | 86 | $node = $this->node ?? 87 | ( 88 | $this->node = ShardMatrix::getConfig()->getNodes()->getNodeByName( 89 | hex2bin( $this->getRamseyUuid()->getFields()->getNode() ) 90 | ) 91 | ); 92 | 93 | if ( $node ) { 94 | $node->setLastUsedTableName( $this->getTable()->getName() ); 95 | } 96 | 97 | return $node; 98 | } 99 | 100 | /** 101 | * @return Table|null 102 | */ 103 | public function getTable(): ?Table { 104 | return $this->table ?? 105 | ( 106 | $this->table = ShardMatrix::getConfig()->getTableGroups()->getTableByHash( 107 | $this->getTableHash() 108 | ) 109 | ); 110 | } 111 | 112 | /** 113 | * @return string 114 | */ 115 | public function __toString() { 116 | return $this->toString(); 117 | } 118 | 119 | /** 120 | * @return string 121 | */ 122 | public function toString(): string { 123 | return $this->uuid ?? ''; 124 | } 125 | 126 | /** 127 | * @return bool 128 | */ 129 | public function isValid(): bool { 130 | 131 | if ( $this->getTable() && $this->getNode() && count( $this->getParts() ) == 6 ) { 132 | return true; 133 | } 134 | 135 | return false; 136 | } 137 | 138 | /** 139 | * @return mixed|string|null 140 | */ 141 | public function jsonSerialize() { 142 | return $this->uuid; 143 | } 144 | } -------------------------------------------------------------------------------- /tests/.mysql-dev-env: -------------------------------------------------------------------------------- 1 | MYSQL_ROOT_PASSWORD=password 2 | MYSQL_DATABASE=shard -------------------------------------------------------------------------------- /tests/.postgres-dev-env: -------------------------------------------------------------------------------- 1 | POSTGRES_PASSWORD=password 2 | POSTGRES_DB=shard 3 | POSTGRES_USER=postgres 4 | PGDATA=/data/postgres -------------------------------------------------------------------------------- /tests/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.5" 2 | 3 | services: 4 | 5 | DB0001: 6 | restart: always 7 | image: percona:5.7 8 | env_file: 9 | - .mysql-dev-env 10 | ports: 11 | - 3308:3306 12 | 13 | DB0002: 14 | restart: always 15 | image: percona:5.7 16 | env_file: 17 | - .mysql-dev-env 18 | ports: 19 | - 3309:3306 20 | 21 | DB0003: 22 | restart: always 23 | image: percona:5.7 24 | env_file: 25 | - .mysql-dev-env 26 | ports: 27 | - 3310:3306 28 | 29 | DB0004: 30 | restart: always 31 | image: percona:5.7 32 | env_file: 33 | - .mysql-dev-env 34 | ports: 35 | - 3311:3306 36 | 37 | DB0005: 38 | restart: always 39 | image: percona:5.7 40 | env_file: 41 | - .mysql-dev-env 42 | ports: 43 | - 3012:3306 44 | 45 | DB0006: 46 | restart: always 47 | image: percona:5.7 48 | env_file: 49 | - .mysql-dev-env 50 | ports: 51 | - 3313:3306 52 | 53 | DB0007: 54 | restart: always 55 | image: postgres 56 | env_file: 57 | - .postgres-dev-env 58 | ports: 59 | - 5414:5432 60 | 61 | gothreaded: 62 | image: jrsaunders/gothreaded 63 | restart: always 64 | environment: 65 | GOTHREADED_DEBUG: "true" 66 | ports: 67 | - 1541:1534 68 | 69 | redis: 70 | image: redis 71 | restart: always 72 | entrypoint: redis-server --appendonly yes 73 | ports: 74 | - 6386:6379 75 | 76 | memcached: 77 | image: memcached 78 | restart: always 79 | ports: 80 | - 11218:11211 81 | -------------------------------------------------------------------------------- /tests/shard_matrix.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | 3 | table_groups: 4 | user: 5 | - users 6 | - payments 7 | - offers 8 | tracking: 9 | - visitors 10 | - sign_ups 11 | published: 12 | - published_offers 13 | 14 | unique_columns: 15 | users: 16 | - email 17 | - username 18 | 19 | nodes: 20 | DB0001: 21 | dsn: mysql:dbname=shard;host=localhost:3308;user=root;password=password 22 | docker_network: DB0001:3306 23 | geo: UK 24 | table_groups: 25 | - user 26 | - published 27 | DB0002: 28 | dsn: mysql:dbname=shard;host=localhost:3309;user=root;password=password 29 | docker_network: DB0002:3306 30 | geo: UK 31 | table_groups: 32 | - user 33 | - published 34 | DB0003: 35 | dsn: mysql:dbname=shard;host=localhost:3310;user=root;password=password 36 | docker_network: DB0003:3306 37 | geo: UK 38 | table_groups: 39 | - user 40 | - published 41 | DB0004: 42 | dsn: mysql:dbname=shard;host=localhost:3311;user=root;password=password 43 | docker_network: DB0004:3306 44 | geo: UK 45 | table_groups: 46 | - published 47 | DB0005: 48 | dsn: mysql:dbname=shard;host=localhost:3312;user=root;password=password 49 | docker_network: DB0005:3306 50 | table_groups: 51 | - tracking 52 | DB0006: 53 | dsn: mysql:dbname=shard;host=localhost:3313;user=root;password=password 54 | docker_network: DB0006:3306 55 | geo: UK 56 | insert_data: false 57 | table_groups: 58 | - tracking 59 | DB0007: 60 | dsn: pgsql:dbname=shard;host=localhost:5414;user=postgres;password=password 61 | docker_network: DB0007:5432 62 | geo: UK 63 | table_groups: 64 | - user 65 | - tracking 66 | 67 | 68 | -------------------------------------------------------------------------------- /tests/src/TestSchema.php: -------------------------------------------------------------------------------- 1 | addServer( 'localhost', 11218 ); 45 | 46 | return new \ShardMatrix\PdoCacheMemcached( $memcached ); 47 | } ); 48 | ShardMatrix::setGoThreadedService( function () { 49 | return new \ShardMatrix\GoThreaded\Client( '127.0.0.1', 1541, 'gothreaded', 'password', 20 ); 50 | } ); 51 | } 52 | 53 | public function testAll() { 54 | $this->initMemcached(); 55 | $this->schemas( 'memcached' ); 56 | $this->initGoThreaded(); 57 | $this->schemas( 'gothread' ); 58 | $this->initFork(); 59 | $this->schemas( 'fork' ); 60 | 61 | } 62 | 63 | private function schemas( $name ) { 64 | try { 65 | Schema::create( 'users', 66 | function ( \Illuminate\Database\Schema\Blueprint $table ) { 67 | 68 | $table->string( 'uuid', 50 )->primary(); 69 | $table->string( 'username', 255 )->unique(); 70 | $table->string( 'email', 255 )->unique(); 71 | $table->json( 'json_data' )->nullable(); 72 | $table->string( 'password', 255 ); 73 | $table->integer( 'something' ); 74 | $table->dateTime( 'created' ); 75 | 76 | } 77 | ); 78 | } catch ( \ShardMatrix\DB\Builder\BuilderException $exception ) { 79 | echo $exception->getMessage() . PHP_EOL; 80 | } 81 | $uuid = DB::table( 'users' )->insert( 82 | [ 83 | 'username' => 'jack-malone', 84 | 'password' => 'poootpooty', 85 | 'json_data' => '{}', 86 | 'created' => ( new \DateTime() )->format( 'Y-m-d H:i:s' ), 87 | 'something' => 4, 88 | 'email' => 'jack.malone@yatti.com', 89 | ] 90 | ); 91 | 92 | $this->assertTrue( $uuid instanceof Uuid, 'UUID Created' ); 93 | 94 | $data = DB::getByUuid( $uuid ); 95 | 96 | $this->assertTrue( ( $data->username == 'jack-malone' ), "Username Correct in inserted data" ); 97 | 98 | $statement = DB::table( 'users' )->whereUuid( $uuid )->getStatement(); 99 | 100 | $this->assertTrue( $statement->isFromCache(), $statement->fetchDataRow()->username . ' user data from cache' ); 101 | 102 | $inserts = 300; 103 | if ( $name == 'fork' ) { 104 | $inserts = 200; 105 | } 106 | $i = 0; 107 | while ( $i < $inserts ) { 108 | $username = 'randy' . rand( 5000, 10000000 ) . uniqid(); 109 | $password = 'cool!!' . rand( 5000, 100000 ); 110 | $email = 'timmy' . rand( 1, 10000000 ) . uniqid() . '@google.com'; 111 | $created = ( new DateTime() )->format( 'Y-m-d H:i:s' ); 112 | $i ++; 113 | try { 114 | DB::shardTable( 'users' )->insert( [ 115 | 'username' => $username, 116 | 'password' => $password, 117 | 'json_data' => '{}', 118 | 'email' => $email, 119 | 'created' => $created, 120 | 'something' => 4 121 | ] ); 122 | } catch ( \ShardMatrix\DB\Builder\BuilderException $exception ) { 123 | echo $exception->getNode()->getName() . ' ' . $exception->getMessage() . PHP_EOL; 124 | } 125 | } 126 | $count = DB::allNodesTable( 'users' )->count(); 127 | $this->assertTrue( $count == ( $inserts + 1 ), $count . ' inserted records' ); 128 | $avg = DB::allNodesTable( 'users' )->avg( 'something' ); 129 | $this->assertTrue( $avg == 4, $avg . ' average of something column' ); 130 | $sum = DB::allNodesTable( 'users' )->sum( 'something' ); 131 | $this->assertTrue( $sum == ( $inserts * 4 ) + 4, $sum . ' sum of something column' ); 132 | 133 | $pagination = DB::allNodesTable( 'users' )->getPagination( [ "*" ], 3, 15, 10 ); 134 | $results = $pagination->countResults(); 135 | 136 | $this->assertTrue( $results == 152, $results . ' pagination results count' ); 137 | $pages = $pagination->countPages(); 138 | $this->assertTrue( $pages == 11, $pages . ' pagination pages count' ); 139 | $nodes = []; 140 | foreach ( $pagination->getResults()->fetchDataRows() as $result ) { 141 | $uuid = $result->getUuid(); 142 | $this->assertTrue( $uuid instanceof Uuid, 'Uuids are correct' ); 143 | $nodes[ $uuid->getNode()->getName() ] = $uuid->getNode()->getName(); 144 | $result->password = 'sillybilly69'; 145 | $result->save(); 146 | $this->uuid = $result->getUuid(); 147 | } 148 | $count = count( $nodes ); 149 | $this->assertTrue( $count == 4, $count . ' Has Written to different Nodes' ); 150 | $changeCount = DB::allNodesTable( 'users' )->where( 'password', '=', 'sillybilly69' )->count(); 151 | $this->assertTrue( $changeCount == 15, $changeCount . ' update via transaction datarow' ); 152 | 153 | $collection = DB::allNodesTable( 'users' )->where( 'username', 'like', 'randy%' )->setUseCache( false )->get(); 154 | $count = $collection->count(); 155 | $this->assertTrue( $count == $inserts, $count . ' collection of randy%' ); 156 | $i = 0; 157 | $collection->each( function ( DBDataRowTransactionsInterface $record ) use ( &$i ) { 158 | $i ++; 159 | if ( $i % 2 == 0 ) { 160 | $record->delete(); 161 | } 162 | } ); 163 | 164 | $collection2 = DB::allNodesTable( 'users' )->where( 'username', 'like', 'randy%' )->setUseCache( false )->get(); 165 | $count = $collection2->count(); 166 | $this->assertTrue( $count == ( $inserts / 2 ), $count . ' collection of randy% half count' ); 167 | 168 | $pagination = DB::allNodesTable( 'users' ) 169 | ->orderBy( 'created', 'desc' ) 170 | ->paginate( $perPage = 15, $columns = [ '*' ], $pageName = 'page', $page = null ); 171 | $uuid = null; 172 | $pagination->each( function ( \ShardMatrix\DB\Interfaces\DBDataRowTransactionsInterface $record ) use ( &$uuid ) { 173 | 174 | $this->assertTrue( ( $record->getUuid() instanceof Uuid ), 'Pagination Uuid instances' ); 175 | $uuid = $record->getUuid(); 176 | } ); 177 | 178 | $this->assertTrue( $pagination->total() == ( ( $inserts / 2 ) + 1 ), $pagination->total() . ' Pagination Total' ); 179 | $this->assertTrue( $pagination->perPage() == 15, $pagination->perPage() . ' Pagination Per Page' ); 180 | 181 | $pagination = DB::table( 'users' )->uuidAsNodeReference( $uuid ) 182 | ->orderBy( 'created', 'desc' ) 183 | ->paginate( $perPage = 15, $columns = [ '*' ], $pageName = 'page', $page = null ); 184 | 185 | $pagination->each( function ( \ShardMatrix\DB\Interfaces\DBDataRowTransactionsInterface $record ) { 186 | 187 | $this->assertTrue( ( $record->getUuid() instanceof Uuid ), 'Pagination Uuid instances on one node' ); 188 | } ); 189 | 190 | $this->assertTrue( $pagination->total() > ( $inserts / 10 ), $pagination->total() . ' Pagination Total' ); 191 | $this->assertTrue( $pagination->perPage() == 15, $pagination->perPage() . ' Pagination Per Page' ); 192 | 193 | Schema::drop( 'users' ); 194 | if ( $name == 'fork' ) { 195 | 196 | array_map( 'unlink', glob( __DIR__ . '/shard_matrix_cache/*' ) ); 197 | rmdir( __DIR__ . '/shard_matrix_cache' ); 198 | } 199 | 200 | } 201 | } --------------------------------------------------------------------------------