diff --git a/src/DI/TablesExtension.php b/src/DI/TablesExtension.php
index 53894afaff6cc73492fdd84665132a890f981684..e07eee4708481e340f84af4c357857e34ec4abc0 100644
--- a/src/DI/TablesExtension.php
+++ b/src/DI/TablesExtension.php
@@ -7,6 +7,7 @@ namespace Grifart\Tables\DI;
 use Grifart\Tables\Database\Identifier;
 use Grifart\Tables\Scaffolding\PostgresReflector;
 use Grifart\Tables\Scaffolding\TablesDefinitions;
+use Grifart\Tables\SingleConnectionTableManager;
 use Grifart\Tables\TableManager;
 use Grifart\Tables\TypeResolver;
 use Nette\DI\CompilerExtension;
@@ -41,7 +42,8 @@ final class TablesExtension extends CompilerExtension
 		$builder = $this->getContainerBuilder();
 
 		$builder->addDefinition($this->prefix('tableManager'))
-			->setFactory(TableManager::class);
+			->setType(TableManager::class)
+			->setFactory(SingleConnectionTableManager::class);
 
 		$builder->addDefinition($this->prefix('scaffolding'))
 			->setFactory(TablesDefinitions::class);
diff --git a/src/Scaffolding/Definitions.php b/src/Scaffolding/Definitions.php
index 5fa50fa9348c9ba2d027b0dae938eebcc23339b9..04f5efe0be327d1679ad5a5e8a64b75bd2218258 100644
--- a/src/Scaffolding/Definitions.php
+++ b/src/Scaffolding/Definitions.php
@@ -11,6 +11,8 @@ use Grifart\ClassScaffolder\Definition\ClassDefinition;
  */
 final class Definitions implements \IteratorAggregate
 {
+	private ?ClassDefinition $factoryClass = null;
+
 	private function __construct(
 		private ClassDefinition $rowClass,
 		private ClassDefinition $modificationsClass,
@@ -63,6 +65,12 @@ final class Definitions implements \IteratorAggregate
 		return $this;
 	}
 
+	public function withFactory(): self
+	{
+		$tableClassName = $this->tableClass->getFullyQualifiedName();
+		$this->factoryClass = (new ClassDefinition($tableClassName . 'Factory'))->with(new TableFactoryImplementation($tableClassName));
+		return $this;
+	}
 
 	public function getIterator(): \Traversable
 	{
@@ -72,6 +80,9 @@ final class Definitions implements \IteratorAggregate
 		if ($this->primaryKeyClass !== null) {
 			yield $this->primaryKeyClass;
 		}
+		if ($this->factoryClass !== null) {
+			yield $this->factoryClass;
+		}
 	}
 
 }
