diff --git a/src/PrimaryKey.php b/src/PrimaryKey.php
index bb3e965ddc7bc5bbcee351bd008c83cb77c0842a..2cead6699d141ada8f41de2dd7ede4223cf8bd61 100644
--- a/src/PrimaryKey.php
+++ b/src/PrimaryKey.php
@@ -17,6 +17,11 @@ interface PrimaryKey
 	 */
 	public function getCondition(Table $table): Condition;
 
+	/**
+	 * @return string[]
+	 */
+	public static function getColumnNames(): array;
+
 	/** @return static */
 	//public static function fromRow($row);
 
diff --git a/src/Scaffolding/PrimaryKeyImplementation.php b/src/Scaffolding/PrimaryKeyImplementation.php
index 56c68dee0a0307a837ca3938fb1c3a390b29f4a7..73ce2da484e5c1747171a72b0ff8decd4c661506 100644
--- a/src/Scaffolding/PrimaryKeyImplementation.php
+++ b/src/Scaffolding/PrimaryKeyImplementation.php
@@ -51,6 +51,12 @@ final class PrimaryKeyImplementation implements Capability
 			),
 		]);
 
+		$classType->addMethod('getColumnNames')
+			->addComment('@return string[]')
+			->setReturnType('array')
+			->setStatic()
+			->addBody('return ?;', [map($definition->getFields(), static fn(Field $field) => $field->getName())]);
+
 		$namespace->addUse(Condition::class);
 		$namespace->addUse(Composite::class);
 		$namespace->addUseFunction('Grifart\Tables\Conditions\equalTo');
diff --git a/src/Scaffolding/TableImplementation.php b/src/Scaffolding/TableImplementation.php
index 44c946141a22ba6861d1c0e1970e5e23968a3aeb..9f850fa7967dfa034fea74cd1ce37ab88f2d4bf8 100644
--- a/src/Scaffolding/TableImplementation.php
+++ b/src/Scaffolding/TableImplementation.php
@@ -322,6 +322,16 @@ final class TableImplementation implements Capability
 				'$this->tableManager->insert($this, $changes);',
 			);
 
+		$classType->addMethod('insertAndGet')
+			->addComment('@throws RowWithGivenPrimaryKeyAlreadyExists')
+			->setReturnType($this->rowClass)
+			->setParameters([
+				(new Code\Parameter('changes'))->setType($this->modificationClass),
+			])
+			->addBody('$row = $this->tableManager->insertAndGet($this, $changes);')
+			->addBody('\assert($row instanceof ?);', [new Code\Literal($namespace->simplifyName($this->rowClass))])
+			->addBody('return $row;');
+
 		$classType->addMethod('update')
 			->addComment('@throws GivenSearchCriteriaHaveNotMatchedAnyRows')
 			->setReturnType('void')
@@ -332,6 +342,41 @@ final class TableImplementation implements Capability
 				'$this->tableManager->update($this, $changes);',
 			);
 
+		$classType->addMethod('updateAndGet')
+			->addComment('@throws GivenSearchCriteriaHaveNotMatchedAnyRows')
+			->setReturnType($this->rowClass)
+			->setParameters([
+				(new Code\Parameter('changes'))->setType($this->modificationClass),
+			])
+			->addBody('$row = $this->tableManager->updateAndGet($this, $changes);')
+			->addBody('\assert($row instanceof ?);', [new Code\Literal($namespace->simplifyName($this->rowClass))])
+			->addBody('return $row;');
+
+		$classType->addMethod('updateBy')
+			->setReturnType('void')
+			->setParameters([
+				(new Code\Parameter('conditions'))->setType(Condition::class . '|array'),
+				(new Code\Parameter('changes'))->setType($this->modificationClass),
+			])
+			->addComment('@param Condition|Condition[] $conditions')
+			->addBody('$this->tableManager->updateBy($this, $conditions, $changes);');
+
+		$classType->addMethod('upsert')
+			->setReturnType('void')
+			->setParameters([
+				(new Code\Parameter('changes'))->setType($this->modificationClass),
+			])
+			->addBody('$this->tableManager->upsert($this, $changes);');
+
+		$classType->addMethod('upsertAndGet')
+			->setReturnType($this->rowClass)
+			->setParameters([
+				(new Code\Parameter('changes'))->setType($this->modificationClass),
+			])
+			->addBody('$row = $this->tableManager->upsertAndGet($this, $changes);')
+			->addBody('\assert($row instanceof ?);', [new Code\Literal($namespace->simplifyName($this->rowClass))])
+			->addBody('return $row;');
+
 		$classType->addMethod('delete')
 			->setReturnType('void')
 			->setParameters([
@@ -343,6 +388,27 @@ final class TableImplementation implements Capability
 			])
 			->addBody('$this->tableManager->delete($this, $primaryKey);');
 
+		$classType->addMethod('deleteAndGet')
+			->setReturnType($this->rowClass)
+			->setParameters([
+				(new Code\Parameter('rowOrKey'))->setType($this->rowClass . '|' . $this->primaryKeyClass)
+			])
+			->addBody('$primaryKey = $rowOrKey instanceof ? \? $rowOrKey : ?::fromRow($rowOrKey);', [
+				new Code\Literal($namespace->simplifyName($this->primaryKeyClass)),
+				new Code\Literal($namespace->simplifyName($this->primaryKeyClass)),
+			])
+			->addBody('$row = $this->tableManager->deleteAndGet($this, $primaryKey);')
+			->addBody('\assert($row instanceof ?);', [new Code\Literal($namespace->simplifyName($this->rowClass))])
+			->addBody('return $row;');
+
+		$classType->addMethod('deleteBy')
+			->setReturnType('void')
+			->setParameters([
+				(new Code\Parameter('conditions'))->setType(Condition::class . '|array'),
+			])
+			->addComment('@param Condition|Condition[] $conditions')
+			->addBody('$this->tableManager->deleteBy($this, $conditions);');
+
 		$namespace->addUse(TableManager::class);
 		$namespace->addUse(TypeResolver::class);
 		$constructor = $classType->addMethod('__construct');
diff --git a/src/SingleConnectionTableManager.php b/src/SingleConnectionTableManager.php
index 94127603689ca7f7556b13c3ae169ba11033fac7..7d5258497824ab00fe48ce664a9eec8dcb89096c 100644
--- a/src/SingleConnectionTableManager.php
+++ b/src/SingleConnectionTableManager.php
@@ -10,6 +10,7 @@ use Grifart\Tables\Conditions\Composite;
 use Grifart\Tables\Conditions\Condition;
 use Grifart\Tables\OrderBy\OrderBy;
 use Grifart\Tables\OrderBy\OrderByDirection;
+use Grifart\Tables\Table as TableType;
 use Nette\Utils\Paginator;
 use function Phun\map;
 use function Phun\mapWithKeys;
@@ -24,32 +25,6 @@ final class SingleConnectionTableManager implements TableManager
 		private IConnection $connection,
 	) {}
 
