├── .gitignore ├── .phpunit.result.cache ├── LICENSE ├── Makefile ├── README.md ├── composer.json ├── demo.yml ├── docker-compose.yml ├── logo.png ├── phpunit.xml.dist ├── shell.gif ├── src ├── Connectors │ └── ConnectionFactory.php ├── Console │ └── PartitionsCommand.php ├── Exceptions │ ├── UnexpectedValueException.php │ └── UnsupportedPartitionException.php ├── Models │ ├── MultipleSchemaModel.php │ └── Partition.php ├── MysqlConnection.php ├── PartitionServiceProvider.php └── Schema │ ├── MySqlGrammar.php │ ├── QueryBuilder.php │ └── Schema.php └── tests └── Unit ├── BaseTestCase.php ├── MysqlConnectionTest.php └── Schema └── SchemaTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | vendor/ 3 | composer.lock 4 | _db/ 5 | build/ -------------------------------------------------------------------------------- /.phpunit.result.cache: -------------------------------------------------------------------------------- 1 | {"version":1,"defects":{"SchemaTest::testDropPartition":3,"SchemaTest::testDropPartitions":5,"SchemaTest::testAnalyzePartition":5,"SchemaTest::testAnalyzePartitions":5,"SchemaTest::testRepairPartition":5,"SchemaTest::testRepairPartitions":5,"SchemaTest::testRebuildPartition":5,"SchemaTest::testRebuildPartitions":5,"SchemaTest::testCheckPartition":5,"SchemaTest::testCheckPartitions":5,"SchemaTest::testOptimizePartition":5,"SchemaTest::testOptimizePartitions":5,"SchemaTest::testTruncatePartition":5,"SchemaTest::testTruncatePartitions":5,"SchemaTest::testPartitionByKey":5,"SchemaTest::testPartitionByHash":5,"SchemaTest::testPartitionByRange":5,"SchemaTest::testPartitionByRangeExcludeFuture":5,"SchemaTest::testPartitionByList":5,"SchemaTest::testPartitionByOneYears":5,"SchemaTest::testPartitionByYears":5,"SchemaTest::testPartitionByYearsAndMonths":5,"SchemaTest::testPartitionByYearsAndMonthsExcludeFuture":5,"MysqlConnectionTest::testGetQueryBuilder":4},"times":{"MysqlConnectionTest::testGetQueryBuilder":0.005,"SchemaTest::testDropPartition":0.004,"SchemaTest::testDropPartitions":0,"SchemaTest::testAnalyzePartition":0.001,"SchemaTest::testAnalyzePartitions":0,"SchemaTest::testRepairPartition":0,"SchemaTest::testRepairPartitions":0,"SchemaTest::testRebuildPartition":0,"SchemaTest::testRebuildPartitions":0,"SchemaTest::testCheckPartition":0,"SchemaTest::testCheckPartitions":0,"SchemaTest::testOptimizePartition":0,"SchemaTest::testOptimizePartitions":0,"SchemaTest::testTruncatePartition":0,"SchemaTest::testTruncatePartitions":0,"SchemaTest::testPartitionByKey":0,"SchemaTest::testPartitionByHash":0,"SchemaTest::testPartitionByRange":0.002,"SchemaTest::testPartitionByRangeExcludeFuture":0,"SchemaTest::testPartitionByList":0,"SchemaTest::testPartitionByOneYears":0,"SchemaTest::testPartitionByYears":0,"SchemaTest::testPartitionByYearsAndMonths":0,"SchemaTest::testPartitionByYearsAndMonthsExcludeFuture":0}} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Original work Copyright (c) 2020 Becchetti Luca 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | V=8.0 2 | DB_DIR=$(shell pwd)/_db-$(V) 3 | mV=10.3 4 | mDB_DIR=$(shell pwd)/_db-$(mV) 5 | 6 | start_db: 7 | @echo Starting MySQL $(V) 8 | docker run --rm -d --name partition-mysql \ 9 | -p 3306:3306 \ 10 | -v $(DB_DIR):/var/lib/mysql \ 11 | -e MYSQL_DATABASE=partition_test \ 12 | -e MYSQL_ALLOW_EMPTY_PASSWORD=yes \ 13 | mysql:$(V) --character-set-server=utf8 --collation-server=utf8_general_ci --default-authentication-plugin=mysql_native_password 14 | 15 | start_db_maria: 16 | @echo Starting MariaDB $(mV) 17 | docker run --rm -d --name partition-mysql \ 18 | -p 3306:3306 \ 19 | -v $(DB_DIR):/var/lib/mysql \ 20 | -e MYSQL_DATABASE=partition_test \ 21 | -e MYSQL_ALLOW_EMPTY_PASSWORD=yes \ 22 | mariadb:$(mV) --character-set-server=utf8 --collation-server=utf8_general_ci --default-authentication-plugin=mysql_native_password 23 | 24 | 25 | rm_db: 26 | docker stop partition-mysql || true 27 | rm -Rf $(DB_DIR) 28 | 29 | refresh_db: rm_db start_db 30 | 31 | get_ip: 32 | @docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' partition-mysql 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel MySQL Partition 2 | 3 | 4 | **Laravel-mysql-partition** is a useful Laravel package to easily work with [MySQL Partition](https://dev.mysql.com/doc/refman/5.7/en/partitioning.html). please check the documentation for your MySQL version. Partitioning require a MySQL's version >= 5.1.0 5 | 6 |

7 | InAppNotify 8 |

9 | 10 |

★★ Star our github repository to help us!, or ☕ pay me a coffee ★★

11 |

Created by Luca Becchetti

12 | 13 | 14 | ## Installation 15 |

16 | InAppNotify 17 |

