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