-	/**
-	 * @template TableType of Table
-	 * @param TableType $table
-	 * @param Modifications<TableType> $changes
-	 * @throws RowWithGivenPrimaryKeyAlreadyExists
-	 */
-	public function insert(Table $table, Modifications $changes): void
-	{
-		\assert($changes->getPrimaryKey() === NULL);
-
-		try {
-			$this->connection->query(
-				'INSERT',
-				'INTO %n.%n', $table::getSchema(), $table::getTableName(),
-				mapWithKeys(
-					$changes->getModifications(),
-					static fn(string $columnName, mixed $value) => $table->getTypeOf($columnName)->toDatabase($value),
-				),
-			);
-		} catch (UniqueConstraintViolationException $e) {
-			throw new RowWithGivenPrimaryKeyAlreadyExists(previous: $e);
-		}
-
-		\assert($this->connection->getAffectedRows() === 1);
-	}
-
 	/**
 	 * @template TableType of Table
 	 * @param TableType $table
@@ -181,6 +156,88 @@ final class SingleConnectionTableManager implements TableManager
 		);
 	}
 
+	/**
+	 * @template TableType of Table
+	 * @param TableType $table
+	 * @param Modifications<TableType> $changes
+	 * @throws RowWithGivenPrimaryKeyAlreadyExists
+	 * @throws GivenSearchCriteriaHaveNotMatchedAnyRows
+	 */
+	public function save(Table $table, Modifications $changes): void {
+		if ($changes->getPrimaryKey() === NULL) {
+			// INSERT
+			$this->insert($table, $changes);
+			return;
+		}
+
+		// UPDATE:
+		$this->update($table, $changes);
+	}
+
+	/**
+	 * @template TableType of Table
+	 * @param TableType $table
+	 * @param Modifications<TableType> $changes
+	 * @throws RowWithGivenPrimaryKeyAlreadyExists
+	 */
+	public function insert(Table $table, Modifications $changes): void
+	{
+		\assert($changes->getPrimaryKey() === NULL);
+
+		try {
+			$this->connection->query(
+				'INSERT',
+				'INTO %n.%n', $table::getSchema(), $table::getTableName(),
+				mapWithKeys(
+					$changes->getModifications(),
+					static fn(string $columnName, mixed $value) => $table->getTypeOf($columnName)->toDatabase($value),
+				),
+			);
+		} catch (UniqueConstraintViolationException $e) {
+			throw new RowWithGivenPrimaryKeyAlreadyExists(previous: $e);
+		}
+
+		\assert($this->connection->getAffectedRows() === 1);
+	}
+
+	/**
+	 * @template TableType of Table
+	 * @param TableType $table
+	 * @param Modifications<TableType> $changes
+	 * @throws RowWithGivenPrimaryKeyAlreadyExists
+	 */
+	public function insertAndGet(Table $table, Modifications $changes): Row
+	{
+		\assert($changes->getPrimaryKey() === NULL);
+
+		try {
+			$result = $this->connection->query(
+				'INSERT INTO %n.%n', $table::getSchema(), $table::getTableName(),
+				mapWithKeys(
+					$changes->getModifications(),
+					static fn(string $columnName, mixed $value) => $table->getTypeOf($columnName)->toDatabase($value),
+				),
+				'RETURNING *',
+			);
+		} catch (UniqueConstraintViolationException $e) {
+			throw new RowWithGivenPrimaryKeyAlreadyExists(previous: $e);
+		}
+
+		\assert($this->connection->getAffectedRows() === 1);
+
+		$dibiRow = $result->fetch();
+		\assert($dibiRow instanceof \Dibi\Row);
+
+		/** @var class-string<Row> $rowClass */
+		$rowClass = $table::getRowClass();
+		return $rowClass::reconstitute(
+			mapWithKeys(
+				$dibiRow->toArray(),
+				static fn(string $columnName, mixed $value) => $table->getTypeOf($columnName)->fromDatabase($value),
+			),
+		);
+	}
+
 	/**
 	 * @template TableType of Table
 	 * @param TableType $table
@@ -210,6 +267,135 @@ final class SingleConnectionTableManager implements TableManager
 		}
 	}
 
+	/**
+	 * @template TableType of Table
+	 * @param TableType $table
+	 * @param Modifications<TableType> $changes
+	 * @throws GivenSearchCriteriaHaveNotMatchedAnyRows
+	 */
+	public function updateAndGet(Table $table, Modifications $changes): Row
+	{
+		$primaryKey = $changes->getPrimaryKey();
+		\assert($primaryKey !== NULL);
+		$result = $this->connection->query(
+			'UPDATE %n.%n', $table::getSchema(), $table::getTableName(),
+			'SET %a',
+			mapWithKeys(
+				$changes->getModifications(),
+				static fn(string $columnName, mixed $value) => $table->getTypeOf($columnName)->toDatabase($value),
+			),
+			'WHERE %ex', $primaryKey->getCondition($table)->toSql()->getValues(),
+			'RETURNING *',
+		);
+
+		$affectedRows = $this->connection->getAffectedRows();
+		if ($affectedRows !== 1) {
+			if ($affectedRows === 0) {
+				throw new GivenSearchCriteriaHaveNotMatchedAnyRows();
+			}
+
+			throw new ProbablyBrokenPrimaryIndexImplementation($table, $affectedRows);
+		}
+
+		$dibiRow = $result->fetch();
+		\assert($dibiRow instanceof \Dibi\Row);
+
+		/** @var class-string<Row> $rowClass */
+		$rowClass = $table::getRowClass();
+		return $rowClass::reconstitute(
+			mapWithKeys(
+				$dibiRow->toArray(),
+				static fn(string $columnName, mixed $value) => $table->getTypeOf($columnName)->fromDatabase($value),
+			),
+		);
+	}
+
+	/**
+	 * @template TableType of Table
+	 * @param TableType $table
+	 * @param Condition|Condition[] $conditions
+	 * @param Modifications<TableType> $changes
+	 */
+	public function updateBy(Table $table, Condition|array $conditions, Modifications $changes): void
+	{
+		\assert($changes->getPrimaryKey() === null);
+
+		$this->connection->query(
+			'UPDATE %n.%n', $table::getSchema(), $table::getTableName(),
+			'SET %a',
+			mapWithKeys(
+				$changes->getModifications(),
+				static fn(string $columnName, mixed $value) => $table->getTypeOf($columnName)->toDatabase($value),
+			),
+			'WHERE %ex', (\is_array($conditions) ? Composite::and(...$conditions) : $conditions)->toSql()->getValues(),
+		);
+	}
+
+	/**
+	 * @template TableType of Table
+	 * @param TableType $table
+	 * @param Modifications<TableType> $changes
+	 */
+	public function upsert(Table $table, Modifications $changes): void
+	{
+		\assert($changes->getPrimaryKey() === null);
+
+		$values = mapWithKeys(
+			$changes->getModifications(),
+			static fn(string $columnName, mixed $value) => $table->getTypeOf($columnName)->toDatabase($value),
+		);
+
+		$primaryKey = $table::getPrimaryKeyClass();
+
+		$this->connection->query(
+			'INSERT INTO %n.%n', $table::getSchema(), $table::getTableName(),
+			$values,
+			'ON CONFLICT (%n)', $primaryKey::getColumnNames(),
+			'DO UPDATE SET %a', $values,
+		);
+
+		\assert($this->connection->getAffectedRows() === 1);
+	}
+
+	/**
+	 * @template TableType of Table
+	 * @param TableType $table
+	 * @param Modifications<TableType> $changes
+	 */
+	public function upsertAndGet(Table $table, Modifications $changes): Row
+	{
+		\assert($changes->getPrimaryKey() === null);
+
+		$values = mapWithKeys(
+			$changes->getModifications(),
+			static fn(string $columnName, mixed $value) => $table->getTypeOf($columnName)->toDatabase($value),
+		);
+
+		$primaryKey = $table::getPrimaryKeyClass();
+
+		$result = $this->connection->query(
+			'INSERT INTO %n.%n', $table::getSchema(), $table::getTableName(),
+			$values,
+			'ON CONFLICT (%n)', $primaryKey::getColumnNames(),
+			'DO UPDATE SET %a', $values,
+			'RETURNING *',
+		);
+
+		\assert($this->connection->getAffectedRows() === 1);
+
+		$dibiRow = $result->fetch();
+		\assert($dibiRow instanceof \Dibi\Row);
+
+		/** @var class-string<Row> $rowClass */
+		$rowClass = $table::getRowClass();
+		return $rowClass::reconstitute(
+			mapWithKeys(
+				$dibiRow->toArray(),
+				static fn(string $columnName, mixed $value) => $table->getTypeOf($columnName)->fromDatabase($value),
+			),
+		);
+	}
+
 	/**
 	 * @template TableType of Table
 	 * @param TableType $table
@@ -228,18 +414,43 @@ final class SingleConnectionTableManager implements TableManager
 	/**
 	 * @template TableType of Table
 	 * @param TableType $table
-	 * @param Modifications<TableType> $changes
-	 * @throws RowWithGivenPrimaryKeyAlreadyExists
-	 * @throws GivenSearchCriteriaHaveNotMatchedAnyRows
+	 * @param PrimaryKey<TableType> $primaryKey
 	 */