18 | 19 | 20 | Add the package using composer: 21 | 22 | ```sh 23 | $ composer require brokenice/laravel-mysql-partition 24 | ``` 25 | 26 | For Laravel versions before 5.5 or if not using auto-discovery, register the service provider in `config/app.php`: 27 | 28 | ```php 29 | 'providers' => [ 30 | /* 31 | * Package Service Providers... 32 | */ 33 | Brokenice\LaravelMysqlPartition\PartitionServiceProvider::class, 34 | ], 35 | ``` 36 | 37 | ## Quickstart 38 | 39 | ### Create a migration 40 | 41 | From the command line: 42 | 43 | ```shell 44 | php artisan make:migration create_partitioned_table 45 | ``` 46 | 47 | Then edit the migration you just created by adding one of the partition schema provided by this package; 48 | 49 | ```php 50 | use Illuminate\Database\Migrations\Migration; 51 | use Illuminate\Database\Schema\Blueprint; 52 | use Brokenice\LaravelMysqlPartition\Models\Partition; 53 | use Brokenice\LaravelMysqlPartition\Schema\Schema; 54 | 55 | class CreatePartitionedTable extends Migration { 56 | 57 | /** 58 | * Run the migrations. 59 | * 60 | * @return void 61 | */ 62 | public function up() 63 | { 64 | Schema::create('partitioned', static function (Blueprint $table) { 65 | $table->bigInteger('id'); 66 | $table->string('name'); 67 | $table->date('date'); 68 | $table->timestamps(); 69 | $table->primary(['id','date']); 70 | }); 71 | 72 | // Force autoincrement of one field in composite primary key 73 | Schema::forceAutoIncrement('partitioned', 'id'); 74 | 75 | // Make partition by LIST 76 | Schema::partitionByList('partitioned', 'id', 77 | [ 78 | new Partition('server_east', Partition::LIST_TYPE, [1,43,65,12,56,73]), 79 | new Partition('server_west', Partition::LIST_TYPE, [534,6422,196,956,22]) 80 | ] 81 | ); 82 | } 83 | 84 | /** 85 | * Reverse the migrations. 86 | * 87 | * @return void 88 | */ 89 | public function down() 90 | { 91 | Schema::drop('partitioned'); 92 | } 93 | } 94 | ``` 95 | 96 | Run the migration: 97 | 98 | ```shell 99 | php artisan migrate 100 | ``` 101 | 102 | ## Partitions support 103 | 104 | Package supports these methods of partitions: 105 | 106 | **Mysql Partition Types Supports:** 107 | 108 | * RANGE 109 | * LIST 110 | * HASH 111 | * KEY 112 | 113 | **Special Partition Types Supports:** 114 | 115 | * YEARS 116 | * YEARS AND MONTH 117 | * MONTH (cooming soon) 118 | * DAYS (cooming soon) 119 | 120 | ### Partition by RANGE 121 | 122 | This type of partitioning assigns rows to partitions based on column values falling within a given range. 123 | 124 | ``` 125 | Schema::partitionByRange('partitioned', 'YEAR(date)', [ 126 | new Partition('anno2000', Partition::RANGE_TYPE, 2000), 127 | new Partition('anno2001', Partition::RANGE_TYPE, 2001), 128 | new Partition('anno2002', Partition::RANGE_TYPE, 2002), 129 | new Partition('anno2003', Partition::RANGE_TYPE, 2003), 130 | ]); 131 | ``` 132 | 133 | > **Note**: Using the above code you will not be able to insert records that do not correspond to any range, for example: 2010, to do this you must specify the creation of a partition for future values, you can do so by specifying the parameter ** includeFuturePartition ** to true, as per example: 134 | 135 | 136 | ``` 137 | Schema::partitionByRange('partitioned', 'YEAR(date)', [ 138 | new Partition('anno2000', Partition::RANGE_TYPE, 2000), 139 | new Partition('anno2001', Partition::RANGE_TYPE, 2001), 140 | new Partition('anno2002', Partition::RANGE_TYPE, 2002), 141 | new Partition('anno2003', Partition::RANGE_TYPE, 2003), 142 | ], true); 143 | ``` 144 | 145 | ### Partition by LIST 146 | 147 | Similar to partitioning by RANGE, except that the partition is selected based on columns matching one of a set of discrete values. 148 | 149 | ``` 150 | Schema::partitionByList('partitioned', 'id', 151 | [ 152 | new Partition('server_east', Partition::LIST_TYPE, [1,43,65,12,56,73]), 153 | new Partition('server_west', Partition::LIST_TYPE, [534,6422,196,956,22]) 154 | ] 155 | ); 156 | ``` 157 | 158 | ### Partition by HASH 159 | 160 | With this type of partitioning, a partition is selected based on the value returned by a user-defined expression that operates on column values in rows to be inserted into the table. The function may consist of any expression valid in MySQL that yields a nonnegative integer value. An extension to this type, LINEAR HASH, is also available. 161 | 162 | ``` 163 | Schema::partitionByHash('partitioned', 'YEAR(date)', 10); 164 | ``` 165 | 166 | ### Partition by KEY 167 | 168 | This type of partitioning is similar to partitioning by HASH, except that only one or more columns to be evaluated are supplied, and the MySQL server provides its own hashing function. These columns can contain other than integer values, since the hashing function supplied by MySQL guarantees an integer result regardless of the column data type. 169 | 170 | ``` 171 | Schema::partitionByKey('partitioned', 10); 172 | ``` 173 | 174 | ### Partition by YEARS 175 | 176 | This type of partitioning allow you to partition a table for a specified years range. 177 | 178 | ``` 179 | Schema::partitionByYears('partitioned', 'date', 2000, 2010); 180 | ``` 181 | 182 | You can omit the end year of range, and current year will be used: 183 | 184 | ``` 185 | Schema::partitionByYears('partitioned', 'date', 2000); 186 | ``` 187 | 188 | ### Partition by YEARS AND MONTHS 189 | 190 | This type of partitioning allow you to partition a table for a specified years range, and sub partition each year for a month. 191 | 192 | ``` 193 | Schema::partitionByYearsAndMonths('test_part', 'date', 2019); 194 | ``` 195 | 196 | You can omit the end year of range, and current year will be used: 197 | 198 | ## Composite primary key 199 | 200 | To partition a table, columns must be an index, if you want to use a different column from **id** you have to change this line of you migration file: 201 | 202 | 203 | ``` 204 | $table->bigIncrements('id'); 205 | ``` 206 | 207 | to this, creating a composite primary key 208 | 209 | ``` 210 | $table->bigInteger('id'); 211 | $table->primary(['id','date']); 212 | ``` 213 | 214 | > **Note**: Using the above code you'll lose the autoincrement for id field, you can force it before run partition, if you need, with this code: 215 | 216 | ``` 217 | Schema::forceAutoIncrement('partitioned', 'id'); 218 | ``` 219 | 220 | ## Querying parition with Eloquent 221 | 222 | Using this package you can query an individual partition, or multiple partitions, directly from eloquent model: 223 | 224 | ### Create a model 225 | ```shell 226 | php artisan make:model Partitioned 227 | ``` 228 | 229 | Then edit the model you just created: 230 | 231 | ```php 232 | namespace App; 233 | 234 | use Illuminate\Database\Eloquent\Model; 235 | 236 | class Partitioned extends Model 237 | { 238 | protected $table = 'partitioned'; 239 | } 240 | ``` 241 | 242 | ### Query to a single partition 243 | 244 | ```php 245 | Psy Shell v0.9.9 (PHP 7.3.6 — cli) by Justin Hileman 246 | >>> use App\Models\Partitioned; 247 | >>> Partitioned::partition('name')->first(); 248 | ``` 249 | 250 | ### Query to a multiple partition 251 | 252 | ```php 253 | Psy Shell v0.9.9 (PHP 7.3.6 — cli) by Justin Hileman 254 | >>> use App\Models\Partitioned; 255 | >>> Partitioned::partitions(['name', 'name1'])->first(); 256 | ``` 257 | 258 | ## Artisan command 259 | 260 | This package comes with a useful set of artisan command: 261 | 262 | ```php 263 | php artisan laravel-mysql-partition 264 | {action : Action to perform} 265 | {--table=} {--method=} {--number=} {--excludeFuture} {--column=} {--partitions=*} 266 | ``` 267 | 268 | ### Available commands 269 | 270 | | Action | Description | 271 | | ------------------------------------------------------------ | ------------------------------------------------------------ | 272 | | `list` | Returns list of partition names for a specific table 273 | | `create` | Create partition on exisiting table 274 | | `delete` | Delete existing partition 275 | | `truncate` | Truncate existing partition 276 | | `optimize` | Optimize existing partition 277 | | `repair` | Repair existing partition 278 | | `check` | Check existing partition 279 | | `analyze` | Analyze existing partition 280 | | `rebuild` | Rebuild existing partition 281 | 282 | Form detail infomration on actions, refere to : [this link](https://dev.mysql.com/doc/refman/5.7/en/partitioning-maintenance.html). 283 | 284 | ### List 285 | ```shell 286 | php artisan laravel-mysql-partition list --table=partitioned 287 | ``` 288 | 289 | ### Create 290 | 291 | ```shell 292 | // Create by RANGE 293 | php artisan laravel-mysql-partition create --table=partitioned --column="YEAR(date)" --method=RANGE 294 | 295 | Enter a comma separated value for partitions of:YEAR(date): 296 | > 2019,2020,2021 297 | 298 | Table did partitioned successfully! 299 | 300 | // Create by LIST 301 | php artisan laravel-mysql-partition create --table=partitioned --column="id" --method=LIST 302 | 303 | How many partition do you want to create?: 304 | > 3 305 | 306 | Enter a comma separated value for list 0: 307 | > 1,2,3 308 | 309 | Enter a comma separated value for list 1: 310 | > 4,5,6 311 | 312 | Enter a comma separated value for list 2: 313 | > 7,8,9 314 | 315 | Table did partitioned successfully! 316 | 317 | // Create by YEAR 318 | php artisan laravel-mysql-partition create --table=partitioned --column=date --method=YEAR 319 | 320 | Enter start year for partition:: 321 | > 2016 322 | 323 | Enter end year for partition (leave blank for current year):: 324 | > 2020 325 | 326 | Table did partitioned successfully! 327 | 328 | // Create by KEY 329 | php artisan laravel-mysql-partition create --table=partitioned --method=KEY --number=10 330 | 331 | Table did partitioned successfully! 332 | 333 | // Create by HASH 334 | php artisan laravel-mysql-partition create --table=partitioned --method=HASH --column="MONTH(date)" --number=10 335 | 336 | Table did partitioned successfully! 337 | ``` 338 | 339 | ### Delete 340 | ``` 341 | php artisan laravel-mysql-partition delete --table=partitioned --partitions=year2018,year2020 342 | Partition year2018,year2020 did delete successfully! 343 | ``` 344 | 345 | ### Truncate 346 | ``` 347 | php artisan laravel-mysql-partition truncate --table=partitioned --partitions=year2019,year2020 348 | Partition year2019,year2020 did truncate successfully! 349 | ``` 350 | 351 | ### Optimize 352 | ``` 353 | php artisan laravel-mysql-partition optimize --table=partitioned --partitions=year2019,year2020 354 | ``` 355 | 356 | ### Repair 357 | ``` 358 | php artisan laravel-mysql-partition repair --table=partitioned --partitions=year2019,year2020 359 | ``` 360 | 361 | ### Check 362 | ``` 363 | php artisan laravel-mysql-partition check --table=partitioned --partitions=year2019,year2020 364 | ``` 365 | 366 | ### Analyze 367 | ``` 368 | php artisan laravel-mysql-partition analyze --table=partitioned --partitions=year2019,year2020 369 | ``` 370 | 371 | ### Rebuild 372 | ``` 373 | php artisan laravel-mysql-partition rebuild --table=partitioned --partitions=year2019,year2020 374 | Partitions year2019,year2020 did rebuilt successfully! 375 | ``` 376 | 377 | 378 | ## Tests 379 | 380 | ```shell 381 | $ composer test 382 | # or 383 | $ composer test:unit 384 | ``` 385 | 386 | ### Comming soon 387 | 388 | ```shell 389 | $ composer test:integration 390 | ``` 391 | 392 | Integration tests will require a running MySQL database. If you have Docker installed, you can start easily start one: 393 | 394 | ```shell 395 | $ make start_db # starts MySQL 8.0 396 | # or 397 | $ make start_db V=5.7 # starts MySQL 5.7 398 | ``` 399 | 400 | ## Contributing 401 | 402 | Recommendations and pull request are most welcome! Pull requests with tests are the best!. 403 | 404 | 405 | ### Are you using this package? 406 | I'm interested in making a list of all projects which use this library. Feel free to open an Issue on GitHub with the name and links of your project; we'll add it to this site. 407 | 408 | ## Credits & License 409 | larave-mysql-partition is owned and maintained by [Luca Becchetti](http://www.lucabecchetti.com) 410 | 411 | As open source creation any help is welcome! 412 | 413 | The code of this library is licensed under MIT License; you can use it in commercial products without any limitation. 414 | 415 | The only requirement is to add a line in your Credits/About section with the text below: 416 | 417 | ``` 418 | Partition by laravel-mysql-partition - http://www.lucabecchetti.com 419 | Created by Becchetti Luca and licensed under MIT License. 420 | ``` 421 | ## About me 422 | 423 | I am a professional programmer with a background in software design and development, currenlty i am a leader of "Brokenice" that provides high-performing, on-demand teams of developers for leading brands. 424 | 425 | In the last years, I developed my qualitative skills on a startup company named "Frind" as Project Manager and ios senior software engineer, we had worked on a new social network application, it includes chat system based on XMPP Framework and Ejabberd server. 426 | 427 | I'm high skilled in Software Design, i have been worked since i was young as webmaster, and i'm a senior Php developer. In the last years i have been worked hard with mobile application programming, Swift for ios world, and Java for Android world. 428 | 429 | I'm an expert mobile developer and architect with several years of experience of team managing, design and development on all the major mobile platforms: iOS, Android (3+ years of experience). 430 | 431 | I'm also has broad experience on Web design and development both on client and server side and API /Networking design. 432 | 433 | All my last works are hosted on AWS Amazon cloud or Google Cloud Platform, i'm able to configure a network, with Unix servers. For my last works i configured apache2, ssl, ejabberd in cluster mode, Api servers with load balancer, and more. 434 | 435 | When i was eleven, i created a CMS in ASP called openasp, it has been used for more years in more than 10.000 websites. The project has been closed when ASP has been deprecated, and i started to learn PHP. 436 | 437 | I live in Assisi (Perugia), a small town in Italy, for any question, [contact me](mailto:luca.becchetti@brokenice.it) 438 | 439 | 440 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "brokenice/laravel-mysql-partition", 3 | "description": "MySQL Partition extension for Laravel.", 4 | "scripts": { 5 | "test": "phpunit -c phpunit.xml.dist", 6 | "test:unit": "phpunit -c phpunit.xml.dist --testsuite unit", 7 | "test:integration": "phpunit -c phpunit.xml.dist --testsuite integration" 8 | }, 9 | "minimum-stability": "dev", 10 | "type": "library", 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Luca Becchetti", 15 | "email": "luca.becchetti@brokenice.it" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=7.1", 20 | "ext-pdo": "*", 21 | "ext-json": "*", 22 | "illuminate/database": "^5.2|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 23 | "ext-mbstring": "*" 24 | }, 25 | "require-dev": { 26 | "phpunit/phpunit": "~4.8|~5.7|^9.5.10", 27 | "mockery/mockery": "~1.3.0|^1.4.4", 28 | "laravel/laravel": "^5.2|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 29 | "doctrine/dbal": "^2.5|^3.5", 30 | "laravel/browser-kit-testing": "^2.0|^6.4", 31 | "php-coveralls/php-coveralls": "^2.0" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "Brokenice\\LaravelMysqlPartition\\": "src/" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "classmap": [ 40 | "tests/Unit" 41 | ] 42 | }, 43 | "extra": { 44 | "branch-alias": { 45 | "dev-master": "1.0.x-dev" 46 | }, 47 | "laravel": { 48 | "providers": [ 49 | "Brokenice\\LaravelMysqlPartition\\PartitionServiceProvider" 50 | ] 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /demo.yml: -------------------------------------------------------------------------------- 1 | # The configurations that used for the recording, feel free to edit them 2 | config: 3 | 4 | # Specify a command to be executed 5 | # like `/bin/bash -l`, `ls`, or any other commands 6 | # the default is bash for Linux 7 | # or powershell.exe for Windows 8 | command: bash -l 9 | 10 | # Specify the current working directory path 11 | # the default is the current working directory path 12 | cwd: /Users/frind/laravel-mysql-partition 13 | 14 | # Export additional ENV variables 15 | env: 16 | recording: true 17 | 18 | # Explicitly set the number of columns 19 | # or use `auto` to take the current 20 | # number of columns of your shell 21 | cols: 133 22 | 23 | # Explicitly set the number of rows 24 | # or use `auto` to take the current 25 | # number of rows of your shell 26 | rows: 32 27 | 28 | # Amount of times to repeat GIF 29 | # If value is -1, play once 30 | # If value is 0, loop indefinitely 31 | # If value is a positive number, loop n times 32 | repeat: 0 33 | 34 | # Quality 35 | # 1 - 100 36 | quality: 100 37 | 38 | # Delay between frames in ms 39 | # If the value is `auto` use the actual recording delays 40 | frameDelay: auto 41 | 42 | # Maximum delay between frames in ms 43 | # Ignored if the `frameDelay` isn't set to `auto` 44 | # Set to `auto` to prevent limiting the max idle time 45 | maxIdleTime: 2000 46 | 47 | # The surrounding frame box 48 | # The `type` can be null, window, floating, or solid` 49 | # To hide the title use the value null 50 | # Don't forget to add a backgroundColor style with a null as type 51 | frameBox: 52 | type: floating 53 | title: Terminalizer 54 | style: 55 | border: 0px black solid 56 | # boxShadow: none 57 | # margin: 0px 58 | 59 | # Add a watermark image to the rendered gif 60 | # You need to specify an absolute path for 61 | # the image on your machine or a URL, and you can also 62 | # add your own CSS styles 63 | watermark: 64 | imagePath: null 65 | style: 66 | position: absolute 67 | right: 15px 68 | bottom: 15px 69 | width: 100px 70 | opacity: 0.9 71 | 72 | # Cursor style can be one of 73 | # `block`, `underline`, or `bar` 74 | cursorStyle: block 75 | 76 | # Font family 77 | # You can use any font that is installed on your machine 78 | # in CSS-like syntax 79 | fontFamily: "Monaco, Lucida Console, Ubuntu Mono, Monospace" 80 | 81 | # The size of the font 82 | fontSize: 12 83 | 84 | # The height of lines 85 | lineHeight: 1 86 | 87 | # The spacing between letters 88 | letterSpacing: 0 89 | 90 | # Theme 91 | theme: 92 | background: "transparent" 93 | foreground: "#afafaf" 94 | cursor: "#c7c7c7" 95 | black: "#232628" 96 | red: "#fc4384" 97 | green: "#b3e33b" 98 | yellow: "#ffa727" 99 | blue: "#75dff2" 100 | magenta: "#ae89fe" 101 | cyan: "#708387" 102 | white: "#d5d5d0" 103 | brightBlack: "#626566" 104 | brightRed: "#ff7fac" 105 | brightGreen: "#c8ed71" 106 | brightYellow: "#ebdf86" 107 | brightBlue: "#75dff2" 108 | brightMagenta: "#ae89fe" 109 | brightCyan: "#b1c6ca" 110 | brightWhite: "#f9f9f4" 111 | 112 | # Records, feel free to edit them 113 | records: 114 | - delay: 782 115 | content: "\e]1337;RemoteHost=frind@Mac-Pro-di-Luca.fritz.box\a\e]1337;CurrentDir=/Users/frind/laravel-mysql-partition\a\e]1337;ShellIntegrationVersion=14;shell=bash\a\r\nThe default interactive shell is now zsh.\r\nTo update your account to use zsh, please run `chsh -s /bin/zsh`.\r\nFor more details, please visit https://support.apple.com/kb/HT208050.\r\n" 116 | - delay: 7 117 | content: "\e]133;C;\a\e]1337;RemoteHost=frind@Mac-Pro-di-Luca.fritz.box\a\e]1337;CurrentDir=/Users/frind/laravel-mysql-partition\a\e[?1034h\e]133;D;0\a\e]133;A\afrind$ \e]133;B\a" 118 | - delay: 9936 119 | content: l 120 | - delay: 67 121 | content: s 122 | - delay: 360 123 | content: "\r\n" 124 | - delay: 9 125 | content: "\e]133;C;\aLICENSE\t\t\tbuild\t\t\tdocker-compose.yml\ttests\r\nMakefile\t\tcomposer.json\t\tphpunit.xml.dist\tvendor\r\nREADME.md\t\tcomposer.lock\t\tsrc\r\n\e]1337;RemoteHost=frind@Mac-Pro-di-Luca.fritz.box\a\e]1337;CurrentDir=/Users/frind/laravel-mysql-partition\a\e]133;D;0\a\e]133;A\afrind$ \e]133;B\a" 126 | - delay: 1455 127 | content: p 128 | - delay: 169 129 | content: h 130 | - delay: 934 131 | content: ' ' 132 | - delay: 101 133 | content: a 134 | - delay: 169 135 | content: r 136 | - delay: 292 137 | content: t 138 | - delay: 11 139 | content: i 140 | - delay: 158 141 | content: s 142 | - delay: 157 143 | content: 'n' 144 | - delay: 158 145 | content: ' ' 146 | - delay: 292 147 | content: "\r\n" 148 | - delay: 10 149 | content: "\e]133;C;\abash: ph: command not found\r\n\e]1337;RemoteHost=frind@Mac-Pro-di-Luca.fritz.box\a\e]1337;CurrentDir=/Users/frind/laravel-mysql-partition\a\e]133;D;127\a\e]133;A\afrind$ \e]133;B\a" 150 | - delay: 3977 151 | content: "logout\r\n" 152 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | services: 3 | db: 4 | image: mysql:5.7 5 | ports: 6 | - "3306:3306" 7 | environment: 8 | MYSQL_DATABASE: 'partition_test' 9 | MYSQL_ROOT_PASSWORD: '' 10 | MYSQL_ALLOW_EMPTY_PASSWORD: 1 11 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucabecchetti/laravel-mysql-partition/d6c58f9d88064bf8bbaf3d7e9ce9116c312ba754/logo.png -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./tests/Unit 14 | 15 | 16 | ./tests/Integration 17 | 18 | 19 | 20 | 22 | 23 | 24 | 25 | 26 | ./src 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /shell.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucabecchetti/laravel-mysql-partition/d6c58f9d88064bf8bbaf3d7e9ce9116c312ba754/shell.gif -------------------------------------------------------------------------------- /src/Connectors/ConnectionFactory.php: -------------------------------------------------------------------------------- 1 | container->bound($key = "db.connection.{$driver}")) { 23 | return $this->container->make($key, [$connection, $database, $prefix, $config]); // @codeCoverageIgnore 24 | } 25 | 26 | if ($driver === 'mysql') { 27 | return new MysqlConnection($connection, $database, $prefix, $config); 28 | } 29 | 30 | return parent::createConnection($driver, $connection, $database, $prefix, $config); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Console/PartitionsCommand.php: -------------------------------------------------------------------------------- 1 | checkForOptions(['table']); 36 | switch ($this->argument('action')) { 37 | case 'list': 38 | $this->checkForOptions(['table']); 39 | $partitions = Schema::getPartitionNames($this->option('database') ?: env('DB_DATABASE'), $this->option('table')); 40 | $this->table( 41 | ['PARTITION_NAME', 'SUBPARTITION_NAME', 'PARTITION_ORDINAL_POSITION', 'TABLE_ROWS', 'PARTITION_METHOD'], 42 | collect($partitions)->map(static function ($item) { return (array) $item;}) 43 | ); 44 | break; 45 | case 'delete': 46 | Schema::deletePartition($this->option('table'), $this->option('partitions')); 47 | $this->info('Partition '.implode(',', $this->option('partitions')).' did delete successfully!'); 48 | break; 49 | case 'truncate': 50 | Schema::truncatePartitionData($this->option('table'), $this->option('partitions')); 51 | $this->info('Partition '.implode(',', $this->option('partitions')).' did truncate successfully!'); 52 | break; 53 | case 'optimize': 54 | $result = Schema::optimizePartitions($this->option('table'), $this->option('partitions')); 55 | $this->parseResultIntoTable($result); 56 | break; 57 | case 'repair': 58 | $result = Schema::repairPartitions($this->option('table'), $this->option('partitions')); 59 | $this->parseResultIntoTable($result); 60 | break; 61 | case 'check': 62 | $result = Schema::checkPartitions($this->option('table'), $this->option('partitions')); 63 | $this->parseResultIntoTable($result); 64 | break; 65 | case 'analyze': 66 | $result = Schema::analyzePartitions($this->option('table'), $this->option('partitions')); 67 | $this->parseResultIntoTable($result); 68 | break; 69 | case 'rebuild': 70 | Schema::rebuildPartitions($this->option('table'), $this->option('partitions')); 71 | $this->info('Partitions '. implode(',', $this->option('partitions')). ' did rebuilt successfully!'); 72 | break; 73 | case 'create': 74 | $this->checkForOptions(['table', 'method']); 75 | switch ($this->option('method')){ 76 | case "HASH": 77 | $this->checkForOptions(['number'], 'numeric'); 78 | $this->checkForOptions(['column']); 79 | Schema::partitionByHash($this->option('table'), $this->option('column'), $this->option('number'), $this->option('database')); 80 | $this->info('Table did partitioned successfully!'); 81 | break; 82 | case "RANGE": 83 | $this->checkForOptions(['column']); 84 | $partitions = $this->askRangePartitions(); 85 | Schema::partitionByRange($this->option('table'), $this->option('column'), $partitions, !$this->option('excludeFuture'), $this->option('database')); 86 | $this->info('Table did partitioned successfully!'); 87 | break; 88 | case "YEAR": 89 | $this->checkForOptions(['column']); 90 | $yearRanges = $this->askforYearRange(); 91 | Schema::partitionByYears($this->option('table'), $this->option('column'), $yearRanges[0], $yearRanges[1] ?: date('Y'), $this->option('database')); 92 | $this->info('Table did partitioned successfully!'); 93 | break; 94 | case "MONTH": 95 | $this->checkForOptions(['column']); 96 | Schema::partitionByMonths($this->option('table'), $this->option('column'), $this->option('database')); 97 | $this->info('Table did partitioned successfully!'); 98 | break; 99 | case "KEY": 100 | $this->checkForOptions(['number'], 'numeric'); 101 | Schema::partitionByKey($this->option('table'), $this->option('number'), $this->option('database')); 102 | $this->info('Table did partitioned successfully!'); 103 | break; 104 | case "LIST": 105 | $this->checkForOptions(['column']); 106 | $partitions = $this->askListPartitions(); 107 | Schema::partitionByList($this->option('table'), $this->option('column'), $partitions, $this->option('database')); 108 | $this->info('Table did partitioned successfully!'); 109 | break; 110 | } 111 | break; 112 | default: 113 | $this->error('unable to find action: ' . $this->argument('action')); 114 | break; 115 | } 116 | } 117 | 118 | /** 119 | * @param $options 120 | * @param string $type 121 | */ 122 | private function checkForOptions($options, $type = '') 123 | { 124 | foreach ($options as $option) { 125 | if (empty($this->option($option)) || $this->option($option) === null) { 126 | $this->error("\n Please, insert $option option! \n"); 127 | die(); 128 | } 129 | switch ($type){ 130 | case "numeric": 131 | if(!is_numeric($this->option($option))){ 132 | $this->error("\n Error, $option option must be a number! \n"); 133 | die(); 134 | } 135 | break; 136 | case "array": 137 | if(count(explode(',',$this->option($option))) <= 0){ 138 | $this->error("\n Error, $option option must be a comma separated string! \n"); 139 | die(); 140 | } 141 | break; 142 | default: 143 | if(!is_string($this->option($option))){ 144 | $this->error("\n Error, $option option must be a string! \n"); 145 | die(); 146 | } 147 | break; 148 | } 149 | } 150 | } 151 | 152 | /** 153 | * Ask user to build list partitions 154 | * @return array 155 | */ 156 | private function askListPartitions() 157 | { 158 | $partitions = []; 159 | do { 160 | $listNumber = $this->ask('How many partition do you want to create?'); 161 | } while (!is_numeric($listNumber)); 162 | for ($i=0, $iMax = $listNumber; $i< $iMax; $i++){ 163 | do{ 164 | $items = explode(',', $this->ask('Enter a comma separated value for list ' . $i)); 165 | }while( !is_array($items) || count($items) <= 0); 166 | $partitions[] = new Partition('list'.$i, Partition::LIST_TYPE, $items); 167 | } 168 | return $partitions; 169 | } 170 | 171 | /** 172 | * Ask user to build list partitions 173 | * @return array 174 | */ 175 | private function askRangePartitions() 176 | { 177 | $partitions = []; 178 | do{ 179 | $items = explode(',', $this->ask('Enter a comma separated value for partitions of:'.$this->option('column'))); 180 | }while( !is_array($items) || count($items) <= 0); 181 | foreach ($items as $value) { 182 | $partitions[] = new Partition('range' . $value, Partition::RANGE_TYPE, $value); 183 | } 184 | return $partitions; 185 | } 186 | 187 | /** 188 | * Ask user for year range 189 | * @return array 190 | */ 191 | private function askforYearRange(){ 192 | do { 193 | $startYear = $this->ask('Enter start year for partition:'); 194 | } while (!is_numeric($startYear)); 195 | do { 196 | $endYear = $this->ask('Enter end year for partition (leave blank for current year):'); 197 | } while ( ($endYear !== null && !is_numeric($endYear)) || (is_numeric($endYear) && $endYear < $startYear) ); 198 | return [$startYear, $endYear]; 199 | } 200 | 201 | /** 202 | * Convert result into a table 203 | * @param $result 204 | */ 205 | private function parseResultIntoTable($result) 206 | { 207 | foreach (collect($result)->map(static function ($item) { return (array) $item;}) as $res){ 208 | $this->table( 209 | array_keys($res), 210 | [$res] 211 | ); 212 | } 213 | } 214 | 215 | } 216 | -------------------------------------------------------------------------------- /src/Exceptions/UnexpectedValueException.php: -------------------------------------------------------------------------------- 1 | databaseName = $name; 26 | return self::save($options); 27 | } 28 | 29 | /** 30 | * Get a new query builder that doesn't have any global scopes or eager loading. 31 | * 32 | * @return \Illuminate\Database\Eloquent\Builder|static 33 | */ 34 | public function newModelQuery() 35 | { 36 | $queryBuilder = $this->newEloquentBuilder( 37 | $this->newBaseQueryBuilder() 38 | )->setModel($this); 39 | if ($this->databaseName !== null){ 40 | $queryBuilder->db($this->databaseName); 41 | } 42 | return $queryBuilder; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/Models/Partition.php: -------------------------------------------------------------------------------- 1 | name = $name; 30 | $this->type = $type; 31 | if ($this->type === 'RANGE' && !is_numeric($value)){ 32 | throw new UnexpectedValueException('Value for range must be an integer'); 33 | } 34 | if ($this->type === 'LIST' && !is_array($value)){ 35 | throw new UnexpectedValueException('Value for list must be an array'); 36 | } 37 | $this->value = $value; 38 | } 39 | 40 | /** 41 | * Convert this partition to sql 42 | * @return string 43 | */ 44 | public function toSQL(){ 45 | if ($this->type === 'RANGE') { 46 | return "PARTITION {$this->name} VALUES LESS THAN ({$this->value})"; 47 | } 48 | 49 | if($this->type === 'LIST') { 50 | return "PARTITION {$this->name} VALUES IN (". implode(',', $this->value) . ")"; 51 | } 52 | return ''; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/MysqlConnection.php: -------------------------------------------------------------------------------- 1 | getQueryGrammar(), 21 | $this->getPostProcessor() 22 | ); 23 | } 24 | 25 | /** 26 | * Get the default query grammar instance. 27 | * 28 | * @return \Illuminate\Database\Query\Grammars\Grammar 29 | */ 30 | protected function getDefaultQueryGrammar() 31 | { 32 | $grammar = new MySqlGrammar($this); 33 | if (method_exists($grammar, 'setConnection')) { 34 | $grammar->setConnection($this); 35 | } 36 | $this->setQueryGrammar($grammar)->setTablePrefix($this->tablePrefix); 37 | return $grammar; 38 | } 39 | 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/PartitionServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton('db.factory', function ($app) { 25 | return new ConnectionFactory($app); 26 | }); 27 | 28 | // The database manager is used to resolve various connections, since multiple 29 | // connections might be managed. It also implements the connection resolver 30 | // interface which may be used by other components requiring connections. 31 | $this->app->singleton('db', function ($app) { 32 | return new DatabaseManager($app, $app['db.factory']); 33 | }); 34 | 35 | $this->registerCommands(); 36 | } 37 | 38 | /** 39 | * Setup the commands for Otter. 40 | * 41 | * @return void 42 | */ 43 | protected function registerCommands() 44 | { 45 | $this->commands([ 46 | Console\PartitionsCommand::class 47 | ]); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Schema/MySqlGrammar.php: -------------------------------------------------------------------------------- 1 | compileDbName($query, $values), parent::compileInsert($query, $values)); 21 | } 22 | 23 | /** 24 | * Compile the "from" portion of the query. 25 | * 26 | * @param \Illuminate\Database\Query\Builder $query 27 | * @param string $table 28 | * @return string 29 | */ 30 | protected function compileFrom(Builder $query, $table) 31 | { 32 | $baseFrom = 'from'.$this->compileDbName($query, $table).$this->wrapTable($table); 33 | if ($query->hasPartitions()){ 34 | return $baseFrom . $this->compilePartitions($query); 35 | } 36 | return $baseFrom; 37 | } 38 | 39 | /** 40 | * Get database name if isset 41 | * 42 | * @param Builder $query 43 | * @param $table 44 | * @return string 45 | */ 46 | private function compileDbName(Builder $query, $table) 47 | { 48 | return $query->getDb() !== null ? ($this->wrap($query->getDb()).'.') : ''; 49 | } 50 | 51 | /** 52 | * Compile the "partition" portion of the query. 53 | * 54 | * @param \Illuminate\Database\Query\Builder $query 55 | * @return string 56 | */ 57 | protected function compilePartitions(Builder $query){ 58 | return ' PARTITION ('.collect($query->getPartitions())->map(static function($partition){ 59 | return "`{$partition}`"; 60 | })->join(', ').')'; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/Schema/QueryBuilder.php: -------------------------------------------------------------------------------- 1 | partitions = $partitions; 28 | return $this; 29 | } 30 | 31 | /** 32 | * Add a "partition" clause to the query. 33 | * @param array $partition 34 | * @return $this 35 | */ 36 | public function partition($partition) { 37 | $this->partitions = [$partition]; 38 | return $this; 39 | } 40 | 41 | /** 42 | * Set database name 43 | * @param $name 44 | * @return $this 45 | */ 46 | public function db($name) 47 | { 48 | $this->databaseName = $name; 49 | return $this; 50 | } 51 | 52 | /** 53 | * @return string|null 54 | */ 55 | public function getDb() 56 | { 57 | return $this->databaseName; 58 | } 59 | 60 | /** 61 | * @return string[] 62 | */ 63 | public function getPartitions() 64 | { 65 | return $this->partitions; 66 | } 67 | 68 | /** 69 | * Check if partitions did set 70 | * @return bool 71 | */ 72 | public function hasPartitions(){ 73 | return count($this->partitions) > 0; 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/Schema/Schema.php: -------------------------------------------------------------------------------- 1 | 'dec', 23 | 1 => 'jan', 24 | 2 => 'feb', 25 | 3 => 'mar', 26 | 4 => 'apr', 27 | 5 => 'may', 28 | 6 => 'jun', 29 | 7 => 'jul', 30 | 8 => 'aug', 31 | 9 => 'sep', 32 | 10 => 'oct', 33 | 11 => 'nov' 34 | ]; 35 | 36 | /** 37 | * returns array of partition names for a specific db/table 38 | * 39 | * @param string $db database name 40 | * @param string $table table name 41 | * 42 | * @access public 43 | * @return array of partition names 44 | */ 45 | public static function getPartitionNames($db, $table) 46 | { 47 | self::assertSupport(); 48 | $query = "SELECT `PARTITION_NAME`, `SUBPARTITION_NAME`, `PARTITION_ORDINAL_POSITION`, `TABLE_ROWS`, `PARTITION_METHOD` FROM `information_schema`.`PARTITIONS`" 49 | . " WHERE `TABLE_SCHEMA` = '" . $db 50 | . "' AND `TABLE_NAME` = '" . $table . "'"; 51 | if (version_compare(static::getAppVersion(),'10.0.0') >= 0) { 52 | return DB::select(DB::raw($query)->getValue(new MySqlGrammar())); 53 | } else { 54 | return DB::select(DB::raw($query)); 55 | } 56 | } 57 | 58 | /** 59 | * checks if MySQL server supports partitioning 60 | * 61 | * @static 62 | * @staticvar boolean $have_partitioning 63 | * @staticvar boolean $already_checked 64 | * @access public 65 | * @return boolean 66 | */ 67 | public static function havePartitioning() 68 | { 69 | if (self::$already_checked) { 70 | return self::$have_partitioning; 71 | } 72 | 73 | if (version_compare(self::version(), 8, '>=')) { 74 | self::$have_partitioning = true; 75 | } elseif (version_compare(self::version(), 5.6, '>=') && version_compare(self::version(), 8, '<')) { 76 | // see http://dev.mysql.com/doc/refman/5.6/en/partitioning.html 77 | $plugins = DB::connection()->getPdo()->query("SHOW PLUGINS")->fetchAll(); 78 | foreach ($plugins as $value) { 79 | if ($value['Name'] === 'partition') { 80 | self::$have_partitioning = true; 81 | break; 82 | } 83 | } 84 | } elseif (version_compare(self::version(), 5.1, '>=') && version_compare(self::version(), 5.6, '<')) { 85 | if (DB::connection()->getPdo()->query("SHOW VARIABLES LIKE 'have_partitioning';")->fetchAll()) { 86 | self::$have_partitioning = true; 87 | } 88 | } else { 89 | self::$have_partitioning = false; 90 | } 91 | 92 | self::$already_checked = true; 93 | return self::$have_partitioning; 94 | } 95 | 96 | /** 97 | * Implode array of partitions with comma 98 | * @param $partitions 99 | * @return string 100 | */ 101 | private static function implodePartitions($partitions) 102 | { 103 | return collect($partitions)->map(static function ($partition) { 104 | return $partition->toSQL(); 105 | })->implode(','); 106 | } 107 | 108 | /** 109 | * @param $table 110 | * @param $column 111 | * @param null $schema 112 | */ 113 | public static function partitionByMonths($table, $column, $schema = null) 114 | { 115 | $appendSchema = $schema !== null ? ($schema . ".") : ''; 116 | // Build query 117 | $query = "ALTER TABLE {$appendSchema}{$table} PARTITION BY RANGE(MONTH({$column})) ( "; 118 | $query .= "PARTITION `jan` VALUES LESS THAN (2),"; 119 | $query .= "PARTITION `feb` VALUES LESS THAN (3),"; 120 | $query .= "PARTITION `mar` VALUES LESS THAN (4),"; 121 | $query .= "PARTITION `apr` VALUES LESS THAN (5),"; 122 | $query .= "PARTITION `may` VALUES LESS THAN (6),"; 123 | $query .= "PARTITION `jun` VALUES LESS THAN (7),"; 124 | $query .= "PARTITION `jul` VALUES LESS THAN (8),"; 125 | $query .= "PARTITION `aug` VALUES LESS THAN (9),"; 126 | $query .= "PARTITION `sep` VALUES LESS THAN (10),"; 127 | $query .= "PARTITION `oct` VALUES LESS THAN (11),"; 128 | $query .= "PARTITION `nov` VALUES LESS THAN (12),"; 129 | $query .= "PARTITION `dec` VALUES LESS THAN (13)"; 130 | $query .= ")"; 131 | if (version_compare(static::getAppVersion(),'10.0.0') >= 0) { 132 | DB::unprepared(DB::raw($query)->getValue(new MySqlGrammar())); 133 | } else { 134 | DB::unprepared(DB::raw($query)->getValue()); 135 | } 136 | } 137 | 138 | /** 139 | * @param $table 140 | * @param $column 141 | * @param $startYear 142 | * @param null $endYear 143 | * @param bool $includeFuturePartition 144 | * @param null $schema 145 | */ 146 | public static function partitionByYearsAndMonths($table, $column, $startYear, $endYear = null, $includeFuturePartition = true, $schema = null) 147 | { 148 | $appendSchema = $schema !== null ? ($schema . ".") : ''; 149 | self::assertSupport(); 150 | $endYear = $endYear ?: date('Y'); 151 | if ($startYear > $endYear) { 152 | throw new UnexpectedValueException("$startYear must be lower than $endYear"); 153 | } 154 | // Build partitions array for years range 155 | $partitions = []; 156 | foreach (range($startYear, $endYear) as $year) { 157 | $partitions[] = new Partition('year' . $year, Partition::RANGE_TYPE, $year + 1); 158 | } 159 | // Build query 160 | $query = "ALTER TABLE {$appendSchema}{$table} PARTITION BY RANGE(YEAR({$column})) SUBPARTITION BY HASH(MONTH({$column})) ( "; 161 | $subPartitionsQuery = collect($partitions)->map(static function ($partition) { 162 | return $partition->toSQL() . "(" . collect(self::$month)->map(static function ($month) use ($partition) { 163 | return "SUBPARTITION {$month}" . ($partition->value - 1); 164 | })->implode(', ') . ' )'; 165 | }); 166 | $query .= collect($subPartitionsQuery)->implode(','); 167 | // Include future partitions if needed 168 | if ($includeFuturePartition) { 169 | $query .= ", PARTITION future VALUES LESS THAN (MAXVALUE) ("; 170 | $query .= collect(self::$month)->map(static function ($month) { 171 | return "SUBPARTITION `{$month}`"; 172 | })->implode(', '); 173 | $query .= ") )"; 174 | } else { 175 | $query .= ")"; 176 | } 177 | if (version_compare(static::getAppVersion(),'10.0.0') >= 0) { 178 | DB::unprepared(DB::raw($query)->getValue(DB::connection()->getQueryGrammar())); 179 | } else { 180 | DB::unprepared(DB::raw($query)); 181 | } 182 | } 183 | 184 | /** 185 | * Partition table by range 186 | * # WARNING 1: A PRIMARY KEY must include all columns in the table's partitioning function 187 | * @param $table 188 | * @param $column 189 | * @param Partition[] $partitions 190 | * @param bool $includeFuturePartition 191 | * @param null $schema 192 | * @static public 193 | */ 194 | public static function partitionByRange($table, $column, $partitions, $includeFuturePartition = true, $schema = null) 195 | { 196 | $appendSchema = $schema !== null ? ($schema . ".") : ''; 197 | self::assertSupport(); 198 | $query = "ALTER TABLE {$appendSchema}{$table} PARTITION BY RANGE({$column}) ("; 199 | $query .= self::implodePartitions($partitions); 200 | if ($includeFuturePartition) { 201 | $query .= ", PARTITION future VALUES LESS THAN (MAXVALUE)"; 202 | } 203 | $query = trim(trim($query), ',') . ')'; 204 | if (version_compare(static::getAppVersion(),'10.0.0') >= 0) { 205 | DB::unprepared(DB::raw($query)->getValue(DB::connection()->getQueryGrammar())); 206 | } else { 207 | DB::unprepared(DB::raw($query)); 208 | } 209 | } 210 | 211 | /** 212 | * @param $table 213 | * @param $column 214 | * @param $startYear 215 | * @param $endYear 216 | */ 217 | public static function partitionByYears($table, $column, $startYear, $endYear = null, $schema = null) 218 | { 219 | $appendSchema = $schema !== null ? ($schema . ".") : ''; 220 | $endYear = $endYear ?: date('Y'); 221 | if ($startYear > $endYear) { 222 | throw new UnexpectedValueException("$startYear must be lower than $endYear"); 223 | } 224 | $partitions = []; 225 | foreach (range($startYear, $endYear) as $year) { 226 | $partitions[] = new Partition('year' . $year, Partition::RANGE_TYPE, $year + 1); 227 | } 228 | self::partitionByRange($table, "YEAR($column)", $partitions, true, $schema); 229 | } 230 | 231 | /** 232 | * Partition table by list 233 | * # WARNING 1: A PRIMARY KEY must include all columns in the table's partitioning function 234 | * @param $table 235 | * @param $column 236 | * @param Partition[] $partitions 237 | * @param null $schema 238 | * @static public 239 | * 240 | */ 241 | public static function partitionByList($table, $column, $partitions, $schema = null) 242 | { 243 | $appendSchema = $schema !== null ? ($schema . ".") : ''; 244 | self::assertSupport(); 245 | $query = "ALTER TABLE {$appendSchema}{$table} PARTITION BY LIST({$column}) ("; 246 | $query .= self::implodePartitions($partitions); 247 | $query .= ')'; 248 | if (version_compare(static::getAppVersion(),'10.0.0') >= 0) { 249 | DB::unprepared(DB::raw($query)->getValue(new MySqlGrammar())); 250 | } else { 251 | DB::unprepared(DB::raw($query)); 252 | } 253 | } 254 | 255 | /** 256 | * Partition table by hash 257 | * # WARNING 1: A PRIMARY KEY must include all columns in the table's partitioning function 258 | * @param $table 259 | * @param $hashColumn 260 | * @param $partitionsNumber 261 | * @param null $schema 262 | * @static public 263 | */ 264 | public static function partitionByHash($table, $hashColumn, $partitionsNumber, $schema = null) 265 | { 266 | $appendSchema = $schema !== null ? ($schema . ".") : ''; 267 | self::assertSupport(); 268 | $query = "ALTER TABLE {$appendSchema}{$table} PARTITION BY HASH({$hashColumn}) "; 269 | $query .= "PARTITIONS {$partitionsNumber};"; 270 | if (version_compare(static::getAppVersion(),'10.0.0') >= 0) { 271 | DB::unprepared(DB::raw($query)->getValue(new MySqlGrammar())); 272 | } else { 273 | DB::unprepared(DB::raw($query)); 274 | } 275 | } 276 | 277 | /** 278 | * Partition table by hash 279 | * # WARNING 1: Are used all primary and unique keys 280 | * @param $table 281 | * @param $partitionsNumber 282 | * @param null $schema 283 | * @static public 284 | */ 285 | public static function partitionByKey($table, $partitionsNumber, $schema = null) 286 | { 287 | $appendSchema = $schema !== null ? ($schema . ".") : ''; 288 | self::assertSupport(); 289 | $query = "ALTER TABLE {$appendSchema}{$table} PARTITION BY KEY() "; 290 | $query .= "PARTITIONS {$partitionsNumber};"; 291 | if (version_compare(static::getAppVersion(),'10.0.0') >= 0) { 292 | DB::unprepared(DB::raw($query)->getValue(new MySqlGrammar())); 293 | } else { 294 | DB::unprepared(DB::raw($query)); 295 | } 296 | } 297 | 298 | /** 299 | * Check mysql version 300 | * 301 | * @static public 302 | * @return string 303 | */ 304 | public static function version() 305 | { 306 | $pdo = DB::connection()->getPdo(); 307 | return $pdo->query('select version()')->fetchColumn(); 308 | } 309 | 310 | /** 311 | * Force field to be autoIncrement 312 | * @param $table 313 | * @param string $field 314 | */ 315 | public static function forceAutoIncrement($table, $field = 'id', $type = 'INTEGER') 316 | { 317 | DB::statement("ALTER TABLE {$table} MODIFY {$field} {$type} NOT NULL AUTO_INCREMENT"); 318 | } 319 | 320 | /** 321 | * Delete the rows of a partition without affecting the rest of the dataset in the table 322 | * @param $table 323 | * @param $partitions 324 | */ 325 | public static function truncatePartitionData($table, $partitions) 326 | { 327 | DB::statement("ALTER TABLE {$table} TRUNCATE PARTITION " . implode(', ', $partitions)); 328 | } 329 | 330 | /** 331 | * Delete the rows of a partition without affecting the rest of the dataset in the table 332 | * @param $table 333 | * @param $partitions 334 | */ 335 | public static function deletePartition($table, $partitions) 336 | { 337 | DB::statement("ALTER TABLE {$table} DROP PARTITION " . implode(', ', $partitions)); 338 | } 339 | 340 | /** 341 | * Rebuilds the partition; this has the same effect as dropping all records stored in the partition, 342 | * then reinserting them. This can be useful for purposes of defragmentation. 343 | * @param $table 344 | * @param string[] $partitions 345 | */ 346 | public static function rebuildPartitions($table, $partitions) 347 | { 348 | DB::statement("ALTER TABLE {$table} REBUILD PARTITION " . implode(', ', $partitions)); 349 | } 350 | 351 | /** 352 | * If you have deleted a large number of rows from a partition or if you have made many changes to a partitioned table 353 | * with variable-length rows (that is, having VARCHAR, BLOB, or TEXT columns), you can use this method 354 | * to reclaim any unused space and to defragment the partition data file. 355 | * @param $table 356 | * @param string[] $partitions 357 | * @return array 358 | */ 359 | public static function optimizePartitions($table, $partitions) 360 | { 361 | $query = "ALTER TABLE {$table} OPTIMIZE PARTITION " . implode(', ', $partitions); 362 | if (version_compare(static::getAppVersion(),'10.0.0') >= 0) { 363 | return DB::select(DB::raw($query)->getValue(new MySqlGrammar())); 364 | } else { 365 | return DB::select(DB::raw($query)); 366 | } 367 | } 368 | 369 | /** 370 | * This reads and stores the key distributions for partitions. 371 | * @param $table 372 | * @param string[] $partitions 373 | * @return array 374 | */ 375 | public static function analyzePartitions($table, $partitions) 376 | { 377 | $query = "ALTER TABLE {$table} ANALYZE PARTITION " . implode(', ', $partitions); 378 | if (version_compare(static::getAppVersion(),'10.0.0') >= 0) { 379 | return DB::select(DB::raw($query)->getValue(new MySqlGrammar())); 380 | } else { 381 | return DB::select(DB::raw($query)); 382 | } 383 | } 384 | 385 | /** 386 | * Normally, REPAIR PARTITION fails when the partition contains duplicate key errors. In MySQL 5.7.2 and later, 387 | * you can use ALTER IGNORE TABLE with this option, in which case all rows that cannot be moved due to the presence 388 | * of duplicate keys are removed from the partition (Bug #16900947). 389 | * @param $table 390 | * @param string[] $partitions 391 | * @return array 392 | */ 393 | public static function repairPartitions($table, $partitions) 394 | { 395 | $query = "ALTER TABLE {$table} REPAIR PARTITION " . implode(', ', $partitions); 396 | if (version_compare(static::getAppVersion(),'10.0.0') >= 0) { 397 | return DB::select(DB::raw($query)->getValue(new MySqlGrammar())); 398 | } else { 399 | return DB::select(DB::raw($query)); 400 | } 401 | } 402 | 403 | /** 404 | * You can check partitions for errors in much the same way that you can use CHECK TABLE with non partitioned tables. 405 | * @param $table 406 | * @param string[] $partitions 407 | * @return array 408 | */ 409 | public static function checkPartitions($table, $partitions) 410 | { 411 | $query = "ALTER TABLE {$table} CHECK PARTITION " . implode(', ', $partitions); 412 | if (version_compare(static::getAppVersion(),'10.0.0') >= 0) { 413 | return DB::select(DB::raw($query)->getValue(new MySqlGrammar())); 414 | } else { 415 | return DB::select(DB::raw($query)); 416 | } 417 | } 418 | 419 | /** 420 | * todo: Reorganize partition 421 | * ALTER TABLE mytable REORGANIZE PARTITION future INTO ( PARTITION yearCurrent VALUES LESS THAN (yearCurrent+1), PARTITION future VALUES LESS THAN MAXVALUE); 422 | */ 423 | 424 | /** 425 | * Assert support for partition 426 | * @throws UnsupportedPartitionException 427 | */ 428 | private static function assertSupport() 429 | { 430 | if (!self::havePartitioning()) { 431 | throw new UnsupportedPartitionException('Partitioning is unsupported on your server version'); 432 | } 433 | } 434 | 435 | /** 436 | * Get app version 437 | * @return string 438 | */ 439 | private static function getAppVersion(): string 440 | { 441 | try { 442 | return method_exists(app(), 'version') ? app()->version() : '9.0.0'; 443 | } catch (\Exception $exception) { 444 | return ''; 445 | } 446 | } 447 | } -------------------------------------------------------------------------------- /tests/Unit/BaseTestCase.php: -------------------------------------------------------------------------------- 1 | getMethod($methodName); 25 | $method->setAccessible(true); 26 | 27 | return $method->invokeArgs($object, $parameters); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Unit/MysqlConnectionTest.php: -------------------------------------------------------------------------------- 1 | 'mysql', 'prefix' => 'prefix', 'database' => 'database', 'name' => 'foo']; 15 | $this->mysqlConnection = new MysqlConnection(new PDOStub(), 'database', 'prefix', $mysqlConfig); 16 | } 17 | 18 | public function testGetQueryBuilder() 19 | { 20 | $builder = $this->mysqlConnection->query(); 21 | 22 | $this->assertInstanceOf(QueryBuilder::class, $builder); 23 | } 24 | } 25 | 26 | class PDOStub extends PDO 27 | { 28 | public function __construct() 29 | { 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Unit/Schema/SchemaTest.php: -------------------------------------------------------------------------------- 1 | with('ALTER TABLE foo DROP PARTITION test')->andReturn(true); 24 | Schema::deletePartition('foo', ['test']); 25 | $this->assertTrue(true); 26 | } 27 | 28 | public function testDropPartitions() 29 | { 30 | DB::shouldReceive("statement")->with('ALTER TABLE foo DROP PARTITION test, test1')->andReturn(true); 31 | Schema::deletePartition('foo', ['test', 'test1']); 32 | $this->assertTrue(true); 33 | } 34 | 35 | public function testAnalyzePartition() 36 | { 37 | DB::shouldReceive('select'); 38 | DB::shouldReceive("raw")->with('ALTER TABLE foo ANALYZE PARTITION test'); 39 | Schema::analyzePartitions('foo', ['test']); 40 | $this->assertTrue(true); 41 | } 42 | 43 | public function testAnalyzePartitions() 44 | { 45 | DB::shouldReceive('select'); 46 | DB::shouldReceive("raw")->with('ALTER TABLE foo ANALYZE PARTITION test, test1')->andReturn(true); 47 | Schema::analyzePartitions('foo', ['test', 'test1']); 48 | $this->assertTrue(true); 49 | } 50 | 51 | public function testRepairPartition() 52 | { 53 | DB::shouldReceive('select'); 54 | DB::shouldReceive("raw")->with('ALTER TABLE foo REPAIR PARTITION test'); 55 | Schema::repairPartitions('foo', ['test']); 56 | $this->assertTrue(true); 57 | } 58 | 59 | public function testRepairPartitions() 60 | { 61 | DB::shouldReceive('select'); 62 | DB::shouldReceive("raw")->with('ALTER TABLE foo REPAIR PARTITION test, test1')->andReturn(true); 63 | Schema::repairPartitions('foo', ['test', 'test1']); 64 | $this->assertTrue(true); 65 | } 66 | 67 | public function testRebuildPartition() 68 | { 69 | DB::shouldReceive('statement')->with('ALTER TABLE foo REBUILD PARTITION test'); 70 | Schema::rebuildPartitions('foo', ['test']); 71 | $this->assertTrue(true); 72 | } 73 | 74 | public function testRebuildPartitions() 75 | { 76 | DB::shouldReceive('statement')->with('ALTER TABLE foo REBUILD PARTITION test, test1')->andReturn(true); 77 | Schema::rebuildPartitions('foo', ['test', 'test1']); 78 | $this->assertTrue(true); 79 | } 80 | 81 | public function testCheckPartition() 82 | { 83 | DB::shouldReceive('select'); 84 | DB::shouldReceive("raw")->with('ALTER TABLE foo CHECK PARTITION test'); 85 | Schema::checkPartitions('foo', ['test']); 86 | $this->assertTrue(true); 87 | } 88 | 89 | public function testCheckPartitions() 90 | { 91 | DB::shouldReceive('select'); 92 | DB::shouldReceive("raw")->with('ALTER TABLE foo CHECK PARTITION test, test1')->andReturn(true); 93 | Schema::checkPartitions('foo', ['test', 'test1']); 94 | $this->assertTrue(true); 95 | } 96 | 97 | public function testOptimizePartition() 98 | { 99 | DB::shouldReceive('select'); 100 | DB::shouldReceive("raw")->with('ALTER TABLE foo OPTIMIZE PARTITION test'); 101 | Schema::optimizePartitions('foo', ['test']); 102 | $this->assertTrue(true); 103 | } 104 | 105 | public function testOptimizePartitions() 106 | { 107 | DB::shouldReceive('select'); 108 | DB::shouldReceive("raw")->with('ALTER TABLE foo OPTIMIZE PARTITION test, test1')->andReturn(true); 109 | Schema::optimizePartitions('foo', ['test', 'test1']); 110 | $this->assertTrue(true); 111 | } 112 | 113 | public function testTruncatePartition() 114 | { 115 | DB::shouldReceive('statement')->with('ALTER TABLE foo TRUNCATE PARTITION test'); 116 | Schema::truncatePartitionData('foo', ['test']); 117 | $this->assertTrue(true); 118 | } 119 | 120 | public function testTruncatePartitions() 121 | { 122 | DB::shouldReceive('statement')->with('ALTER TABLE foo TRUNCATE PARTITION test, test1')->andReturn(true); 123 | Schema::truncatePartitionData('foo', ['test', 'test1']); 124 | $this->assertTrue(true); 125 | } 126 | 127 | public function testPartitionByKey(){ 128 | DB::shouldReceive('unprepared'); 129 | DB::shouldReceive("raw")->with('ALTER TABLE foo PARTITION BY KEY() PARTITIONS 10;')->andReturn(true); 130 | Schema::partitionByKey('foo', 10); 131 | $this->assertTrue(true); 132 | } 133 | 134 | public function testPartitionByHash(){ 135 | DB::shouldReceive('unprepared'); 136 | DB::shouldReceive("raw")->with('ALTER TABLE foo PARTITION BY HASH(date) PARTITIONS 10;')->andReturn(true); 137 | Schema::partitionByHash('foo', 'date', 10); 138 | $this->assertTrue(true); 139 | } 140 | 141 | public function testPartitionByRange(){ 142 | DB::shouldReceive('unprepared'); 143 | DB::shouldReceive("raw")->with('ALTER TABLE foo PARTITION BY RANGE(date) (PARTITION anno2000 VALUES LESS THAN (2000),PARTITION anno2001 VALUES LESS THAN (2001),PARTITION anno2002 VALUES LESS THAN (2002),PARTITION anno2003 VALUES LESS THAN (2003), PARTITION future VALUES LESS THAN (MAXVALUE))')->andReturn(true); 144 | Schema::partitionByRange('foo', 'date', [ 145 | new Partition('anno2000', Partition::RANGE_TYPE, 2000), 146 | new Partition('anno2001', Partition::RANGE_TYPE, 2001), 147 | new Partition('anno2002', Partition::RANGE_TYPE, 2002), 148 | new Partition('anno2003', Partition::RANGE_TYPE, 2003), 149 | ]); 150 | $this->assertTrue(true); 151 | } 152 | 153 | public function testPartitionByRangeExcludeFuture(){ 154 | DB::shouldReceive('unprepared'); 155 | DB::shouldReceive("raw")->with('ALTER TABLE foo PARTITION BY RANGE(date) (PARTITION anno2000 VALUES LESS THAN (2000),PARTITION anno2001 VALUES LESS THAN (2001),PARTITION anno2002 VALUES LESS THAN (2002),PARTITION anno2003 VALUES LESS THAN (2003))')->andReturn(true); 156 | Schema::partitionByRange('foo', 'date', [ 157 | new Partition('anno2000', Partition::RANGE_TYPE, 2000), 158 | new Partition('anno2001', Partition::RANGE_TYPE, 2001), 159 | new Partition('anno2002', Partition::RANGE_TYPE, 2002), 160 | new Partition('anno2003', Partition::RANGE_TYPE, 2003), 161 | ], false); 162 | $this->assertTrue(true); 163 | } 164 | 165 | public function testPartitionByList(){ 166 | DB::shouldReceive('unprepared'); 167 | DB::shouldReceive("raw")->with('ALTER TABLE foo PARTITION BY LIST(id) (PARTITION server_east VALUES IN (1,43,65,12,56,73),PARTITION server_west VALUES IN (534,6422,196,956,22))')->andReturn(true); 168 | Schema::partitionByList('foo', 'id', 169 | [ 170 | new Partition('server_east', Partition::LIST_TYPE, [1,43,65,12,56,73]), 171 | new Partition('server_west', Partition::LIST_TYPE, [534,6422,196,956,22]) 172 | ] 173 | ); 174 | $this->assertTrue(true); 175 | } 176 | 177 | public function testPartitionByOneYears(){ 178 | DB::shouldReceive('unprepared'); 179 | DB::shouldReceive("raw")->with('ALTER TABLE foo PARTITION BY RANGE(YEAR(date)) (PARTITION year2020 VALUES LESS THAN (2021), PARTITION future VALUES LESS THAN (MAXVALUE))')->andReturn(true); 180 | Schema::partitionByYears('foo', 'date', 2020, 2020); 181 | $this->assertTrue(true); 182 | } 183 | 184 | public function testPartitionByYears(){ 185 | DB::shouldReceive('unprepared'); 186 | DB::shouldReceive("raw")->with('ALTER TABLE foo PARTITION BY RANGE(YEAR(date)) (PARTITION year2019 VALUES LESS THAN (2020),PARTITION year2020 VALUES LESS THAN (2021), PARTITION future VALUES LESS THAN (MAXVALUE))')->andReturn(true); 187 | Schema::partitionByYears('foo', 'date', 2019, 2020); 188 | $this->assertTrue(true); 189 | } 190 | 191 | public function testPartitionByYearsAndMonths(){ 192 | DB::shouldReceive('unprepared'); 193 | DB::shouldReceive("raw")->with('ALTER TABLE foo PARTITION BY RANGE(YEAR(date)) SUBPARTITION BY HASH(MONTH(date)) ( PARTITION year2019 VALUES LESS THAN (2020)(SUBPARTITION dec2019, SUBPARTITION jan2019, SUBPARTITION feb2019, SUBPARTITION mar2019, SUBPARTITION apr2019, SUBPARTITION may2019, SUBPARTITION jun2019, SUBPARTITION jul2019, SUBPARTITION aug2019, SUBPARTITION sep2019, SUBPARTITION oct2019, SUBPARTITION nov2019 ),PARTITION year2020 VALUES LESS THAN (2021)(SUBPARTITION dec2020, SUBPARTITION jan2020, SUBPARTITION feb2020, SUBPARTITION mar2020, SUBPARTITION apr2020, SUBPARTITION may2020, SUBPARTITION jun2020, SUBPARTITION jul2020, SUBPARTITION aug2020, SUBPARTITION sep2020, SUBPARTITION oct2020, SUBPARTITION nov2020 ), PARTITION future VALUES LESS THAN (MAXVALUE) (SUBPARTITION `dec`, SUBPARTITION `jan`, SUBPARTITION `feb`, SUBPARTITION `mar`, SUBPARTITION `apr`, SUBPARTITION `may`, SUBPARTITION `jun`, SUBPARTITION `jul`, SUBPARTITION `aug`, SUBPARTITION `sep`, SUBPARTITION `oct`, SUBPARTITION `nov`) )')->andReturn(true); 194 | Schema::partitionByYearsAndMonths('foo', 'date', 2019, 2020); 195 | $this->assertTrue(true); 196 | } 197 | 198 | public function testPartitionByYearsAndMonthsExcludeFuture(){ 199 | DB::shouldReceive('unprepared'); 200 | DB::shouldReceive("raw")->with('ALTER TABLE foo PARTITION BY RANGE(YEAR(date)) SUBPARTITION BY HASH(MONTH(date)) ( PARTITION year2019 VALUES LESS THAN (2020)(SUBPARTITION dec2019, SUBPARTITION jan2019, SUBPARTITION feb2019, SUBPARTITION mar2019, SUBPARTITION apr2019, SUBPARTITION may2019, SUBPARTITION jun2019, SUBPARTITION jul2019, SUBPARTITION aug2019, SUBPARTITION sep2019, SUBPARTITION oct2019, SUBPARTITION nov2019 ),PARTITION year2020 VALUES LESS THAN (2021)(SUBPARTITION dec2020, SUBPARTITION jan2020, SUBPARTITION feb2020, SUBPARTITION mar2020, SUBPARTITION apr2020, SUBPARTITION may2020, SUBPARTITION jun2020, SUBPARTITION jul2020, SUBPARTITION aug2020, SUBPARTITION sep2020, SUBPARTITION oct2020, SUBPARTITION nov2020 ))')->andReturn(true); 201 | Schema::partitionByYearsAndMonths('foo', 'date', 2019, 2020, false); 202 | $this->assertTrue(true); 203 | } 204 | } 205 | --------------------------------------------------------------------------------