Skip to content

Commit d64eeec

Browse files
committed
Make InsertBulkQuery.php faster.
1 parent fbc6aec commit d64eeec

File tree

4 files changed

+106
-19
lines changed

4 files changed

+106
-19
lines changed

docs/updating-the-database.md

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Updating the Database
22

3-
Once you have defined the model, (see [Getting Started](getting-started-model.md)) you can start to interact with the database
4-
and doing queries, updates, and deletes.
3+
Once you have defined the model, (see [Getting Started](getting-started-model.md)) you can start to
4+
interact with the database and doing queries, updates, and deletes.
55

66
## Update
77

@@ -27,9 +27,10 @@ $users->name = "New name";
2727
$repository->save($users);
2828
```
2929

30-
## Using the UpdateQuery (Special Cases)
30+
## Using the UpdateQuery for Multiple Records
3131

32-
In some cases you need to update multiples records at once. See an example:
32+
UpdateQuery allows you to update multiple records simultaneously with a single query. This is more efficient than
33+
retrieving and updating individual records one by one when you need to apply the same changes to many records.
3334

3435
```php
3536
<?php
@@ -42,7 +43,7 @@ $updateQuery->where('fld1 > :id', ['id' => 10]);
4243
```
4344

4445
This code will update the table `test` and set the fields `fld1`, `fld2`, and `fld3` to `A`, `B`, and `C`
45-
respectively where the `fld1` is greater than 10.
46+
respectively for all records where `fld1` is greater than 10.
4647

4748
## Insert records with InsertQuery
4849

@@ -91,6 +92,23 @@ $insertBulk->values(['fld1' => 'D', 'fld2' => 'E']);
9192
$insertBulk->values(['fld1' => 'G', 'fld2' => 'H']);
9293
```
9394

95+
By default, InsertBulkQuery uses a faster but less secure approach. To use parameterized queries for
96+
better security (especially when handling user input), you can enable safe mode:
97+
98+
```php
99+
<?php
100+
$insertBulk = new InsertBulkQuery('test', ['fld1', 'fld2']);
101+
$insertBulk->withSafeParameters(); // Enable safe parameterized queries
102+
$insertBulk->values(['fld1' => $userInput1, 'fld2' => $userInput2]);
103+
$insertBulk->values(['fld1' => $userInput3, 'fld2' => $userInput4]);
104+
```
105+
106+
> **⚠️ Security Warning:** By default, the `InsertBulkQuery` implementation uses direct value embedding with
107+
> basic escaping rather than parameterized queries. This makes it faster but potentially vulnerable to
108+
> SQL injection attacks with untrusted data. Use the `withSafeParameters()` method when dealing with
109+
> user input for better security, although this may reduce performance for large batch operations.
110+
> For maximum security with user input, consider using the `InsertQuery` for individual inserts.
111+
94112
## Delete records
95113

96114
You can delete records using the `DeleteQuery` object. See an example:
@@ -100,4 +118,4 @@ You can delete records using the `DeleteQuery` object. See an example:
100118
$deleteQuery = new \ByJG\MicroOrm\DeleteQuery();
101119
$deleteQuery->table('test');
102120
$deleteQuery->where('fld1 = :value', ['value' => 'A']);
103-
```
121+
```

src/InsertBulkQuery.php

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use ByJG\AnyDataset\Db\DbFunctionsInterface;
66
use ByJG\MicroOrm\Exception\OrmInvalidFieldsException;
77
use ByJG\MicroOrm\Interface\QueryBuilderInterface;
8+
use ByJG\MicroOrm\Literal\Literal;
89
use InvalidArgumentException;
910

1011
class InsertBulkQuery extends Updatable
@@ -15,6 +16,8 @@ class InsertBulkQuery extends Updatable
1516

1617
protected ?SqlObject $sqlObject = null;
1718

19+
protected bool $safe = false;
20+
1821
public function __construct(string $table, array $fieldNames)
1922
{
2023
$this->table($table);
@@ -30,6 +33,12 @@ public static function getInstance(string $table, array $fieldNames): static
3033
return new InsertBulkQuery($table, $fieldNames);
3134
}
3235

36+
public function withSafeParameters(): static
37+
{
38+
$this->safe = true;
39+
return $this;
40+
}
41+
3342
/**
3443
* @throws OrmInvalidFieldsException
3544
*/
@@ -82,7 +91,15 @@ public function build(DbFunctionsInterface $dbHelper = null): SqlObject
8291
foreach ($columns as $j => $col) {
8392
$paramKey = "p{$i}_$j"; // Generate the parameter key
8493
$rowPlaceholders[] = ":$paramKey"; // Add to row placeholders
85-
$params[$paramKey] = $this->fields[$col][$i]; // Map parameter key to value
94+
if ($this->safe) {
95+
$params[$paramKey] = $this->fields[$col][$i];
96+
} else {
97+
$value = str_replace("'", "''", $this->fields[$col][$i]);
98+
if (!is_numeric($value)) {
99+
$value = $dbHelper?->delimiterField($value) ?? "'{$value}'";
100+
}
101+
$params[$paramKey] = new Literal($value); // Map parameter key to value
102+
}
86103
}
87104
$placeholders[] = '(' . implode(', ', $rowPlaceholders) . ')'; // Add row placeholders to query
88105
}

tests/InsertBulkQueryTest.php

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,31 @@
33
namespace Tests;
44

55
use ByJG\MicroOrm\InsertBulkQuery;
6+
use ByJG\MicroOrm\Literal\Literal;
67
use PHPUnit\Framework\TestCase;
78