-	public function save(Table $table, Modifications $changes): void {
-		if ($changes->getPrimaryKey() === NULL) {
-			// INSERT
-			$this->insert($table, $changes);
-			return;
-		}
+	public function deleteAndGet(Table $table, PrimaryKey $primaryKey): Row
+	{
+		$result = $this->connection->query(
+			'DELETE',
+			'FROM %n.%n', $table::getSchema(), $table::getTableName(),
+			'WHERE %ex', $primaryKey->getCondition($table)->toSql()->getValues(),
+			'RETURNING *',
+		);
 
-		// UPDATE:
-		$this->update($table, $changes);
+		\assert($this->connection->getAffectedRows() === 1);
+
+		$dibiRow = $result->fetch();
+		\assert($dibiRow instanceof \Dibi\Row);
+
+		/** @var class-string<Row> $rowClass */
+		$rowClass = $table::getRowClass();
+		return $rowClass::reconstitute(
+			mapWithKeys(
+				$dibiRow->toArray(),
+				static fn(string $columnName, mixed $value) => $table->getTypeOf($columnName)->fromDatabase($value),
+			),
+		);
+	}
+
+	/**
+	 * @template TableType of Table
+	 * @param TableType $table
+	 * @param Condition|Condition[] $conditions
+	 */
+	public function deleteBy(Table $table, Condition|array $conditions): void
+	{
+		$this->connection->query(
+			'DELETE',
+			'FROM %n.%n', $table::getSchema(), $table::getTableName(),
+			'WHERE %ex', (\is_array($conditions) ? Composite::and(...$conditions) : $conditions)->toSql()->getValues(),
+		);
 	}
 }
diff --git a/src/TableManager.php b/src/TableManager.php
index 7575c1c8e0ea5acefac4a8183b093db270e7bf51..370994e863d38aff1198aada3af0de1f63e4975e 100644
--- a/src/TableManager.php
+++ b/src/TableManager.php
@@ -11,14 +11,6 @@ use Nette\Utils\Paginator;
 
 interface TableManager
 {
-	/**
-	 * @template TableType of Table
-	 * @param TableType $table
-	 * @param Modifications<TableType> $changes
-	 * @throws RowWithGivenPrimaryKeyAlreadyExists
-	 */
-	public function insert(Table $table, Modifications $changes): void;
-
 	/**
 	 * @template TableType of Table
 	 * @param TableType $table
@@ -55,6 +47,31 @@ interface TableManager
 	 */
 	public function findOneBy(Table $table, Condition|array $conditions, array $orderBy = [], bool $required = true, bool $unique = true): ?Row;
 
+	/**
+	 * @template TableType of Table
+	 * @param TableType $table
+	 * @param Modifications<TableType> $changes
+	 * @throws RowWithGivenPrimaryKeyAlreadyExists
+	 * @throws GivenSearchCriteriaHaveNotMatchedAnyRows
+	 */
+	public function save(Table $table, Modifications $changes): void;
+
+	/**
+	 * @template TableType of Table
+	 * @param TableType $table
+	 * @param Modifications<TableType> $changes
+	 * @throws RowWithGivenPrimaryKeyAlreadyExists
+	 */
+	public function insert(Table $table, Modifications $changes): void;
+
+	/**
+	 * @template TableType of Table
+	 * @param TableType $table
+	 * @param Modifications<TableType> $changes
+	 * @throws RowWithGivenPrimaryKeyAlreadyExists
+	 */
+	public function insertAndGet(Table $table, Modifications $changes): Row;
+
 	/**
 	 * @template TableType of Table
 	 * @param TableType $table
@@ -63,6 +80,36 @@ interface TableManager
 	 */
 	public function update(Table $table, Modifications $changes): void;
 
+	/**
+	 * @template TableType of Table
+	 * @param TableType $table
+	 * @param Modifications<TableType> $changes
+	 * @throws GivenSearchCriteriaHaveNotMatchedAnyRows if no rows matches given criteria
+	 */
+	public function updateAndGet(Table $table, Modifications $changes): Row;
+
+	/**
+	 * @template TableType of Table
+	 * @param TableType $table
+	 * @param Condition|Condition[] $conditions
+	 * @param Modifications<TableType> $changes
+	 */
+	public function updateBy(Table $table, Condition|array $conditions, Modifications $changes): void;
+
+	/**
+	 * @template TableType of Table
+	 * @param TableType $table
+	 * @param Modifications<TableType> $changes
+	 */
+	public function upsert(Table $table, Modifications $changes): void;
+
+	/**
+	 * @template TableType of Table
+	 * @param TableType $table
+	 * @param Modifications<TableType> $changes
+	 */
+	public function upsertAndGet(Table $table, Modifications $changes): Row;
+
 	/**
 	 * @template TableType of Table
 	 * @param TableType $table
@@ -73,9 +120,14 @@ interface TableManager
 	/**
 	 * @template TableType of Table
 	 * @param TableType $table
-	 * @param Modifications<TableType> $changes
-	 * @throws RowWithGivenPrimaryKeyAlreadyExists
-	 * @throws GivenSearchCriteriaHaveNotMatchedAnyRows
+	 * @param PrimaryKey<TableType> $primaryKey
 	 */
-	public function save(Table $table, Modifications $changes): void;
+	public function deleteAndGet(Table $table, PrimaryKey $primaryKey): Row;
+
+	/**
+	 * @template TableType of Table
+	 * @param TableType $table
+	 * @param Condition|Condition[] $conditions
+	 */
+	public function deleteBy(Table $table, Condition|array $conditions): void;
 }
