Skip to content

Commit 7ff5ad5

Browse files
Copilotsamdark
andcommitted
Add QueryDataWriter implementation with tests and documentation
Co-authored-by: samdark <[email protected]>
1 parent 1b3af3b commit 7ff5ad5

File tree

5 files changed

+476
-2
lines changed

5 files changed

+476
-2
lines changed

.phpunit.result.cache

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"version":1,"defects":{"Yiisoft\\Data\\Db\\Tests\\Sqlite\\QueryDataWriterTest::testWriteWithEmptyItems":4,"Yiisoft\\Data\\Db\\Tests\\Sqlite\\QueryDataWriterTest::testWriteSkipsEmptyItem":4,"Yiisoft\\Data\\Db\\Tests\\Sqlite\\QueryDataWriterTest::testDeleteThrowsExceptionOnMissingPrimaryKey":4,"Yiisoft\\Data\\Db\\Tests\\Sqlite\\QueryDataWriterTest::testDeleteWithCompositeKey":4,"Yiisoft\\Data\\Db\\Tests\\Sqlite\\QueryDataWriterTest::testDeleteWithEmptyItems":4,"Yiisoft\\Data\\Db\\Tests\\Sqlite\\QueryDataWriterTest::testDelete":4,"Yiisoft\\Data\\Db\\Tests\\Sqlite\\QueryDataWriterTest::testDeleteThrowsExceptionOnInvalidItem":4,"Yiisoft\\Data\\Db\\Tests\\Sqlite\\QueryDataWriterTest::testWriteUpserts":4,"Yiisoft\\Data\\Db\\Tests\\Sqlite\\QueryDataWriterTest::testWriteThrowsExceptionOnInvalidItem":4,"Yiisoft\\Data\\Db\\Tests\\Sqlite\\QueryDataWriterTest::testWriteInserts":4,"Yiisoft\\Data\\Db\\Tests\\Sqlite\\QueryDataWriterTest::testDeleteSkipsEmptyItem":4},"times":{"Yiisoft\\Data\\Db\\Tests\\Sqlite\\QueryDataWriterTest::testWriteWithEmptyItems":0,"Yiisoft\\Data\\Db\\Tests\\Sqlite\\QueryDataWriterTest::testWriteSkipsEmptyItem":0,"Yiisoft\\Data\\Db\\Tests\\Sqlite\\QueryDataWriterTest::testDeleteThrowsExceptionOnMissingPrimaryKey":0,"Yiisoft\\Data\\Db\\Tests\\Sqlite\\QueryDataWriterTest::testDeleteWithCompositeKey":0,"Yiisoft\\Data\\Db\\Tests\\Sqlite\\QueryDataWriterTest::testDeleteWithEmptyItems":0,"Yiisoft\\Data\\Db\\Tests\\Sqlite\\QueryDataWriterTest::testDelete":0,"Yiisoft\\Data\\Db\\Tests\\Sqlite\\QueryDataWriterTest::testDeleteThrowsExceptionOnInvalidItem":0,"Yiisoft\\Data\\Db\\Tests\\Sqlite\\QueryDataWriterTest::testWriteUpserts":0,"Yiisoft\\Data\\Db\\Tests\\Sqlite\\QueryDataWriterTest::testWriteThrowsExceptionOnInvalidItem":0,"Yiisoft\\Data\\Db\\Tests\\Sqlite\\QueryDataWriterTest::testWriteInserts":0,"Yiisoft\\Data\\Db\\Tests\\Sqlite\\QueryDataWriterTest::testDeleteSkipsEmptyItem":0}}

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22

33
## 1.0.0 under development
44

5+
- New #55: Add `QueryDataWriter` for writing and deleting data to/from database tables (@copilot)
56
- Initial release.
67

