├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src ├── Query.php └── functions.php └── tests ├── FunctionsTest.php └── QueryTest.php /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: 'Continuous Integration' 2 | 3 | on: 4 | pull_request: ~ 5 | push: ~ 6 | 7 | jobs: 8 | unit-tests: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | php: ["8.0", "8.1", "8.2", "8.3"] 13 | steps: 14 | - 15 | name: 'Checkout The Code' 16 | uses: actions/checkout@v4 17 | - 18 | name: 'Install Dependencies' 19 | uses: php-actions/composer@v6 20 | with: 21 | php_version: "${{ matrix.php }}" 22 | - name: 'Unit Tests' 23 | uses: php-actions/phpunit@v4 24 | with: 25 | php_version: "${{ matrix.php }}" 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /vendor 3 | .phpunit.result.cache -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Jan Iwanow 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Doctrine/DBAL Bulk Insert 2 | The library is based on the [gist](https://gist.github.com/gskema/a182aaf7cc04001aebba9c1aad86b40b) and provides bulk insert functionality to the [Doctrine/DBAL](https://github.com/doctrine/dbal). 3 | 4 | ## Usage 5 | 6 | ```php 7 | execute('foo', [ 17 | ['foo' => 111, 'bar' => 222], 18 | ['foo' => 333, 'bar' => 444], 19 | ]); 20 | ``` -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "library", 3 | "name": "franzose/doctrine-bulk-insert", 4 | "description": "Bulk insert functionality for the Doctrine/DBAL", 5 | "license": "MIT", 6 | "require": { 7 | "php": "^8.0", 8 | "doctrine/dbal": "^2.5|^3.3|^4.0" 9 | }, 10 | "require-dev": { 11 | "mockery/mockery": "^1.5", 12 | "phpunit/phpunit": "^9.5" 13 | }, 14 | "config": { 15 | "preferred-install": { 16 | "*": "dist" 17 | }, 18 | "sort-packages": true 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "Franzose\\DoctrineBulkInsert\\": "src/" 23 | }, 24 | "files": [ 25 | "src/functions.php" 26 | ] 27 | }, 28 | "autoload-dev": { 29 | "psr-4": { 30 | "Franzose\\DoctrineBulkInsert\\Tests\\": "tests/" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | tests 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Query.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 16 | } 17 | 18 | public function execute(string $table, array $dataset, array $types = []): int 19 | { 20 | if (empty($dataset)) { 21 | return 0; 22 | } 23 | 24 | $sql = sql($this->connection->getDatabasePlatform(), new Identifier($table), $dataset); 25 | 26 | if (method_exists($this->connection, 'executeStatement')) { 27 | return $this->connection->executeStatement($sql, parameters($dataset), types($types, count($dataset))); 28 | } 29 | 30 | return $this->connection->executeUpdate($sql, parameters($dataset), types($types, count($dataset))); 31 | } 32 | 33 | public function transactional(string $table, array $dataset, array $types = []): int 34 | { 35 | return $this->connection->transactional(fn () => $this->execute($table, $dataset, $types)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/functions.php: -------------------------------------------------------------------------------- 1 | getQuotedName($platform), 16 | stringify_columns($columns), 17 | generate_placeholders(count($columns), count($dataset)) 18 | ); 19 | 20 | return $sql; 21 | } 22 | 23 | function extract_columns(array $dataset): array 24 | { 25 | if (empty($dataset)) { 26 | return []; 27 | } 28 | 29 | $first = reset($dataset); 30 | 31 | return array_keys($first); 32 | } 33 | 34 | function quote_columns(AbstractPlatform $platform, array $columns): array 35 | { 36 | $mapper = static fn (string $column) => (new Identifier($column))->getQuotedName($platform); 37 | 38 | return array_map($mapper, $columns); 39 | } 40 | 41 | function stringify_columns(array $columns): string 42 | { 43 | return empty($columns) ? '' : sprintf('(%s)', implode(', ', $columns)); 44 | } 45 | 46 | function generate_placeholders(int $columnsLength, int $datasetLength): string 47 | { 48 | // (?, ?, ?, ?) 49 | $placeholders = sprintf('(%s)', implode(', ', array_fill(0, $columnsLength, '?'))); 50 | 51 | // (?, ?), (?, ?) 52 | return implode(', ', array_fill(0, $datasetLength, $placeholders)); 53 | } 54 | 55 | function parameters(array $dataset): array 56 | { 57 | $reducer = static fn (array $flattenedValues, array $dataset) => array_merge($flattenedValues, array_values($dataset)); 58 | 59 | return array_reduce($dataset, $reducer, []); 60 | } 61 | 62 | function types(array $types, int $datasetLength): array 63 | { 64 | if (empty($types)) { 65 | return []; 66 | } 67 | 68 | $types = array_values($types); 69 | 70 | $positionalTypes = []; 71 | 72 | for ($idx = 1; $idx <= $datasetLength; $idx++) { 73 | $positionalTypes = array_merge($positionalTypes, $types); 74 | } 75 | 76 | return $positionalTypes; 77 | } 78 | -------------------------------------------------------------------------------- /tests/FunctionsTest.php: -------------------------------------------------------------------------------- 1 | 111, 24 | 'bar' => 222, 25 | 'qux' => 333 26 | ], 27 | [ 28 | 'foo' => 444, 29 | 'bar' => 555, 30 | 'qux' => 777 31 | ], 32 | ]; 33 | 34 | public function testExtractColumns(): void 35 | { 36 | static::assertEquals(['foo', 'bar', 'qux'], extract_columns(static::DATASET)); 37 | static::assertEquals([], extract_columns([])); 38 | } 39 | 40 | public function testStringifyColumns(): void 41 | { 42 | static::assertEquals('(foo, bar, qux)', stringify_columns(['foo', 'bar', 'qux'])); 43 | static::assertEquals('', stringify_columns([])); 44 | } 45 | 46 | public function testPlaceholders(): void 47 | { 48 | static::assertEquals( 49 | '(?, ?, ?, ?, ?), (?, ?, ?, ?, ?)', 50 | generate_placeholders(5, 2) 51 | ); 52 | } 53 | 54 | public function testParameters(): void 55 | { 56 | static::assertEquals([111, 222, 333, 444, 555, 777], parameters(static::DATASET)); 57 | static::assertEquals([], parameters([])); 58 | } 59 | 60 | public function testTypes(): void 61 | { 62 | $types = ['string', 'text', 'json']; 63 | 64 | $expected = [ 65 | 'string', 'text', 'json', 66 | 'string', 'text', 'json' 67 | ]; 68 | 69 | static::assertEquals($expected, types($types, 2)); 70 | } 71 | 72 | public function testSql(): void 73 | { 74 | $sql = sql(new PostgreSQLPlatform(), new Identifier('foo'), static::DATASET); 75 | $expected = 'INSERT INTO foo (foo, bar, qux) VALUES (?, ?, ?), (?, ?, ?);'; 76 | 77 | static::assertEquals($expected, $sql); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/QueryTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('getDatabasePlatform') 21 | ->once() 22 | ->andReturn(new PostgreSQLPlatform()); 23 | 24 | $mockedMethod = method_exists(Connection::class, 'executeStatement') 25 | ? 'executeStatement' 26 | : 'executeUpdate'; 27 | 28 | $connection->shouldReceive($mockedMethod) 29 | ->once() 30 | ->with('INSERT INTO foo (foo, bar) VALUES (?, ?), (?, ?);', [111, 222, 333, 444], []) 31 | ->andReturn(2); 32 | 33 | $rows = (new Query($connection))->execute('foo', [ 34 | ['foo' => 111, 'bar' => 222], 35 | ['foo' => 333, 'bar' => 444], 36 | ]); 37 | 38 | static::assertEquals(2, $rows); 39 | } 40 | 41 | public function testExecuteWithEmptyDataset(): void 42 | { 43 | $connection = Mockery::mock(Connection::class); 44 | $connection->shouldNotReceive('getDatabasePlatform', 'executeUpdate'); 45 | 46 | $rows = (new Query($connection))->execute('foo', []); 47 | 48 | static::assertEquals(0, $rows); 49 | } 50 | } 51 | --------------------------------------------------------------------------------