diff --git a/tests/BulkTableTest.phpt b/tests/BulkTableTest.phpt
new file mode 100644
index 0000000000000000000000000000000000000000..577c20b7b7a21ebd2c21fac7c57a89f7a15ce916
--- /dev/null
+++ b/tests/BulkTableTest.phpt
@@ -0,0 +1,52 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Grifart\Tables\Tests;
+
+use Grifart\Tables\RowNotFound;
+use Grifart\Tables\Tests\Fixtures\BulkModifications;
+use Grifart\Tables\Tests\Fixtures\BulkTable;
+use Grifart\Tables\Tests\Fixtures\TestFixtures;
+use Grifart\Tables\TooManyRowsFound;
+use Tester\Assert;
+use function Grifart\Tables\Conditions\lesserThan;
+
+require __DIR__ . '/bootstrap.php';
+
+$connection = connect();
+
+$connection->nativeQuery("TRUNCATE TABLE public.bulk");
+$connection->nativeQuery("INSERT INTO public.bulk (id, value) VALUES ('2e166649-da0f-4c0e-bc3a-4759aac50092', 42), ('6c554e1c-6be0-4d52-87e2-602782bba59e', -5), ('a7723ed8-ec2e-4e06-9a84-7da20532103e', 0);");
+
+$table = new BulkTable(
+	TestFixtures::createTableManager($connection),
+	TestFixtures::createTypeResolver($connection),
+);
+
+[$a, $b, $c] = $table->getAll([$table->value()->ascending()]);
+Assert::same(-5, $a->getValue());
+Assert::same(0, $b->getValue());
+Assert::same(42, $c->getValue());
+
+$changes = BulkModifications::new();
+$changes->modifyFlagged(true);
+
+$table->updateBy(
+	$table->value()->is(lesserThan(0)),
+	$changes,
+);
+
+[$a, $b, $c] = $table->getAll([$table->value()->ascending()]);
+Assert::true($a->getFlagged());
+Assert::false($b->getFlagged());
+Assert::false($c->getFlagged());
+
+$table->deleteBy(
+	$table->flagged()->is(true),
+);
+
+$all = $table->getAll([$table->value()->ascending()]);
+Assert::count(2, $all);
+Assert::same(0, $all[0]->getValue());
+Assert::same(42, $all[1]->getValue());
diff --git a/tests/Fixtures/.definition.php b/tests/Fixtures/.definition.php
index 155eba0013b2416eaa2b2634d37c2c72653c0a57..515a14db9d0580fcc50ccf83765dd9033c6d643c 100644
--- a/tests/Fixtures/.definition.php
+++ b/tests/Fixtures/.definition.php
@@ -48,4 +48,12 @@ return [
 		GeneratedTable::class,
 		GeneratedPrimaryKey::class,
 	),
+	...$tableDefinitions->for(
+		'public',
+		'bulk',
+		BulkRow::class,
+		BulkModifications::class,
+		BulkTable::class,
+		BulkPrimaryKey::class,
+	),
 ];