README.md

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
[![type-coverage](https://shepherd.dev/github/yiisoft/data-db/coverage.svg)](https://shepherd.dev/github/yiisoft/data-db)
1515
[![psalm-level](https://shepherd.dev/github/yiisoft/data-db/level.svg)](https://shepherd.dev/github/yiisoft/data-db)
1616

17-
The package provides [data reader](https://github.com/yiisoft/data?tab=readme-ov-file#reading-data) implementation based
18-
on [Yii DB](https://github.com/yiisoft/db) query and a set of DB-specific filters.
17+
The package provides [data reader](https://github.com/yiisoft/data?tab=readme-ov-file#reading-data) and
18+
[data writer](https://github.com/yiisoft/data?tab=readme-ov-file#writing-data) implementations based
19+
on [Yii DB](https://github.com/yiisoft/db) and a set of DB-specific filters.
1920

2021
Detailed build statuses:
2122

@@ -119,6 +120,56 @@ foreach ($dataReader->read() as $item) {
119120
}
120121
```
121122

123+
### QueryDataWriter
124+
125+
The `QueryDataWriter` allows writing (inserting/updating) and deleting data to/from a database table:
126+
127+
```php
128+
use Yiisoft\Data\Db\QueryDataWriter;
129+
130+
$writer = new QueryDataWriter($db, 'customer');
131+
132+
// Write items (insert or update)
133+
$writer->write([
134+
['id' => 1, 'name' => 'John', 'email' => '[email protected]'],
135+
['id' => 2, 'name' => 'Jane', 'email' => '[email protected]'],
136+
]);
137+
138+
// Delete items
139+
$writer->delete([
140+
['id' => 1],
141+
['id' => 2],
142+
]);
143+
```
144+
145+
By default, `QueryDataWriter` uses UPSERT operations (insert or update). You can configure this behavior:
146+
147+
```php
148+
// Use plain INSERT instead of UPSERT
149+
$writer = new QueryDataWriter(
150+
db: $db,
151+
table: 'customer',
152+
primaryKey: ['id'],
153+
useUpsert: false
154+
);
155+
```
156+
157+
For tables with composite primary keys:
158+
159+
```php
160+
$writer = new QueryDataWriter(
161+
db: $db,
162+
table: 'order_items',
163+
primaryKey: ['order_id', 'product_id']
164+
);
165+
166+
// Delete with composite key
167+
$writer->delete([
168+
['order_id' => 1, 'product_id' => 101],
169+
['order_id' => 1, 'product_id' => 102],
170+
]);
171+
```
172+
122173
## Documentation
123174

124175
- [Internals](docs/internals.md)

src/QueryDataWriter.php

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\Data\Db;
6+
7+
use Throwable;
8+
use Yiisoft\Data\Writer\DataWriterException;
9+
use Yiisoft\Data\Writer\DataWriterInterface;
10+
use Yiisoft\Db\Connection\ConnectionInterface;
11+
12+
use function is_array;
13+
14+
/**
15+
* QueryDataWriter allows writing (inserting/updating) and deleting data to/from a database table.
16+
*
17+
* @template TKey as array-key
18+
* @template TValue as array
19+
*
20+
* @implements DataWriterInterface<TKey, TValue>
21+
*
22+
* Example usage:
23+
*
24+
* ```php
25+
* $writer = new QueryDataWriter($db, 'customer');
26+
*
27+
* // Write (insert or update) items
28+
* $writer->write([
29+
* ['id' => 1, 'name' => 'John', 'email' => '[email protected]'],
30+
* ['id' => 2, 'name' => 'Jane', 'email' => '[email protected]'],
31+
* ]);
32+
*
33+
* // Delete items
34+
* $writer->delete([
35+
* ['id' => 1],
36+
* ['id' => 2],
37+
* ]);
38+
* ```
39+
*
40+
* @psalm-suppress ClassMustBeFinal This class can be extended.
41+
*/
42+
class QueryDataWriter implements DataWriterInterface
43+
{
44+
/**
45+
* @param ConnectionInterface $db The database connection instance.
46+
* @param string $table The name of the table to write to or delete from.
47+
* @param array<string> $primaryKey The primary key column name(s). Defaults to ['id'].
48+
* @param bool $useUpsert Whether to use UPSERT (insert or update) instead of plain INSERT.
49+
* When true, existing records will be updated, otherwise only new records will be inserted.
50+
* Defaults to true.
51+
*/
52+
public function __construct(
53+
private readonly ConnectionInterface $db,
54+
private readonly string $table,
55+
private readonly array $primaryKey = ['id'],
56+
private readonly bool $useUpsert = true,
57+
) {
58+
}
59+
60+
/**
61+
* Write items to the database table.
62+
*
63+
* If `$useUpsert` is true (default), this will insert new records or update existing ones.
64+
* If `$useUpsert` is false, this will only insert new records.
65+
*
66+
* @param iterable $items Items to write. Each item must be an associative array of column => value.
67+
*
68+
* @throws DataWriterException If there is an error while writing items.
69+
*/
70+
public function write(iterable $items): void
71+
{
72+
try {
73+
foreach ($items as $item) {
74+
if (!is_array($item)) {
75+
throw new DataWriterException('Each item must be an array.');
76+
}
77+
78+
if (empty($item)) {
79+
continue;
80+
}
81+
82+
if ($this->useUpsert) {
83+
$this->db->createCommand()->upsert($this->table, $item)->execute();
84+
} else {
85+
$this->db->createCommand()->insert($this->table, $item)->execute();
86+
}
87+
}
88+
} catch (Throwable $e) {
89+
throw new DataWriterException(
90+
'Failed to write items to table "' . $this->table . '": ' . $e->getMessage(),
91+
$e->getCode(),
92+
$e,
93+
);
94+
}
95+
}
96+
97+
/**
98+
* Delete items from the database table.
99+
*
100+
* Each item should contain the primary key column(s) used to identify the record to delete.
101+
*
102+
* @param iterable $items Items to delete. Each item must be an associative array containing at least
103+
* the primary key column(s).
104+
*
105+
* @throws DataWriterException If there is an error deleting items.
106+
*/
107+
public function delete(iterable $items): void
108+
{
109+
try {
110+
foreach ($items as $item) {
111+
if (!is_array($item)) {
112+
throw new DataWriterException('Each item must be an array.');
113+
}
114+
115+
if (empty($item)) {
116+
continue;
117+
}
118+
119+
$condition = [];
120+
foreach ($this->primaryKey as $key) {
121+
if (!isset($item[$key])) {
122+
throw new DataWriterException(
123+
'Item must contain primary key column "' . $key . '" for deletion.',
124+
);
125+
}
126+
$condition[$key] = $item[$key];
127+
}
128+
129+
$this->db->createCommand()->delete($this->table, $condition)->execute();
130+
}
131+
} catch (DataWriterException $e) {
132+
throw $e;
133+
} catch (Throwable $e) {
134+
throw new DataWriterException(
135+
'Failed to delete items from table "' . $this->table . '": ' . $e->getMessage(),
136+
$e->getCode(),
137+
$e,
138+
);
139+
}
140+
}
141+
}

0 commit comments

Comments
 (0)