diff --git a/src/Scaffolding/TableFactoryImplementation.php b/src/Scaffolding/TableFactoryImplementation.php
new file mode 100644
index 0000000000000000000000000000000000000000..81bbede154007967c12574b50bc695d1fc0805f4
--- /dev/null
+++ b/src/Scaffolding/TableFactoryImplementation.php
@@ -0,0 +1,73 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Grifart\Tables\Scaffolding;
+
+use Dibi\IConnection;
+use Grifart\ClassScaffolder\Capabilities\Capability;
+use Grifart\ClassScaffolder\ClassInNamespace;
+use Grifart\ClassScaffolder\Definition\ClassDefinition;
+use Grifart\Tables\SingleConnectionTableManager;
+use Grifart\Tables\TableFactory;
+use Grifart\Tables\TableManager;
+use Grifart\Tables\TypeResolver;
+use Nette\PhpGenerator\Literal;
+use Nette\PhpGenerator\Parameter;
+use Nette\PhpGenerator\PromotedParameter;
+
+final readonly class TableFactoryImplementation implements Capability
+{
+	public function __construct(
+		private string $tableClass,
+	) {}
+
+	public function applyTo(
+		ClassDefinition $definition,
+		ClassInNamespace $draft,
+		?ClassInNamespace $current,
+	): void
+	{
+		$namespace = $draft->getNamespace();
+		$classType = $draft->getClassType();
+
+		$classType->addImplement(TableFactory::class);
+		$classType->setReadOnly();
+
+		$namespace->addUse(TypeResolver::class);
+		$namespace->addUse(TableManager::class);
+
+		$classType->addMethod('__construct')
+			->setPublic()
+			->setParameters([
+				(new PromotedParameter('tableManager'))
+					->setPrivate()
+					->setType(TableManager::class),
+				(new PromotedParameter('typeResolver'))
+					->setPrivate()
+					->setType(TypeResolver::class),
+			]);
+
+		$namespace->addUse($this->tableClass);
+
+		$classType->addMethod('create')
+			->setPublic()
+			->setReturnType($this->tableClass)
+			->setBody('return new ?($this->tableManager, $this->typeResolver);', [new Literal($namespace->simplifyName($this->tableClass))]);
+
+		$classType->addMethod('withTableManager')
+			->setPublic()
+			->setReturnType($this->tableClass)
+			->setParameters([(new Parameter('tableManager'))->setType(TableManager::class)])
+			->setBody('return new ?($tableManager, $this->typeResolver);', [new Literal($namespace->simplifyName($this->tableClass))]);
+
+		$namespace->addUse(IConnection::class);
+		$namespace->addUse(SingleConnectionTableManager::class);
+		$classType->addMethod('withConnection')
+			->setPublic()
+			->setReturnType($this->tableClass)
+			->setParameters([(new Parameter('connection'))->setType(IConnection::class)])
+			->addBody('$tableManager = new ?($connection);', [new Literal($namespace->simplifyName(SingleConnectionTableManager::class))])
+			->addBody('return new ?($tableManager, $this->typeResolver);', [new Literal($namespace->simplifyName($this->tableClass))]);
+	}
+}
diff --git a/src/Scaffolding/TablesDefinitions.php b/src/Scaffolding/TablesDefinitions.php
index 0a5b6290434a0d50cf22c54745cd9bfb8e8e03d0..8f6a1b15c1e000750283f5bc9ff798a3b5b449fe 100644
--- a/src/Scaffolding/TablesDefinitions.php
+++ b/src/Scaffolding/TablesDefinitions.php
@@ -17,7 +17,6 @@ use function Grifart\ClassScaffolder\Capabilities\implementedInterface;
 use function Grifart\ClassScaffolder\Capabilities\namedConstructor;
 use function Grifart\ClassScaffolder\Capabilities\privatizedConstructor;
 use function Grifart\ClassScaffolder\Definition\Types\nullable;
-use function Phun\map;
 use function Phun\mapWithKeys;
 
 final class TablesDefinitions