diff --git a/tests/Fixtures/BulkModifications.php b/tests/Fixtures/BulkModifications.php
new file mode 100644
index 0000000000000000000000000000000000000000..21ae1810bc8cf35bca36530c12fbe1b2789dc43e
--- /dev/null
+++ b/tests/Fixtures/BulkModifications.php
@@ -0,0 +1,56 @@
+<?php
+
+/**
+ * Do not edit. This is generated file. Modify definition file instead.
+ */
+
+declare(strict_types=1);
+
+namespace Grifart\Tables\Tests\Fixtures;
+
+use Grifart\Tables\Modifications;
+use Grifart\Tables\ModificationsTrait;
+
+/**
+ * @implements Modifications<BulkTable>
+ */
+final class BulkModifications implements Modifications
+{
+	/** @use ModificationsTrait<BulkTable> */
+	use ModificationsTrait;
+
+	public static function update(BulkPrimaryKey $primaryKey): self
+	{
+		return self::_update($primaryKey);
+	}
+
+
+	public static function new(): self
+	{
+		return self::_new();
+	}
+
+
+	public static function forTable(): string
+	{
+		return BulkTable::class;
+	}
+
+
+	public function modifyId(Uuid $id): void
+	{
+		$this->modifications['id'] = $id;
+	}
+
+
+	public function modifyValue(int $value): void
+	{
+		$this->modifications['value'] = $value;
+	}
+
+
+	public function modifyFlagged(bool $flagged): void
+	{
+		$this->modifications['flagged'] = $flagged;
+	}
+}
diff --git a/tests/Fixtures/BulkPrimaryKey.php b/tests/Fixtures/BulkPrimaryKey.php
new file mode 100644
index 0000000000000000000000000000000000000000..38f4abae608298b7fe9e96cbd704451d98ecf9ae
--- /dev/null
+++ b/tests/Fixtures/BulkPrimaryKey.php
@@ -0,0 +1,61 @@
+<?php
+
+/**
+ * Do not edit. This is generated file. Modify definition file instead.
+ */
+
+declare(strict_types=1);
+
+namespace Grifart\Tables\Tests\Fixtures;
+
+use Grifart\Tables\Conditions\Composite;
+use Grifart\Tables\Conditions\Condition;
+use Grifart\Tables\PrimaryKey;
+use Grifart\Tables\Table;
+use function Grifart\Tables\Conditions\equalTo;
+
+/**
+ * @implements PrimaryKey<BulkTable>
+ */
+final class BulkPrimaryKey implements PrimaryKey
+{
+	private function __construct(
+		private Uuid $id,
+	) {
+	}
+
+
+	public static function from(Uuid $id): self
+	{
+		return new self($id);
+	}
+
+
+	public static function fromRow(BulkRow $row): self
+	{
+		return self::from($row->getId());
+	}
+
+
+	/**
+	 * @return string[]
+	 */
+	public static function getColumnNames(): array
+	{
+		return ['id'];
+	}
+
+
+	public function getCondition(Table $table): Condition
+	{
+		return Composite::and(
+			$table->id()->is(equalTo($this->id)),
+		);
+	}
+
+
+	public function getId(): Uuid
+	{
+		return $this->id;
+	}
+}
diff --git a/tests/Fixtures/BulkRow.php b/tests/Fixtures/BulkRow.php
new file mode 100644
index 0000000000000000000000000000000000000000..30853abc487f98ecf529f323c029951da2f2fa7c
--- /dev/null
+++ b/tests/Fixtures/BulkRow.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * Do not edit. This is generated file. Modify definition file instead.
+ */
+
+declare(strict_types=1);
+
+namespace Grifart\Tables\Tests\Fixtures;
+
+use Grifart\Tables\Row;
+
+final class BulkRow implements Row
+{
+	private function __construct(
+		private Uuid $id,
+		private int $value,
+		private bool $flagged,
+	) {
+	}
+
+
+	public function getId(): Uuid
+	{
+		return $this->id;
+	}
+
+
+	public function getValue(): int
+	{
+		return $this->value;
+	}
+
+
+	public function getFlagged(): bool
+	{
+		return $this->flagged;
+	}
+
+
+	public static function reconstitute(array $values): static
+	{
+		/** @var array{id: Uuid, value: int, flagged: bool} $values */
+		return new static($values['id'], $values['value'], $values['flagged']);
+	}
+}
diff --git a/tests/Fixtures/BulkTable.php b/tests/Fixtures/BulkTable.php
new file mode 100644
index 0000000000000000000000000000000000000000..6d1038025f2464e2e23a2f4529b7a626a42bd804
--- /dev/null
+++ b/tests/Fixtures/BulkTable.php
@@ -0,0 +1,378 @@
+<?php
+
+/**
+ * Do not edit. This is generated file. Modify definition file instead.
+ */
+
+declare(strict_types=1);
+
+namespace Grifart\Tables\Tests\Fixtures;
+
+use Grifart\Tables\Column;
+use Grifart\Tables\ColumnMetadata;
+use Grifart\Tables\ColumnNotFound;
+use Grifart\Tables\Conditions\Condition;
+use Grifart\Tables\DefaultOrExistingValue;
+use Grifart\Tables\Expression;
+use Grifart\Tables\GivenSearchCriteriaHaveNotMatchedAnyRows;
+use Grifart\Tables\OrderBy\OrderBy;
+use Grifart\Tables\RowNotFound;
+use Grifart\Tables\RowWithGivenPrimaryKeyAlreadyExists;
+use Grifart\Tables\Table;
+use Grifart\Tables\TableManager;
+use Grifart\Tables\TooManyRowsFound;
+use Grifart\Tables\Type;
+use Grifart\Tables\TypeResolver;
+use Nette\Utils\Paginator;
+
+final class BulkTable implements Table
+{
+	public const ID = 'id';
+	public const VALUE = 'value';
+	public const FLAGGED = 'flagged';
+
+	/** @var array{id: Column<self, Uuid>, value: Column<self, int>, flagged: Column<self, bool>} */
+	private array $columns;
+
+
+	public static function getSchema(): string
+	{
+		return 'public';
+	}
+
+
+	public static function getTableName(): string
+	{
+		return 'bulk';
+	}
+
+
+	public static function getPrimaryKeyClass(): string
+	{
+		return BulkPrimaryKey::class;
+	}
+
+
+	public static function getRowClass(): string
+	{
+		return BulkRow::class;
+	}
+
+
+	public static function getModificationClass(): string
+	{
+		return BulkModifications::class;
+	}
+
+
+	/**
+	 * @return ColumnMetadata[]
+	 */
+	public static function getDatabaseColumns(): array
+	{
+		return [
+			'id' => new ColumnMetadata('id', 'uuid', false, false, false),
+			'value' => new ColumnMetadata('value', 'integer', false, false, false),
+			'flagged' => new ColumnMetadata('flagged', 'boolean', false, true, false)
+		];
+	}
+
+
+	public function find(BulkPrimaryKey $primaryKey): ?BulkRow
+	{
+		$row = $this->tableManager->find($this, $primaryKey, required: false);
+		\assert($row instanceof BulkRow || $row === null);
+		return $row;
+	}
+
+
+	/**
+	 * @throws RowNotFound
+	 */
+	public function get(BulkPrimaryKey $primaryKey): BulkRow
+	{
+		$row = $this->tableManager->find($this, $primaryKey, required: true);
+		\assert($row instanceof BulkRow);
+		return $row;
+	}
+
+
+	/**
+	 * @param OrderBy[] $orderBy
+	 * @return BulkRow[]
+	 */
+	public function getAll(array $orderBy = [], ?Paginator $paginator = null): array
+	{
+		/** @var BulkRow[] $result */
+		$result = $this->tableManager->getAll($this, $orderBy, $paginator);
+		return $result;
+	}
+
+
+	/**
+	 * @param Condition|Condition[] $conditions
+	 * @param array<OrderBy|Expression<mixed>> $orderBy
+	 * @return BulkRow[]
+	 */
+	public function findBy(Condition|array $conditions, array $orderBy = [], ?Paginator $paginator = null): array
+	{
+		/** @var BulkRow[] $result */
+		$result = $this->tableManager->findBy($this, $conditions, $orderBy, $paginator);
+		return $result;
+	}
+
+
+	/**
+	 * @param Condition|Condition[] $conditions
+	 * @return BulkRow
+	 * @throws RowNotFound
+	 */
+	public function getUniqueBy(Condition|array $conditions): BulkRow
+	{
+		$row = $this->tableManager->findOneBy($this, $conditions, required: true, unique: true);
+		\assert($row instanceof BulkRow);
+		return $row;
+	}
+
+
+	/**
+	 * @param Condition|Condition[] $conditions
+	 * @return BulkRow|null
+	 * @throws RowNotFound
+	 */
+	public function findUniqueBy(Condition|array $conditions): ?BulkRow
+	{
+		$row = $this->tableManager->findOneBy($this, $conditions, required: false, unique: true);
+		\assert($row instanceof BulkRow || $row === null);
+		return $row;
+	}
+
+
+	/**
+	 * @param Condition|Condition[] $conditions
+	 * @param array<OrderBy|Expression<mixed>> $orderBy
+	 * @return BulkRow
+	 * @throws RowNotFound
+	 */
+	public function getFirstBy(Condition|array $conditions, array $orderBy = []): BulkRow
+	{
+		$row = $this->tableManager->findOneBy($this, $conditions, $orderBy, required: true, unique: false);
+		\assert($row instanceof BulkRow);
+		return $row;
+	}
+
+
+	/**
+	 * @param Condition|Condition[] $conditions
+	 * @param array<OrderBy|Expression<mixed>> $orderBy
+	 * @return BulkRow|null
+	 */
+	public function findFirstBy(Condition|array $conditions, array $orderBy = []): ?BulkRow
+	{
+		$row = $this->tableManager->findOneBy($this, $conditions, $orderBy, required: false, unique: false);
+		\assert($row instanceof BulkRow || $row === null);
+		return $row;
+	}
+
+
+	/**
+	 * @param Condition|Condition[] $conditions
+	 * @return BulkRow
+	 * @throws RowNotFound
+	 */
+	#[\Deprecated('Use getUniqueBy() instead.')]
+	public function getBy(Condition|array $conditions): BulkRow
+	{
+		return $this->getUniqueBy($conditions);
+	}
+
+
+	public function new(
+		Uuid $id,
+		int $value,
+		bool|DefaultOrExistingValue $flagged = \Grifart\Tables\DefaultValue,
+	): BulkModifications
+	{
+		$modifications = BulkModifications::new();
+		$modifications->modifyId($id);
+		$modifications->modifyValue($value);
+		if (!$flagged instanceof DefaultOrExistingValue) {
+			$modifications->modifyFlagged($flagged);
+		}
+		return $modifications;
+	}
+
+
+	public function edit(
+		BulkRow|BulkPrimaryKey $rowOrKey,
+		Uuid|DefaultOrExistingValue $id = \Grifart\Tables\Unchanged,
+		int|DefaultOrExistingValue $value = \Grifart\Tables\Unchanged,
+		bool|DefaultOrExistingValue $flagged = \Grifart\Tables\Unchanged,
+	): BulkModifications
+	{
+		$primaryKey = $rowOrKey instanceof BulkPrimaryKey ? $rowOrKey : BulkPrimaryKey::fromRow($rowOrKey);
+		$modifications = BulkModifications::update($primaryKey);
+		if (!$id instanceof DefaultOrExistingValue) {
+			$modifications->modifyId($id);
+		}
+		if (!$value instanceof DefaultOrExistingValue) {
+			$modifications->modifyValue($value);
+		}
+		if (!$flagged instanceof DefaultOrExistingValue) {
+			$modifications->modifyFlagged($flagged);
+		}
+		return $modifications;
+	}
+
+
+	/**
+	 * @throws RowWithGivenPrimaryKeyAlreadyExists
+	 * @throws GivenSearchCriteriaHaveNotMatchedAnyRows
+	 */
+	public function save(BulkModifications $changes): void
+	{
+		$this->tableManager->save($this, $changes);
+	}
+
+
+	/**
+	 * @throws RowWithGivenPrimaryKeyAlreadyExists
+	 */
+	public function insert(BulkModifications $changes): void
+	{
+		$this->tableManager->insert($this, $changes);
+	}
+
+
+	/**
+	 * @throws RowWithGivenPrimaryKeyAlreadyExists
+	 */
+	public function insertAndGet(BulkModifications $changes): BulkRow
+	{
+		$row = $this->tableManager->insertAndGet($this, $changes);
+		\assert($row instanceof BulkRow);
+		return $row;
+	}
+
+
+	/**
+	 * @throws GivenSearchCriteriaHaveNotMatchedAnyRows
+	 */
+	public function update(BulkModifications $changes): void
+	{
+		$this->tableManager->update($this, $changes);
+	}
+
+
+	/**
+	 * @throws GivenSearchCriteriaHaveNotMatchedAnyRows
+	 */
+	public function updateAndGet(BulkModifications $changes): BulkRow
+	{
+		$row = $this->tableManager->updateAndGet($this, $changes);
+		\assert($row instanceof BulkRow);
+		return $row;
+	}
+
+
+	/**
+	 * @param Condition|Condition[] $conditions
+	 */
+	public function updateBy(Condition|array $conditions, BulkModifications $changes): void
+	{
+		$this->tableManager->updateBy($this, $conditions, $changes);
+	}
+
+
+	public function upsert(BulkModifications $changes): void
+	{
+		$this->tableManager->upsert($this, $changes);
+	}
+
+
+	public function upsertAndGet(BulkModifications $changes): BulkRow
+	{
+		$row = $this->tableManager->upsertAndGet($this, $changes);
+		\assert($row instanceof BulkRow);
+		return $row;
+	}
+
+
+	public function delete(BulkRow|BulkPrimaryKey $rowOrKey): void
+	{
+		$primaryKey = $rowOrKey instanceof BulkPrimaryKey ? $rowOrKey : BulkPrimaryKey::fromRow($rowOrKey);
+		$this->tableManager->delete($this, $primaryKey);
+	}
+
+
+	public function deleteAndGet(BulkRow|BulkPrimaryKey $rowOrKey): BulkRow
+	{
+		$primaryKey = $rowOrKey instanceof BulkPrimaryKey ? $rowOrKey : BulkPrimaryKey::fromRow($rowOrKey);
+		$row = $this->tableManager->deleteAndGet($this, $primaryKey);
+		\assert($row instanceof BulkRow);
+		return $row;
+	}
+
+
+	/**
+	 * @param Condition|Condition[] $conditions
+	 */
+	public function deleteBy(Condition|array $conditions): void
+	{
+		$this->tableManager->deleteBy($this, $conditions);
+	}
+
+
+	public function __construct(
+		private TableManager $tableManager,
+		private TypeResolver $typeResolver,
+	) {
+		/** @var Column<self, Uuid> $id */
+		$id = Column::from($this, self::getDatabaseColumns()['id'], $this->typeResolver);
+		/** @var Column<self, int> $value */
+		$value = Column::from($this, self::getDatabaseColumns()['value'], $this->typeResolver);
+		/** @var Column<self, bool> $flagged */
+		$flagged = Column::from($this, self::getDatabaseColumns()['flagged'], $this->typeResolver);
+		$this->columns = ['id' => $id, 'value' => $value, 'flagged' => $flagged];
+	}
+
+
+	/**
+	 * @return Column<self, Uuid>
+	 */
+	public function id(): Column
+	{
+		return $this->columns['id'];
+	}
+
+
+	/**
+	 * @return Column<self, int>
+	 */
+	public function value(): Column
+	{
+		return $this->columns['value'];
+	}
+
+
+	/**
+	 * @return Column<self, bool>
+	 */
+	public function flagged(): Column
+	{
+		return $this->columns['flagged'];
+	}
+
+
+	/**
+	 * @internal
+	 * @return Type<mixed>
+	 */
+	public function getTypeOf(string $columnName): Type
+	{
+		$column = $this->columns[$columnName] ?? throw ColumnNotFound::of($columnName, \get_class($this));
+		/** @var Type<mixed> $type */
+		$type = $column->getType();
+		return $type;
+	}
+}
diff --git a/tests/Fixtures/ConfigPrimaryKey.php b/tests/Fixtures/ConfigPrimaryKey.php
index 1200f310109a73c4b305556838112b90938bc8c4..b6067a1160f3dd7304224321a075ee932fccfeef 100644
--- a/tests/Fixtures/ConfigPrimaryKey.php
+++ b/tests/Fixtures/ConfigPrimaryKey.php
@@ -37,6 +37,15 @@ final class ConfigPrimaryKey implements PrimaryKey
 	}
 
 