89
class InsertBulkQueryTest extends TestCase
910
{
1011
public function testInsert()
12+
{
13+
$insertBulk = new InsertBulkQuery('test', ['fld1', 'fld2']);
14+
$insertBulk->values(['fld1' => 'A', 'fld2' => 'B']);
15+
$insertBulk->values(['fld1' => 'D', 'fld2' => new Literal("E")]);
16+
$insertBulk->values(['fld1' => "G'1", 'fld2' => 'H']);
17+
18+
$sqlObject = $insertBulk->build();
19+
20+
$this->assertEquals("INSERT INTO test (fld1, fld2) VALUES ('A', 'B'), ('D', 'E'), ('G''1', 'H')", $sqlObject->getSql());
21+
$this->assertEquals([], $sqlObject->getParameters());
22+
}
23+
24+
public function testInsertSafe()
1125
{
1226
$insertBulk = new InsertBulkQuery('test', ['fld1', 'fld2']);
1327
$insertBulk->values(['fld1' => 'A', 'fld2' => 'B']);
1428
$insertBulk->values(['fld1' => 'D', 'fld2' => 'E']);
1529
$insertBulk->values(['fld1' => 'G', 'fld2' => 'H']);
30+
$insertBulk->withSafeParameters();
1631

1732
$sqlObject = $insertBulk->build();
1833

@@ -27,6 +42,7 @@ public function testInsert()
2742
], $sqlObject->getParameters());
2843
}
2944

45+
3046
public function testInsertDifferentOrder()
3147
{
3248
$insertBulk = new InsertBulkQuery('test', ['fld1', 'fld2', 'fld3']);
@@ -36,18 +52,8 @@ public function testInsertDifferentOrder()
3652

3753
$sqlObject = $insertBulk->build();
3854

39-
$this->assertEquals('INSERT INTO test (fld1, fld2, fld3) VALUES (:p0_0, :p0_1, :p0_2), (:p1_0, :p1_1, :p1_2), (:p2_0, :p2_1, :p2_2)', $sqlObject->getSql());
40-
$this->assertEquals([
41-
'p0_0' => 'A',
42-
'p0_1' => 'B',
43-
'p0_2' => 'C',
44-
'p1_0' => 'D',
45-
'p1_1' => 'E',
46-
'p1_2' => 'F',
47-
'p2_0' => 'G',
48-
'p2_1' => 'H',
49-
'p2_2' => 'I',
50-
], $sqlObject->getParameters());
55+
$this->assertEquals("INSERT INTO test (fld1, fld2, fld3) VALUES ('A', 'B', 'C'), ('D', 'E', 'F'), ('G', 'H', 'I')", $sqlObject->getSql());
56+
$this->assertEquals([], $sqlObject->getParameters());
5157
}
5258

5359
public function testWrongFieldsValuesCount()

tests/ORMTest.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
namespace Tests;
44

55
use ByJG\MicroOrm\FieldMapping;
6+
use ByJG\MicroOrm\Literal\HexUuidLiteral;
7+
use ByJG\MicroOrm\Literal\Literal;
68
use ByJG\MicroOrm\Mapper;
79
use ByJG\MicroOrm\ORM;
10+
use ByJG\MicroOrm\ORMHelper;
811
use PHPUnit\Framework\TestCase;
912
use Tests\Model\Class1;
1013
use Tests\Model\Class2;
@@ -121,4 +124,47 @@ public function testGetQuery()
121124
$query = ORM::getQueryInstance('table2', 'table3');
122125
$this->assertEquals("SELECT * FROM table1 INNER JOIN table2 ON table1.id = table2.id_table1 INNER JOIN table3 ON table1.id = table3.id_table1", $query->build()->getSql());
123126
}
127+
128+
public function testProcessLiteral()
129+
{
130+
$query = ORM::getQueryInstance('table1');
131+
$query->where('field1 = :value', ['value' => new Literal('upper(field1)')]);
132+
$this->assertEquals("SELECT * FROM table1 WHERE field1 = upper(field1)", $query->build()->getSql());
133+
134+
}
135+
136+
public function testProcessHexUuidLiteral()
137+
{
138+
$query = ORM::getQueryInstance('table1');
139+
$query->where('field1 = :value', ['value' => new HexUuidLiteral(hex2bin('01010101010101010101010101010101'))]);
140+
$this->assertEquals("SELECT * FROM table1 WHERE field1 = X'01010101010101010101010101010101'", $query->build()->getSql());
141+
}
142+
143+
public function testProcessLiteralUnsafe()
144+
{
145+
$query = ORM::getQueryInstance('table1');
146+
$query->where('field1 = :value', ['value' => new Literal(10)]);
147+
148+
$sqlObject = $query->build();
149+
$sql = $sqlObject->getSql();
150+
$params = $sqlObject->getParameters();
151+
152+
$sql = ORMHelper::processLiteral($sql, $params);
153+
$this->assertEquals("SELECT * FROM table1 WHERE field1 = 10", $sql);
154+
}
155+
156+
public function testProcessLiteralString()
157+
{
158+
$query = ORM::getQueryInstance('table1');
159+
$query->where('field1 = :value', ['value' => new Literal("'testando'")]);
160+
$query->where('field2 = :value2', ['value2' => new Literal("'Joana D''Arc'")]);
161+
162+
$sqlObject = $query->build();
163+
$sql = $sqlObject->getSql();
164+
$params = $sqlObject->getParameters();
165+
166+
$sql = ORMHelper::processLiteral($sql, $params);
167+
$this->assertEquals("SELECT * FROM table1 WHERE field1 = 'testando' AND field2 = 'Joana D''Arc'", $sql);
168+
$this->assertEquals([], $params);
169+
}
124170
}

0 commit comments

Comments
 (0)