diff --git a/src/SingleConnectionTableManager.php b/src/SingleConnectionTableManager.php
new file mode 100644
index 0000000000000000000000000000000000000000..94127603689ca7f7556b13c3ae169ba11033fac7
--- /dev/null
+++ b/src/SingleConnectionTableManager.php
@@ -0,0 +1,245 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Grifart\Tables;
+
+use Dibi\IConnection;
+use Dibi\UniqueConstraintViolationException;
+use Grifart\Tables\Conditions\Composite;
+use Grifart\Tables\Conditions\Condition;
+use Grifart\Tables\OrderBy\OrderBy;
+use Grifart\Tables\OrderBy\OrderByDirection;
+use Nette\Utils\Paginator;
+use function Phun\map;
+use function Phun\mapWithKeys;
+
+// todo: error handling
+// todo: mapping of exceptions
+
+final class SingleConnectionTableManager implements TableManager
+{
+
+	public function __construct(
+		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
+	 * @param PrimaryKey<TableType> $primaryKey
+	 * @return ($required is true ? Row : Row|null)
+	 * @throws RowNotFound
+	 */
+	public function find(Table $table, PrimaryKey $primaryKey, bool $required = true): ?Row
+	{
+		return $this->findOneBy($table, $primaryKey->getCondition($table), required: $required);
+	}
+
+	/**
+	 * @template TableType of Table
+	 * @param TableType $table
+	 * @param array<OrderBy|Expression<mixed>> $orderBy
+	 * @return Row[]
+	 */
+	public function getAll(Table $table, array $orderBy = [], ?Paginator $paginator = null): array
+	{
+		return $this->findBy($table, [], $orderBy, $paginator);
+	}
+
+	/**
+	 * @template TableType of Table
+	 * @param TableType $table
+	 * @param Condition|Condition[] $conditions
+	 * @param array<OrderBy|Expression<mixed>> $orderBy
+	 * @return Row[] (subclass of row)
+	 */
+	public function findBy(Table $table, Condition|array $conditions, array $orderBy = [], ?Paginator $paginator = null): array
+	{
+		$result = $this->connection->query(
+			'SELECT *',
+			'FROM %n.%n', $table::getSchema(), $table::getTableName(),
+			'WHERE %ex', (\is_array($conditions) ? Composite::and(...$conditions) : $conditions)->toSql()->getValues(),
+			'ORDER BY %by', \count($orderBy) > 0
+				? map($orderBy, function (OrderBy|Expression $orderBy) {
+					if ($orderBy instanceof Expression) {
+						$orderBy = new OrderByDirection($orderBy);
+					}
+
+					return $orderBy->toSql()->getValues();
+				})
+				: [['%sql', 'true::boolean']],
+			'%lmt', $paginator?->getItemsPerPage(),
+			'%ofs', $paginator?->getOffset(),
+		);
+
+		foreach ($table::getDatabaseColumns() as $column) {
+			$result->setType($column->getName(), NULL);
+		}
+
+		$dibiRows = $result->fetchAll();
+
+		/** @var class-string<Row> $rowClass */
+		$rowClass = $table::getRowClass();
+		$modelRows = [];
+		foreach ($dibiRows as $dibiRow) {
+			\assert($dibiRow instanceof \Dibi\Row);
+			$modelRows[] = $rowClass::reconstitute(
+				mapWithKeys(
+					$dibiRow->toArray(),
+					static fn(string $columnName, mixed $value) => $table->getTypeOf($columnName)->fromDatabase($value),
+				),
+			);
+		}
+
+		if ($paginator !== null) {
+			$totalCount = $this->connection->query(
+				'SELECT COUNT(*)',
+				'FROM %n.%n', $table::getSchema(), $table::getTableName(),
+				'WHERE %ex', (\is_array($conditions) ? Composite::and(...$conditions) : $conditions)->toSql()->getValues(),
+			)->fetchSingle();
+
+			\assert(\is_int($totalCount));
+			$paginator->setItemCount($totalCount);
+		}
+
+		return $modelRows;
+	}
+
+	/**
+	 * @template TableType of Table
+	 * @param TableType $table
+	 * @param Condition|Condition[] $conditions
+	 * @param array<OrderBy|Expression<mixed>> $orderBy
+	 * @return ($required is true ? Row : Row|null)
+	 * @throws RowNotFound
+	 */
+	public function findOneBy(Table $table, Condition|array $conditions, array $orderBy = [], bool $required = true, bool $unique = true): ?Row
+	{
+		$result = $this->connection->query(
+			'SELECT *',
+			'FROM %n.%n', $table::getSchema(), $table::getTableName(),
+			'WHERE %ex', (\is_array($conditions) ? Composite::and(...$conditions) : $conditions)->toSql()->getValues(),
+			'ORDER BY %by', \count($orderBy) > 0
+				? map($orderBy, function (OrderBy|Expression $orderBy) {
+					if ($orderBy instanceof Expression) {
+						$orderBy = new OrderByDirection($orderBy);
+					}
+
+					return $orderBy->toSql()->getValues();
+				})
+				: [['%sql', 'true::boolean']],
+		);
+
+		foreach ($table::getDatabaseColumns() as $column) {
+			$result->setType($column->getName(), NULL);
+		}
+
+		$dibiRow = $result->fetch();
+		if ($dibiRow === null) {
+			return ! $required ? null : throw new RowNotFound();
+		}
+
+		if ($unique && $result->fetch() !== null) {
+			throw new TooManyRowsFound();
+		}
+
+		/** @var class-string<Row> $rowClass */
+		$rowClass = $table::getRowClass();
+		\assert($dibiRow instanceof \Dibi\Row);
+		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 Modifications<TableType> $changes
+	 * @throws GivenSearchCriteriaHaveNotMatchedAnyRows if no rows matches given criteria
+	 */
+	public function update(Table $table, Modifications $changes): void
+	{
+		$primaryKey = $changes->getPrimaryKey();
+		\assert($primaryKey !== 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', $primaryKey->getCondition($table)->toSql()->getValues(),
+		);
+		$affectedRows = $this->connection->getAffectedRows();
+		if ($affectedRows !== 1) {
+			if ($affectedRows === 0) {
+				throw new GivenSearchCriteriaHaveNotMatchedAnyRows();
+			}
+
+			throw new ProbablyBrokenPrimaryIndexImplementation($table, $affectedRows);
+		}
+	}
+
+	/**
+	 * @template TableType of Table
+	 * @param TableType $table
+	 * @param PrimaryKey<TableType> $primaryKey
+	 */
+	public function delete(Table $table, PrimaryKey $primaryKey): void
+	{
+		$this->connection->query(
+			'DELETE',
+			'FROM %n.%n', $table::getSchema(), $table::getTableName(),
+			'WHERE %ex', $primaryKey->getCondition($table)->toSql()->getValues(),
+		);
+		\assert($this->connection->getAffectedRows() === 1);
+	}
+
+	/**
+	 * @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);
+	}
+}
diff --git a/src/TableFactory.php b/src/TableFactory.php
new file mode 100644
index 0000000000000000000000000000000000000000..aa29745abaa9c0591ef639100c9470e92afcbb74
--- /dev/null
+++ b/src/TableFactory.php
@@ -0,0 +1,14 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Grifart\Tables;
+
+use Dibi\IConnection;
+
+interface TableFactory
+{
+	public function create(): Table;
+	public function withTableManager(TableManager $tableManager): Table;
+	public function withConnection(IConnection $connection): Table;
+}
diff --git a/src/TableManager.php b/src/TableManager.php
index e17e9ad2af5f6a2f74bd418dc2806989f430e9ec..7575c1c8e0ea5acefac4a8183b093db270e7bf51 100644
--- a/src/TableManager.php
+++ b/src/TableManager.php
@@ -4,51 +4,20 @@ declare(strict_types=1);
 
 namespace Grifart\Tables;
 
-use Dibi\Connection;
-use Dibi\UniqueConstraintViolationException;
-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;
 
-// todo: error handling
-// todo: mapping of exceptions
-
-final class TableManager
+interface TableManager
 {
-
-	public function __construct(
-		private Connection $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);
-	}
+	public function insert(Table $table, Modifications $changes): void;
 
 	/**
 	 * @template TableType of Table
@@ -57,21 +26,15 @@ final class TableManager
 	 * @return ($required is true ? Row : Row|null)
 	 * @throws RowNotFound
 	 */
-	public function find(Table $table, PrimaryKey $primaryKey, bool $required = true): ?Row
-	{
-		return $this->findOneBy($table, $primaryKey->getCondition($table), required: $required);
-	}
+	public function find(Table $table, PrimaryKey $primaryKey, bool $required = true): ?Row;
 
 	/**
 	 * @template TableType of Table
 	 * @param TableType $table
-	 * @param OrderBy[] $orderBy
+	 * @param array<OrderBy|Expression<mixed>> $orderBy
 	 * @return Row[]
 	 */
-	public function getAll(Table $table, array $orderBy = [], ?Paginator $paginator = null): array
-	{
-		return $this->findBy($table, [], $orderBy, $paginator);
-	}
+	public function getAll(Table $table, array $orderBy = [], ?Paginator $paginator = null): array;
 
 	/**
 	 * @template TableType of Table
@@ -80,57 +43,7 @@ final class TableManager
 	 * @param array<OrderBy|Expression<mixed>> $orderBy
 	 * @return Row[] (subclass of row)
 	 */
-	public function findBy(Table $table, Condition|array $conditions, array $orderBy = [], ?Paginator $paginator = null): array
-	{
-		$result = $this->connection->query(
-			'SELECT *',
-			'FROM %n.%n', $table::getSchema(), $table::getTableName(),
-			'WHERE %ex', (\is_array($conditions) ? Composite::and(...$conditions) : $conditions)->toSql()->getValues(),
-			'ORDER BY %by', \count($orderBy) > 0
-				? map($orderBy, function (OrderBy|Expression $orderBy) {
-					if ($orderBy instanceof Expression) {
-						$orderBy = new OrderByDirection($orderBy);
-					}
-
-					return $orderBy->toSql()->getValues();
-				})
-				: [['%sql', 'true::boolean']],
-			'%lmt', $paginator?->getItemsPerPage(),
-			'%ofs', $paginator?->getOffset(),
-		);
-
-		foreach ($table::getDatabaseColumns() as $column) {
-			$result->setType($column->getName(), NULL);
-		}
-
-		$dibiRows = $result->fetchAll();
-
-		/** @var class-string<Row> $rowClass */
-		$rowClass = $table::getRowClass();
-		$modelRows = [];
-		foreach ($dibiRows as $dibiRow) {
-			\assert($dibiRow instanceof \Dibi\Row);
-			$modelRows[] = $rowClass::reconstitute(
-				mapWithKeys(
-					$dibiRow->toArray(),
-					static fn(string $columnName, mixed $value) => $table->getTypeOf($columnName)->fromDatabase($value),
-				),
-			);
-		}
-
-		if ($paginator !== null) {
-			$totalCount = $this->connection->query(
-				'SELECT COUNT(*)',
-				'FROM %n.%n', $table::getSchema(), $table::getTableName(),
-				'WHERE %ex', (\is_array($conditions) ? Composite::and(...$conditions) : $conditions)->toSql()->getValues(),
-			)->fetchSingle();
-
-			\assert(\is_int($totalCount));
-			$paginator->setItemCount($totalCount);
-		}
-
-		return $modelRows;
-	}
+	public function findBy(Table $table, Condition|array $conditions, array $orderBy = [], ?Paginator $paginator = null): array;
 
 	/**
 	 * @template TableType of Table
@@ -140,46 +53,7 @@ final class TableManager
 	 * @return ($required is true ? Row : Row|null)
 	 * @throws RowNotFound
 	 */
-	public function findOneBy(Table $table, Condition|array $conditions, array $orderBy = [], bool $required = true, bool $unique = true): ?Row
-	{
-		$result = $this->connection->query(
-			'SELECT *',
-			'FROM %n.%n', $table::getSchema(), $table::getTableName(),
-			'WHERE %ex', (\is_array($conditions) ? Composite::and(...$conditions) : $conditions)->toSql()->getValues(),
-			'ORDER BY %by', \count($orderBy) > 0
-				? map($orderBy, function (OrderBy|Expression $orderBy) {
-					if ($orderBy instanceof Expression) {
-						$orderBy = new OrderByDirection($orderBy);
-					}
-
-					return $orderBy->toSql()->getValues();
-				})
-				: [['%sql', 'true::boolean']],
-		);
-
-		foreach ($table::getDatabaseColumns() as $column) {
-			$result->setType($column->getName(), NULL);
-		}
-
-		$dibiRow = $result->fetch();
-		if ($dibiRow === null) {
-			return ! $required ? null : throw new RowNotFound();
-		}
-
-		if ($unique && $result->fetch() !== null) {
-			throw new TooManyRowsFound();
-		}
-
-		/** @var class-string<Row> $rowClass */
-		$rowClass = $table::getRowClass();
-		\assert($dibiRow instanceof \Dibi\Row);
-		return $rowClass::reconstitute(
-			mapWithKeys(
-				$dibiRow->toArray(),
-				static fn(string $columnName, mixed $value) => $table->getTypeOf($columnName)->fromDatabase($value),
-			),
-		);
-	}
+	public function findOneBy(Table $table, Condition|array $conditions, array $orderBy = [], bool $required = true, bool $unique = true): ?Row;
 
 	/**
 	 * @template TableType of Table
@@ -187,43 +61,14 @@ final class TableManager
 	 * @param Modifications<TableType> $changes
 	 * @throws GivenSearchCriteriaHaveNotMatchedAnyRows if no rows matches given criteria
 	 */
-	public function update(Table $table, Modifications $changes): void
-	{
-		$primaryKey = $changes->getPrimaryKey();
-		\assert($primaryKey !== 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', $primaryKey->getCondition($table)->toSql()->getValues(),
-		);
-		$affectedRows = $this->connection->getAffectedRows();
-		if ($affectedRows !== 1) {
-			if ($affectedRows === 0) {
-				throw new GivenSearchCriteriaHaveNotMatchedAnyRows();
-			}
-
-			throw new ProbablyBrokenPrimaryIndexImplementation($table, $affectedRows);
-		}
-	}
+	public function update(Table $table, Modifications $changes): void;
 
 	/**
 	 * @template TableType of Table
 	 * @param TableType $table
 	 * @param PrimaryKey<TableType> $primaryKey
 	 */
-	public function delete(Table $table, PrimaryKey $primaryKey): void
-	{
-		$this->connection->query(
-			'DELETE',
-			'FROM %n.%n', $table::getSchema(), $table::getTableName(),
-			'WHERE %ex', $primaryKey->getCondition($table)->toSql()->getValues(),
-		);
-		\assert($this->connection->getAffectedRows() === 1);
-	}
+	public function delete(Table $table, PrimaryKey $primaryKey): void;
 
 	/**
 	 * @template TableType of Table
@@ -232,14 +77,5 @@ final class TableManager
 	 * @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);
-	}
+	public function save(Table $table, Modifications $changes): void;
 }
diff --git a/tests/DI/TablesExtensionTest.phpt b/tests/DI/TablesExtensionTest.phpt
index cb362b3d8ee8ea263d3da59663a5fa436d276d29..e602f9094b74ec7e66fe8bdd6f121feb6ddfbfe0 100644
--- a/tests/DI/TablesExtensionTest.phpt
+++ b/tests/DI/TablesExtensionTest.phpt
@@ -4,11 +4,10 @@ declare(strict_types=1);
 
 namespace Grifart\Tables\Tests\DI;
 
-use Dibi\Connection;
 use Grifart\Tables\Database\Identifier;
 use Grifart\Tables\Scaffolding\TablesDefinitions;
+use Grifart\Tables\SingleConnectionTableManager;
 use Grifart\Tables\TableManager;
-use Grifart\Tables\Tests\Fixtures\UuidType;
 use Grifart\Tables\Tests\Fixtures\VersionType;
 use Grifart\Tables\TypeResolver;
 use Grifart\Tables\Types\DecimalType;
@@ -40,7 +39,7 @@ $createContainer = function (string $configFile): Container
 
 (function () use ($createContainer) {
 	$container = $createContainer('default');
-	Assert::type(TableManager::class, $container->getByType(TableManager::class));
+	Assert::type(SingleConnectionTableManager::class, $container->getByType(TableManager::class));
 	Assert::type(TablesDefinitions::class, $container->getByType(TablesDefinitions::class));
 })();
 
diff --git a/tests/Fixtures/.definition.php b/tests/Fixtures/.definition.php
index 7f6dc2a5f3b9bb40a4c2a93755d9c7c99eb7cb81..155eba0013b2416eaa2b2634d37c2c72653c0a57 100644
--- a/tests/Fixtures/.definition.php
+++ b/tests/Fixtures/.definition.php
@@ -23,7 +23,7 @@ return [
 		TestModifications::class,
 		TestsTable::class,
 		TestPrimaryKey::class,
-	),
+	)->withFactory(),
 	...$tableDefinitions->for(
 		'public',
 		'config',
diff --git a/tests/Fixtures/TestFixtures.php b/tests/Fixtures/TestFixtures.php
index 212d43e7a11c3587ece4f9ee3b0958086c7c808e..a0f18aa620c31a3d26fe0508b4a65f506a6e7487 100644
--- a/tests/Fixtures/TestFixtures.php
+++ b/tests/Fixtures/TestFixtures.php
@@ -6,7 +6,7 @@ namespace Grifart\Tables\Tests\Fixtures;
 
 use Dibi\Connection;
 use Grifart\Tables\Database\Identifier;
-use Grifart\Tables\TableManager;
+use Grifart\Tables\SingleConnectionTableManager;
 use Grifart\Tables\TypeResolver;
 use Grifart\Tables\Types\ArrayType;
 use Grifart\Tables\Types\IntType;
@@ -16,9 +16,9 @@ final class TestFixtures
 {
 	use StaticClass;
 
-	public static function createTableManager(Connection $connection): TableManager
+	public static function createTableManager(Connection $connection): SingleConnectionTableManager
 	{
-		return new TableManager($connection);
+		return new SingleConnectionTableManager($connection);
 	}
 
 	public static function createTypeResolver(Connection $connection): TypeResolver
diff --git a/tests/Fixtures/TestsTableFactory.php b/tests/Fixtures/TestsTableFactory.php
new file mode 100644
index 0000000000000000000000000000000000000000..7b7bf9d18b3a0926a4a489b939b07fda2b99df93
--- /dev/null
+++ b/tests/Fixtures/TestsTableFactory.php
@@ -0,0 +1,42 @@
+<?php
+
+/**
+ * Do not edit. This is generated file. Modify definition file instead.
+ */
+
+declare(strict_types=1);
+
+namespace Grifart\Tables\Tests\Fixtures;
+
+use Dibi\IConnection;
+use Grifart\Tables\SingleConnectionTableManager;
+use Grifart\Tables\TableManager;
+use Grifart\Tables\TypeResolver;
+
+final readonly class TestsTableFactory implements \Grifart\Tables\TableFactory
+{
+	public function __construct(
+		private TableManager $tableManager,
+		private TypeResolver $typeResolver,
+	) {
+	}
+
+
+	public function create(): TestsTable
+	{
+		return new TestsTable($this->tableManager, $this->typeResolver);
+	}
+
+
+	public function withTableManager(TableManager $tableManager): TestsTable
+	{
+		return new TestsTable($tableManager, $this->typeResolver);
+	}
+
+
+	public function withConnection(IConnection $connection): TestsTable
+	{
+		$tableManager = new SingleConnectionTableManager($connection);
+		return new TestsTable($tableManager, $this->typeResolver);
+	}
+}
diff --git a/tests/Scaffolding/ScaffoldingTest.phpt b/tests/Scaffolding/ScaffoldingTest.phpt
index 148d0c58d70eb9418dd35eb7372bc148c54cca1d..c484dcbca5bf23ba1b7c0ebba4b35f3ba2994385 100644
--- a/tests/Scaffolding/ScaffoldingTest.phpt
+++ b/tests/Scaffolding/ScaffoldingTest.phpt
@@ -34,5 +34,5 @@ $results = $fileProcessor->processFile(
 	},
 );
 
-Assert::count(16, $results->getDefinitions());
+Assert::count(17, $results->getDefinitions());
 Assert::true($results->isSuccessful());