+	/**
+	 * @return string[]
+	 */
+	public static function getColumnNames(): array
+	{
+		return ['id'];
+	}
+
+
 	public function getCondition(Table $table): Condition
 	{
 		return Composite::and(
diff --git a/tests/Fixtures/ConfigTable.php b/tests/Fixtures/ConfigTable.php
index 8ab3b8bcaa6c9aa727d2ed187cd2eeb681dab589..5c5f4a2955c12f8632b78aac9d111f49b83cf78b 100644
--- a/tests/Fixtures/ConfigTable.php
+++ b/tests/Fixtures/ConfigTable.php
@@ -238,6 +238,17 @@ final class ConfigTable implements Table
 	}
 
 
+	/**
+	 * @throws RowWithGivenPrimaryKeyAlreadyExists
+	 */
+	public function insertAndGet(ConfigModifications $changes): ConfigRow
+	{
+		$row = $this->tableManager->insertAndGet($this, $changes);
+		\assert($row instanceof ConfigRow);
+		return $row;
+	}
+
+
 	/**
 	 * @throws GivenSearchCriteriaHaveNotMatchedAnyRows
 	 */
@@ -247,6 +258,40 @@ final class ConfigTable implements Table
 	}
 
 
+	/**
+	 * @throws GivenSearchCriteriaHaveNotMatchedAnyRows
+	 */
+	public function updateAndGet(ConfigModifications $changes): ConfigRow
+	{
+		$row = $this->tableManager->updateAndGet($this, $changes);
+		\assert($row instanceof ConfigRow);
+		return $row;
+	}
+
+
+	/**
+	 * @param Condition|Condition[] $conditions
+	 */
+	public function updateBy(Condition|array $conditions, ConfigModifications $changes): void
+	{
+		$this->tableManager->updateBy($this, $conditions, $changes);
+	}
+
+
+	public function upsert(ConfigModifications $changes): void
+	{
+		$this->tableManager->upsert($this, $changes);
+	}
+
+
+	public function upsertAndGet(ConfigModifications $changes): ConfigRow
+	{
+		$row = $this->tableManager->upsertAndGet($this, $changes);
+		\assert($row instanceof ConfigRow);
+		return $row;
+	}
+
+
 	public function delete(ConfigRow|ConfigPrimaryKey $rowOrKey): void
 	{
 		$primaryKey = $rowOrKey instanceof ConfigPrimaryKey ? $rowOrKey : ConfigPrimaryKey::fromRow($rowOrKey);
@@ -254,6 +299,24 @@ final class ConfigTable implements Table
 	}
 
 
+	public function deleteAndGet(ConfigRow|ConfigPrimaryKey $rowOrKey): ConfigRow
+	{
+		$primaryKey = $rowOrKey instanceof ConfigPrimaryKey ? $rowOrKey : ConfigPrimaryKey::fromRow($rowOrKey);
+		$row = $this->tableManager->deleteAndGet($this, $primaryKey);
+		\assert($row instanceof ConfigRow);
+		return $row;
+	}
+
+
+	/**
+	 * @param Condition|Condition[] $conditions
+	 */
+	public function deleteBy(Condition|array $conditions): void
+	{
+		$this->tableManager->deleteBy($this, $conditions);
+	}
+
+
 	public function __construct(
 		private TableManager $tableManager,
 		private TypeResolver $typeResolver,
diff --git a/tests/Fixtures/GeneratedPrimaryKey.php b/tests/Fixtures/GeneratedPrimaryKey.php
index d11b63f45c8f328ea51f2e5edc4eba7d0bbb3d80..472ce03af5ec63fac0fd6a0442e27ca8754902f8 100644
--- a/tests/Fixtures/GeneratedPrimaryKey.php
+++ b/tests/Fixtures/GeneratedPrimaryKey.php
@@ -37,6 +37,15 @@ final class GeneratedPrimaryKey implements PrimaryKey
 	}
 
 
+	/**
+	 * @return string[]
+	 */
+	public static function getColumnNames(): array
+	{
+		return ['id'];
+	}
+
+
 	public function getCondition(Table $table): Condition
 	{
 		return Composite::and(
diff --git a/tests/Fixtures/GeneratedTable.php b/tests/Fixtures/GeneratedTable.php
index 71a7011bdb90daaf246e93a8e1d1c37bc676431c..f5631b36750ae0be52333980e3050f0df6ec68fb 100644
--- a/tests/Fixtures/GeneratedTable.php
+++ b/tests/Fixtures/GeneratedTable.php
@@ -228,6 +228,17 @@ final class GeneratedTable implements Table
 	}
 
 
+	/**
+	 * @throws RowWithGivenPrimaryKeyAlreadyExists
+	 */
+	public function insertAndGet(GeneratedModifications $changes): GeneratedRow
+	{
+		$row = $this->tableManager->insertAndGet($this, $changes);
+		\assert($row instanceof GeneratedRow);
+		return $row;
+	}
+
+
 	/**
 	 * @throws GivenSearchCriteriaHaveNotMatchedAnyRows
 	 */
@@ -237,6 +248,40 @@ final class GeneratedTable implements Table
 	}
 
 
+	/**
+	 * @throws GivenSearchCriteriaHaveNotMatchedAnyRows
+	 */
+	public function updateAndGet(GeneratedModifications $changes): GeneratedRow
+	{
+		$row = $this->tableManager->updateAndGet($this, $changes);
+		\assert($row instanceof GeneratedRow);
+		return $row;
+	}
+
+
+	/**
+	 * @param Condition|Condition[] $conditions
+	 */
+	public function updateBy(Condition|array $conditions, GeneratedModifications $changes): void
+	{
+		$this->tableManager->updateBy($this, $conditions, $changes);
+	}
+
+
+	public function upsert(GeneratedModifications $changes): void
+	{
+		$this->tableManager->upsert($this, $changes);
+	}
+
+
+	public function upsertAndGet(GeneratedModifications $changes): GeneratedRow
+	{
+		$row = $this->tableManager->upsertAndGet($this, $changes);
+		\assert($row instanceof GeneratedRow);
+		return $row;
+	}
+
+
 	public function delete(GeneratedRow|GeneratedPrimaryKey $rowOrKey): void
 	{
 		$primaryKey = $rowOrKey instanceof GeneratedPrimaryKey ? $rowOrKey : GeneratedPrimaryKey::fromRow($rowOrKey);
@@ -244,6 +289,24 @@ final class GeneratedTable implements Table
 	}
 
 
+	public function deleteAndGet(GeneratedRow|GeneratedPrimaryKey $rowOrKey): GeneratedRow
+	{
+		$primaryKey = $rowOrKey instanceof GeneratedPrimaryKey ? $rowOrKey : GeneratedPrimaryKey::fromRow($rowOrKey);
+		$row = $this->tableManager->deleteAndGet($this, $primaryKey);
+		\assert($row instanceof GeneratedRow);
+		return $row;
+	}
+
+
+	/**
+	 * @param Condition|Condition[] $conditions
+	 */
+	public function deleteBy(Condition|array $conditions): void
+	{
+		$this->tableManager->deleteBy($this, $conditions);
+	}
+
+
 	public function __construct(
 		private TableManager $tableManager,
 		private TypeResolver $typeResolver,
diff --git a/tests/Fixtures/PackagePrimaryKey.php b/tests/Fixtures/PackagePrimaryKey.php
index 931dec420a653b76f74ca7610dc3047d55db47ea..6430ee5f1cbd53f1edc4235d34aa9916347128fb 100644
--- a/tests/Fixtures/PackagePrimaryKey.php
+++ b/tests/Fixtures/PackagePrimaryKey.php
@@ -37,6 +37,15 @@ final class PackagePrimaryKey implements PrimaryKey
 	}
 
 
+	/**
+	 * @return string[]
+	 */
+	public static function getColumnNames(): array
+	{
+		return ['name'];
+	}
+
+
 	public function getCondition(Table $table): Condition
 	{
 		return Composite::and(
diff --git a/tests/Fixtures/PackagesTable.php b/tests/Fixtures/PackagesTable.php
index c60e1ebd157b8d78fd979aeb6da1a7a7731706c9..640f5aa2397cde5ffec11ba61311c6a2a79fe3ea 100644
--- a/tests/Fixtures/PackagesTable.php
+++ b/tests/Fixtures/PackagesTable.php
@@ -246,6 +246,17 @@ final class PackagesTable implements Table
 	}
 
 
+	/**
+	 * @throws RowWithGivenPrimaryKeyAlreadyExists
+	 */
+	public function insertAndGet(PackageModifications $changes): PackageRow
+	{
+		$row = $this->tableManager->insertAndGet($this, $changes);
+		\assert($row instanceof PackageRow);
+		return $row;
+	}
+
+
 	/**
 	 * @throws GivenSearchCriteriaHaveNotMatchedAnyRows
 	 */
@@ -255,6 +266,40 @@ final class PackagesTable implements Table
 	}
 
 
+	/**
+	 * @throws GivenSearchCriteriaHaveNotMatchedAnyRows
+	 */
+	public function updateAndGet(PackageModifications $changes): PackageRow
+	{
+		$row = $this->tableManager->updateAndGet($this, $changes);
+		\assert($row instanceof PackageRow);
+		return $row;
+	}
+
+
+	/**
+	 * @param Condition|Condition[] $conditions
+	 */
+	public function updateBy(Condition|array $conditions, PackageModifications $changes): void
+	{
+		$this->tableManager->updateBy($this, $conditions, $changes);
+	}
+
+
+	public function upsert(PackageModifications $changes): void
+	{
+		$this->tableManager->upsert($this, $changes);
+	}
+
+
+	public function upsertAndGet(PackageModifications $changes): PackageRow
+	{
+		$row = $this->tableManager->upsertAndGet($this, $changes);
+		\assert($row instanceof PackageRow);
+		return $row;
+	}
+
+
 	public function delete(PackageRow|PackagePrimaryKey $rowOrKey): void
 	{
 		$primaryKey = $rowOrKey instanceof PackagePrimaryKey ? $rowOrKey : PackagePrimaryKey::fromRow($rowOrKey);
@@ -262,6 +307,24 @@ final class PackagesTable implements Table
 	}
 
 
+	public function deleteAndGet(PackageRow|PackagePrimaryKey $rowOrKey): PackageRow
+	{
+		$primaryKey = $rowOrKey instanceof PackagePrimaryKey ? $rowOrKey : PackagePrimaryKey::fromRow($rowOrKey);
+		$row = $this->tableManager->deleteAndGet($this, $primaryKey);
+		\assert($row instanceof PackageRow);
+		return $row;
+	}
+
+
+	/**
+	 * @param Condition|Condition[] $conditions
+	 */
+	public function deleteBy(Condition|array $conditions): void
+	{
+		$this->tableManager->deleteBy($this, $conditions);
+	}
+
+
 	public function __construct(
 		private TableManager $tableManager,
 		private TypeResolver $typeResolver,
diff --git a/tests/Fixtures/TestFixtures.php b/tests/Fixtures/TestFixtures.php
index a0f18aa620c31a3d26fe0508b4a65f506a6e7487..0e946f587e6eb9be19e81cc4c33748c9487b4e7e 100644
--- a/tests/Fixtures/TestFixtures.php
+++ b/tests/Fixtures/TestFixtures.php
@@ -26,6 +26,7 @@ final class TestFixtures
 		$typeResolver = new TypeResolver($connection);
 		$typeResolver->addResolutionByLocation(new Identifier('public', 'test', 'id'), new UuidType());
 		$typeResolver->addResolutionByLocation(new Identifier('public', 'test', 'score'), IntType::integer());
+		$typeResolver->addResolutionByLocation(new Identifier('public', 'bulk', 'id'), new UuidType());
 		$typeResolver->addResolutionByLocation(new Identifier('public', 'config', 'id'), new UuidType());
 		$typeResolver->addResolutionByLocation(new Identifier('public', 'package', 'version'), new TupleVersionType());
 		$typeResolver->addResolutionByLocation(new Identifier('public', 'package', 'previousVersions'), ArrayType::of(new VersionType()));
diff --git a/tests/Fixtures/TestPrimaryKey.php b/tests/Fixtures/TestPrimaryKey.php
index 7e6569881ddef5b9d431d62936e4850d78f5dd05..ace89d418e60ba72e0096b89be5bea64043603dc 100644
--- a/tests/Fixtures/TestPrimaryKey.php
+++ b/tests/Fixtures/TestPrimaryKey.php
@@ -37,6 +37,15 @@ final class TestPrimaryKey implements PrimaryKey
 	}
 
 
+	/**
+	 * @return string[]
+	 */
+	public static function getColumnNames(): array
+	{
+		return ['id'];
+	}
+
+
 	public function getCondition(Table $table): Condition
 	{
 		return Composite::and(
diff --git a/tests/Fixtures/TestsTable.php b/tests/Fixtures/TestsTable.php
index a5660bc9bb02944620646285ff7f18753410786a..0359c0d94d5e78b933bf4daaa279b950af66f61c 100644
--- a/tests/Fixtures/TestsTable.php
+++ b/tests/Fixtures/TestsTable.php
@@ -244,6 +244,17 @@ final class TestsTable implements Table
 	}
 
 
+	/**
+	 * @throws RowWithGivenPrimaryKeyAlreadyExists
+	 */
+	public function insertAndGet(TestModifications $changes): TestRow
+	{
+		$row = $this->tableManager->insertAndGet($this, $changes);
+		\assert($row instanceof TestRow);
+		return $row;
+	}
+
+
 	/**
 	 * @throws GivenSearchCriteriaHaveNotMatchedAnyRows
 	 */
@@ -253,6 +264,40 @@ final class TestsTable implements Table
 	}
 
 
+	/**
+	 * @throws GivenSearchCriteriaHaveNotMatchedAnyRows
+	 */
+	public function updateAndGet(TestModifications $changes): TestRow
+	{
+		$row = $this->tableManager->updateAndGet($this, $changes);
+		\assert($row instanceof TestRow);
+		return $row;
+	}
+
+
+	/**
+	 * @param Condition|Condition[] $conditions
+	 */
+	public function updateBy(Condition|array $conditions, TestModifications $changes): void
+	{
+		$this->tableManager->updateBy($this, $conditions, $changes);
+	}
+
+
+	public function upsert(TestModifications $changes): void
+	{
+		$this->tableManager->upsert($this, $changes);
+	}
+
+
+	public function upsertAndGet(TestModifications $changes): TestRow
+	{
+		$row = $this->tableManager->upsertAndGet($this, $changes);
+		\assert($row instanceof TestRow);
+		return $row;
+	}
+
+
 	public function delete(TestRow|TestPrimaryKey $rowOrKey): void
 	{
 		$primaryKey = $rowOrKey instanceof TestPrimaryKey ? $rowOrKey : TestPrimaryKey::fromRow($rowOrKey);
@@ -260,6 +305,24 @@ final class TestsTable implements Table
 	}
 
 
+	public function deleteAndGet(TestRow|TestPrimaryKey $rowOrKey): TestRow
+	{
+		$primaryKey = $rowOrKey instanceof TestPrimaryKey ? $rowOrKey : TestPrimaryKey::fromRow($rowOrKey);
+		$row = $this->tableManager->deleteAndGet($this, $primaryKey);
+		\assert($row instanceof TestRow);
+		return $row;
+	}
+
+
+	/**
+	 * @param Condition|Condition[] $conditions
+	 */
+	public function deleteBy(Condition|array $conditions): void
+	{
+		$this->tableManager->deleteBy($this, $conditions);
+	}
+
+
 	public function __construct(
 		private TableManager $tableManager,
 		private TypeResolver $typeResolver,
diff --git a/tests/Scaffolding/ScaffoldingTest.phpt b/tests/Scaffolding/ScaffoldingTest.phpt
index c484dcbca5bf23ba1b7c0ebba4b35f3ba2994385..bf13af98d27b8af98b3403ad5090a3888a9c5e0b 100644
--- a/tests/Scaffolding/ScaffoldingTest.phpt
+++ b/tests/Scaffolding/ScaffoldingTest.phpt
@@ -34,5 +34,5 @@ $results = $fileProcessor->processFile(
 	},
 );
 
-Assert::count(17, $results->getDefinitions());
+Assert::count(21, $results->getDefinitions());
 Assert::true($results->isSuccessful());
diff --git a/tests/TableTest.php b/tests/TableTest.php
index 791cbeec8118e15e62bebf8b7e00f49033b4b21f..d28a618dac2ee9e7db367b19f8ee6c5b9a1ebe80 100644
--- a/tests/TableTest.php
+++ b/tests/TableTest.php
@@ -6,6 +6,8 @@ namespace Grifart\Tables\Tests;
 
 use Grifart\Tables\Expression;
 use Grifart\Tables\OrderBy\Nulls;
+use Grifart\Tables\RowNotFound;
+use Grifart\Tables\RowWithGivenPrimaryKeyAlreadyExists;
 use Grifart\Tables\Tests\Fixtures\TestFixtures;
 use Grifart\Tables\Tests\Fixtures\TestPrimaryKey;
 use Grifart\Tables\Tests\Fixtures\TestsTable;
@@ -127,3 +129,25 @@ Assert::same('nada', $updatedZero->getDetails());
 
 $table->delete(TestPrimaryKey::fromRow($updatedZero));
 Assert::null($table->find(TestPrimaryKey::fromRow($updatedZero)));
+
+$newRow = $table->insertAndGet($table->new($id = new Uuid('7ec810dd-4d52-4bb9-ae96-6f558ee4890f'), 7));
+Assert::same(7, $newRow->getScore());
+
+$updatedRow = $table->updateAndGet($table->edit($newRow, score: -7));
+Assert::same(-7, $updatedRow->getScore());
+
+// upsert
+
+Assert::throws(fn() => $table->insert($table->new($id, 11)), RowWithGivenPrimaryKeyAlreadyExists::class);
+
+$table->upsert($table->new($id, 17));
+Assert::same(17, $table->get(TestPrimaryKey::from($id))->getScore());
+
+$upsertedRow = $table->upsertAndGet($table->new($id, 11));
+Assert::same(11, $upsertedRow->getScore());
+
+// deleteAndGet
+
+$deleted = $table->deleteAndGet(TestPrimaryKey::fromRow($upsertedRow));
+Assert::same(11, $deleted->getScore());
+Assert::throws(fn() => $table->get(TestPrimaryKey::fromRow($deleted)), RowNotFound::class);
diff --git a/tests/initializeDatabase.php b/tests/initializeDatabase.php
index 7f388be4f30b7bc835b452ca41176609d2e15772..0b195964b996f7d685dc923700fa8c7b59db127b 100644
--- a/tests/initializeDatabase.php
+++ b/tests/initializeDatabase.php
@@ -55,3 +55,11 @@ CREATE TABLE IF NOT EXISTS public.generated (
     direct INT NOT NULL
 )
 SQL);
+
+$connection->nativeQuery(<<<SQL
+CREATE TABLE IF NOT EXISTS public.bulk (
+    id uuid NOT NULL PRIMARY KEY,
+    value int NOT NULL,
+    flagged boolean NOT NULL DEFAULT false
+)
+SQL);