diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..d09a5f7
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,7 @@
+/.gitattributes export-ignore
+/.gitignore export-ignore
+/.github export-ignore
+/.scrutinizer.yml export-ignore
+/doc export-ignore
+/phpunit.xml export-ignore
+/tests export-ignore
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 0000000..206c8af
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,51 @@
+name: build
+
+on:
+  push:
+    branches: [ "master" ]
+  pull_request:
+    branches: [ "master" ]
+
+permissions:
+  contents: read
+
+jobs:
+  build:
+
+    runs-on: ubuntu-latest
+
+    steps:
+    - uses: actions/checkout@v3
+
+    - name: Validate composer.json and composer.lock
+      run: composer validate --strict
+
+    - name: Cache Composer packages
+      id: composer-cache
+      uses: actions/cache@v3
+      with:
+        path: vendor
+        key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
+        restore-keys: |
+          ${{ runner.os }}-php-
+
+    - name: Install dependencies
+      run: composer install --prefer-dist --no-progress
+
+    - name: Create runtime cache folder
+      run: mkdir -v -p -m 777 tests/runtime/cache
+
+    - name: Create sqlite folder
+      run: mkdir -v -p -m 777 tests/runtime/sqlite && touch tests/runtime/sqlite/database.db
+
+    - name: Execute Tests
+      run: vendor/bin/phpunit tests --colors=always --coverage-clover=coverage.clover
+      env:
+        XDEBUG_MODE: coverage
+      
+    - name: Upload coverage reports to Codecov
+      continue-on-error: true
+      uses: codecov/codecov-action@v3
+      with:
+        token: ${{ secrets.CODECOV_TOKEN }}
+        file: ./coverage.clover
diff --git a/.gitignore b/.gitignore
index a4cb13c..caa2c69 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,8 @@
 .idea
 .phpunit.result.cache
+.phpunit.cache
+coverage.clover
+coverage.txt
 vendor/
 composer.lock
 tests/runtime/
diff --git a/.scrutinizer.yml b/.scrutinizer.yml
new file mode 100644
index 0000000..b9d8d04
--- /dev/null
+++ b/.scrutinizer.yml
@@ -0,0 +1,16 @@
+checks:
+  php: true
+
+filter:
+  paths:
+    - "src/*"
+
+tools:
+  external_code_coverage:
+    timeout: 900    # Timeout in seconds.
+    runs: 2         # How many code coverage submissions Scrutinizer will wait
+
+build:
+  image: default-bionic
+  environment:
+    php: 8.1.2
\ No newline at end of file
diff --git a/README.md b/README.md
index c4f3f59..256a728 100644
--- a/README.md
+++ b/README.md
@@ -1,47 +1,37 @@
 # What is Composite DB
+[![Latest Stable Version](https://poser.pugx.org/compositephp/db/v/stable)](https://packagist.org/packages/compositephp/db)
+[![Build Status](https://github.com/compositephp/db/actions/workflows/main.yml/badge.svg)](https://github.com/compositephp/db/actions)
+[![Codecov](https://codecov.io/gh/compositephp/db/branch/master/graph/badge.svg)](https://codecov.io/gh/compositephp/db/)
 
-Composite DB is lightweight and fast PHP ORM, DataMapper and Table Gateway which allows you to represent your SQL tables 
+Composite DB is lightweight and fast PHP DataMapper and Table Gateway which allows you to represent your SQL tables 
 scheme in OOP style using full power of PHP 8.1+ class syntax. 
 
 It also gives you CRUD, query builder and automatic caching out of the box, so you can start
 to work with your database from php code in a minutes!
 
 Overview:
-* [Mission](#mission)
+* [Features](#features)
 * [Requirements](#requirements)
 * [Installation](#installation)
 * [Quick example](#quick-example)
 * [Documentation](doc/README.md)
 
-## Mission
-You probably may ask, why do you need another ORM if there are already popular Doctrine, CycleORM, etc.?
-
-Composite DB solves multiple problems:
+## Features
 
 * **Lightweight** - easier entity schema, no getters and setters, you don't need attributes for each column definition, 
 just use native php class syntax.
-* **Speed** - it's 1.5x faster in pure SQL queries mode and many times faster in automatic caching mode.
-* **Easy caching** - gives you CRUD operations caching out of the box and in general its much easier to work with cached "selects".
+* **Speed** - it's 1.5x faster in pure SQL queries mode and many times faster in automatic caching mode (see [benchmark](https://github.com/compositephp/php-orm-benchmark)).
+* **Easy caching** - gives you CRUD operations caching out of the box and in general it's much easier to work with cached "selects".
 * **Strict types** - Composite DB forces you to be more strict typed and makes your IDE happy.
 * **Hydration** - you can serialize your Entities to plain array or json and deserialize them back.
-* **Flexibility** - gives you more freedom to extend Repositories, for example its easier to build sharding tables.
+* **Flexibility** - gives you more freedom to extend Repositories, for example it's easier to build sharding tables.
 * **Code generation** - you can generate Entity and Repository classes from your SQL tables.
-* **Division of responsibility** - there is no "god" entity manager, every Entity has its own Repository class and its the only entry point to make queries to your table.
+* **Division of responsibility** - every Entity has its own Repository class, and it's the only entry point to make queries to your table.
 
 It also has many popular features such as:
 
 * **Query Builder** - build your queries with constructor, based on [doctrine/dbal](https://github.com/doctrine/dbal)
-* **Migrations** - based on [doctrine/migrations](https://github.com/doctrine/migrations)
-
-But there is 1 sacrifice for all these features - there is no support for relations in Composite DB. Its too much
-uncontrollable magic and hidden bottlenecks with "JOINs" and its not possible to implement automatic caching with
-relations. We recommend to have full control and make several cached select queries instead of "JOINs".
-
-### When you shouldn't use Composite DB
-
-1. If you have intricate structure with many foreign keys in your database 
-2. You 100% sure in your indexes and fully trust "JOINs" performance
-3. You dont want to do extra cached select queries and want some magic
+* **Migrations** - synchronise your php entities with database tables
 
 ## Requirements
 
@@ -177,7 +167,7 @@ $user = User::fromArray([
 ]);
 ```
 
-And thats it, no special getters or setters, no "behaviours" or extra code, smart entity casts everything automatically.
+And that's it, no special getters or setters, no "behaviours" or extra code, smart entity casts everything automatically.
 More about Entity and supported auto casting types you can find [here](doc/entity.md).
 
 ## License:
diff --git a/composer.json b/composer.json
index bc13c29..a6c1c43 100644
--- a/composer.json
+++ b/composer.json
@@ -1,6 +1,6 @@
 {
     "name": "compositephp/db",
-    "description": "PHP 8.1+ ORM and Table Gateway",
+    "description": "PHP 8.1+ DataMapper and Table Gateway",
     "type": "library",
     "license": "MIT",
     "minimum-stability": "dev",
@@ -13,18 +13,16 @@
     ],
     "require": {
         "php": "^8.1",
+        "ext-pdo": "*",
         "psr/simple-cache":  "1 - 3",
-        "compositephp/entity": "^0.1.2",
-        "doctrine/dbal": "^3.5",
-        "doctrine/inflector": "^2.0",
-        "iamcal/sql-parser": "^0.4.0",
-        "nette/php-generator": "^4.0",
-        "symfony/console": "2 - 6"
+        "compositephp/entity": "^v0.1.11",
+        "doctrine/dbal": "^3.5"
     },
     "require-dev": {
         "kodus/file-cache": "^2.0",
-        "phpunit/phpunit": "^9.5",
-        "phpstan/phpstan": "^1.9"
+        "phpunit/phpunit": "^10.1",
+        "phpstan/phpstan": "^1.9",
+        "phpunit/php-code-coverage": "^10.1"
     },
     "autoload": {
         "psr-4": {
diff --git a/doc/cache.md b/doc/cache.md
index e3ca6ab..a5d6340 100644
--- a/doc/cache.md
+++ b/doc/cache.md
@@ -6,7 +6,7 @@ To start using auto-cache feature you need:
 to `Composite\DB\AbstractCachedTable`
 3. Implement method `getFlushCacheKeys()`
 4. Change all internal select methods to their cached versions (example: `findByPkInternal()` 
-to `findByPkCachedInternal()` etc.)
+to `_findByPkCached()` etc.)
 
 You can also generate cached version of your table with console command:
 
@@ -46,7 +46,7 @@ class PostsTable extends AbstractCachedTable
 
     public function findByPk(int $id): ?Post
     {
-        return $this->createEntity($this->findByPkInternalCached($id));
+        return $this->_findByPkCached($id);
     }
     
     /**
@@ -54,15 +54,12 @@ class PostsTable extends AbstractCachedTable
     */
     public function findAllFeatured(): array
     {
-        return $this->createEntities($this->findAllInternal(
-            'is_featured = :is_featured',
-            ['is_featured' => true],
-        ));
+        return $this->_findAll(['is_featured' => true]);
     }
     
     public function countAllFeatured(): int
     {
-        return $this->countAllCachedInternal(
+        return $this->_countAllCached(
             'is_featured = :is_featured',
             ['is_featured' => true],
         );
diff --git a/doc/code-generators.md b/doc/code-generators.md
index 276f294..ce53614 100644
--- a/doc/code-generators.md
+++ b/doc/code-generators.md
@@ -1,27 +1,90 @@
 # Code generators
 
-Before start, you need to [configure](configuration.md#configure-console-commands) code generators.
+Code generation is on of key features of the Composite Sync package.
+This enables you to generate Entity classes directly from SQL tables, thereby enabling a literal reflection of the SQL table schema into native PHP classes.
 
-## Entity class generator
-Arguments: 
-1. `db` - DatabaseManager database name
-2. `table` - SQL table name
-3. `entity` - Full classname of new entity
-4. `--force` - option if existing file should be overwritten
+## Supported Databases
+- MySQL
+- Postgres
+- SQLite
+
+## Getting Started
+
+To begin using Composite Sync in your project, follow these steps:
+
+### 1. Install package via composer:
+ ```shell
+ $ composer require compositephp/sync
+ ```
+### 2. Configure connections
+You need to configure ConnectionManager, see instructions [here](configuration.md)
+
+### 3. Configure commands
+
+Add [symfony/console](https://symfony.com/doc/current/components/console.html) commands to your application:
+- Composite\Sync\Commands\MigrateCommand
+- Composite\Sync\Commands\MigrateNewCommand
+- Composite\Sync\Commands\MigrateDownCommand
+
+Here is an example of a minimalist, functional PHP file if you don't have configured symfony/console:
+
+```php
+<?php declare(strict_types=1);
+include 'vendor/autoload.php';
+
+use Composite\Sync\Commands;
+use Symfony\Component\Console\Application;
+
+//may be changed with .env file
+putenv('CONNECTIONS_CONFIG_FILE=/path/to/your/connections/config.php');
+
+$app = new Application();
+$app->addCommands([
+    new Commands\GenerateEntityCommand(),
+    new Commands\GenerateTableCommand(),
+]);
+$app->run();
+```
+## Available commands
+
+* ### composite:generate-entity
+
+The command examines the specific SQL table and generates an [Composite\Entity\AbstractEntity](https://github.com/compositephp/entity) PHP class.
+This class embodies the table structure using native PHP syntax, thereby representing the original SQL table in a more PHP-friendly format.
 
-Example:
 ```shell
-$ php console.php composite-db:generate-entity dbName Users 'App\User' --force
+php cli.php composite:generate-entity connection_name TableName 'App\Models\EntityName'
 ```
 
-## Table class generator
-Arguments:
-1. `entity` - Entity full class name
-2. `table` - Table full class name
-3. `--cached` - Option if cached version of table class should be generated
-4. `--force` - Option if existing file should be overwritten
+| Argument   | Required | Description                                          |
+|------------|----------|------------------------------------------------------|
+| connection | Yes      | Name of connection from connection config file       |
+| table      | Yes      | Name of SQL table                                    |
+| entity     | Yes      | Full classname of the class that needs to be created |
+
+Options:
+
+| Option  | Description             |
+|---------|-------------------------|
+| --force | Overwrite existing file |
+
+* ### composite:generate-table
+
+The command examines the specific Entity and generates a [Table](https://github.com/compositephp/db) PHP class.
+This class acts as a gateway to a specific SQL table, providing user-friendly CRUD tools for interacting with SQL right off the bat.
 
-Example:
 ```shell
-$ php console.php composite-db:generate-table 'App\User' 'App\UsersTable'
-```
\ No newline at end of file
+php cli.php composite:generate-table connection_name TableName 'App\Models\EntityName'
+```
+
+| Argument  | Required | Description                                   |
+|-----------|----------|-----------------------------------------------|
+| entity    | Yes      | Full Entity classname                         |
+| table     | No       | Full Table classname that needs to be created |
+
+Options:
+
+| Option   | Description                                |
+|----------|--------------------------------------------|
+| --cached | Generate cached version of PHP Table class |
+| --force  | Overwrite existing file                    |
\ No newline at end of file
diff --git a/doc/example.md b/doc/example.md
index 096ea6f..4709c67 100644
--- a/doc/example.md
+++ b/doc/example.md
@@ -43,7 +43,7 @@ class UsersTable extends \Composite\DB\AbstractTable
 
     public function findByPk(int $id): ?User
     {
-        return $this->createEntity($this->findByPkInternal($id));
+        return $this->_findByPk($id);
     }
 
     /**
@@ -51,17 +51,13 @@ class UsersTable extends \Composite\DB\AbstractTable
      */
     public function findAllActive(): array
     {
-        return $this->createEntities($this->findAllInternal(
-            'status = :status',
-            ['status' => Status::ACTIVE->name],
-        ));
+        return $this->_findAll(['status' => Status::ACTIVE]);
     }
 
     public function countAllActive(): int
     {
-        return $this->countAllInternal(
-            'status = :status',
-            ['status' => Status::ACTIVE->name],
+        return $this->_countAll(
+            ['status' => Status::ACTIVE],
         );
     }
 
diff --git a/doc/migrations.md b/doc/migrations.md
index da0fe68..f772d25 100644
--- a/doc/migrations.md
+++ b/doc/migrations.md
@@ -2,96 +2,157 @@
 
 > **_NOTE:_**  This is experimental feature
 
-Migrations used a bridge to [doctrine/migrations](https://github.com/doctrine/migrations) package.
-If you are not familiar with it, please read documentation before using composite bridge.
-
-1. Install package:
-    ```shell
-    $ composer require compositephp/doctrine-migrations
-    ```
-
-2. Configure bridge:
-    ```php
-    $bridge = new \Composite\DoctrineMigrations\SchemaProviderBridge(
-        entityDirs: [
-           '/path/to/your/src', //path to your source code, where bridge will search for entities
-        ],
-        connectionName: 'sqlite', //only entities with this connection name will be affected 
-        connection: $connection, //Doctrine\DBAL\Connection instance 
-    ); 
-    ```
-   
-3. Inject bridge into `\Doctrine\Migrations\DependencyFactory` as `\Doctrine\Migrations\Provider\SchemaProvider`
-instance.
-    ```php
-    $dependencyFactory->setDefinition(SchemaProvider::class, static fn () => $bridge);
-    ```
-
-Full example:
+Migrations enable you to maintain your database schema within your PHP entity classes.
+Any modification made in your class triggers the generation of migration files.
+These files execute SQL queries which synchronize the schema from the PHP class to the corresponding SQL table.
+This mechanism ensures consistent alignment between your codebase and the database structure.
+
+## Supported Databases
+- MySQL
+- Postgres (Coming soon)
+- SQLite (Coming soon)
+
+## Getting Started
+
+To begin using migrations you need to add Composite Sync package into your project and configure it:
+
+### 1. Install package via composer:
+ ```shell
+ $ composer require compositephp/sync
+ ```
+### 2. Configure connections
+You need to configure ConnectionManager, see instructions [here](configuration.md)
+
+### 3. Configure commands
+
+Add [symfony/console](https://symfony.com/doc/current/components/console.html) commands to your application:
+- Composite\Sync\Commands\MigrateCommand
+- Composite\Sync\Commands\MigrateNewCommand
+- Composite\Sync\Commands\MigrateDownCommand
+- Composite\Sync\Commands\GenerateEntityCommand
+- Composite\Sync\Commands\GenerateTableCommand
+
+Here is an example of a minimalist, functional PHP file:
+
 ```php
 <?php declare(strict_types=1);
+include 'vendor/autoload.php';
 
-use Doctrine\DBAL\DriverManager;
-use Doctrine\Migrations\Configuration\Configuration;
-use Doctrine\Migrations\Configuration\Connection\ExistingConnection;
-use Doctrine\Migrations\Configuration\Migration\ExistingConfiguration;
-use Doctrine\Migrations\DependencyFactory;
-use Doctrine\Migrations\Metadata\Storage\TableMetadataStorageConfiguration;
-use Doctrine\Migrations\Provider\SchemaProvider;
-use Doctrine\Migrations\Tools\Console\Command;
+use Composite\Sync\Commands;
 use Symfony\Component\Console\Application;
 
-include __DIR__ . '/vendor/autoload.php';
-
-$connection = DriverManager::getConnection([
-    'driver' => 'pdo_mysql',
-    'dbname' => 'test',
-    'user' => 'test',
-    'password' => 'test',
-    'host' => '127.0.0.1',
+//may be changed with .env file
+putenv('CONNECTIONS_CONFIG_FILE=/path/to/your/connections/config.php');
+putenv('ENTITIES_DIR=/path/to/your/source/dir'); // e.g. "./src"
+putenv('MIGRATIONS_DIR=/path/to/your/migrations/dir'); // e.g. "./src/Migrations"
+putenv('MIGRATIONS_NAMESPACE=Migrations\Namespace'); // e.g. "App\Migrations"
+
+$app = new Application();
+$app->addCommands([
+     new Commands\MigrateCommand(),
+     new Commands\MigrateNewCommand(),
+     new Commands\MigrateDownCommand(),
+     new Commands\GenerateEntityCommand(),
+     new Commands\GenerateTableCommand(),
 ]);
+$app->run();
+```
+## Available commands
+
+* ### composite:migrate
+
+This command performs two primary functions depending on its usage context. Initially, when called for the first time,
+it scans all entities located in the `ENTITIES_DIR` directory and generates migration files corresponding to these entities.
+This initial step prepares the necessary migration scripts based on the current entity definitions. Upon its second
+invocation, the command shifts its role to apply these generated migration scripts to the database. This two-step process
+ensures that the database schema is synchronized with the entity definitions, first by preparing the migration scripts
+and then by executing them to update the database.
+
+```shell
+php cli.php composite:migrate
+```
+
+| Option       | Short | Description                                               |
+|--------------|-------|-----------------------------------------------------------|
+| --connection | -c    | Check migrations for all entities with desired connection |
+| --entity     | -e    | Check migrations only for entity class                    |
+| --run        | -r    | Run migrations without asking for confirmation            |
+| --dry        | -d    | Dry run mode, no real SQL queries will be executed        |
+
+* ### composite:migrate-new
+
+This command generates a new, empty migration file. The file is provided as a template for the user to fill with the
+necessary database schema changes or updates. This command is typically used for initiating a new database migration,
+where the user can define the specific changes to be applied to the database schema. The generated file needs to be
+manually edited to include the desired migration logic before it can be executed with the migration commands.
+
+```shell
+php cli.php composite:migrate-new
+```
+
+| Argument    | Required | Description                              |
+|-------------|----------|------------------------------------------|
+| connection  | No       | Name of connection from your config file |
+| description | No       | Short description of desired changes     |
+
+* ### composite:migrate-down
+
+This command rolls back the most recently applied migration. It is useful for undoing the last schema change made to
+the database. This can be particularly helpful during development or testing phases, where you might need to revert
+recent changes quickly.
+
+```shell
+php cli.php composite:migrate-down
+```
+
+| Argument   | Required | Description                                                               |
+|------------|----------|---------------------------------------------------------------------------|
+| connection | No       | Name of connection from your config file                                  |
+| limit      | No       | Number of migrations should be rolled back from current state, default: 1 |
+
+
+| Option | Short | Description                                         |
+|--------|-------|-----------------------------------------------------|
+| --dry  | -d    | Dry run mode, no real SQL queries will be executed  |
+
+* ### composite:generate-entity
+
+The command examines the specific SQL table and generates an [Composite\Entity\AbstractEntity](https://github.com/compositephp/entity) PHP class.
+This class embodies the table structure using native PHP syntax, thereby representing the original SQL table in a more PHP-friendly format.
+
+```shell
+php cli.php composite:generate-entity connection_name TableName 'App\Models\EntityName'
+```
+
+| Argument   | Required | Description                                          |
+|------------|----------|------------------------------------------------------|
+| connection | Yes      | Name of connection from connection config file       |
+| table      | Yes      | Name of SQL table                                    |
+| entity     | Yes      | Full classname of the class that needs to be created |
+
+Options:
+
+| Option  | Short | Description             |
+|---------|-------|-------------------------|
+| --force | -f    | Overwrite existing file |
+
+* ### composite:generate-table
+
+The command examines the specific Entity and generates a [Table](https://github.com/compositephp/db) PHP class.
+This class acts as a gateway to a specific SQL table, providing user-friendly CRUD tools for interacting with SQL right off the bat.
+
+```shell
+php cli.php composite:generate-table 'App\Models\EntityName'
+```
+
+| Argument  | Required | Description                                   |
+|-----------|----------|-----------------------------------------------|
+| entity    | Yes      | Full Entity classname                         |
+| table     | No       | Full Table classname that needs to be created |
+
+Options:
 
-$configuration = new Configuration();
-
-$configuration->addMigrationsDirectory('Composite\DoctrineMigrations\Tests\runtime\migrations', __DIR__ . '/tests/runtime/migrations');
-$configuration->setAllOrNothing(true);
-$configuration->setCheckDatabasePlatform(false);
-
-$storageConfiguration = new TableMetadataStorageConfiguration();
-$storageConfiguration->setTableName('doctrine_migration_versions');
-
-$configuration->setMetadataStorageConfiguration($storageConfiguration);
-
-$dependencyFactory = DependencyFactory::fromConnection(
-    new ExistingConfiguration($configuration),
-    new ExistingConnection($connection)
-);
-
-$bridge = new \Composite\DoctrineMigrations\SchemaProviderBridge(
-    entityDirs: [
-        __DIR__ . '/src',
-    ],
-    connectionName: 'mysql',
-    connection: $connection,
-);
-$dependencyFactory->setDefinition(SchemaProvider::class, static fn () => $bridge);
-
-$cli = new Application('Migrations');
-$cli->setCatchExceptions(true);
-
-$cli->addCommands(array(
-    new Command\DumpSchemaCommand($dependencyFactory),
-    new Command\ExecuteCommand($dependencyFactory),
-    new Command\GenerateCommand($dependencyFactory),
-    new Command\LatestCommand($dependencyFactory),
-    new Command\ListCommand($dependencyFactory),
-    new Command\MigrateCommand($dependencyFactory),
-    new Command\DiffCommand($dependencyFactory),
-    new Command\RollupCommand($dependencyFactory),
-    new Command\StatusCommand($dependencyFactory),
-    new Command\SyncMetadataCommand($dependencyFactory),
-    new Command\VersionCommand($dependencyFactory),
-));
-
-$cli->run();
-```
\ No newline at end of file
+| Option   | Short | Description                                |
+|----------|-------|--------------------------------------------|
+| --cached | -c    | Generate cached version of PHP Table class |
+| --force  | -f    | Overwrite existing file                    |
\ No newline at end of file
diff --git a/doc/sync_illustration.png b/doc/sync_illustration.png
new file mode 100644
index 0000000..94c451a
Binary files /dev/null and b/doc/sync_illustration.png differ
diff --git a/doc/table.md b/doc/table.md
index 326f095..852c55b 100644
--- a/doc/table.md
+++ b/doc/table.md
@@ -38,7 +38,7 @@ class UsersTable extends AbstractTable
 
     public function findOne(int $id): ?User
     {
-        return $this->createEntity($this->findOneInternal($id));
+        return $this->_findByPk($id);
     }
 
     /**
@@ -46,12 +46,12 @@ class UsersTable extends AbstractTable
      */
     public function findAll(): array
     {
-        return $this->createEntities($this->findAllInternal());
+        return $this->_findAll();
     }
 
     public function countAll(): int
     {
-        return $this->countAllInternal();
+        return $this->_countAll();
     }
 }
 ```
@@ -67,15 +67,30 @@ Example with internal helper:
  */
 public function findAllActiveAdults(): array
 {
-    $rows = $this->findAllInternal(
-        'age > :age AND status = :status',
-        ['age' => 18, 'status' => Status::ACTIVE->name],
+    return $this->_findAll(
+        new Where(
+            'age > :age AND status = :status',
+            ['age' => 18, 'status' => Status::ACTIVE->name],
+        )
     );
-    return $this->createEntities($rows);
 }
 ```
 
-Example with pure query builder
+Or it might be simplified to:
+```php
+/**
+ * @return User[]
+ */
+public function findAllActiveAdults(): array
+{
+    return $this->_findAll([
+        'age' => ['>', 18],
+        'status' => Status:ACTIVE,
+    ]);
+}
+```
+
+Or you can use standard Doctrine QueryBuilder
 ```php
 /**
  * @return User[]
@@ -93,27 +108,36 @@ public function findCustom(): array
 ```
 
 ## Transactions
+In order to encapsulate your operations within a single transaction, you have two strategies at your disposal:
+1. Use the internal table class method transaction() if your operations are confined to a single table.
+2. Use the Composite\DB\CombinedTransaction class if your operations involve multiple tables within a single transaction.
 
-To wrap you operations in 1 transactions there are 2 ways:
-1. Use internal table class method `transaction()` if you are working only with 1 table.
-2. Use class `Composite\DB\CombinedTransaction` if you need to work with several tables in 1 transaction.
+Below is a sample code snippet illustrating how you can use the CombinedTransaction class:
 
    ```php
+   // Create instances of the tables you want to work with
    $usersTable = new UsersTable();
    $photosTable = new PhotosTable();
-    
+   
+   // Instantiate the CombinedTransaction class
    $transaction = new CombinedTransaction();
    
+   // Create a new user and add it to the users table within the transaction
    $user = new User(...);
    $transaction->save($usersTable, $user);
    
+   // Create a new photo associated with the user and add it to the photos table within the transaction
    $photo = new Photo(
        user_id: $user->id, 
        ...
    );
    $transaction->save($photosTable, $photo);
+   
+   // Commit the transaction to finalize the changes
    $transaction->commit();
    ```
+
+Remember, using a transaction ensures that your operations are atomic. This means that either all changes are committed to the database, or if an error occurs, no changes are made.
    
 ## Locks
 If you worry about concurrency updates during your transaction and want to be sure that only 1 process changing your 
diff --git a/phpunit.xml b/phpunit.xml
index 23ced23..1389feb 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -1,6 +1,27 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<phpunit
-        bootstrap="./tests/bootstrap.php"
-        verbose="true"
-        colors="true"
-></phpunit>
\ No newline at end of file
+<?xml version="1.0" encoding="UTF-8"?>
+<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.1/phpunit.xsd" bootstrap="tests/bootstrap.php"
+         executionOrder="depends,defects" beStrictAboutOutputDuringTests="true" failOnRisky="true" failOnWarning="true"
+         colors="true" cacheDirectory=".phpunit.cache" requireCoverageMetadata="false"
+         displayDetailsOnTestsThatTriggerDeprecations="true"
+         displayDetailsOnTestsThatTriggerErrors="true"
+         displayDetailsOnTestsThatTriggerNotices="true"
+         displayDetailsOnPhpunitDeprecations="true"
+         displayDetailsOnTestsThatTriggerWarnings="true"
+         beStrictAboutCoverageMetadata="true">
+    <testsuites>
+        <testsuite name="default">
+            <directory>tests</directory>
+        </testsuite>
+    </testsuites>
+    <coverage>
+        <report>
+            <html outputDirectory=".phpunit.cache"/>
+        </report>
+    </coverage>
+    <source>
+        <include>
+            <directory suffix=".php">src</directory>
+        </include>
+    </source>
+</phpunit>
diff --git a/src/AbstractCachedTable.php b/src/AbstractCachedTable.php
index 9d3064e..24ff5f9 100644
--- a/src/AbstractCachedTable.php
+++ b/src/AbstractCachedTable.php
@@ -2,12 +2,14 @@
 
 namespace Composite\DB;
 
-use Composite\DB\Exceptions\DbException;
 use Composite\Entity\AbstractEntity;
 use Psr\SimpleCache\CacheInterface;
+use Ramsey\Uuid\UuidInterface;
 
 abstract class AbstractCachedTable extends AbstractTable
 {
+    use Helpers\SelectRawTrait;
+
     protected const CACHE_VERSION = 1;
 
     public function __construct(
@@ -24,72 +26,58 @@ abstract protected function getFlushCacheKeys(AbstractEntity $entity): array;
     /**
      * @throws \Throwable
      */
-    public function save(AbstractEntity &$entity): void
+    public function save(AbstractEntity $entity): void
     {
-        $this->getConnection()->transactional(function () use (&$entity) {
-            $cacheKeys = $this->collectCacheKeysByEntity($entity);
-            parent::save($entity);
-            if ($cacheKeys && !$this->cache->deleteMultiple(array_unique($cacheKeys))) {
-                throw new DbException('Failed to flush cache keys');
-            }
-        });
+        $cacheKeys = $this->collectCacheKeysByEntity($entity);
+        parent::save($entity);
+        if ($cacheKeys) {
+            $this->cache->deleteMultiple($cacheKeys);
+        }
     }
 
     /**
      * @param AbstractEntity[] $entities
-     * @return  AbstractEntity[]
      * @throws \Throwable
      */
-    public function saveMany(array $entities): array
+    public function saveMany(array $entities): void
     {
-        return $this->getConnection()->transactional(function() use ($entities) {
-            $cacheKeys = [];
-            foreach ($entities as $entity) {
-                $cacheKeys = array_merge($cacheKeys, $this->collectCacheKeysByEntity($entity));
-            }
-            foreach ($entities as $entity) {
-                parent::save($entity);
-            }
-            if ($cacheKeys && !$this->cache->deleteMultiple(array_unique($cacheKeys))) {
-                throw new DbException('Failed to flush cache keys');
-            }
-            return $entities;
-        });
+        $cacheKeys = [];
+        foreach ($entities as $entity) {
+            $cacheKeys = array_merge($cacheKeys, $this->collectCacheKeysByEntity($entity));
+        }
+        parent::saveMany($entities);
+        if ($cacheKeys) {
+            $this->cache->deleteMultiple(array_unique($cacheKeys));
+        }
     }
 
     /**
      * @throws \Throwable
      */
-    public function delete(AbstractEntity &$entity): void
+    public function delete(AbstractEntity $entity): void
     {
-        $this->getConnection()->transactional(function () use (&$entity) {
-            $cacheKeys = $this->collectCacheKeysByEntity($entity);
-            parent::delete($entity);
-            if ($cacheKeys && !$this->cache->deleteMultiple(array_unique($cacheKeys))) {
-                throw new DbException('Failed to flush cache keys');
-            }
-        });
+        $cacheKeys = $this->collectCacheKeysByEntity($entity);
+        parent::delete($entity);
+        if ($cacheKeys) {
+            $this->cache->deleteMultiple($cacheKeys);
+        }
     }
 
     /**
      * @param AbstractEntity[] $entities
      * @throws \Throwable
      */
-    public function deleteMany(array $entities): bool
+    public function deleteMany(array $entities): void
     {
-        return $this->getConnection()->transactional(function() use ($entities) {
-            $cacheKeys = [];
-            foreach ($entities as $entity) {
-                $cacheKeys = array_merge($cacheKeys, $this->collectCacheKeysByEntity($entity));
-            }
-            foreach ($entities as $entity) {
-                parent::delete($entity);
-            }
-            if ($cacheKeys && !$this->cache->deleteMultiple(array_unique($cacheKeys))) {
-                throw new DbException('Failed to flush cache keys');
-            }
-            return true;
-        });
+        $cacheKeys = [];
+        foreach ($entities as $entity) {
+            $cacheKeys = array_merge($cacheKeys, $this->collectCacheKeysByEntity($entity));
+            parent::delete($entity);
+        }
+        parent::deleteMany($entities);
+        if ($cacheKeys) {
+            $this->cache->deleteMultiple($cacheKeys);
+        }
     }
 
     /**
@@ -102,62 +90,64 @@ private function collectCacheKeysByEntity(AbstractEntity $entity): array
         if (!$entity->isNew() || !$this->getConfig()->autoIncrementKey) {
             $keys[] = $this->getOneCacheKey($entity);
         }
-        return $keys;
+        return array_unique($keys);
     }
 
     /**
-     * @return array<string, mixed>|null
+     * @return AbstractEntity|null
      */
-    protected function findByPkCachedInternal(mixed $pk, null|int|\DateInterval $ttl = null): ?array
+    protected function _findByPkCached(mixed $pk, null|int|\DateInterval $ttl = null): mixed
     {
-        return $this->findOneCachedInternal($this->getPkCondition($pk), $ttl);
+        return $this->_findOneCached($this->getPkCondition($pk), $ttl);
     }
 
     /**
-     * @param array<string, mixed> $condition
+     * @param array<string, mixed> $where
      * @param int|\DateInterval|null $ttl
-     * @return array<string, mixed>|null
+     * @return AbstractEntity|null
      */
-    protected function findOneCachedInternal(array $condition, null|int|\DateInterval $ttl = null): ?array
+    protected function _findOneCached(array $where, null|int|\DateInterval $ttl = null): mixed
     {
-        return $this->getCached(
-            $this->getOneCacheKey($condition),
-            fn() => $this->findOneInternal($condition),
+        $row = $this->getCached(
+            $this->getOneCacheKey($where),
+            fn() => $this->_findOneRaw($where),
             $ttl,
-        ) ?: null;
+        );
+        return $this->createEntity($row);
     }
 
     /**
+     * @param array<string, mixed>|Where $where
      * @param array<string, string>|string $orderBy
-     * @return array<string, mixed>[]
+     * @return array<AbstractEntity>|array<array-key, AbstractEntity>
      */
-    protected function findAllCachedInternal(
-        string $whereString = '',
-        array $whereParams = [],
+    protected function _findAllCached(
+        array|Where $where = [],
         array|string $orderBy = [],
         ?int $limit = null,
         null|int|\DateInterval $ttl = null,
+        ?string $keyColumnName = null,
     ): array
     {
-        return $this->getCached(
-            $this->getListCacheKey($whereString, $whereParams, $orderBy, $limit),
-            fn() => $this->findAllInternal(whereString: $whereString, whereParams: $whereParams, orderBy: $orderBy, limit: $limit),
+        $rows = $this->getCached(
+            $this->getListCacheKey($where, $orderBy, $limit),
+            fn() => $this->_findAllRaw(where: $where, orderBy: $orderBy, limit: $limit),
             $ttl,
         );
+        return $this->createEntities($rows, $keyColumnName);
     }
 
     /**
-     * @param array<string, mixed> $whereParams
+     * @param array<string, mixed>|Where $where
      */
-    protected function countAllCachedInternal(
-        string $whereString = '',
-        array $whereParams = [],
+    protected function _countByAllCached(
+        array|Where $where = [],
         null|int|\DateInterval $ttl = null,
     ): int
     {
         return (int)$this->getCached(
-            $this->getCountCacheKey($whereString, $whereParams),
-            fn() => $this->countAllInternal(whereString: $whereString, whereParams: $whereParams),
+            $this->getCountCacheKey($where),
+            fn() => $this->_countAll(where: $where),
             $ttl,
         );
     }
@@ -175,7 +165,17 @@ protected function getCached(string $cacheKey, callable $dataCallback, null|int|
         return $data;
     }
 
-    protected function findMultiCachedInternal(array $ids, null|int|\DateInterval $ttl = null): array
+    /**
+     * @param mixed[] $ids
+     * @param int|\DateInterval|null $ttl
+     * @return array<AbstractEntity>|array<array-key, AbstractEntity>
+     * @throws \Psr\SimpleCache\InvalidArgumentException
+     */
+    protected function _findMultiCached(
+        array $ids,
+        null|int|\DateInterval $ttl = null,
+        ?string $keyColumnName = null,
+    ): array
     {
         $result = $cacheKeys = $foundIds = [];
         foreach ($ids as $id) {
@@ -184,22 +184,27 @@ protected function findMultiCachedInternal(array $ids, null|int|\DateInterval $t
         }
         $cache = $this->cache->getMultiple(array_keys($cacheKeys));
         foreach ($cache as $cacheKey => $cachedRow) {
-            $result[] = $cachedRow;
-            if (empty($cacheKeys[$cacheKey])) {
+            if ($cachedRow === null) {
                 continue;
             }
-            $foundIds[] = $cacheKeys[$cacheKey];
+            if (isset($cacheKeys[$cacheKey])) {
+                $result[] = $cachedRow;
+                $foundIds[] = $cacheKeys[$cacheKey];
+            }
         }
         $ids = array_diff($ids, $foundIds);
         foreach ($ids as $id) {
-            if ($row = $this->findOneCachedInternal($id, $ttl)) {
+            if ($row = $this->_findByPkCached($id, $ttl)) {
                 $result[] = $row;
             }
         }
-        return $result;
+        return $this->createEntities($result, $keyColumnName);
     }
 
-    protected function getOneCacheKey(string|int|array|AbstractEntity $keyOrEntity): string
+    /**
+     * @param string|int|array<string, mixed>|AbstractEntity $keyOrEntity
+     */
+    protected function getOneCacheKey(string|int|array|AbstractEntity|UuidInterface $keyOrEntity): string
     {
         if (!is_array($keyOrEntity)) {
             $condition = $this->getPkCondition($keyOrEntity);
@@ -209,31 +214,36 @@ protected function getOneCacheKey(string|int|array|AbstractEntity $keyOrEntity):
         return $this->buildCacheKey('o', $condition ?: 'one');
     }
 
+    /**
+     * @param array<string, mixed>|Where $where
+     * @param array<string, string>|string $orderBy
+     */
     protected function getListCacheKey(
-        string $whereString = '',
-        array $whereParams = [],
+        array|Where $where = [],
         array|string $orderBy = [],
         ?int $limit = null
     ): string
     {
-        $wherePart = $this->prepareWhereKey($whereString, $whereParams);
+        $wherePart = is_array($where) ? $where : $this->prepareWhereKey($where);
         return $this->buildCacheKey(
             'l',
-            $wherePart ?? 'all',
+            $wherePart ?: 'all',
             $orderBy ? ['ob' => $orderBy] : null,
             $limit ? ['limit' => $limit] : null,
         );
     }
 
+    /**
+     * @param array<string, mixed>|Where $where
+     */
     protected function getCountCacheKey(
-        string $whereString = '',
-        array $whereParams = [],
+        array|Where $where = [],
     ): string
     {
-        $wherePart = $this->prepareWhereKey($whereString, $whereParams);
+        $wherePart = is_array($where) ? $where : $this->prepareWhereKey($where);
         return $this->buildCacheKey(
             'c',
-            $wherePart ?? 'all',
+            $wherePart ?: 'all',
         );
     }
 
@@ -244,7 +254,7 @@ protected function buildCacheKey(mixed ...$parts): string
             $formattedParts = [];
             foreach ($parts as $part) {
                 if (is_array($part)) {
-                    $string = json_encode($part);
+                    $string = json_encode($part, JSON_THROW_ON_ERROR);
                 } else {
                     $string = strval($part);
                 }
@@ -268,24 +278,18 @@ protected function buildCacheKey(mixed ...$parts): string
 
     private function formatStringForCacheKey(string $string): string
     {
-        $string = mb_strtolower($string);
+        $string = strtolower($string);
         $string = str_replace(['!=', '<>', '>', '<', '='], ['_not_', '_not_', '_gt_', '_lt_', '_eq_'], $string);
-        $string =  preg_replace('/\W/', '_', $string);
-        return trim(preg_replace('/_+/', '_', $string), '_');
+        $string =  (string)preg_replace('/\W/', '_', $string);
+        return trim((string)preg_replace('/_+/', '_', $string), '_');
     }
 
-    private function prepareWhereKey(string $whereString, array $whereParams): ?string
+    private function prepareWhereKey(Where $where): string
     {
-        if (!$whereString) {
-            return null;
-        }
-        if (!$whereParams) {
-            return $whereString;
-        }
         return str_replace(
-            array_map(fn (string $key): string => ':' . $key, array_keys($whereParams)),
-            array_values($whereParams),
-            $whereString,
+            array_map(fn (string $key): string => ':' . $key, array_keys($where->params)),
+            array_values($where->params),
+            $where->condition,
         );
     }
 }
diff --git a/src/AbstractTable.php b/src/AbstractTable.php
index 4d465f1..e144cd3 100644
--- a/src/AbstractTable.php
+++ b/src/AbstractTable.php
@@ -2,16 +2,21 @@
 
 namespace Composite\DB;
 
-use Composite\Entity\AbstractEntity;
 use Composite\DB\Exceptions\DbException;
-use Composite\Entity\Exceptions\EntityException;
+use Composite\DB\MultiQuery\MultiInsert;
+use Composite\DB\MultiQuery\MultiSelect;
+use Composite\Entity\AbstractEntity;
+use Composite\Entity\Helpers\DateTimeHelper;
 use Doctrine\DBAL\Connection;
-use Doctrine\DBAL\Query\QueryBuilder;
+use Ramsey\Uuid\UuidInterface;
 
 abstract class AbstractTable
 {
+    use Helpers\SelectRawTrait;
+    use Helpers\DatabaseSpecificTrait;
+
     protected readonly TableConfig $config;
-    private ?QueryBuilder $selectQuery = null;
+
 
     abstract protected function getConfig(): TableConfig;
 
@@ -40,167 +45,223 @@ public function getConnectionName(): string
      * @return void
      * @throws \Throwable
      */
-    public function save(AbstractEntity &$entity): void
+    public function save(AbstractEntity $entity): void
     {
         $this->config->checkEntity($entity);
         if ($entity->isNew()) {
             $connection = $this->getConnection();
-            $insertData = $entity->toArray();
+            $this->checkUpdatedAt($entity);
+
+            $insertData = $this->prepareDataForSql($entity->toArray());
             $this->getConnection()->insert($this->getTableName(), $insertData);
 
-            if ($this->config->autoIncrementKey) {
-                $insertData[$this->config->autoIncrementKey] = intval($connection->lastInsertId());
-                $entity = $entity::fromArray($insertData);
-            } else {
-                $entity->resetChangedColumns();
+            if ($this->config->autoIncrementKey && ($lastInsertedId = $connection->lastInsertId())) {
+                $insertData[$this->config->autoIncrementKey] = intval($lastInsertedId);
+                $entity::schema()
+                    ->getColumn($this->config->autoIncrementKey)
+                    ->setValue($entity, $insertData[$this->config->autoIncrementKey]);
             }
+            $entity->resetChangedColumns($insertData);
         } else {
             if (!$changedColumns = $entity->getChangedColumns()) {
                 return;
             }
-            $connection = $this->getConnection();
-            $where = $this->getPkCondition($entity);
-            $this->enrichCondition($where);
+            $changedColumns = $this->prepareDataForSql($changedColumns);
+            if ($this->config->hasUpdatedAt() && property_exists($entity, 'updated_at')) {
+                $entity->updated_at = new \DateTimeImmutable();
+                $changedColumns['updated_at'] = DateTimeHelper::dateTimeToString($entity->updated_at);
+            }
+            $whereParams = $this->getPkCondition($entity);
+            if ($this->config->hasOptimisticLock()
+                && method_exists($entity, 'getVersion')
+                && method_exists($entity, 'incrementVersion')) {
+                $whereParams['lock_version'] = $entity->getVersion();
+                $entity->incrementVersion();
+                $changedColumns['lock_version'] = $entity->getVersion();
+            }
+            $updateString = implode(', ', array_map(fn ($key) => $this->escapeIdentifier($key) . "=?", array_keys($changedColumns)));
+            $whereString = implode(' AND ', array_map(fn ($key) => $this->escapeIdentifier($key) . "=?", array_keys($whereParams)));
 
-            if ($this->config->isOptimisticLock && isset($entity->version)) {
-                $currentVersion = $entity->version;
-                try {
-                    $connection->beginTransaction();
-                    $connection->update(
-                        $this->getTableName(),
-                        $changedColumns,
-                        $where
-                    );
-                    $versionUpdated = $connection->update(
-                        $this->getTableName(),
-                        ['version' => $currentVersion + 1],
-                        $where + ['version' => $currentVersion]
-                    );
-                    if (!$versionUpdated) {
-                        throw new DbException('Failed to update entity version, concurrency modification, rolling back.');
-                    }
-                    $connection->commit();
-                } catch (\Throwable $e) {
-                    $connection->rollBack();
-                    throw $e;
-                }
-            } else {
-                $connection->update(
-                    $this->getTableName(),
-                    $changedColumns,
-                    $where
-                );
+            $entityUpdated = (bool)$this->getConnection()->executeStatement(
+                sql: "UPDATE " . $this->escapeIdentifier($this->getTableName()) . " SET $updateString WHERE $whereString;",
+                params: array_merge(array_values($changedColumns), array_values($whereParams)),
+            );
+            if ($this->config->hasOptimisticLock() && !$entityUpdated) {
+                throw new Exceptions\LockException('Failed to update entity version, concurrency modification, rolling back.');
             }
-            $entity->resetChangedColumns();
+            $entity->resetChangedColumns($changedColumns);
         }
     }
 
     /**
      * @param AbstractEntity[] $entities
-     * @return AbstractEntity[] $entities
      * @throws \Throwable
      */
-    public function saveMany(array $entities): array
+    public function saveMany(array $entities): void
     {
-        return $this->getConnection()->transactional(function() use ($entities) {
+        $rowsToInsert = [];
+        foreach ($entities as $i => $entity) {
+            if ($entity->isNew()) {
+                $this->config->checkEntity($entity);
+                $this->checkUpdatedAt($entity);
+                $rowsToInsert[] = $this->prepareDataForSql($entity->toArray());
+                unset($entities[$i]);
+            }
+        }
+        $connection = $this->getConnection();
+        $connection->beginTransaction();
+        try {
             foreach ($entities as $entity) {
                 $this->save($entity);
             }
-            return $entities;
-        });
+            if ($rowsToInsert) {
+                $chunks = array_chunk($rowsToInsert, 1000);
+                $connection = $this->getConnection();
+                foreach ($chunks as $chunk) {
+                    $multiInsert = new MultiInsert(
+                        connection: $connection,
+                        tableName: $this->getTableName(),
+                        rows: $chunk,
+                    );
+                    if ($multiInsert->getSql()) {
+                        $connection->executeStatement($multiInsert->getSql(), $multiInsert->getParameters());
+                    }
+                }
+            }
+            $connection->commit();
+        } catch (\Throwable $e) {
+            $connection->rollBack();
+            throw $e;
+        }
     }
 
     /**
-     * @throws EntityException
+     * @param AbstractEntity $entity
+     * @throws \Throwable
      */
-    public function delete(AbstractEntity &$entity): void
+    public function delete(AbstractEntity $entity): void
     {
         $this->config->checkEntity($entity);
-        if ($this->config->isSoftDelete) {
+        if ($this->config->hasSoftDelete()) {
             if (method_exists($entity, 'delete')) {
                 $entity->delete();
                 $this->save($entity);
             }
         } else {
-            $where = $this->getPkCondition($entity);
-            $this->enrichCondition($where);
-            $this->getConnection()->delete($this->getTableName(), $where);
+            $whereParams = $this->getPkCondition($entity);
+            $whereString = implode(' AND ', array_map(fn ($key) => $this->escapeIdentifier($key) . "=?", array_keys($whereParams)));
+            $this->getConnection()->executeQuery(
+                sql: "DELETE FROM " . $this->escapeIdentifier($this->getTableName()) . " WHERE $whereString;",
+                params: array_values($whereParams),
+            );
         }
     }
 
     /**
      * @param AbstractEntity[] $entities
+     * @throws \Throwable
      */
-    public function deleteMany(array $entities): bool
+    public function deleteMany(array $entities): void
     {
-        return $this->getConnection()->transactional(function() use ($entities) {
+        $connection = $this->getConnection();
+        $connection->beginTransaction();
+        try {
             foreach ($entities as $entity) {
                 $this->delete($entity);
             }
-            return true;
-        });
+            $connection->commit();
+        } catch (\Throwable $e) {
+            $connection->rollBack();
+            throw $e;
+        }
     }
 
-    protected function countAllInternal(string $whereString = '', array $whereParams = []): int
+    /**
+     * @param array<string, mixed>|Where $where
+     * @throws \Doctrine\DBAL\Exception
+     */
+    protected function _countAll(array|Where $where = []): int
     {
         $query = $this->select('COUNT(*)');
-        if ($whereString) {
-            $query->where($whereString);
-            foreach ($whereParams as $param => $value) {
+        if (is_array($where)) {
+            $this->buildWhere($query, $where);
+        } else {
+            $query->where($where->condition);
+            foreach ($where->params as $param => $value) {
                 $query->setParameter($param, $value);
             }
         }
-        $this->enrichCondition($query);
         return intval($query->executeQuery()->fetchOne());
     }
 
-    protected function findByPkInternal(mixed $pk): ?array
+    /**
+     * @throws \Doctrine\DBAL\Exception
+     * @return AbstractEntity|null
+     */
+    protected function _findByPk(mixed $pk): mixed
     {
-        $where = $this->getPkCondition($pk);
-        return $this->findOneInternal($where);
+        $whereParams = $this->getPkCondition($pk);
+        $whereString = implode(' AND ', array_map(fn ($key) => $this->escapeIdentifier($key) . "=?", array_keys($whereParams)));
+        $row = $this->getConnection()
+            ->executeQuery(
+                sql: "SELECT * FROM " . $this->escapeIdentifier($this->getTableName()) . " WHERE $whereString;",
+                params: array_values($whereParams),
+            )
+            ->fetchAssociative();
+        return $this->createEntity($row);
     }
 
-    protected function findOneInternal(array $where): ?array
+    /**
+     * @param array<string, mixed>|Where $where
+     * @param array<string, string>|string $orderBy
+     * @return AbstractEntity|null
+     * @throws \Doctrine\DBAL\Exception
+     */
+    protected function _findOne(array|Where $where, array|string $orderBy = []): mixed
+    {
+        return $this->createEntity($this->_findOneRaw($where, $orderBy));
+    }
+
+    /**
+     * @param array<int|string|array<string,mixed>> $pkList
+     * @return array<AbstractEntity>| array<array-key, AbstractEntity>
+     * @throws DbException
+     * @throws \Doctrine\DBAL\Exception
+     */
+    protected function _findMulti(array $pkList, ?string $keyColumnName = null): array
     {
-        $query = $this->select();
-        $this->enrichCondition($where);
-        $this->buildWhere($query, $where);
-        return $query->fetchAssociative() ?: null;
+        if (!$pkList) {
+            return [];
+        }
+        $multiSelect = new MultiSelect($this->getConnection(), $this->config, $pkList);
+        return $this->createEntities(
+            $multiSelect->getQueryBuilder()->executeQuery()->fetchAllAssociative(),
+            $keyColumnName,
+        );
     }
 
-    protected function findAllInternal(
-        string $whereString = '',
-        array $whereParams = [],
+    /**
+     * @param array<string, mixed>|Where $where
+     * @param array<string, string>|string $orderBy
+     * @return array<AbstractEntity>| array<array-key, AbstractEntity>
+     */
+    protected function _findAll(
+        array|Where $where = [],
         array|string $orderBy = [],
         ?int $limit = null,
         ?int $offset = null,
+        ?string $keyColumnName = null,
     ): array
     {
-        $query = $this->select();
-        if ($whereString) {
-            $query->where($whereString);
-            foreach ($whereParams as $param => $value) {
-                $query->setParameter($param, $value);
-            }
-        }
-        $this->enrichCondition($query);
-
-        if ($orderBy) {
-            if (is_array($orderBy)) {
-                foreach ($orderBy as $column => $direction) {
-                    $query->addOrderBy($column, $direction);
-                }
-            } else {
-                $query->orderBy($orderBy);
-            }
-        }
-        if ($limit > 0) {
-            $query->setMaxResults($limit);
-        }
-        if ($offset > 0) {
-            $query->setFirstResult($offset);
-        }
-        return $query->executeQuery()->fetchAllAssociative();
+        return $this->createEntities(
+            data: $this->_findAllRaw(
+                where: $where,
+                orderBy: $orderBy,
+                limit: $limit,
+                offset: $offset,
+            ),
+            keyColumnName: $keyColumnName,
+        );
     }
 
     final protected function createEntity(mixed $data): mixed
@@ -209,7 +270,7 @@ final protected function createEntity(mixed $data): mixed
             return null;
         }
         try {
-            /** @psalm-var class-string<AbstractEntity> $entityClass */
+            /** @var class-string<AbstractEntity> $entityClass */
             $entityClass = $this->config->entityClass;
             return $entityClass::fromArray($data);
         } catch (\Throwable) {
@@ -217,7 +278,10 @@ final protected function createEntity(mixed $data): mixed
         }
     }
 
-    final protected function createEntities(mixed $data): array
+    /**
+     * @return AbstractEntity[]
+     */
+    final protected function createEntities(mixed $data, ?string $keyColumnName = null): array
     {
         if (!is_array($data)) {
             return [];
@@ -227,10 +291,19 @@ final protected function createEntities(mixed $data): array
             $entityClass = $this->config->entityClass;
             $result = [];
             foreach ($data as $datum) {
-                if (!is_array($datum)) {
-                    continue;
+                if (is_array($datum)) {
+                    if ($keyColumnName && isset($datum[$keyColumnName])) {
+                        $result[$datum[$keyColumnName]] = $entityClass::fromArray($datum);
+                    } else {
+                        $result[] = $entityClass::fromArray($datum);
+                    }
+                } elseif ($datum instanceof $this->config->entityClass) {
+                    if ($keyColumnName && property_exists($datum, $keyColumnName)) {
+                        $result[$datum->{$keyColumnName}] = $datum;
+                    } else {
+                        $result[] = $datum;
+                    }
                 }
-                $result[] = $entityClass::fromArray($datum);
             }
         } catch (\Throwable) {
             return [];
@@ -238,11 +311,22 @@ final protected function createEntities(mixed $data): array
         return $result;
     }
 
-    protected function getPkCondition(int|string|array|AbstractEntity $data): array
+    /**
+     * @param int|string|array<string, mixed>|AbstractEntity $data
+     * @return array<string, mixed>
+     */
+    protected function getPkCondition(int|string|array|AbstractEntity|UuidInterface $data): array
     {
         $condition = [];
         if ($data instanceof AbstractEntity) {
-            $data = $data->toArray();
+            if ($data->isNew()) {
+                $data = $data->toArray();
+            } else {
+                foreach ($this->config->primaryKeys as $key) {
+                    $condition[$key] = $data->getOldValue($key);
+                }
+                return $condition;
+            }
         }
         if (is_array($data)) {
             foreach ($this->config->primaryKeys as $key) {
@@ -256,36 +340,10 @@ protected function getPkCondition(int|string|array|AbstractEntity $data): array
         return $condition;
     }
 
-    protected function enrichCondition(array|QueryBuilder &$query): void
+    private function checkUpdatedAt(AbstractEntity $entity): void
     {
-        if ($this->config->isSoftDelete) {
-            if ($query instanceof QueryBuilder) {
-                $query->andWhere('deleted_at IS NULL');
-            } else {
-                if (!isset($query['deleted_at'])) {
-                    $query['deleted_at'] = null;
-                }
-            }
-        }
-    }
-
-    protected function select(string $select = '*'): QueryBuilder
-    {
-        if ($this->selectQuery === null) {
-            $this->selectQuery = $this->getConnection()->createQueryBuilder()->from($this->getTableName());
-        }
-        return (clone $this->selectQuery)->select($select);
-    }
-
-    private function buildWhere(QueryBuilder $query, array $where): void
-    {
-        foreach ($where as $column => $value) {
-            if ($value === null) {
-                $query->andWhere("$column IS NULL");
-            } else {
-                $query->andWhere("$column = :" . $column);
-                $query->setParameter($column, $value);
-            }
+        if ($this->config->hasUpdatedAt() && property_exists($entity, 'updated_at') && $entity->updated_at === null) {
+            $entity->updated_at = new \DateTimeImmutable();
         }
     }
 }
diff --git a/src/Attributes/Column.php b/src/Attributes/Column.php
deleted file mode 100644
index 11ac3ca..0000000
--- a/src/Attributes/Column.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Composite\DB\Attributes;
-
-#[\Attribute]
-class Column
-{
-    public function __construct(
-        public readonly string|int|float|bool|null $default = null,
-        public readonly ?int $size = null,
-        public readonly ?int $precision = null,
-        public readonly ?int $scale = null,
-        public readonly ?bool $unsigned = null,
-    ) {}
-}
diff --git a/src/Attributes/Index.php b/src/Attributes/Index.php
deleted file mode 100644
index b16b850..0000000
--- a/src/Attributes/Index.php
+++ /dev/null
@@ -1,13 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Composite\DB\Attributes;
-
-#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS)]
-class Index
-{
-    public function __construct(
-        public readonly array $columns,
-        public readonly bool $isUnique = false,
-        public readonly ?string $name = null,
-    ) {}
-}
\ No newline at end of file
diff --git a/src/Attributes/PrimaryKey.php b/src/Attributes/PrimaryKey.php
index a9540bc..d8db396 100644
--- a/src/Attributes/PrimaryKey.php
+++ b/src/Attributes/PrimaryKey.php
@@ -2,7 +2,7 @@
 
 namespace Composite\DB\Attributes;
 
-#[\Attribute]
+#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_PARAMETER)]
 class PrimaryKey
 {
     public function __construct(
diff --git a/src/Attributes/Strict.php b/src/Attributes/Strict.php
deleted file mode 100644
index db186b4..0000000
--- a/src/Attributes/Strict.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Composite\DB\Attributes;
-
-#[\Attribute]
-class Strict {}
\ No newline at end of file
diff --git a/src/Attributes/Table.php b/src/Attributes/Table.php
index 8f09858..3c8eefc 100644
--- a/src/Attributes/Table.php
+++ b/src/Attributes/Table.php
@@ -2,7 +2,7 @@
 
 namespace Composite\DB\Attributes;
 
-#[\Attribute]
+#[\Attribute(\Attribute::TARGET_CLASS)]
 class Table
 {
     public function __construct(
diff --git a/src/CombinedTransaction.php b/src/CombinedTransaction.php
index e5f2671..bb5a590 100644
--- a/src/CombinedTransaction.php
+++ b/src/CombinedTransaction.php
@@ -19,18 +19,29 @@ class CombinedTransaction
      */
     public function save(AbstractTable $table, AbstractEntity &$entity): void
     {
-        try {
-            $connectionName = $table->getConnectionName();
-            if (empty($this->transactions[$connectionName])) {
-                $connection = ConnectionManager::getConnection($connectionName);
-                $connection->beginTransaction();
-                $this->transactions[$connectionName] = $connection;
-            }
+        $this->doInTransaction($table, function () use ($table, &$entity) {
             $table->save($entity);
-        } catch (\Throwable $e) {
-            $this->rollback();
-            throw new Exceptions\DbException($e->getMessage(), 500, $e);
-        }
+        });
+    }
+
+    /**
+     * @param AbstractTable $table
+     * @param AbstractEntity[] $entities
+     * @throws DbException
+     */
+    public function saveMany(AbstractTable $table, array $entities): void
+    {
+        $this->doInTransaction($table, fn () => $table->saveMany($entities));
+    }
+
+    /**
+     * @param AbstractTable $table
+     * @param AbstractEntity[] $entities
+     * @throws DbException
+     */
+    public function deleteMany(AbstractTable $table, array $entities): void
+    {
+        $this->doInTransaction($table, fn () => $table->deleteMany($entities));
     }
 
     /**
@@ -38,14 +49,18 @@ public function save(AbstractTable $table, AbstractEntity &$entity): void
      */
     public function delete(AbstractTable $table, AbstractEntity &$entity): void
     {
-        try {
-            $connectionName = $table->getConnectionName();
-            if (empty($this->transactions[$connectionName])) {
-                $connection = ConnectionManager::getConnection($connectionName);
-                $connection->beginTransaction();
-                $this->transactions[$connectionName] = $connection;
-            }
+        $this->doInTransaction($table, function () use ($table, &$entity) {
             $table->delete($entity);
+        });
+    }
+
+    /**
+     * @throws Exceptions\DbException
+     */
+    public function try(callable $callback): void
+    {
+        try {
+            $callback();
         } catch (\Throwable $e) {
             $this->rollback();
             throw new Exceptions\DbException($e->getMessage(), 500, $e);
@@ -70,34 +85,32 @@ public function commit(): void
                 if (!$connection->commit()) {
                     throw new Exceptions\DbException("Could not commit transaction for database `$connectionName`");
                 }
+            // I have no idea how to simulate failed commit
+            // @codeCoverageIgnoreStart
             } catch (\Throwable $e) {
                 $this->rollback();
                 throw new Exceptions\DbException($e->getMessage(), 500, $e);
             }
+            // @codeCoverageIgnoreEnd
         }
         $this->finish();
     }
 
     /**
      * Pessimistic lock
+     * @param string[] $keyParts
      * @throws DbException
+     * @throws InvalidArgumentException
      */
     public function lock(CacheInterface $cache, array $keyParts, int $duration = 10): void
     {
         $this->cache = $cache;
-        $this->lockKey = implode('.', array_merge(['composite', 'lock'], $keyParts));
-        if (strlen($this->lockKey) > 64) {
-            $this->lockKey = sha1($this->lockKey);
+        $this->lockKey = $this->buildLockKey($keyParts);
+        if ($this->cache->get($this->lockKey)) {
+            throw new DbException("Failed to get lock `{$this->lockKey}`");
         }
-        try {
-            if ($this->cache->get($this->lockKey)) {
-                throw new DbException("Failed to get lock `{$this->lockKey}`");
-            }
-            if (!$this->cache->set($this->lockKey, 1, $duration)) {
-                throw new DbException("Failed to save lock `{$this->lockKey}`");
-            }
-        } catch (InvalidArgumentException) {
-            throw new DbException("Lock key is invalid `{$this->lockKey}`");
+        if (!$this->cache->set($this->lockKey, 1, $duration)) {
+            throw new DbException("Failed to save lock `{$this->lockKey}`");
         }
     }
 
@@ -106,9 +119,37 @@ public function releaseLock(): void
         if (!$this->cache || !$this->lockKey) {
             return;
         }
+        $this->cache->delete($this->lockKey);
+    }
+
+    private function doInTransaction(AbstractTable $table, callable $callback): void
+    {
         try {
-            $this->cache->delete($this->lockKey);
-        } catch (InvalidArgumentException) {}
+            $connectionName = $table->getConnectionName();
+            if (empty($this->transactions[$connectionName])) {
+                $connection = ConnectionManager::getConnection($connectionName);
+                $connection->beginTransaction();
+                $this->transactions[$connectionName] = $connection;
+            }
+            $callback();
+        } catch (\Throwable $e) {
+            $this->rollback();
+            throw new Exceptions\DbException($e->getMessage(), 500, $e);
+        }
+    }
+
+    /**
+     * @param string[] $keyParts
+     * @return string
+     */
+    private function buildLockKey(array $keyParts): string
+    {
+        $keyParts = array_merge(['composite', 'lock'], $keyParts);
+        $result = implode('.', $keyParts);
+        if (strlen($result) > 64) {
+            $result = sha1($result);
+        }
+        return $result;
     }
 
     private function finish(): void
diff --git a/src/Commands/CommandHelperTrait.php b/src/Commands/CommandHelperTrait.php
deleted file mode 100644
index 9c37488..0000000
--- a/src/Commands/CommandHelperTrait.php
+++ /dev/null
@@ -1,108 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Composite\DB\Commands;
-
-use Composer\Autoload\ClassLoader;
-use Symfony\Component\Console\Command\Command;
-use Symfony\Component\Console\Helper\QuestionHelper;
-use Symfony\Component\Console\Input\InputInterface;
-use Symfony\Component\Console\Output\OutputInterface;
-use Symfony\Component\Console\Question\ConfirmationQuestion;
-use Symfony\Component\Console\Question\Question;
-
-trait CommandHelperTrait
-{
-    private function showSuccess(OutputInterface $output, string $text): int
-    {
-        $output->writeln("<fg=green>$text</fg=green>");
-        return Command::SUCCESS;
-    }
-
-    private function showAlert(OutputInterface $output, string $text): int
-    {
-        $output->writeln("<fg=yellow>$text</fg=yellow>");
-        return Command::SUCCESS;
-    }
-
-    private function showError(OutputInterface $output, string $text): int
-    {
-        $output->writeln("<fg=red>$text</fg=red>");
-        return Command::INVALID;
-    }
-
-    protected function ask(InputInterface $input, OutputInterface $output, Question $question): mixed
-    {
-        return (new QuestionHelper())->ask($input, $output, $question);
-    }
-
-    private function saveClassToFile(InputInterface $input, OutputInterface $output, string $class, string $content): bool
-    {
-        if (!$filePath = $this->getClassFilePath($class)) {
-            return false;
-        }
-        $fileState = 'new';
-        if (file_exists($filePath)) {
-            $fileState = 'overwrite';
-            if (!$input->getOption('force')
-                && !$this->ask($input, $output, new ConfirmationQuestion("File `$filePath` is already exists, do you want to overwrite it?[y/n]: "))) {
-                return true;
-            }
-        }
-        if (file_put_contents($filePath, $content)) {
-            $this->showSuccess($output, "File `$filePath` was successfully generated ($fileState)");
-            return true;
-        } else {
-            $this->showError($output, "Something went wrong can `$filePath` was successfully generated ($fileState)");
-            return false;
-        }
-    }
-
-    protected function getClassFilePath(string $class): ?string
-    {
-        $class = trim($class, '\\');
-        $namespaceParts = explode('\\', $class);
-
-        $loaders = ClassLoader::getRegisteredLoaders();
-        $matchedPrefixes = $matchedDirs = [];
-        foreach ($loaders as $loader) {
-            foreach ($loader->getPrefixesPsr4() as $prefix => $dir) {
-                $prefixParts = explode('\\', trim($prefix, '\\'));
-                foreach ($namespaceParts as $i => $namespacePart) {
-                    if (!isset($prefixParts[$i]) || $prefixParts[$i] !== $namespacePart) {
-                        break;
-                    }
-                    if (!isset($matchedPrefixes[$prefix])) {
-                        $matchedPrefixes[$prefix] = 0;
-                        $matchedDirs[$prefix] = $dir;
-                    }
-                    $matchedPrefixes[$prefix] += 1;
-                }
-            }
-        }
-        if (empty($matchedPrefixes)) {
-            throw new \Exception("Failed to determine directory for class `$class` from psr4 autoloading");
-        }
-        arsort($matchedPrefixes);
-        $prefix = key($matchedPrefixes);
-        $dirs = $matchedDirs[$prefix];
-
-        $namespaceParts = explode('\\', str_replace($prefix, '', $class));
-        $filename = array_pop($namespaceParts) . '.php';
-
-        $relativeDir = implode(
-            DIRECTORY_SEPARATOR,
-            array_merge(
-                $dirs,
-                $namespaceParts,
-            )
-        );
-        if (!$realDir = realpath($relativeDir)) {
-            $dirCreateResult = mkdir($relativeDir, 0755, true);
-            if (!$dirCreateResult) {
-                throw new \Exception("Directory `$relativeDir` not exists and failed to create it, please create it manually.");
-            }
-            $realDir = realpath($relativeDir);
-        }
-        return $realDir . DIRECTORY_SEPARATOR . $filename;
-    }
-}
diff --git a/src/Commands/GenerateEntityCommand.php b/src/Commands/GenerateEntityCommand.php
deleted file mode 100644
index 16c3cdd..0000000
--- a/src/Commands/GenerateEntityCommand.php
+++ /dev/null
@@ -1,90 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Composite\DB\Commands;
-
-use Composite\DB\ConnectionManager;
-use Composite\DB\Generator\EntityClassBuilder;
-use Composite\DB\Generator\EnumClassBuilder;
-use Composite\DB\Generator\Schema\SQLEnum;
-use Composite\DB\Generator\Schema\SQLSchema;
-use Composite\DB\Helpers\ClassHelper;
-use Doctrine\Inflector\Rules\English\InflectorFactory;
-use Symfony\Component\Console\Command\Command;
-use Symfony\Component\Console\Input\InputArgument;
-use Symfony\Component\Console\Input\InputInterface;
-use Symfony\Component\Console\Input\InputOption;
-use Symfony\Component\Console\Output\OutputInterface;
-use Symfony\Component\Console\Question\ConfirmationQuestion;
-use Symfony\Component\Console\Question\Question;
-
-class GenerateEntityCommand extends Command
-{
-    use CommandHelperTrait;
-
-    protected static $defaultName = 'composite-db:generate-entity';
-
-    protected function configure(): void
-    {
-        $this
-            ->addArgument('connection', InputArgument::REQUIRED, 'Connection name')
-            ->addArgument('table', InputArgument::REQUIRED, 'Table name')
-            ->addArgument('entity', InputArgument::OPTIONAL, 'Entity full class name')
-            ->addOption('force', 'f', InputOption::VALUE_NONE, 'If existing file should be overwritten');
-    }
-
-    /**
-     * @throws \Exception
-     */
-    protected function execute(InputInterface $input, OutputInterface $output): int
-    {
-        $connectionName = $input->getArgument('connection');
-        $tableName = $input->getArgument('table');
-        $connection = ConnectionManager::getConnection($connectionName);
-
-        if (!$entityClass = $input->getArgument('entity')) {
-            $entityClass = $this->ask($input, $output, new Question('Enter entity full class name: '));
-        }
-        $entityClass = str_replace('\\\\', '\\', $entityClass);
-
-        $schema = SQLSchema::generate($connection, $tableName);
-        $enums = [];
-        foreach ($schema->enums as $columnName => $sqlEnum) {
-            if ($enumClass = $this->generateEnum($input, $output, $entityClass, $sqlEnum)) {
-                $enums[$columnName] = $enumClass;
-            }
-        }
-        $entityBuilder = new EntityClassBuilder($schema, $connectionName, $entityClass, $enums);
-        $content = $entityBuilder->getClassContent();
-
-        $this->saveClassToFile($input, $output, $entityClass, $content);
-        return Command::SUCCESS;
-    }
-
-    private function generateEnum(InputInterface $input, OutputInterface $output, string $entityClass, SQLEnum $enum): ?string
-    {
-        $name = $enum->name;
-        $values = $enum->values;
-        $this->showAlert($output, "Found enum `$name` with values [" . implode(', ', $values) . "]");
-        if (!$this->ask($input, $output, new ConfirmationQuestion('Do you want to generate Enum class?[y/n]: '))) {
-            return null;
-        }
-        $enumShortClassName = ucfirst((new InflectorFactory())->build()->camelize($name));
-        $entityNamespace = ClassHelper::extractNamespace($entityClass);
-        $proposedClass = $entityNamespace . '\\Enums\\' . $enumShortClassName;
-        $enumClass = $this->ask(
-            $input,
-            $output,
-            new Question("Enter enum full class name [skip to use $proposedClass]: ")
-        );
-        if (!$enumClass) {
-            $enumClass = $proposedClass;
-        }
-        $enumClassBuilder = new EnumClassBuilder($enumClass, $values);
-
-        $content = $enumClassBuilder->getClassContent();
-        if (!$this->saveClassToFile($input, $output, $enumClass, $content)) {
-            return null;
-        }
-        return $enumClass;
-    }
-}
\ No newline at end of file
diff --git a/src/Commands/GenerateTableCommand.php b/src/Commands/GenerateTableCommand.php
deleted file mode 100644
index fa14cbb..0000000
--- a/src/Commands/GenerateTableCommand.php
+++ /dev/null
@@ -1,95 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Composite\DB\Commands;
-
-use Composite\DB\Attributes;
-use Composite\DB\Generator\CachedTableClassBuilder;
-use Composite\DB\Generator\TableClassBuilder;
-use Composite\DB\TableConfig;
-use Composite\Entity\AbstractEntity;
-use Symfony\Component\Console\Command\Command;
-use Symfony\Component\Console\Input\InputArgument;
-use Symfony\Component\Console\Input\InputInterface;
-use Symfony\Component\Console\Input\InputOption;
-use Symfony\Component\Console\Output\OutputInterface;
-use Symfony\Component\Console\Question\Question;
-
-class GenerateTableCommand extends Command
-{
-    use CommandHelperTrait;
-
-    protected static $defaultName = 'composite-db:generate-table';
-
-    protected function configure(): void
-    {
-        $this
-            ->addArgument('entity', InputArgument::REQUIRED, 'Entity full class name')
-            ->addArgument('table', InputArgument::OPTIONAL, 'Table full class name')
-            ->addOption('cached', 'c', InputOption::VALUE_NONE, 'Generate cache version')
-            ->addOption('force', 'f', InputOption::VALUE_NONE, 'Overwrite existing table class file');
-    }
-
-    /**
-     * @throws \Exception
-     */
-    protected function execute(InputInterface $input, OutputInterface $output): int
-    {
-        /** @var class-string<AbstractEntity> $entityClass */
-        $entityClass = $input->getArgument('entity');
-        $reflection = new \ReflectionClass($entityClass);
-
-        if (!$reflection->isSubclassOf(AbstractEntity::class)) {
-            return $this->showError($output, "Class `$entityClass` must be subclass of " . AbstractEntity::class);
-        }
-        $schema = $entityClass::schema();
-        $tableConfig = TableConfig::fromEntitySchema($schema);
-        $tableName = $tableConfig->tableName;
-
-        if (!$tableClass = $input->getArgument('table')) {
-            $proposedClass = preg_replace('/\w+$/', 'Tables', $reflection->getNamespaceName()) . "\\{$tableName}Table";
-            $tableClass = $this->ask(
-                $input,
-                $output,
-                new Question("Enter table full class name [skip to use $proposedClass]: ")
-            );
-            if (!$tableClass) {
-                $tableClass = $proposedClass;
-            }
-        }
-        if (str_starts_with($tableClass, '\\')) {
-            $tableClass = substr($tableClass, 1);
-        }
-
-        if (!preg_match('/^(.+)\\\(\w+)$/', $tableClass)) {
-            return $this->showError($output, "Table class `$tableClass` is incorrect");
-        }
-        if ($input->getOption('cached')) {
-            $template = new CachedTableClassBuilder(
-                tableClass: $tableClass,
-                schema: $schema,
-                tableConfig: $tableConfig,
-            );
-        } else {
-            $template = new TableClassBuilder(
-                tableClass: $tableClass,
-                schema: $schema,
-                tableConfig: $tableConfig,
-            );
-        }
-        $template->generate();
-        $fileContent = $template->getFileContent();
-
-        $fileState = 'new';
-        if (!$filePath = $this->getClassFilePath($tableClass)) {
-            return Command::FAILURE;
-        }
-        if (file_exists($filePath)) {
-            if (!$input->getOption('force')) {
-                return $this->showError($output, "File `$filePath` already exists, use --force flag to overwrite it");
-            }
-            $fileState = 'overwrite';
-        }
-        file_put_contents($filePath, $fileContent);
-        return $this->showSuccess($output, "File `$filePath` was successfully generated ($fileState)");
-    }
-}
diff --git a/src/ConnectionManager.php b/src/ConnectionManager.php
index 5ff27db..7854ed6 100644
--- a/src/ConnectionManager.php
+++ b/src/ConnectionManager.php
@@ -8,11 +8,14 @@
 use Doctrine\DBAL\Connection;
 use Doctrine\DBAL\DriverManager;
 use Doctrine\DBAL\Exception;
+use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory;
 
 class ConnectionManager
 {
     private const CONNECTIONS_CONFIG_ENV_VAR = 'CONNECTIONS_CONFIG_FILE';
+    /** @var array<string, array<string, mixed>>|null  */
     private static ?array $configs = null;
+    /** @var array<string, Connection>  */
     private static array $connections = [];
 
     /**
@@ -22,6 +25,12 @@ public static function getConnection(string $name, ?Configuration $config = null
     {
         if (!isset(self::$connections[$name])) {
             try {
+                if (!$config) {
+                    $config = new Configuration();
+                }
+                if (!$config->getSchemaManagerFactory()) {
+                    $config->setSchemaManagerFactory(new DefaultSchemaManagerFactory());
+                }
                 self::$connections[$name] = DriverManager::getConnection(
                     params: self::getConnectionParams($name),
                     config: $config,
@@ -35,50 +44,53 @@ public static function getConnection(string $name, ?Configuration $config = null
     }
 
     /**
+     * @return array<string, mixed>
      * @throws DbException
      */
     private static function getConnectionParams(string $name): array
     {
         if (self::$configs === null) {
-            $configFile = getenv(self::CONNECTIONS_CONFIG_ENV_VAR, true);
-            if (empty($configFile)) {
-                throw new DbException(sprintf(
-                    'ConnectionManager is not configured, please call ConnectionManager::configure() method or setup putenv(\'%s=/path/to/config/file.php\') variable',
-                    self::CONNECTIONS_CONFIG_ENV_VAR
-                ));
-            }
-            if (!file_exists($configFile)) {
-                throw new DbException(sprintf(
-                    'Connections config file `%s` does not exist',
-                    $configFile
-                ));
-            }
-            $configContent = require_once $configFile;
-            if (empty($configContent) || !is_array($configContent)) {
-                throw new DbException(sprintf(
-                    'Connections config file `%s` should return array of connection params',
-                    $configFile
-                ));
-            }
-            self::configure($configContent);
+            self::$configs = self::loadConfigs();
         }
         return self::$configs[$name] ?? throw new DbException("Connection config `$name` not found");
     }
 
     /**
+     * @return array<string, array<string, mixed>>
      * @throws DbException
      */
-    private static function configure(array $configs): void
+    private static function loadConfigs(): array
     {
-        foreach ($configs as $name => $connectionConfig) {
+        $configFile = getenv(self::CONNECTIONS_CONFIG_ENV_VAR, true) ?: ($_ENV[self::CONNECTIONS_CONFIG_ENV_VAR] ?? false);
+        if (empty($configFile)) {
+            throw new DbException(sprintf(
+                'ConnectionManager is not configured, please define ENV variable `%s`',
+                self::CONNECTIONS_CONFIG_ENV_VAR
+            ));
+        }
+        if (!file_exists($configFile)) {
+            throw new DbException(sprintf(
+                'Connections config file `%s` does not exist',
+                $configFile
+            ));
+        }
+        $configFileContent = require $configFile;
+        if (empty($configFileContent) || !is_array($configFileContent)) {
+            throw new DbException(sprintf(
+                'Connections config file `%s` should return array of connection params',
+                $configFile
+            ));
+        }
+        $result = [];
+        foreach ($configFileContent as $name => $connectionConfig) {
             if (empty($name) || !is_string($name)) {
                 throw new DbException('Config has invalid connection name ' . var_export($name, true));
             }
             if (empty($connectionConfig) || !is_array($connectionConfig)) {
                 throw new DbException("Connection `$name` has invalid connection params");
             }
-            self::$configs[$name] = $connectionConfig;
+            $result[$name] = $connectionConfig;
         }
-        self::$configs = $configs;
+        return $result;
     }
 }
\ No newline at end of file
diff --git a/src/Exceptions/LockException.php b/src/Exceptions/LockException.php
new file mode 100644
index 0000000..650194d
--- /dev/null
+++ b/src/Exceptions/LockException.php
@@ -0,0 +1,7 @@
+<?php declare(strict_types=1);
+
+namespace Composite\DB\Exceptions;
+
+class LockException extends DbException
+{
+}
\ No newline at end of file
diff --git a/src/Generator/AbstractTableClassBuilder.php b/src/Generator/AbstractTableClassBuilder.php
deleted file mode 100644
index f1e7063..0000000
--- a/src/Generator/AbstractTableClassBuilder.php
+++ /dev/null
@@ -1,68 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Composite\DB\Generator;
-
-use Composite\DB\Helpers\ClassHelper;
-use Composite\DB\TableConfig;
-use Composite\Entity\Columns\AbstractColumn;
-use Composite\Entity\Schema;
-use Doctrine\Inflector\Rules\English\InflectorFactory;
-use Nette\PhpGenerator\Method;
-use Nette\PhpGenerator\PhpFile;
-
-abstract class AbstractTableClassBuilder
-{
-    protected readonly PhpFile $file;
-    protected readonly string $entityClassShortName;
-
-    public function __construct(
-        protected readonly string $tableClass,
-        protected readonly Schema $schema,
-        protected readonly TableConfig $tableConfig,
-    )
-    {
-        $this->entityClassShortName = ClassHelper::extractShortName($this->schema->class);
-        $this->file = new PhpFile();
-    }
-
-    abstract public function getParentNamespace(): string;
-    abstract public function generate(): void;
-
-    final public function getFileContent(): string
-    {
-        return (string)$this->file;
-    }
-
-    protected function generateGetConfig(): Method
-    {
-        return (new Method('getConfig'))
-            ->setProtected()
-            ->setReturnType(TableConfig::class)
-            ->setBody('return TableConfig::fromEntitySchema(' . $this->entityClassShortName . '::schema());');
-    }
-
-    protected function buildVarsList(array $vars): string
-    {
-        if (count($vars) === 1) {
-            $var = current($vars);
-            return '$' . $var;
-        }
-        $vars = array_map(
-            fn ($var) => "'$var' => \$" . $var,
-            $vars
-        );
-        return '[' . implode(', ', $vars) . ']';
-    }
-
-    /**
-     * @param AbstractColumn[] $columns
-     */
-    protected function addMethodParameters(Method $method, array $columns): void
-    {
-        foreach ($columns as $column) {
-            $method
-                ->addParameter($column->name)
-                ->setType($column->type);
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/Generator/CachedTableClassBuilder.php b/src/Generator/CachedTableClassBuilder.php
deleted file mode 100644
index 760ad44..0000000
--- a/src/Generator/CachedTableClassBuilder.php
+++ /dev/null
@@ -1,97 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Composite\DB\Generator;
-
-use Composite\DB\AbstractCachedTable;
-use Composite\DB\TableConfig;
-use Composite\Entity\AbstractEntity;
-use Composite\Entity\Columns\AbstractColumn;
-use Composite\DB\Helpers\ClassHelper;
-use Nette\PhpGenerator\Method;
-
-class CachedTableClassBuilder extends AbstractTableClassBuilder
-{
-    public function getParentNamespace(): string
-    {
-        return AbstractCachedTable::class;
-    }
-
-    public function generate(): void
-    {
-        $this->file
-            ->addNamespace(ClassHelper::extractNamespace($this->tableClass))
-            ->addUse(AbstractEntity::class)
-            ->addUse(AbstractCachedTable::class)
-            ->addUse(TableConfig::class)
-            ->addUse($this->schema->class)
-            ->addClass(ClassHelper::extractShortName($this->tableClass))
-            ->setExtends(AbstractCachedTable::class)
-            ->setMethods($this->getMethods());
-    }
-
-    private function getMethods(): array
-    {
-        return array_filter([
-            $this->generateGetConfig(),
-            $this->generateGetFlushCacheKeys(),
-            $this->generateFindOne(),
-            $this->generateFindAll(),
-            $this->generateCountAll(),
-        ]);
-    }
-
-    protected function generateGetFlushCacheKeys(): Method
-    {
-        $method = (new Method('getFlushCacheKeys'))
-            ->setProtected()
-            ->setReturnType('array')
-            ->addBody('return [')
-            ->addBody('    $this->getListCacheKey(),')
-            ->addBody('    $this->getCountCacheKey(),')
-            ->addBody('];');
-
-        $type = $this->schema->class . '|' . AbstractEntity::class;
-        $method
-            ->addParameter('entity')
-            ->setType($type);
-        return $method;
-    }
-
-    protected function generateFindOne(): ?Method
-    {
-        $primaryColumns = array_map(
-            fn(string $key): AbstractColumn => $this->schema->getColumn($key) ?? throw new \Exception("Primary key column `$key` not found in entity."),
-            $this->tableConfig->primaryKeys
-        );
-        if (count($this->tableConfig->primaryKeys) === 1) {
-            $body = 'return $this->createEntity($this->findByPkCachedInternal(' . $this->buildVarsList($this->tableConfig->primaryKeys) . '));';
-        } else {
-            $body = 'return $this->createEntity($this->findOneCachedInternal(' . $this->buildVarsList($this->tableConfig->primaryKeys) . '));';
-        }
-
-        $method = (new Method('findByPk'))
-            ->setPublic()
-            ->setReturnType($this->schema->class)
-            ->setReturnNullable()
-            ->setBody($body);
-        $this->addMethodParameters($method, $primaryColumns);
-        return $method;
-    }
-
-    protected function generateFindAll(): Method
-    {
-        return (new Method('findAll'))
-            ->setPublic()
-            ->setComment('@return ' . $this->entityClassShortName . '[]')
-            ->setReturnType('array')
-            ->setBody('return $this->createEntities($this->findAllCachedInternal());');
-    }
-
-    protected function generateCountAll(): Method
-    {
-        return (new Method('countAll'))
-            ->setPublic()
-            ->setReturnType('int')
-            ->setBody('return $this->countAllCachedInternal();');
-    }
-}
\ No newline at end of file
diff --git a/src/Generator/EntityClassBuilder.php b/src/Generator/EntityClassBuilder.php
deleted file mode 100644
index 96453bd..0000000
--- a/src/Generator/EntityClassBuilder.php
+++ /dev/null
@@ -1,264 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Composite\DB\Generator;
-
-use Composite\Entity\AbstractEntity;
-use Composite\DB\Generator\Schema\ColumnType;
-use Composite\DB\Generator\Schema\SQLColumn;
-use Composite\DB\Generator\Schema\SQLSchema;
-use Composite\DB\Helpers\DateTimeHelper;
-
-class EntityClassBuilder
-{
-    /** @var string[] */
-    private array $useNamespaces = [
-        AbstractEntity::class,
-    ];
-    /** @var string[] */
-    private array $useAttributes = [
-        'Table',
-    ];
-
-    public function __construct(
-        private readonly SQLSchema $schema,
-        private readonly string $connectionName,
-        private readonly string $entityClass,
-        private readonly array $enums,
-    ) {}
-
-    /**
-     * @throws \Exception
-     */
-    public function getClassContent(): string
-    {
-        return $this->renderTemplate('EntityTemplate', $this->getVars());
-    }
-
-    /**
-     * @return array<string, mixed>
-     * @throws \Exception
-     */
-    private function getVars(): array
-    {
-        $traits = $properties = [];
-        $constructorParams = $this->getEntityProperties();
-        if (!empty($this->schema->columns['deleted_at'])) {
-            $traits[] = 'Traits\SoftDelete';
-            $this->useNamespaces[] = 'Composite\DB\Traits';
-            unset($constructorParams['deleted_at']);
-        }
-        foreach ($constructorParams as $name => $constructorParam) {
-            if ($this->schema->columns[$name]->isAutoincrement) {
-                $properties[$name] = $constructorParam;
-                unset($constructorParams[$name]);
-            }
-        }
-        if (!preg_match('/^(.+)\\\(\w+)$/', $this->entityClass, $matches)) {
-            throw new \Exception("Entity class `$this->entityClass` is incorrect");
-        }
-
-        return [
-            'phpOpener' => '<?php declare(strict_types=1);',
-            'connectionName' => $this->connectionName,
-            'tableName' => $this->schema->tableName,
-            'pkNames' => "'" . implode("', '", $this->schema->primaryKeys) . "'",
-            'indexes' => $this->getIndexes(),
-            'traits' => $traits,
-            'entityNamespace' => $matches[1],
-            'entityClassShortname' => $matches[2],
-            'properties' => $properties,
-            'constructorParams' => $constructorParams,
-            'useNamespaces' => array_unique($this->useNamespaces),
-            'useAttributes' => array_unique($this->useAttributes),
-        ];
-    }
-
-    private function getEntityProperties(): array
-    {
-        $noDefaultValue = $hasDefaultValue = [];
-        foreach ($this->schema->columns as $column) {
-            $attributes = [];
-            if ($this->schema->isPrimaryKey($column->name)) {
-                $this->useAttributes[] = 'PrimaryKey';
-                $autoIncrement = $column->isAutoincrement ? '(autoIncrement: true)' : '';
-                $attributes[] = '#[PrimaryKey' . $autoIncrement . ']';
-            }
-            if ($columnAttributeProperties = $column->getColumnAttributeProperties()) {
-                $this->useAttributes[] = 'Column';
-                $attributes[] = '#[Column(' . implode(', ', $columnAttributeProperties) . ')]';
-            }
-            $propertyParts = [$this->getPropertyVisibility($column)];
-            if ($this->isReadOnly($column)) {
-                $propertyParts[] = 'readonly';
-            }
-            $propertyParts[] = $this->getColumnType($column);
-            $propertyParts[] = '$' . $column->name;
-            if ($column->hasDefaultValue) {
-                $defaultValue = $this->getDefaultValue($column);
-                $propertyParts[] = '= ' . $defaultValue;
-                $hasDefaultValue[$column->name] = [
-                    'attributes' => $attributes,
-                    'var' => implode(' ', $propertyParts),
-                ];
-            } else {
-                $noDefaultValue[$column->name] = [
-                    'attributes' => $attributes,
-                    'var' => implode(' ', $propertyParts),
-                ];
-            }
-        }
-        return array_merge($noDefaultValue, $hasDefaultValue);
-    }
-
-    private function getPropertyVisibility(SQLColumn $column): string
-    {
-        return 'public';
-    }
-
-    private function isReadOnly(SQLColumn $column): bool
-    {
-        if ($column->isAutoincrement) {
-            return true;
-        }
-        $readOnlyColumns = array_merge(
-            $this->schema->primaryKeys,
-            [
-                'created_at',
-                'createdAt',
-            ]
-        );
-        return in_array($column->name, $readOnlyColumns);
-    }
-
-    private function getColumnType(SQLColumn $column): string
-    {
-        if ($column->type === ColumnType::Enum) {
-            if (!$type = $this->getEnumName($column->name)) {
-                $type = 'string';
-            }
-        } else {
-            $type = $column->type->value;
-        }
-        if ($column->isNullable) {
-            $type = '?' . $type;
-        }
-        return $type;
-    }
-
-    public function getDefaultValue(SQLColumn $column): mixed
-    {
-        $defaultValue = $column->defaultValue;
-        if ($defaultValue === null) {
-            return 'null';
-        }
-        if ($column->type === ColumnType::Datetime) {
-            $currentTimestamp = stripos($defaultValue, 'current_timestamp') === 0 || $defaultValue === 'now()';
-            if ($currentTimestamp) {
-                $defaultValue = "new \DateTimeImmutable()";
-            } else {
-                if ($defaultValue === 'epoch') {
-                    $defaultValue = '1970-01-01 00:00:00';
-                } elseif ($defaultValue instanceof \DateTimeInterface) {
-                    $defaultValue = DateTimeHelper::dateTimeToString($defaultValue);
-                }
-                $defaultValue = "new \DateTimeImmutable('" . $defaultValue . "')";
-            }
-        } elseif ($column->type === ColumnType::Enum) {
-            if ($enumName = $this->getEnumName($column->name)) {
-                $valueName = null;
-                /** @var \UnitEnum $enumClass */
-                $enumClass = $this->enums[$column->name];
-                foreach ($enumClass::cases() as $enumCase) {
-                    if ($enumCase->name === $defaultValue) {
-                        $valueName = $enumCase->name;
-                    }
-                }
-                if ($valueName) {
-                    $defaultValue = $enumName . '::' . $valueName;
-                } else {
-                    return 'null';
-                }
-            } else {
-                $defaultValue = "'$defaultValue'";
-            }
-        } elseif ($column->type === ColumnType::Boolean) {
-            if (strcasecmp($defaultValue, 'false') === 0) {
-                return 'false';
-            }
-            if (strcasecmp($defaultValue, 'true') === 0) {
-                return 'true';
-            }
-            return !empty($defaultValue) ? 'true' : 'false';
-        } elseif ($column->type === ColumnType::Array) {
-            if ($defaultValue === '{}' || $defaultValue === '[]') {
-                return '[]';
-            }
-            if ($decoded = json_decode($defaultValue, true)) {
-                return var_export($decoded, true);
-            }
-            return $defaultValue;
-        } else {
-            if ($column->type !== ColumnType::Integer && $column->type !== ColumnType::Float) {
-                $defaultValue = "'$defaultValue'";
-            }
-        }
-        return $defaultValue;
-    }
-
-    private function getEnumName(string $columnName): ?string
-    {
-        if (empty($this->enums[$columnName])) {
-            return null;
-        }
-        $enumClass = $this->enums[$columnName];
-        if (!\in_array($enumClass, $this->useNamespaces)) {
-            $this->useNamespaces[] = $enumClass;
-        }
-        return substr(strrchr($enumClass, "\\"), 1);
-    }
-
-    private function getIndexes(): array
-    {
-        $result = [];
-        foreach ($this->schema->indexes as $index) {
-            $properties = [
-                "columns: ['" . implode("', '", $index->columns) . "']",
-            ];
-            if ($index->isUnique) {
-                $properties[] = "isUnique: true";
-            }
-            if ($index->sort) {
-                $sortParts = [];
-                foreach ($index->sort as $key => $direction) {
-                    $sortParts[] = "'$key' => '$direction'";
-                }
-                $properties[] = 'sort: [' . implode(', ', $sortParts) . ']';
-            }
-            if ($index->name) {
-                $properties[] = "name: '" . $index->name . "'";
-            }
-            $this->useAttributes[] = 'Index';
-            $result[] = '#[Index(' . implode(', ', $properties) . ')]';
-        }
-        return $result;
-    }
-
-    private function renderTemplate(string $templateName, array $variables = []): string
-    {
-        $filePath = implode(
-            DIRECTORY_SEPARATOR,
-            [
-                __DIR__,
-                'Templates',
-                "$templateName.php",
-            ]
-        );
-        if (!file_exists($filePath)) {
-            throw new \Exception("File `$filePath` not found");
-        }
-        extract($variables, EXTR_SKIP);
-        ob_start();
-        include $filePath;
-        return ob_get_clean();
-    }
-}
\ No newline at end of file
diff --git a/src/Generator/EnumClassBuilder.php b/src/Generator/EnumClassBuilder.php
deleted file mode 100644
index b2bd580..0000000
--- a/src/Generator/EnumClassBuilder.php
+++ /dev/null
@@ -1,32 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Composite\DB\Generator;
-
-use Nette\PhpGenerator\EnumCase;
-use Nette\PhpGenerator\PhpFile;
-
-class EnumClassBuilder
-{
-    public function __construct(
-        private readonly string $enumClass,
-        private readonly array $cases,
-    ) {}
-
-    /**
-     * @throws \Exception
-     */
-    public function getClassContent(): string
-    {
-        $enumCases = [];
-        foreach ($this->cases as $case) {
-            $enumCases[] = new EnumCase($case);
-        }
-        $file = new PhpFile();
-        $file
-            ->setStrictTypes()
-            ->addEnum($this->enumClass)
-            ->setCases($enumCases);
-
-        return (string)$file;
-    }
-}
\ No newline at end of file
diff --git a/src/Generator/Schema/ColumnType.php b/src/Generator/Schema/ColumnType.php
deleted file mode 100644
index 5f20634..0000000
--- a/src/Generator/Schema/ColumnType.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Composite\DB\Generator\Schema;
-
-enum ColumnType: string
-{
-    case String = 'string';
-    case Integer = 'int';
-    case Float = 'float';
-    case Boolean = 'bool';
-    case Datetime = '\DateTimeImmutable';
-    case Array = 'array';
-    case Object = '\stdClass';
-    case Enum = 'enum';
-}
\ No newline at end of file
diff --git a/src/Generator/Schema/Parsers/MySQLSchemaParser.php b/src/Generator/Schema/Parsers/MySQLSchemaParser.php
deleted file mode 100644
index b9d640d..0000000
--- a/src/Generator/Schema/Parsers/MySQLSchemaParser.php
+++ /dev/null
@@ -1,122 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Composite\DB\Generator\Schema\Parsers;
-
-use Composite\DB\Generator\Schema\ColumnType;
-use Composite\DB\Generator\Schema\SQLColumn;
-use Composite\DB\Generator\Schema\SQLEnum;
-use Composite\DB\Generator\Schema\SQLIndex;
-use Composite\DB\Generator\Schema\SQLSchema;
-use Doctrine\DBAL\Connection;
-use iamcal\SQLParser;
-
-class MySQLSchemaParser
-{
-    private readonly string $sql;
-
-    /**
-     * @throws \Exception
-     */
-    public function __construct(
-        Connection $connection,
-        string $tableName,
-    ) {
-        $showResult = $connection
-            ->executeQuery("SHOW CREATE TABLE $tableName")
-            ->fetchAssociative();
-        $this->sql = $showResult['Create Table'] ?? throw new \Exception("Table `$tableName` not found");
-    }
-
-    public function getSchema(): SQLSchema
-    {
-        $columns = $enums = $primaryKeys = $indexes = [];
-        $parser = new SQLParser();
-        $tokens = $parser->parse($this->sql);
-        $table = current($tokens);
-        $tableName = $table['name'];
-
-        foreach ($table['fields'] as $field) {
-            $name = $field['name'];
-            $precision = $scale = null;
-            $sqlType = $field['type'];
-            $size = !empty($field['length']) ? (int)$field['length'] : null;
-            $type = $this->getType($sqlType, $size);
-
-            if ($type === ColumnType::Enum) {
-                $enums[$name] = new SQLEnum(name: $name, values: $field['values']);
-            } elseif ($type === ColumnType::Float) {
-                $precision = $size;
-                $scale = !empty($field['decimals']) ? (int)$field['decimals'] : null;
-                $size = null;
-            }
-            if (isset($field['default'])) {
-                $hasDefaultValue = true;
-                $defaultValue = $this->getDefaultValue($type, $field['default']);
-            } else {
-                $hasDefaultValue = false;
-                $defaultValue = null;
-            }
-            $column = new SQLColumn(
-                name: $name,
-                sql: $sqlType,
-                type: $type,
-                size: $size,
-                precision: $precision,
-                scale: $scale,
-                isNullable: !empty($field['null']),
-                hasDefaultValue: $hasDefaultValue,
-                defaultValue: $defaultValue,
-                isAutoincrement: !empty($field['auto_increment']),
-            );
-            $columns[$column->name] = $column;
-        }
-        foreach ($table['indexes'] as $index) {
-            $indexType = strtolower($index['type']);
-            $cols = [];
-            foreach ($index['cols'] as $col) {
-                $colName = $col['name'];
-                $cols[] = $colName;
-            }
-            if ($indexType === 'primary') {
-                $primaryKeys = $cols;
-                continue;
-            }
-            $indexes[] = new SQLIndex(
-                name: $index['name'] ?? null,
-                isUnique: $indexType === 'unique',
-                columns: $cols,
-            );
-        }
-        return new SQLSchema(
-            tableName: $tableName,
-            columns: $columns,
-            enums: $enums,
-            primaryKeys: array_unique($primaryKeys),
-            indexes: $indexes,
-        );
-    }
-
-    private function getType(string $type, ?int $size): ColumnType
-    {
-        $type = strtolower($type);
-        if ($type === 'tinyint' && $size === 1) {
-            return ColumnType::Boolean;
-        }
-        return match ($type) {
-            'integer', 'int', 'smallint', 'tinyint', 'mediumint', 'bigint' => ColumnType::Integer,
-            'float', 'double', 'numeric', 'decimal' => ColumnType::Float,
-            'timestamp', 'datetime' => ColumnType::Datetime,
-            'json', 'set' => ColumnType::Array,
-            'enum' => ColumnType::Enum,
-            default => ColumnType::String,
-        };
-    }
-
-    private function getDefaultValue(ColumnType $type, mixed $value): mixed
-    {
-        if ($value === null || (is_string($value) && strcasecmp($value, 'null') === 0)) {
-            return  null;
-        }
-        return $value;
-    }
-}
\ No newline at end of file
diff --git a/src/Generator/Schema/Parsers/PostgresSchemaParser.php b/src/Generator/Schema/Parsers/PostgresSchemaParser.php
deleted file mode 100644
index bcf7ebd..0000000
--- a/src/Generator/Schema/Parsers/PostgresSchemaParser.php
+++ /dev/null
@@ -1,221 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Composite\DB\Generator\Schema\Parsers;
-
-use Composite\DB\Generator\Schema\ColumnType;
-use Composite\DB\Generator\Schema\SQLColumn;
-use Composite\DB\Generator\Schema\SQLEnum;
-use Composite\DB\Generator\Schema\SQLIndex;
-use Composite\DB\Generator\Schema\SQLSchema;
-use Doctrine\DBAL\Connection;
-
-class PostgresSchemaParser
-{
-    public const COLUMNS_SQL = "
-        SELECT * FROM information_schema.columns 
-        WHERE table_schema = 'public' AND table_name = :tableName;
-     ";
-
-    public const INDEXES_SQL = "
-        SELECT * FROM pg_indexes
-        WHERE schemaname = 'public' AND tablename = :tableName;
-     ";
-
-    public const PRIMARY_KEY_SQL = <<<SQL
-        SELECT a.attname as column_name
-        FROM pg_index i
-        JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY (i.indkey)
-        WHERE i.indrelid = '":tableName"'::regclass AND i.indisprimary;
-     SQL;
-
-    public const ALL_ENUMS_SQL = "
-        SELECT t.typname as enum_name, e.enumlabel as enum_value
-        FROM pg_type t
-        JOIN pg_enum e ON t.oid = e.enumtypid
-        JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
-        WHERE n.nspname = 'public';
-    ";
-
-    private readonly string $tableName;
-    private readonly array $informationSchemaColumns;
-    private readonly array $informationSchemaIndexes;
-    private readonly array $primaryKeys;
-    private readonly array $allEnums;
-
-    public static function getPrimaryKeySQL(string $tableName): string
-    {
-        return "
-            SELECT a.attname as column_name
-            FROM pg_index i
-            JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY (i.indkey)
-            WHERE i.indrelid = '\"" . $tableName . "\"'::regclass AND i.indisprimary;
-        ";
-    }
-
-    public function __construct(Connection $connection, string $tableName) {
-        $this->tableName = $tableName;
-        $this->informationSchemaColumns = $connection->executeQuery(
-            sql: PostgresSchemaParser::COLUMNS_SQL,
-            params: ['tableName' => $tableName],
-        )->fetchAllAssociative();
-        $this->informationSchemaIndexes = $connection->executeQuery(
-            sql: PostgresSchemaParser::INDEXES_SQL,
-            params: ['tableName' => $tableName],
-        )->fetchAllAssociative();
-
-        if ($primaryKeySQL = PostgresSchemaParser::getPrimaryKeySQL($tableName)) {
-            $primaryKeys = array_map(
-                fn(array $row): string => $row['column_name'],
-                $connection->executeQuery($primaryKeySQL)->fetchAllAssociative()
-            );
-        } else {
-            $primaryKeys = [];
-        }
-        $this->primaryKeys = $primaryKeys;
-
-        $allEnumsRaw = $connection->executeQuery(PostgresSchemaParser::ALL_ENUMS_SQL)->fetchAllAssociative();
-        $allEnums = [];
-        foreach ($allEnumsRaw as $enumRaw) {
-            $name = $enumRaw['enum_name'];
-            $value = $enumRaw['enum_value'];
-            if (!isset($allEnums[$name])) {
-                $allEnums[$name] = [];
-            }
-            $allEnums[$name][] = $value;
-        }
-        $this->allEnums = $allEnums;
-    }
-
-    public function getSchema(): SQLSchema
-    {
-        $columns = $enums = [];
-        foreach ($this->informationSchemaColumns as $informationSchemaColumn) {
-            $name = $informationSchemaColumn['column_name'];
-            $type = $this->getType($informationSchemaColumn);
-            $sqlDefault = $informationSchemaColumn['column_default'];
-            $isNullable = $informationSchemaColumn['is_nullable'] === 'YES';
-            $defaultValue = $this->getDefaultValue($type, $sqlDefault);
-            $hasDefaultValue = $defaultValue !== null || $isNullable;
-            $isAutoincrement = $sqlDefault && str_starts_with($sqlDefault, 'nextval(');
-
-            if ($type === ColumnType::Enum) {
-                $udtName = $informationSchemaColumn['udt_name'];
-                $enums[$name] = new SQLEnum(name: $udtName, values: $this->allEnums[$udtName]);
-            }
-            $column = new SQLColumn(
-                name: $name,
-                sql: $informationSchemaColumn['udt_name'],
-                type: $type,
-                size: $this->getSize($type, $informationSchemaColumn),
-                precision: $this->getPrecision($type, $informationSchemaColumn),
-                scale: $this->getScale($type, $informationSchemaColumn),
-                isNullable: $isNullable,
-                hasDefaultValue: $hasDefaultValue,
-                defaultValue: $defaultValue,
-                isAutoincrement: $isAutoincrement,
-            );
-            $columns[$column->name] = $column;
-        }
-        return new SQLSchema(
-            tableName: $this->tableName,
-            columns: $columns,
-            enums: $enums,
-            primaryKeys: $this->primaryKeys,
-            indexes: $this->parseIndexes(),
-        );
-    }
-
-    private function getType(array $informationSchemaColumn): ColumnType
-    {
-        $dataType = $informationSchemaColumn['data_type'];
-        $udtName = $informationSchemaColumn['udt_name'];
-        if ($dataType === 'USER-DEFINED' && !empty($this->allEnums[$udtName])) {
-            return ColumnType::Enum;
-        }
-        if (preg_match('/^int(\d?)$/', $udtName)) {
-            return ColumnType::Integer;
-        }
-        if (preg_match('/^float(\d?)$/', $udtName)) {
-            return ColumnType::Float;
-        }
-        $matchType = match ($udtName) {
-            'numeric' => ColumnType::Float,
-            'timestamp', 'timestamptz' => ColumnType::Datetime,
-            'json', 'array' => ColumnType::Array,
-            'bool' => ColumnType::Boolean,
-            default => null,
-        };
-        return $matchType ?? ColumnType::String;
-    }
-
-    private function getSize(ColumnType $type, array $informationSchemaColumn): ?int
-    {
-        if ($type === ColumnType::String) {
-            return $informationSchemaColumn['character_maximum_length'];
-        }
-        return null;
-    }
-
-    private function getPrecision(ColumnType $type, array $informationSchemaColumn): ?int
-    {
-        if ($type !== ColumnType::Float) {
-            return null;
-        }
-        return $informationSchemaColumn['numeric_precision'];
-    }
-
-    private function getScale(ColumnType $type, array $informationSchemaColumn): ?int
-    {
-        if ($type !== ColumnType::Float) {
-            return null;
-        }
-        return $informationSchemaColumn['numeric_scale'];
-    }
-
-    private function getDefaultValue(ColumnType $type, ?string $sqlValue): mixed
-    {
-        if ($sqlValue === null || strcasecmp($sqlValue, 'null') === 0) {
-            return  null;
-        }
-        if (str_starts_with($sqlValue, 'nextval(')) {
-            return null;
-        }
-        $parts = explode('::', $sqlValue);
-        return trim($parts[0], '\'');
-    }
-
-    private function parseIndexes(): array
-    {
-        $result = [];
-        foreach ($this->informationSchemaIndexes as $informationSchemaIndex) {
-            $name = $informationSchemaIndex['indexname'];
-            $sql = $informationSchemaIndex['indexdef'];
-            $isUnique = stripos($sql, ' unique index ') !== false;
-
-            if (!preg_match('/\(([`"\',\s\w]+)\)/', $sql, $columnsMatch)) {
-                continue;
-            }
-            $columnsRaw = array_map(
-                fn (string $column) => str_replace(['`', '\'', '"'], '', trim($column)),
-                explode(',', $columnsMatch[1])
-            );
-            $columns = $sort = [];
-            foreach ($columnsRaw as $columnRaw) {
-                $parts = explode(' ', $columnRaw);
-                $columns[] = $parts[0];
-                if (!empty($parts[1])) {
-                    $sort[$parts[0]] = strtoupper($parts[1]);
-                }
-            }
-            if ($columns === $this->primaryKeys) {
-                continue;
-            }
-            $result[] = new SQLIndex(
-                name: $name,
-                isUnique: $isUnique,
-                columns: $columns,
-            );
-        }
-        return $result;
-    }
-}
\ No newline at end of file
diff --git a/src/Generator/Schema/Parsers/SQLiteSchemaParser.php b/src/Generator/Schema/Parsers/SQLiteSchemaParser.php
deleted file mode 100644
index c11c49a..0000000
--- a/src/Generator/Schema/Parsers/SQLiteSchemaParser.php
+++ /dev/null
@@ -1,247 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Composite\DB\Generator\Schema\Parsers;
-
-use Composite\DB\Generator\Schema\ColumnType;
-use Composite\DB\Generator\Schema\SQLColumn;
-use Composite\DB\Generator\Schema\SQLEnum;
-use Composite\DB\Generator\Schema\SQLIndex;
-use Composite\DB\Generator\Schema\SQLSchema;
-use Doctrine\DBAL\Connection;
-
-class SQLiteSchemaParser
-{
-    public const TABLE_SQL = "SELECT sql FROM sqlite_schema WHERE name = :tableName";
-    public const INDEXES_SQL = "SELECT sql FROM sqlite_master WHERE type = 'index' and tbl_name = :tableName";
-
-    private const TABLE_NAME_PATTERN = '/^create table (?:`|\"|\')?(\w+)(?:`|\"|\')?/i';
-    private const COLUMN_PATTERN = '/^(?!constraint|primary key)(?:`|\"|\')?(\w+)(?:`|\"|\')? ([a-zA-Z]+)\s?(\(([\d,\s]+)\))?/i';
-    private const CONSTRAINT_PATTERN = '/^(?:constraint) (?:`|\"|\')?\w+(?:`|\"|\')? primary key \(([\w\s,\'\"`]+)\)/i';
-    private const PRIMARY_KEY_PATTERN = '/^primary key \(([\w\s,\'\"`]+)\)/i';
-    private const ENUM_PATTERN = '/check \((?:`|\"|\')?(\w+)(?:`|\"|\')? in \((.+)\)\)/i';
-
-    private readonly string $tableSql;
-    private readonly array $indexesSql;
-
-    public function __construct(
-        Connection $connection,
-        string $tableName,
-    ) {
-        $this->tableSql = $connection->executeQuery(
-            sql: self::TABLE_SQL,
-            params: ['tableName' => $tableName],
-        )->fetchOne();
-        $this->indexesSql = $connection->executeQuery(
-            sql: self::INDEXES_SQL,
-            params: ['tableName' => $tableName],
-        )->fetchFirstColumn();
-    }
-
-    public function getSchema(): SQLSchema
-    {
-        $columns = $enums = $primaryKeys = [];
-        $columnsStarted = false;
-        $tableName = '';
-        $lines = array_map(
-            fn ($line) => trim(preg_replace("/\s+/", " ", $line)),
-            explode("\n", $this->tableSql),
-        );
-        for ($i = 0; $i < count($lines); $i++) {
-            $line = $lines[$i];
-            if (!$line) {
-                continue;
-            }
-            if (!$tableName && preg_match(self::TABLE_NAME_PATTERN, $line, $matches)) {
-                $tableName = $matches[1];
-            }
-            if (!$columnsStarted) {
-                if (str_starts_with($line, '(') || str_ends_with($line, '(')) {
-                    $columnsStarted = true;
-                }
-                continue;
-            }
-            if ($line === ')') {
-                break;
-            }
-            if (!str_ends_with($line, ',')) {
-                if (!empty($lines[$i + 1]) && !str_starts_with($lines[$i + 1], ')')) {
-                    $lines[$i + 1] = $line . ' ' . $lines[$i + 1];
-                    continue;
-                }
-            }
-            if ($column = $this->parseSQLColumn($line)) {
-                $columns[$column->name] = $column;
-            }
-            $primaryKeys = array_merge($primaryKeys, $this->parsePrimaryKeys($line));
-            if ($enum = $this->parseEnum($line)) {
-                $enums[$column?->name ?? $enum->name] = $enum;
-            }
-        }
-        return new SQLSchema(
-            tableName: $tableName,
-            columns: $columns,
-            enums: $enums,
-            primaryKeys: array_unique($primaryKeys),
-            indexes: $this->getIndexes(),
-        );
-    }
-
-    private function parseSQLColumn(string $sqlLine): ?SQLColumn
-    {
-        if (!preg_match(self::COLUMN_PATTERN, $sqlLine, $matches)) {
-            return null;
-        }
-        $name = $matches[1];
-        $rawType = $matches[2];
-        $rawTypeParams = !empty($matches[4]) ? str_replace(' ', '', $matches[4]) : null;
-        $type = $this->getColumnType($rawType) ?? ColumnType::String;
-        $hasDefaultValue = stripos($sqlLine, ' default ') !== false;
-        return new SQLColumn(
-            name: $name,
-            sql: $sqlLine,
-            type: $type,
-            size: $this->getColumnSize($type, $rawTypeParams),
-            precision: $this->getColumnPrecision($type, $rawTypeParams),
-            scale: $this->getScale($type, $rawTypeParams),
-            isNullable: stripos($sqlLine, ' not null') === false,
-            hasDefaultValue: $hasDefaultValue,
-            defaultValue: $hasDefaultValue ? $this->getDefaultValue($sqlLine) : null,
-            isAutoincrement: stripos($sqlLine, ' autoincrement') !== false,
-        );
-    }
-
-    private function getColumnType(string $rawType): ?ColumnType
-    {
-        if (!preg_match('/^([a-zA-Z]+).*/', $rawType, $matches)) {
-            return null;
-        }
-        $type = strtolower($matches[1]);
-        return match ($type) {
-            'integer', 'int' => ColumnType::Integer,
-            'real' => ColumnType::Float,
-            'timestamp' => ColumnType::Datetime,
-            'enum' => ColumnType::Enum,
-            default => ColumnType::String,
-        };
-    }
-
-    private function getColumnSize(ColumnType $type, ?string $typeParams): ?int
-    {
-        if ($type !== ColumnType::String || !$typeParams) {
-            return null;
-        }
-        return (int)$typeParams;
-    }
-
-    private function getColumnPrecision(ColumnType $type, ?string $typeParams): ?int
-    {
-        if ($type !== ColumnType::Float || !$typeParams) {
-            return null;
-        }
-        $parts = explode(',', $typeParams);
-        return (int)$parts[0];
-    }
-
-    private function getScale(ColumnType $type, ?string $typeParams): ?int
-    {
-        if ($type !== ColumnType::Float || !$typeParams) {
-            return null;
-        }
-        $parts = explode(',', $typeParams);
-        return !empty($parts[1]) ? (int)$parts[1] : null;
-    }
-
-    private function getDefaultValue(string $sqlLine): mixed
-    {
-        $sqlLine = $this->cleanCheckEnum($sqlLine);
-        if (preg_match('/default\s+\'(.*)\'/iu', $sqlLine, $matches)) {
-            return $matches[1];
-        } elseif (preg_match('/default\s+([\w.]+)/iu', $sqlLine, $matches)) {
-            $defaultValue = $matches[1];
-            if (strtolower($defaultValue) === 'null') {
-                return null;
-            }
-            return $defaultValue;
-        }
-        return null;
-    }
-
-    private function parsePrimaryKeys(string $sqlLine): array
-    {
-        if (preg_match(self::COLUMN_PATTERN, $sqlLine, $matches)) {
-            $name = $matches[1];
-            return stripos($sqlLine, ' primary key') !== false ? [$name] : [];
-        }
-        if (!preg_match(self::CONSTRAINT_PATTERN, $sqlLine, $matches)
-            && !preg_match(self::PRIMARY_KEY_PATTERN, $sqlLine, $matches)) {
-            return [];
-        }
-        $primaryColumnsRaw = $matches[1];
-        $primaryColumnsRaw = str_replace(['\'', '"', '`', ' '], '', $primaryColumnsRaw);
-        return explode(',', $primaryColumnsRaw);
-    }
-
-    private function parseEnum(string $sqlLine): ?SQLEnum
-    {
-        if (!preg_match(self::ENUM_PATTERN, $sqlLine, $matches)) {
-            return null;
-        }
-        $name = $matches[1];
-        $values = [];
-        $sqlValues = array_map('trim', explode(',', $matches[2]));
-        foreach ($sqlValues as $value) {
-            $value = trim($value);
-            if (str_starts_with($value, '\'')) {
-                $value = trim($value, '\'');
-            } elseif (str_starts_with($value, '"')) {
-                $value = trim($value, '"');
-            }
-            $values[] = $value;
-        }
-        return new SQLEnum(name: $name, values: $values);
-    }
-
-    /**
-     * @return SQLIndex[]
-     */
-    private function getIndexes(): array
-    {
-        $result = [];
-        foreach ($this->indexesSql as $indexSql) {
-            if (!$indexSql) continue;
-            $indexSql = trim(str_replace("\n", " ", $indexSql));
-            $indexSql = preg_replace("/\s+/", " ", $indexSql);
-            if (!preg_match('/index\s+(?:`|\"|\')?(\w+)(?:`|\"|\')?/i', $indexSql, $nameMatch)) {
-                continue;
-            }
-            $name = $nameMatch[1];
-            if (!preg_match('/\(([`"\',\s\w]+)\)/', $indexSql, $columnsMatch)) {
-                continue;
-            }
-            $columnsRaw = array_map(
-                fn (string $column) => str_replace(['`', '\'', '"'], '', trim($column)),
-                explode(',', $columnsMatch[1])
-            );
-            $columns = $sort = [];
-            foreach ($columnsRaw as $columnRaw) {
-                $parts = explode(' ', $columnRaw);
-                $columns[] = $parts[0];
-                if (!empty($parts[1])) {
-                    $sort[$parts[0]] = strtolower($parts[1]);
-                }
-            }
-            $result[] = new SQLIndex(
-                name: $name,
-                isUnique: stripos($indexSql, ' unique index ') !== false,
-                columns: $columns,
-                sort: $sort,
-            );
-        }
-        return $result;
-    }
-
-    private function cleanCheckEnum(string $sqlLine): string
-    {
-        return preg_replace('/ check \(\"\w+\" IN \(.+\)\)/i', '', $sqlLine);
-    }
-}
\ No newline at end of file
diff --git a/src/Generator/Schema/SQLColumn.php b/src/Generator/Schema/SQLColumn.php
deleted file mode 100644
index 00ad7d3..0000000
--- a/src/Generator/Schema/SQLColumn.php
+++ /dev/null
@@ -1,45 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Composite\DB\Generator\Schema;
-
-class SQLColumn
-{
-    public function __construct(
-        public readonly string $name,
-        public readonly string|array $sql,
-        public readonly ColumnType $type,
-        public readonly ?int $size,
-        public readonly ?int $precision,
-        public readonly ?int $scale,
-        public readonly bool $isNullable,
-        public readonly bool $hasDefaultValue,
-        public readonly mixed $defaultValue,
-        public readonly bool $isAutoincrement,
-    ) {}
-
-    public function sizeIsDefault(): bool
-    {
-        if ($this->type !== ColumnType::String) {
-            return true;
-        }
-        if ($this->size === null) {
-            return true;
-        }
-        return $this->size === 255;
-    }
-
-    public function getColumnAttributeProperties(): array
-    {
-        $result = [];
-        if ($this->size && !$this->sizeIsDefault()) {
-            $result[] = 'size: ' . $this->size;
-        }
-        if ($this->precision) {
-            $result[] = 'precision: ' . $this->precision;
-        }
-        if ($this->scale) {
-            $result[] = 'scale: ' . $this->scale;
-        }
-        return $result;
-    }
-}
\ No newline at end of file
diff --git a/src/Generator/Schema/SQLEnum.php b/src/Generator/Schema/SQLEnum.php
deleted file mode 100644
index ba726df..0000000
--- a/src/Generator/Schema/SQLEnum.php
+++ /dev/null
@@ -1,11 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Composite\DB\Generator\Schema;
-
-class SQLEnum
-{
-    public function __construct(
-        public readonly string $name,
-        public readonly array $values = [],
-    ) {}
-}
\ No newline at end of file
diff --git a/src/Generator/Schema/SQLIndex.php b/src/Generator/Schema/SQLIndex.php
deleted file mode 100644
index 5ad6561..0000000
--- a/src/Generator/Schema/SQLIndex.php
+++ /dev/null
@@ -1,13 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Composite\DB\Generator\Schema;
-
-class SQLIndex
-{
-    public function __construct(
-        public readonly ?string $name,
-        public readonly bool $isUnique,
-        public readonly array $columns,
-        public readonly array $sort = [],
-    ) {}
-}
\ No newline at end of file
diff --git a/src/Generator/Schema/SQLSchema.php b/src/Generator/Schema/SQLSchema.php
deleted file mode 100644
index ec41d92..0000000
--- a/src/Generator/Schema/SQLSchema.php
+++ /dev/null
@@ -1,52 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Composite\DB\Generator\Schema;
-
-use Composite\DB\Generator\Schema\Parsers\MySQLSchemaParser;
-use Composite\DB\Generator\Schema\Parsers\PostgresSchemaParser;
-use Composite\DB\Generator\Schema\Parsers\SQLiteSchemaParser;
-use Doctrine\DBAL\Connection;
-use Doctrine\DBAL\Driver;
-
-class SQLSchema
-{
-    /**
-     * @param string $tableName
-     * @param SQLColumn[] $columns
-     * @param SQLEnum[] $enums
-     * @param SQLIndex[] $indexes
-     * @param string[] $primaryKeys
-     */
-    public function __construct(
-        public readonly string $tableName,
-        public readonly array $columns,
-        public readonly array $enums,
-        public readonly array $primaryKeys,
-        public readonly array $indexes,
-    ) {}
-
-    /**
-     * @throws \Exception
-     */
-    public static function generate(Connection $connection, string $tableName): SQLSchema
-    {
-        $driver = $connection->getDriver();
-        if ($driver instanceof Driver\AbstractSQLiteDriver) {
-            $parser = new SQLiteSchemaParser($connection, $tableName);
-            return $parser->getSchema();
-        } elseif ($driver instanceof Driver\AbstractMySQLDriver) {
-            $parser = new MySQLSchemaParser($connection, $tableName);
-            return $parser->getSchema();
-        } elseif ($driver instanceof Driver\AbstractPostgreSQLDriver) {
-            $parser = new PostgresSchemaParser($connection, $tableName);
-            return $parser->getSchema();
-        } else {
-            throw new \Exception("Driver `" . $driver::class . "` is not yet supported");
-        }
-    }
-
-    public function isPrimaryKey(string $name): bool
-    {
-        return \in_array($name, $this->primaryKeys);
-    }
-}
\ No newline at end of file
diff --git a/src/Generator/TableClassBuilder.php b/src/Generator/TableClassBuilder.php
deleted file mode 100644
index ba840f8..0000000
--- a/src/Generator/TableClassBuilder.php
+++ /dev/null
@@ -1,77 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Composite\DB\Generator;
-
-use Composite\DB\AbstractTable;
-use Composite\DB\TableConfig;
-use Composite\Entity\Columns\AbstractColumn;
-use Composite\DB\Helpers\ClassHelper;
-use Nette\PhpGenerator\Method;
-
-class TableClassBuilder extends AbstractTableClassBuilder
-{
-    public function getParentNamespace(): string
-    {
-        return AbstractTable::class;
-    }
-
-    public function generate(): void
-    {
-        $this->file
-            ->setStrictTypes()
-            ->addNamespace(ClassHelper::extractNamespace($this->tableClass))
-            ->addUse(AbstractTable::class)
-            ->addUse(TableConfig::class)
-            ->addUse($this->schema->class)
-            ->addClass(ClassHelper::extractShortName($this->tableClass))
-            ->setExtends(AbstractTable::class)
-            ->setMethods($this->getMethods());
-    }
-
-    private function getMethods(): array
-    {
-        return array_filter([
-            $this->generateGetConfig(),
-            $this->generateFindOne(),
-            $this->generateFindAll(),
-            $this->generateCountAll(),
-        ]);
-    }
-
-    protected function generateFindOne(): ?Method
-    {
-        $primaryColumns = array_map(
-            fn(string $key): AbstractColumn => $this->schema->getColumn($key) ?? throw new \Exception("Primary key column `$key` not found in entity."),
-            $this->tableConfig->primaryKeys
-        );
-        if (count($this->tableConfig->primaryKeys) === 1) {
-            $body = 'return $this->createEntity($this->findByPkInternal(' . $this->buildVarsList($this->tableConfig->primaryKeys) . '));';
-        } else {
-            $body = 'return $this->createEntity($this->findOneInternal(' . $this->buildVarsList($this->tableConfig->primaryKeys) . '));';
-        }
-        $method = (new Method('findByPk'))
-            ->setPublic()
-            ->setReturnType($this->schema->class)
-            ->setReturnNullable()
-            ->setBody($body);
-        $this->addMethodParameters($method, $primaryColumns);
-        return $method;
-    }
-
-    protected function generateFindAll(): Method
-    {
-        return (new Method('findAll'))
-            ->setPublic()
-            ->setComment('@return ' . $this->entityClassShortName . '[]')
-            ->setReturnType('array')
-            ->setBody('return $this->createEntities($this->findAllInternal());');
-    }
-
-    protected function generateCountAll(): Method
-    {
-        return (new Method('countAll'))
-            ->setPublic()
-            ->setReturnType('int')
-            ->setBody('return $this->countAllInternal();');
-    }
-}
\ No newline at end of file
diff --git a/src/Generator/Templates/EntityTemplate.php b/src/Generator/Templates/EntityTemplate.php
deleted file mode 100644
index 9384d5b..0000000
--- a/src/Generator/Templates/EntityTemplate.php
+++ /dev/null
@@ -1,41 +0,0 @@
-<?= $phpOpener ?? '' ?>
-
-
-namespace <?= $entityNamespace ?? '' ?>;
-
-<?php if (!empty($useAttributes)) : ?>
-use Composite\DB\Attributes\{<?= implode(', ', $useAttributes) ?>};
-<?php endif; ?>
-<?php foreach($useNamespaces ?? [] as $namespace) : ?>
-use <?=$namespace?>;
-<?php endforeach; ?>
-
-#[Table(connection: '<?= $connectionName ?? '' ?>', name: '<?= $tableName ?? '' ?>')]
-<?php foreach($indexes ?? [] as $index) : ?>
-<?=$index?>
-
-<?php endforeach; ?>
-class <?=$entityClassShortname??''?> extends AbstractEntity
-{
-<?php foreach($traits ?? [] as $trait) : ?>
-    use <?= $trait ?>;
-
-<?php endforeach; ?>
-<?php foreach($properties ?? [] as $property) : ?>
-<?php foreach($property['attributes'] as $attribute) : ?>
-    <?= $attribute ?>
-
-<?php endforeach; ?>
-    <?= $property['var'] ?>;
-
-<?php endforeach; ?>
-    public function __construct(
-<?php foreach($constructorParams ?? [] as $param) : ?>
-<?php foreach($param['attributes'] as $attribute) : ?>
-        <?= $attribute ?>
-
-<?php endforeach; ?>
-        <?= $param['var'] ?>,
-<?php endforeach; ?>
-    ) {}
-}
diff --git a/src/Helpers/ClassHelper.php b/src/Helpers/ClassHelper.php
deleted file mode 100644
index fa7b7f8..0000000
--- a/src/Helpers/ClassHelper.php
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Composite\DB\Helpers;
-
-class ClassHelper
-{
-    public static function extractNamespace(string $name): string
-    {
-        return ($pos = strrpos($name, '\\')) ? substr($name, 0, $pos) : '';
-    }
-
-    public static function extractShortName(string $name): string
-    {
-        return ($pos = strrpos($name, '\\')) === false
-            ? $name
-            : substr($name, $pos + 1);
-    }
-}
\ No newline at end of file
diff --git a/src/Helpers/DatabaseSpecificTrait.php b/src/Helpers/DatabaseSpecificTrait.php
new file mode 100644
index 0000000..7769175
--- /dev/null
+++ b/src/Helpers/DatabaseSpecificTrait.php
@@ -0,0 +1,64 @@
+<?php declare(strict_types=1);
+
+namespace Composite\DB\Helpers;
+
+use Composite\DB\Exceptions\DbException;
+use Doctrine\DBAL\Driver;
+
+trait DatabaseSpecificTrait
+{
+    private ?bool $isPostgreSQL = null;
+    private ?bool $isMySQL = null;
+    private ?bool $isSQLite = null;
+
+    private function identifyPlatform(): void
+    {
+        if ($this->isPostgreSQL !== null) {
+            return;
+        }
+        $driver = $this->getConnection()->getDriver();
+        if ($driver instanceof Driver\AbstractPostgreSQLDriver) {
+            $this->isPostgreSQL = true;
+            $this->isMySQL = $this->isSQLite = false;
+        } elseif ($driver instanceof Driver\AbstractSQLiteDriver) {
+            $this->isSQLite = true;
+            $this->isPostgreSQL = $this->isMySQL = false;
+        } elseif ($driver instanceof Driver\AbstractMySQLDriver) {
+            $this->isMySQL = true;
+            $this->isPostgreSQL = $this->isSQLite = false;
+        } else {
+            // @codeCoverageIgnoreStart
+            throw new DbException('Unsupported driver ' . $driver::class);
+            // @codeCoverageIgnoreEnd
+        }
+    }
+
+    /**
+     * @param array<string, mixed> $data
+     * @return array<string, mixed>
+     */
+    private function prepareDataForSql(array $data): array
+    {
+        $this->identifyPlatform();
+        foreach ($data as $columnName => $value) {
+            if (is_bool($value) && !$this->isPostgreSQL) {
+                $data[$columnName] = $value ? 1 : 0;
+            }
+        }
+        return $data;
+    }
+
+    protected function escapeIdentifier(string $key): string
+    {
+        $this->identifyPlatform();
+        if ($this->isMySQL) {
+            if (strpos($key, '.')) {
+                return implode('.', array_map(fn ($part) => "`$part`", explode('.', $key)));
+            } else {
+                return "`$key`";
+            }
+        } else {
+            return '"' . $key . '"';
+        }
+    }
+}
diff --git a/src/Helpers/DateTimeHelper.php b/src/Helpers/DateTimeHelper.php
deleted file mode 100644
index 59eea94..0000000
--- a/src/Helpers/DateTimeHelper.php
+++ /dev/null
@@ -1,37 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Composite\DB\Helpers;
-
-class DateTimeHelper
-{
-    final public const DEFAULT_TIMESTAMP = '1970-01-01 00:00:01';
-    final public const DEFAULT_TIMESTAMP_MICRO = '1970-01-01 00:00:01.000000';
-    final public const DEFAULT_DATETIME = '1000-01-01 00:00:00';
-    final public const DEFAULT_DATETIME_MICRO = '1000-01-01 00:00:00.000000';
-    final public const DATETIME_FORMAT = 'Y-m-d H:i:s';
-    final public const DATETIME_MICRO_FORMAT = 'Y-m-d H:i:s.u';
-
-    public static function getDefaultDateTimeImmutable() : \DateTimeImmutable
-    {
-        return new \DateTimeImmutable(self::DEFAULT_TIMESTAMP);
-    }
-
-    public static function dateTimeToString(\DateTimeInterface $dateTime, bool $withMicro = true): string
-    {
-        return $dateTime->format($withMicro ? self::DATETIME_MICRO_FORMAT : self::DATETIME_FORMAT);
-    }
-
-    public static function isDefault(mixed $value): bool
-    {
-        if (!$value) {
-            return true;
-        }
-        if ($value instanceof \DateTimeInterface) {
-            $value = self::dateTimeToString($value);
-        }
-        return in_array(
-            $value,
-            [self::DEFAULT_TIMESTAMP, self::DEFAULT_TIMESTAMP_MICRO, self::DEFAULT_DATETIME, self::DEFAULT_DATETIME_MICRO]
-        );
-    }
-}
diff --git a/src/Helpers/SelectRawTrait.php b/src/Helpers/SelectRawTrait.php
new file mode 100644
index 0000000..b02bb89
--- /dev/null
+++ b/src/Helpers/SelectRawTrait.php
@@ -0,0 +1,132 @@
+<?php declare(strict_types=1);
+
+namespace Composite\DB\Helpers;
+
+use Composite\DB\Where;
+use Doctrine\DBAL\Query\QueryBuilder;
+
+trait SelectRawTrait
+{
+    /** @var string[]  */
+    private array $comparisonSigns = ['=', '!=', '>', '<', '>=', '<=', '<>'];
+
+    private ?QueryBuilder $selectQuery = null;
+
+    protected function select(string $select = '*'): QueryBuilder
+    {
+        if ($this->selectQuery === null) {
+            $this->selectQuery = $this->getConnection()->createQueryBuilder()->from($this->getTableName());
+        }
+        return (clone $this->selectQuery)->select($select);
+    }
+
+    /**
+     * @param array<string, mixed>|Where $where
+     * @param array<string, string>|string $orderBy
+     * @return array<string, mixed>|null
+     * @throws \Doctrine\DBAL\Exception
+     */
+    private function _findOneRaw(array|Where $where, array|string $orderBy = []): ?array
+    {
+        $query = $this->select();
+        $this->buildWhere($query, $where);
+        $this->applyOrderBy($query, $orderBy);
+        return $query->fetchAssociative() ?: null;
+    }
+
+    /**
+     * @param array<string, mixed>|Where $where
+     * @param array<string, string>|string $orderBy
+     * @return list<array<string,mixed>>
+     * @throws \Doctrine\DBAL\Exception
+     */
+    private function _findAllRaw(
+        array|Where $where = [],
+        array|string $orderBy = [],
+        ?int $limit = null,
+        ?int $offset = null,
+    ): array
+    {
+        $query = $this->select();
+        $this->buildWhere($query, $where);
+        $this->applyOrderBy($query, $orderBy);
+        if ($limit > 0) {
+            $query->setMaxResults($limit);
+        }
+        if ($offset > 0) {
+            $query->setFirstResult($offset);
+        }
+        return $query->executeQuery()->fetchAllAssociative();
+    }
+
+
+    /**
+     * @param array<string, mixed>|Where $where
+     */
+    private function buildWhere(QueryBuilder $query, array|Where $where): void
+    {
+        if (is_array($where)) {
+            foreach ($where as $column => $value) {
+                if ($value instanceof \BackedEnum) {
+                    $value = $value->value;
+                } elseif ($value instanceof \UnitEnum) {
+                    $value = $value->name;
+                }
+
+                if (is_null($value)) {
+                    $query->andWhere($column . ' IS NULL');
+                } elseif (is_array($value) && count($value) === 2 && \in_array($value[0], $this->comparisonSigns)) {
+                    $comparisonSign = $value[0];
+                    $comparisonValue = $value[1];
+
+                    // Handle special case of "!= null"
+                    if ($comparisonSign === '!=' && is_null($comparisonValue)) {
+                        $query->andWhere($column . ' IS NOT NULL');
+                    } else {
+                        $query->andWhere($column . ' ' . $comparisonSign . ' :' . $column)
+                            ->setParameter($column, $comparisonValue);
+                    }
+                } elseif (is_array($value)) {
+                    $placeholders = [];
+                    foreach ($value as $index => $val) {
+                        $placeholders[] = ':' . $column . $index;
+                        $query->setParameter($column . $index, $val);
+                    }
+                    $query->andWhere($column . ' IN(' . implode(', ', $placeholders) . ')');
+                } else {
+                    $query->andWhere($column . ' = :' . $column)
+                        ->setParameter($column, $value);
+                }
+            }
+        } else {
+            $query->where($where->condition);
+            foreach ($where->params as $param => $value) {
+                $query->setParameter($param, $value);
+            }
+        }
+    }
+
+    /**
+     * @param array<string, string>|string $orderBy
+     */
+    private function applyOrderBy(QueryBuilder $query, string|array $orderBy): void
+    {
+        if (!$orderBy) {
+            return;
+        }
+        if (is_array($orderBy)) {
+            foreach ($orderBy as $column => $direction) {
+                $query->addOrderBy($column, $direction);
+            }
+        } else {
+            foreach (explode(',', $orderBy) as $orderByPart) {
+                $orderByPart = trim($orderByPart);
+                if (preg_match('/(.+)\s(asc|desc)$/i', $orderByPart, $orderByPartMatch)) {
+                    $query->addOrderBy($orderByPartMatch[1], $orderByPartMatch[2]);
+                } else {
+                    $query->addOrderBy($orderByPart);
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/MultiQuery/MultiInsert.php b/src/MultiQuery/MultiInsert.php
new file mode 100644
index 0000000..bfd3fe0
--- /dev/null
+++ b/src/MultiQuery/MultiInsert.php
@@ -0,0 +1,62 @@
+<?php declare(strict_types=1);
+
+namespace Composite\DB\MultiQuery;
+
+use Composite\DB\Helpers\DatabaseSpecificTrait;
+use Doctrine\DBAL\Connection;
+
+class MultiInsert
+{
+    use DatabaseSpecificTrait;
+
+    private Connection $connection;
+    private string $sql = '';
+    /** @var array<string, mixed> */
+    private array $parameters = [];
+
+    /**
+     * @param string $tableName
+     * @param list<array<string, mixed>> $rows
+     */
+    public function __construct(Connection $connection, string $tableName, array $rows) {
+        if (!$rows) {
+            return;
+        }
+        $this->connection = $connection;
+        $firstRow = reset($rows);
+        $columnNames = array_map(fn ($columnName) => $this->escapeIdentifier($columnName), array_keys($firstRow));
+        $this->sql = "INSERT INTO " . $this->escapeIdentifier($tableName)  . " (" . implode(', ', $columnNames) . ") VALUES ";
+        $valuesSql = [];
+
+        $index = 0;
+        foreach ($rows as $row) {
+            $valuePlaceholder = [];
+            foreach ($row as $column => $value) {
+                $valuePlaceholder[] = ":$column$index";
+                $this->parameters["$column$index"] = $value;
+            }
+            $valuesSql[] = '(' . implode(', ', $valuePlaceholder) . ')';
+            $index++;
+        }
+
+        $this->sql .= implode(', ', $valuesSql) . ';';
+    }
+
+    public function getSql(): string
+    {
+        return $this->sql;
+    }
+
+    /**
+     * @return array<string, mixed>
+     */
+    public function getParameters(): array
+    {
+        return $this->parameters;
+    }
+
+    private function getConnection(): Connection
+    {
+        return $this->connection;
+    }
+}
\ No newline at end of file
diff --git a/src/MultiQuery/MultiSelect.php b/src/MultiQuery/MultiSelect.php
new file mode 100644
index 0000000..c5171a7
--- /dev/null
+++ b/src/MultiQuery/MultiSelect.php
@@ -0,0 +1,68 @@
+<?php declare(strict_types=1);
+
+namespace Composite\DB\MultiQuery;
+
+use Composite\DB\Exceptions\DbException;
+use Composite\DB\TableConfig;
+use Composite\Entity\AbstractEntity;
+use Doctrine\DBAL\Connection;
+use Doctrine\DBAL\Query\QueryBuilder;
+
+class MultiSelect
+{
+    private readonly QueryBuilder $queryBuilder;
+
+    /**
+     * @param array<string, mixed>|array<string|int> $condition
+     * @throws DbException
+     */
+    public function __construct(
+        Connection $connection,
+        TableConfig $tableConfig,
+        array $condition,
+    ) {
+        $query = $connection->createQueryBuilder()->select('*')->from($tableConfig->tableName);
+        /** @var class-string<AbstractEntity> $class */
+        $class = $tableConfig->entityClass;
+
+        $pkColumns = [];
+        foreach ($tableConfig->primaryKeys as $primaryKeyName) {
+            $pkColumns[$primaryKeyName] = $class::schema()->getColumn($primaryKeyName);
+        }
+
+        if (count($pkColumns) === 1) {
+            if (!array_is_list($condition)) {
+                throw new DbException('Input argument $pkList must be list');
+            }
+            /** @var \Composite\Entity\Columns\AbstractColumn $pkColumn */
+            $pkColumn = reset($pkColumns);
+            $preparedPkValues = array_map(fn ($pk) => (string)$pkColumn->uncast($pk), $condition);
+            $query->andWhere($query->expr()->in($pkColumn->name, $preparedPkValues));
+        } else {
+            $expressions = [];
+            foreach ($condition as $i => $pkArray) {
+                if (!is_array($pkArray)) {
+                    throw new DbException('For tables with composite keys, input array must consist associative arrays');
+                }
+                $pkOrExpr = [];
+                foreach ($pkArray as $pkName => $pkValue) {
+                    if (is_string($pkName) && isset($pkColumns[$pkName])) {
+                        $preparedPkValue = $pkColumns[$pkName]->cast($pkValue);
+                        $pkOrExpr[] = $query->expr()->eq($pkName, ':' . $pkName . $i);
+                        $query->setParameter($pkName . $i, $preparedPkValue);
+                    }
+                }
+                if ($pkOrExpr) {
+                    $expressions[] = $query->expr()->and(...$pkOrExpr);
+                }
+            }
+            $query->where($query->expr()->or(...$expressions));
+        }
+        $this->queryBuilder = $query;
+    }
+
+    public function getQueryBuilder(): QueryBuilder
+    {
+        return $this->queryBuilder;
+    }
+}
\ No newline at end of file
diff --git a/src/TableConfig.php b/src/TableConfig.php
index c19c23e..a33b693 100644
--- a/src/TableConfig.php
+++ b/src/TableConfig.php
@@ -9,16 +9,23 @@
 
 class TableConfig
 {
+    /** @var array<class-string, true> */
+    private readonly array $entityTraits;
+
+    /**
+     * @param class-string<AbstractEntity> $entityClass
+     * @param string[] $primaryKeys
+     */
     public function __construct(
         public readonly string $connectionName,
         public readonly string $tableName,
         public readonly string $entityClass,
         public readonly array $primaryKeys,
         public readonly ?string $autoIncrementKey = null,
-        public readonly bool $isSoftDelete = false,
-        public readonly bool $isOptimisticLock = false,
     )
-    {}
+    {
+        $this->entityTraits = array_fill_keys(class_uses($entityClass), true);
+    }
 
     /**
      * @throws EntityException
@@ -26,12 +33,7 @@ public function __construct(
     public static function fromEntitySchema(Schema $schema): TableConfig
     {
         /** @var Attributes\Table|null $tableAttribute */
-        $tableAttribute = null;
-        foreach ($schema->attributes as $attribute) {
-            if ($attribute instanceof Attributes\Table) {
-                $tableAttribute = $attribute;
-            }
-        }
+        $tableAttribute = $schema->getFirstAttributeByClass(Attributes\Table::class);
         if (!$tableAttribute) {
             throw new EntityException(sprintf(
                 'Attribute `%s` not found in Entity `%s`',
@@ -40,7 +42,6 @@ public static function fromEntitySchema(Schema $schema): TableConfig
         }
         $primaryKeys = [];
         $autoIncrementKey = null;
-        $isSoftDelete = $isOptimisticLock = false;
 
         foreach ($schema->columns as $column) {
             foreach ($column->attributes as $attribute) {
@@ -52,21 +53,12 @@ public static function fromEntitySchema(Schema $schema): TableConfig
                 }
             }
         }
-        foreach (class_uses($schema->class) as $traitClass) {
-            if ($traitClass === Traits\SoftDelete::class) {
-                $isSoftDelete = true;
-            } elseif ($traitClass === Traits\OptimisticLock::class) {
-                $isOptimisticLock = true;
-            }
-        }
         return new TableConfig(
             connectionName: $tableAttribute->connection,
             tableName: $tableAttribute->name,
             entityClass: $schema->class,
             primaryKeys: $primaryKeys,
             autoIncrementKey: $autoIncrementKey,
-            isSoftDelete: $isSoftDelete,
-            isOptimisticLock: $isOptimisticLock,
         );
     }
 
@@ -82,4 +74,19 @@ public function checkEntity(AbstractEntity $entity): void
             );
         }
     }
+
+    public function hasSoftDelete(): bool
+    {
+        return !empty($this->entityTraits[Traits\SoftDelete::class]);
+    }
+
+    public function hasOptimisticLock(): bool
+    {
+        return !empty($this->entityTraits[Traits\OptimisticLock::class]);
+    }
+
+    public function hasUpdatedAt(): bool
+    {
+        return !empty($this->entityTraits[Traits\UpdatedAt::class]);
+    }
 }
\ No newline at end of file
diff --git a/src/Traits/OptimisticLock.php b/src/Traits/OptimisticLock.php
index 150c328..73e2a9a 100644
--- a/src/Traits/OptimisticLock.php
+++ b/src/Traits/OptimisticLock.php
@@ -4,5 +4,15 @@
 
 trait OptimisticLock
 {
-    public int $version = 1;
+    protected int $lock_version = 1;
+
+    public function getVersion(): int
+    {
+        return $this->lock_version;
+    }
+
+    public function incrementVersion(): void
+    {
+        $this->lock_version++;
+    }
 }
diff --git a/src/Traits/UpdatedAt.php b/src/Traits/UpdatedAt.php
new file mode 100644
index 0000000..1e8d878
--- /dev/null
+++ b/src/Traits/UpdatedAt.php
@@ -0,0 +1,8 @@
+<?php declare(strict_types=1);
+
+namespace Composite\DB\Traits;
+
+trait UpdatedAt
+{
+    public ?\DateTimeImmutable $updated_at = null;
+}
diff --git a/src/Where.php b/src/Where.php
new file mode 100644
index 0000000..147b3aa
--- /dev/null
+++ b/src/Where.php
@@ -0,0 +1,16 @@
+<?php declare(strict_types=1);
+
+namespace Composite\DB;
+
+class Where
+{
+    /**
+     * @param string $condition free format where string, example: "user_id = :user_id OR user_id > 0"
+     * @param array<string, mixed> $params params with placeholders, which used in $condition, example: ['user_id' => 123],
+     */
+    public function __construct(
+        public readonly string $condition,
+        public readonly array $params,
+    ) {
+    }
+}
\ No newline at end of file
diff --git a/tests/Attributes/PrimaryKeyAttributeTest.php b/tests/Attributes/PrimaryKeyAttributeTest.php
index 26b6c55..1fd6c72 100644
--- a/tests/Attributes/PrimaryKeyAttributeTest.php
+++ b/tests/Attributes/PrimaryKeyAttributeTest.php
@@ -5,10 +5,11 @@
 use Composite\DB\TableConfig;
 use Composite\Entity\AbstractEntity;
 use Composite\DB\Attributes;
+use PHPUnit\Framework\Attributes\DataProvider;
 
 final class PrimaryKeyAttributeTest extends \PHPUnit\Framework\TestCase
 {
-    public function primaryKey_dataProvider(): array
+    public static function primaryKey_dataProvider(): array
     {
         return [
             [
@@ -34,9 +35,7 @@ public function __construct(
         ];
     }
 
-    /**
-     * @dataProvider primaryKey_dataProvider
-     */
+    #[DataProvider('primaryKey_dataProvider')]
     public function test_primaryKey(AbstractEntity $entity, array $expected): void
     {
         $schema = $entity::schema();
diff --git a/tests/Connection/ConnectionManagerTest.php b/tests/Connection/ConnectionManagerTest.php
new file mode 100644
index 0000000..d17d625
--- /dev/null
+++ b/tests/Connection/ConnectionManagerTest.php
@@ -0,0 +1,70 @@
+<?php declare(strict_types=1);
+
+namespace Composite\DB\Tests\Connection;
+
+use Composite\DB\ConnectionManager;
+use Composite\DB\Exceptions\DbException;
+use Doctrine\DBAL\Connection;
+use PHPUnit\Framework\Attributes\DataProvider;
+
+final class ConnectionManagerTest extends \PHPUnit\Framework\TestCase
+{
+    public function test_getConnection(): void
+    {
+        $connection = ConnectionManager::getConnection('sqlite');
+        $this->assertInstanceOf(Connection::class, $connection);
+    }
+
+    public static function invalidConfig_dataProvider(): array
+    {
+        $testStandConfigsBaseDir = __DIR__ . '/../TestStand/configs/';
+        return [
+            [
+                '',
+            ],
+            [
+                'invalid/path',
+            ],
+            [
+                $testStandConfigsBaseDir . 'empty_config.php',
+            ],
+            [
+                $testStandConfigsBaseDir . 'wrong_content_config.php',
+            ],
+            [
+                $testStandConfigsBaseDir . 'wrong_name_config.php',
+            ],
+            [
+                $testStandConfigsBaseDir . 'wrong_params_config.php',
+            ],
+            [
+                $testStandConfigsBaseDir . 'wrong_doctrine_config.php',
+            ],
+        ];
+    }
+
+    #[DataProvider('invalidConfig_dataProvider')]
+    public function test_invalidConfig(string $configPath): void
+    {
+        $reflection = new \ReflectionClass(ConnectionManager::class);
+        $reflection->setStaticPropertyValue('configs', null);
+        $currentPath = getenv('CONNECTIONS_CONFIG_FILE');
+        putenv('CONNECTIONS_CONFIG_FILE=' . $configPath);
+
+        try {
+            ConnectionManager::getConnection('db1');
+            $this->fail('This line should not be reached');
+        } catch (DbException) {
+            $this->assertTrue(true);
+        } finally {
+            putenv('CONNECTIONS_CONFIG_FILE=' . $currentPath);
+            $reflection->setStaticPropertyValue('configs', null);
+        }
+    }
+
+    public function test_getConnectionWithMissingName(): void
+    {
+        $this->expectException(DbException::class);
+        ConnectionManager::getConnection('invalid_name');
+    }
+}
\ No newline at end of file
diff --git a/tests/Table/BaseTableTest.php b/tests/Helpers/CacheHelper.php
similarity index 83%
rename from tests/Table/BaseTableTest.php
rename to tests/Helpers/CacheHelper.php
index 2b811c7..a6ad2ec 100644
--- a/tests/Table/BaseTableTest.php
+++ b/tests/Helpers/CacheHelper.php
@@ -1,11 +1,11 @@
 <?php declare(strict_types=1);
 
-namespace Composite\DB\Tests\Table;
+namespace Composite\DB\Tests\Helpers;
 
 use Kodus\Cache\FileCache;
 use Psr\SimpleCache\CacheInterface;
 
-abstract class BaseTableTest extends \PHPUnit\Framework\TestCase
+class CacheHelper
 {
     private static ?CacheInterface $cache = null;
 
diff --git a/tests/Helpers/FalseCache.php b/tests/Helpers/FalseCache.php
new file mode 100644
index 0000000..40da6d5
--- /dev/null
+++ b/tests/Helpers/FalseCache.php
@@ -0,0 +1,49 @@
+<?php declare(strict_types=1);
+
+namespace Composite\DB\Tests\Helpers;
+
+use Psr\SimpleCache\CacheInterface;
+
+class FalseCache implements CacheInterface
+{
+
+    public function get(string $key, mixed $default = null): mixed
+    {
+        return null;
+    }
+
+    public function set(string $key, mixed $value, \DateInterval|int|null $ttl = null): bool
+    {
+        return false;
+    }
+
+    public function delete(string $key): bool
+    {
+        return false;
+    }
+
+    public function clear(): bool
+    {
+        return false;
+    }
+
+    public function getMultiple(iterable $keys, mixed $default = null): iterable
+    {
+        return [];
+    }
+
+    public function setMultiple(iterable $values, \DateInterval|int|null $ttl = null): bool
+    {
+        return false;
+    }
+
+    public function deleteMultiple(iterable $keys): bool
+    {
+        return false;
+    }
+
+    public function has(string $key): bool
+    {
+        return false;
+    }
+}
diff --git a/tests/Helpers/StringHelper.php b/tests/Helpers/StringHelper.php
new file mode 100644
index 0000000..f94fa3b
--- /dev/null
+++ b/tests/Helpers/StringHelper.php
@@ -0,0 +1,11 @@
+<?php declare(strict_types=1);
+
+namespace Composite\DB\Tests\Helpers;
+
+class StringHelper
+{
+    public static function getUniqueName(): string
+    {
+        return (new \DateTime())->format('U') . '_' . uniqid();
+    }
+}
\ No newline at end of file
diff --git a/tests/MultiQuery/MultiInsertTest.php b/tests/MultiQuery/MultiInsertTest.php
new file mode 100644
index 0000000..69631a9
--- /dev/null
+++ b/tests/MultiQuery/MultiInsertTest.php
@@ -0,0 +1,58 @@
+<?php declare(strict_types=1);
+
+namespace Composite\DB\Tests\MultiQuery;
+
+use Composite\DB\ConnectionManager;
+use Composite\DB\MultiQuery\MultiInsert;
+use PHPUnit\Framework\Attributes\DataProvider;
+
+class MultiInsertTest extends \PHPUnit\Framework\TestCase
+{
+    #[DataProvider('multiInsertQuery_dataProvider')]
+    public function test_multiInsertQuery($tableName, $rows, $expectedSql, $expectedParameters)
+    {
+        $connection = ConnectionManager::getConnection('sqlite');
+        $multiInserter = new MultiInsert($connection, $tableName, $rows);
+
+        $this->assertEquals($expectedSql, $multiInserter->getSql());
+        $this->assertEquals($expectedParameters, $multiInserter->getParameters());
+    }
+
+    public static function multiInsertQuery_dataProvider()
+    {
+        return [
+            [
+                'testTable',
+                [],
+                '',
+                []
+            ],
+            [
+                'testTable',
+                [
+                    ['a' => 'value1_1', 'b' => 'value2_1'],
+                ],
+                'INSERT INTO "testTable" ("a", "b") VALUES (:a0, :b0);',
+                ['a0' => 'value1_1', 'b0' => 'value2_1']
+            ],
+            [
+                'testTable',
+                [
+                    ['a' => 'value1_1', 'b' => 'value2_1'],
+                    ['a' => 'value1_2', 'b' => 'value2_2']
+                ],
+                'INSERT INTO "testTable" ("a", "b") VALUES (:a0, :b0), (:a1, :b1);',
+                ['a0' => 'value1_1', 'b0' => 'value2_1', 'a1' => 'value1_2', 'b1' => 'value2_2']
+            ],
+            [
+                'testTable',
+                [
+                    ['column1' => 'value1_1'],
+                    ['column1' => 123]
+                ],
+                'INSERT INTO "testTable" ("column1") VALUES (:column10), (:column11);',
+                ['column10' => 'value1_1', 'column11' => 123]
+            ]
+        ];
+    }
+}
diff --git a/tests/Table/AbstractCachedTableTest.php b/tests/Table/AbstractCachedTableTest.php
index 544c09b..d9e706e 100644
--- a/tests/Table/AbstractCachedTableTest.php
+++ b/tests/Table/AbstractCachedTableTest.php
@@ -3,16 +3,21 @@
 namespace Composite\DB\Tests\Table;
 
 use Composite\DB\AbstractCachedTable;
-use Composite\DB\AbstractTable;
 use Composite\DB\Tests\TestStand\Entities;
 use Composite\DB\Tests\TestStand\Tables;
+use Composite\DB\Where;
 use Composite\Entity\AbstractEntity;
+use Composite\DB\Tests\Helpers;
+use PHPUnit\Framework\Attributes\DataProvider;
+use Ramsey\Uuid\Uuid;
 
-final class AbstractCachedTableTest extends BaseTableTest
+final class AbstractCachedTableTest extends \PHPUnit\Framework\TestCase
 {
-    public function getOneCacheKey_dataProvider(): array
+    public static function getOneCacheKey_dataProvider(): array
     {
-        $cache = self::getCache();
+        $cache = Helpers\CacheHelper::getCache();
+        $uuid = Uuid::uuid4();
+        $uuidCacheKey = str_replace('-', '_', (string)$uuid);
         return [
             [
                 new Tables\TestAutoincrementCachedTable($cache),
@@ -26,24 +31,19 @@ public function getOneCacheKey_dataProvider(): array
             ],
             [
                 new Tables\TestUniqueCachedTable($cache),
-                new Entities\TestUniqueEntity(id: '123abc', name: 'John'),
-                'sqlite.TestUnique.v1.o.id_123abc',
+                new Entities\TestUniqueEntity(id: $uuid, name: 'John'),
+                'sqlite.TestUnique.v1.o.id_' . $uuidCacheKey,
             ],
             [
-                new Tables\TestUniqueCachedTable($cache),
-                new Entities\TestUniqueEntity(
-                    id: implode('', array_fill(0, 100, 'a')),
-                    name: 'John',
-                ),
-                'ed66f06444d851a981a9ddcecbbf4d5860cd3131',
+                new Tables\TestCompositeCachedTable($cache),
+                new Entities\TestCompositeEntity(user_id: PHP_INT_MAX, post_id: PHP_INT_MAX, message: 'Text'),
+                '69b5bbf599d78f0274feb5cb0e6424f35cca0b57',
             ],
         ];
     }
 
-    /**
-     * @dataProvider getOneCacheKey_dataProvider
-     */
-    public function test_getOneCacheKey(AbstractTable $table, AbstractEntity $object, string $expected): void
+    #[DataProvider('getOneCacheKey_dataProvider')]
+    public function test_getOneCacheKey(AbstractCachedTable $table, AbstractEntity $object, string $expected): void
     {
         $reflectionMethod = new \ReflectionMethod($table, 'getOneCacheKey');
         $actual = $reflectionMethod->invoke($table, $object);
@@ -51,89 +51,102 @@ public function test_getOneCacheKey(AbstractTable $table, AbstractEntity $object
     }
 
 
-    public function getCountCacheKey_dataProvider(): array
+    public static function getCountCacheKey_dataProvider(): array
     {
         return [
             [
-                '',
                 [],
                 'sqlite.TestAutoincrement.v1.c.all',
             ],
             [
-                'name = :name',
-                ['name' => 'John'],
+                new Where('name = :name', ['name' => 'John']),
                 'sqlite.TestAutoincrement.v1.c.name_eq_john',
             ],
             [
-                '     name        =     :name    ',
                 ['name' => 'John'],
+                'sqlite.TestAutoincrement.v1.c.name_john',
+            ],
+            [
+                new Where('     name        =     :name    ', ['name' => 'John']),
                 'sqlite.TestAutoincrement.v1.c.name_eq_john',
             ],
             [
-                'name=:name',
-                ['name' => 'John'],
+                new Where('name=:name', ['name' => 'John']),
                 'sqlite.TestAutoincrement.v1.c.name_eq_john',
             ],
             [
-                'name = :name AND id > :id',
-                ['name' => 'John', 'id' => 10],
+                new Where('name = :name AND id > :id', ['name' => 'John', 'id' => 10]),
                 'sqlite.TestAutoincrement.v1.c.name_eq_john_and_id_gt_10',
             ],
+            [
+                ['name' => 'John', 'id' => ['>', 10]],
+                'sqlite.TestAutoincrement.v1.c.name_john_id_gt_10',
+            ],
         ];
     }
 
-    /**
-     * @dataProvider getCountCacheKey_dataProvider
-     */
-    public function test_getCountCacheKey(string $whereString, array $whereParams, string $expected): void
+    #[DataProvider('getCountCacheKey_dataProvider')]
+    public function test_getCountCacheKey(array|Where $where, string $expected): void
     {
-        $table = new Tables\TestAutoincrementCachedTable(self::getCache());
+        $table = new Tables\TestAutoincrementCachedTable(Helpers\CacheHelper::getCache());
         $reflectionMethod = new \ReflectionMethod($table, 'getCountCacheKey');
-        $actual = $reflectionMethod->invoke($table, $whereString, $whereParams);
+        $actual = $reflectionMethod->invoke($table, $where);
         $this->assertEquals($expected, $actual);
     }
 
-    public function getListCacheKey_dataProvider(): array
+    public static function getListCacheKey_dataProvider(): array
     {
         return [
             [
-                '',
                 [],
                 [],
                 null,
                 'sqlite.TestAutoincrement.v1.l.all',
             ],
             [
-                '',
                 [],
                 [],
                 10,
                 'sqlite.TestAutoincrement.v1.l.all.limit_10',
             ],
             [
-                '',
                 [],
                 ['id' => 'DESC'],
                 10,
                 'sqlite.TestAutoincrement.v1.l.all.ob_id_desc.limit_10',
             ],
             [
-                'name = :name',
+                new Where('name = :name', ['name' => 'John']),
+                [],
+                null,
+                'sqlite.TestAutoincrement.v1.l.name_eq_john',
+            ],
+            [
                 ['name' => 'John'],
                 [],
                 null,
+                'sqlite.TestAutoincrement.v1.l.name_john',
+            ],
+            [
+                new Where('name = :name', ['name' => 'John']),
+                [],
+                null,
                 'sqlite.TestAutoincrement.v1.l.name_eq_john',
             ],
             [
-                'name = :name AND id > :id',
-                ['name' => 'John', 'id' => 10],
+                new Where('name = :name AND id > :id', ['name' => 'John', 'id' => 10]),
                 [],
                 null,
                 'sqlite.TestAutoincrement.v1.l.name_eq_john_and_id_gt_10',
             ],
             [
-                'name = :name AND id > :id',
-                ['name' => 'John', 'id' => 10],
+                ['name' => 'John', 'id' => ['>', 10]],
+                [],
+                null,
+                'sqlite.TestAutoincrement.v1.l.name_john_id_gt_10',
+            ],
+            [
+                new Where('name = :name AND id > :id', ['name' => 'John', 'id' => 10]),
                 ['id' => 'ASC'],
                 20,
                 'bbcf331b765b682da02c4d21dbaa3342bf2c3f18', //sha1('sqlite.TestAutoincrement.v1.l.name_eq_john_and_id_gt_10.ob_id_asc.limit_20')
@@ -141,19 +154,17 @@ public function getListCacheKey_dataProvider(): array
         ];
     }
 
-    /**
-     * @dataProvider getListCacheKey_dataProvider
-     */
-    public function test_getListCacheKey(string $whereString, array $whereArray, array $orderBy, ?int $limit, string $expected): void
+    #[DataProvider('getListCacheKey_dataProvider')]
+    public function test_getListCacheKey(array|Where $where, array $orderBy, ?int $limit, string $expected): void
     {
-        $table = new Tables\TestAutoincrementCachedTable(self::getCache());
+        $table = new Tables\TestAutoincrementCachedTable(Helpers\CacheHelper::getCache());
         $reflectionMethod = new \ReflectionMethod($table, 'getListCacheKey');
-        $actual = $reflectionMethod->invoke($table, $whereString, $whereArray, $orderBy, $limit);
+        $actual = $reflectionMethod->invoke($table, $where, $orderBy, $limit);
         $this->assertEquals($expected, $actual);
     }
 
 
-    public function getCustomCacheKey_dataProvider(): array
+    public static function getCustomCacheKey_dataProvider(): array
     {
         return [
             [
@@ -179,23 +190,23 @@ public function getCustomCacheKey_dataProvider(): array
         ];
     }
 
-    /**
-     * @dataProvider getCustomCacheKey_dataProvider
-     */
+    #[DataProvider('getCustomCacheKey_dataProvider')]
     public function test_getCustomCacheKey(array $parts, string $expected): void
     {
-        $table = new Tables\TestAutoincrementCachedTable(self::getCache());
+        $table = new Tables\TestAutoincrementCachedTable(Helpers\CacheHelper::getCache());
         $reflectionMethod = new \ReflectionMethod($table, 'buildCacheKey');
         $actual = $reflectionMethod->invoke($table, ...$parts);
         $this->assertEquals($expected, $actual);
     }
 
-    public function collectCacheKeysByEntity_dataProvider(): array
+    public static function collectCacheKeysByEntity_dataProvider(): array
     {
+        $uuid = Uuid::uuid4();
+        $uuidCacheKey = str_replace('-', '_', (string)$uuid);
         return [
             [
                 new Entities\TestAutoincrementEntity(name: 'foo'),
-                new Tables\TestAutoincrementCachedTable(self::getCache()),
+                new Tables\TestAutoincrementCachedTable(Helpers\CacheHelper::getCache()),
                 [
                     'sqlite.TestAutoincrement.v1.o.name_foo',
                     'sqlite.TestAutoincrement.v1.l.name_eq_foo',
@@ -204,7 +215,7 @@ public function collectCacheKeysByEntity_dataProvider(): array
             ],
             [
                 Entities\TestAutoincrementEntity::fromArray(['id' => 123, 'name' => 'bar']),
-                new Tables\TestAutoincrementCachedTable(self::getCache()),
+                new Tables\TestAutoincrementCachedTable(Helpers\CacheHelper::getCache()),
                 [
                     'sqlite.TestAutoincrement.v1.o.name_bar',
                     'sqlite.TestAutoincrement.v1.l.name_eq_bar',
@@ -213,33 +224,54 @@ public function collectCacheKeysByEntity_dataProvider(): array
                 ],
             ],
             [
-                new Entities\TestUniqueEntity(id: '123abc', name: 'foo'),
-                new Tables\TestUniqueCachedTable(self::getCache()),
+                new Entities\TestUniqueEntity(id: $uuid, name: 'foo'),
+                new Tables\TestUniqueCachedTable(Helpers\CacheHelper::getCache()),
                 [
                     'sqlite.TestUnique.v1.l.name_eq_foo',
                     'sqlite.TestUnique.v1.c.name_eq_foo',
-                    'sqlite.TestUnique.v1.o.id_123abc',
+                    'sqlite.TestUnique.v1.o.id_' . $uuidCacheKey,
                 ],
             ],
             [
-                Entities\TestUniqueEntity::fromArray(['id' => '456def', 'name' => 'bar']),
-                new Tables\TestUniqueCachedTable(self::getCache()),
+                Entities\TestUniqueEntity::fromArray(['id' => $uuid, 'name' => 'bar']),
+                new Tables\TestUniqueCachedTable(Helpers\CacheHelper::getCache()),
                 [
                     'sqlite.TestUnique.v1.l.name_eq_bar',
                     'sqlite.TestUnique.v1.c.name_eq_bar',
-                    'sqlite.TestUnique.v1.o.id_456def',
+                    'sqlite.TestUnique.v1.o.id_' . $uuidCacheKey,
                 ],
             ],
         ];
     }
 
-    /**
-     * @dataProvider collectCacheKeysByEntity_dataProvider
-     */
+    #[DataProvider('collectCacheKeysByEntity_dataProvider')]
     public function test_collectCacheKeysByEntity(AbstractEntity $entity, AbstractCachedTable $table, array $expected): void
     {
         $reflectionMethod = new \ReflectionMethod($table, 'collectCacheKeysByEntity');
         $actual = $reflectionMethod->invoke($table, $entity);
         $this->assertEquals($expected, $actual);
     }
+
+    public function test_findMulti(): void
+    {
+        $table = new Tables\TestAutoincrementCachedTable(Helpers\CacheHelper::getCache());
+        $e1 = new Entities\TestAutoincrementEntity('John');
+        $e2 = new Entities\TestAutoincrementEntity('Constantine');
+
+        $table->save($e1);
+        $table->save($e2);
+
+        $multi1 = $table->findMulti([$e1->id], 'id');
+        $this->assertEquals($e1, $multi1[$e1->id]);
+
+        $multi2 = $table->findMulti([$e1->id, $e2->id]);
+        $this->assertEquals($e1, $multi2[0]);
+        $this->assertEquals($e2, $multi2[1]);
+
+        $e11 = $table->findByPk($e1->id);
+        $this->assertEquals($e1, $e11);
+
+        $e111 = $table->findByPk($e1->id);
+        $this->assertEquals($e1, $e111);
+    }
 }
\ No newline at end of file
diff --git a/tests/Table/AbstractTableTest.php b/tests/Table/AbstractTableTest.php
index 0a7e78c..c14e58a 100644
--- a/tests/Table/AbstractTableTest.php
+++ b/tests/Table/AbstractTableTest.php
@@ -3,17 +3,19 @@
 namespace Composite\DB\Tests\Table;
 
 use Composite\DB\AbstractTable;
-use Composite\DB\ConnectionManager;
-use Composite\DB\Exceptions\DbException;
 use Composite\DB\Tests\TestStand\Entities;
 use Composite\DB\Tests\TestStand\Tables;
 use Composite\Entity\AbstractEntity;
 use Composite\Entity\Exceptions\EntityException;
+use PHPUnit\Framework\Attributes\DataProvider;
+use Ramsey\Uuid\Uuid;
+use Ramsey\Uuid\UuidInterface;
 
-final class AbstractTableTest extends BaseTableTest
+final class AbstractTableTest extends \PHPUnit\Framework\TestCase
 {
-    public function getPkCondition_dataProvider(): array
+    public static function getPkCondition_dataProvider(): array
     {
+        $uuid = Uuid::uuid4();
         return [
             [
                 new Tables\TestAutoincrementTable(),
@@ -37,156 +39,162 @@ public function getPkCondition_dataProvider(): array
             ],
             [
                 new Tables\TestUniqueTable(),
-                new Entities\TestUniqueEntity(id: '123abc', name: 'John'),
-                ['id' => '123abc'],
+                new Entities\TestUniqueEntity(id: $uuid, name: 'John'),
+                ['id' => $uuid->toString()],
             ],
             [
                 new Tables\TestUniqueTable(),
-                '123abc',
-                ['id' => '123abc'],
+                $uuid,
+                ['id' => $uuid->toString()],
             ],
             [
                 new Tables\TestAutoincrementSdTable(),
                 Entities\TestAutoincrementSdEntity::fromArray(['id' => 123, 'name' => 'John']),
                 ['id' => 123],
             ],
-            [
-                new Tables\TestCompositeSdTable(),
-                new Entities\TestCompositeSdEntity(user_id: 123, post_id: 456, message: 'Text'),
-                ['user_id' => 123, 'post_id' => 456],
-            ],
-            [
-                new Tables\TestUniqueSdTable(),
-                new Entities\TestUniqueSdEntity(id: '123abc', name: 'John'),
-                ['id' => '123abc'],
-            ],
         ];
     }
 
-    /**
-     * @dataProvider getPkCondition_dataProvider
-     */
-    public function test_getPkCondition(AbstractTable $table, int|string|array|AbstractEntity $object, array $expected): void
+    #[DataProvider('getPkCondition_dataProvider')]
+    public function test_getPkCondition(AbstractTable $table, int|string|array|AbstractEntity|UuidInterface $object, array $expected): void
     {
         $reflectionMethod = new \ReflectionMethod($table, 'getPkCondition');
         $actual = $reflectionMethod->invoke($table, $object);
         $this->assertEquals($expected, $actual);
     }
 
-    public function enrichCondition_dataProvider(): array
+    public function test_illegalEntitySave(): void
     {
-        return [
-            [
-                new Tables\TestAutoincrementTable(),
-                ['id' => 123],
-                ['id' => 123],
-            ],
-            [
-                new Tables\TestCompositeTable(),
-                ['user_id' => 123, 'post_id' => 456],
-                ['user_id' => 123, 'post_id' => 456],
-            ],
-            [
-                new Tables\TestUniqueTable(),
-                ['id' => '123abc'],
-                ['id' => '123abc'],
-            ],
-            [
-                new Tables\TestAutoincrementSdTable(),
-                ['id' => 123],
-                ['id' => 123, 'deleted_at' => null],
-            ],
-            [
-                new Tables\TestCompositeSdTable(),
-                ['user_id' => 123, 'post_id' => 456],
-                ['user_id' => 123, 'post_id' => 456, 'deleted_at' => null],
-            ],
-            [
-                new Tables\TestUniqueSdTable(),
-                ['id' => '123abc'],
-                ['id' => '123abc', 'deleted_at' => null],
-            ],
-        ];
-    }
+        $entity = new Entities\TestAutoincrementEntity(name: 'Foo');
+        $compositeTable = new Tables\TestUniqueTable();
 
-    /**
-     * @dataProvider enrichCondition_dataProvider
-     */
-    public function test_enrichCondition(AbstractTable $table, array $condition, array $expected): void
-    {
-        $reflectionMethod = new \ReflectionMethod($table, 'enrichCondition');
-        $reflectionMethod->invokeArgs($table, [&$condition]);
-        $this->assertEquals($expected, $condition);
+        $this->expectException(EntityException::class);
+        $compositeTable->save($entity);
     }
 
-    public function test_illegalEntitySave(): void
+    public function test_illegalCreateEntity(): void
     {
-        $entity = new Entities\TestAutoincrementEntity(name: 'Foo');
-        $compositeTable = new Tables\TestUniqueTable();
+        $table = new Tables\TestStrictTable();
+        $null = $table->buildEntity(['dti1' => 'abc']);
+        $this->assertNull($null);
+
+        $empty = $table->buildEntities([['dti1' => 'abc']]);
+        $this->assertEmpty($empty);
+
+        $empty = $table->buildEntities([]);
+        $this->assertEmpty($empty);
 
-        $exceptionCatch = false;
-        try {
-            $compositeTable->save($entity);
-        } catch (EntityException) {
-            $exceptionCatch = true;
-        }
-        $this->assertTrue($exceptionCatch);
+        $empty = $table->buildEntities(false);
+        $this->assertEmpty($empty);
+
+        $empty = $table->buildEntities('abc');
+        $this->assertEmpty($empty);
+
+        $empty = $table->buildEntities(['abc']);
+        $this->assertEmpty($empty);
     }
 
-    public function test_optimisticLock(): void
+    #[DataProvider('buildWhere_dataProvider')]
+    public function test_buildWhere($where, $expectedSQL, $expectedParams): void
     {
-        //checking that problem exists
-        $aiEntity1 = new Entities\TestAutoincrementEntity(name: 'John');
-        $aiTable1 = new Tables\TestAutoincrementTable();
-        $aiTable2 = new Tables\TestAutoincrementTable();
+        $table = new Tables\TestStrictTable();
 
-        $aiTable1->save($aiEntity1);
+        $selectReflection = new \ReflectionMethod($table, 'select');
+        $selectReflection->setAccessible(true);
 
-        $aiEntity2 = $aiTable2->findByPk($aiEntity1->id);
+        $queryBuilder = $selectReflection->invoke($table);
 
-        $db = ConnectionManager::getConnection($aiTable1->getConnectionName());
+        $buildWhereReflection = new \ReflectionMethod($table, 'buildWhere');
+        $buildWhereReflection->setAccessible(true);
 
-        $db->beginTransaction();
-        $aiEntity1->name = 'John1';
-        $aiTable1->save($aiEntity1);
+        $buildWhereReflection->invokeArgs($table, [$queryBuilder, $where]);
 
-        $aiEntity2->name = 'John2';
-        $aiTable2->save($aiEntity2);
+        $this->assertEquals($expectedSQL, $queryBuilder->getSQL());
+        $this->assertEquals($expectedParams, $queryBuilder->getParameters());
+    }
 
-        $this->assertTrue($db->commit());
+    public static function buildWhere_dataProvider(): array
+    {
+        return [
+            // Scalar value
+            [
+                ['column' => 1],
+                'SELECT * FROM Strict WHERE column = :column',
+                ['column' => 1]
+            ],
 
-        $aiEntity3 = $aiTable1->findByPk($aiEntity1->id);
-        $this->assertEquals('John2', $aiEntity3->name);
+            // Null value
+            [
+                ['column' => null],
+                'SELECT * FROM Strict WHERE column IS NULL',
+                []
+            ],
 
-        //Checking optimistic lock
-        $olEntity1 = new Entities\TestOptimisticLockEntity(name: 'John');
-        $olTable1 = new Tables\TestOptimisticLockTable();
-        $olTable2 = new Tables\TestOptimisticLockTable();
+            // Greater than comparison
+            [
+                ['column' => ['>', 0]],
+                'SELECT * FROM Strict WHERE column > :column',
+                ['column' => 0]
+            ],
 
-        $olTable1->init();
+            // Less than comparison
+            [
+                ['column' => ['<', 5]],
+                'SELECT * FROM Strict WHERE column < :column',
+                ['column' => 5]
+            ],
+
+            // Greater than or equal to comparison
+            [
+                ['column' => ['>=', 3]],
+                'SELECT * FROM Strict WHERE column >= :column',
+                ['column' => 3]
+            ],
 
-        $olTable1->save($olEntity1);
+            // Less than or equal to comparison
+            [
+                ['column' => ['<=', 7]],
+                'SELECT * FROM Strict WHERE column <= :column',
+                ['column' => 7]
+            ],
 
-        $olEntity2 = $olTable2->findByPk($olEntity1->id);
+            // Not equal to comparison with scalar value
+            [
+                ['column' => ['<>', 10]],
+                'SELECT * FROM Strict WHERE column <> :column',
+                ['column' => 10]
+            ],
 
-        $db->beginTransaction();
-        $olEntity1->name = 'John1';
-        $olTable1->save($olEntity1);
+            // Not equal to comparison with null
+            [
+                ['column' => ['!=', null]],
+                'SELECT * FROM Strict WHERE column IS NOT NULL',
+                []
+            ],
 
-        $olEntity2->name = 'John2';
+            // IN condition
+            [
+                ['column' => [1, 2, 3]],
+                'SELECT * FROM Strict WHERE column IN(:column0, :column1, :column2)',
+                ['column0' => 1, 'column1' => 2, 'column2' => 3]
+            ],
 
-        $exceptionCaught = false;
-        try {
-            $olTable2->save($olEntity2);
-        } catch (DbException) {
-            $exceptionCaught = true;
-        }
-        $this->assertTrue($exceptionCaught);
+            // Multiple conditions
+            [
+                ['column1' => 1, 'column2' => null, 'column3' => ['>', 5]],
+                'SELECT * FROM Strict WHERE (column1 = :column1) AND (column2 IS NULL) AND (column3 > :column3)',
+                ['column1' => 1, 'column3' => 5]
+            ]
+        ];
+    }
 
-        $this->assertTrue($db->rollBack());
+    public function test_databaseSpecific(): void
+    {
+        $mySQLTable = new Tables\TestMySQLTable();
+        $this->assertEquals('`column`', $mySQLTable->escapeIdentifierPub('column'));
+        $this->assertEquals('`Database`.`Table`', $mySQLTable->escapeIdentifierPub('Database.Table'));
 
-        $olEntity3 = $olTable1->findByPk($olEntity1->id);
-        $this->assertEquals(1, $olEntity3->version);
-        $this->assertEquals('John', $olEntity3->name);
+        $postgresTable = new Tables\TestPostgresTable();
+        $this->assertEquals('"column"', $postgresTable->escapeIdentifierPub('column'));
     }
 }
\ No newline at end of file
diff --git a/tests/Table/AutoIncrementTableTest.php b/tests/Table/AutoIncrementTableTest.php
index c1a3535..03a7569 100644
--- a/tests/Table/AutoIncrementTableTest.php
+++ b/tests/Table/AutoIncrementTableTest.php
@@ -2,19 +2,18 @@
 
 namespace Composite\DB\Tests\Table;
 
+use Composite\DB\AbstractTable;
+use Composite\DB\Exceptions\DbException;
+use Composite\DB\TableConfig;
+use Composite\DB\Tests\Helpers;
 use Composite\DB\Tests\TestStand\Tables;
 use Composite\DB\Tests\TestStand\Entities;
 use Composite\DB\Tests\TestStand\Interfaces\IAutoincrementTable;
+use PHPUnit\Framework\Attributes\DataProvider;
 
-final class AutoIncrementTableTest extends BaseTableTest
+final class AutoIncrementTableTest extends \PHPUnit\Framework\TestCase
 {
-    public static function setUpBeforeClass(): void
-    {
-        (new Tables\TestAutoincrementTable())->init();
-        (new Tables\TestAutoincrementSdTable())->init();
-    }
-
-    public function crud_dataProvider(): array
+    public static function crud_dataProvider(): array
     {
         return [
             [
@@ -26,25 +25,28 @@ public function crud_dataProvider(): array
                 Entities\TestAutoincrementSdEntity::class,
             ],
             [
-                new Tables\TestAutoincrementCachedTable(self::getCache()),
+                new Tables\TestAutoincrementCachedTable(Helpers\CacheHelper::getCache()),
                 Entities\TestAutoincrementEntity::class,
             ],
             [
-                new Tables\TestAutoincrementSdCachedTable(self::getCache()),
+                new Tables\TestAutoincrementSdCachedTable(Helpers\CacheHelper::getCache()),
                 Entities\TestAutoincrementSdEntity::class,
             ],
         ];
     }
 
     /**
-     * @dataProvider crud_dataProvider
+     * @param class-string<Entities\TestAutoincrementEntity|Entities\TestAutoincrementSdEntity> $class
      */
-    public function test_crud(IAutoincrementTable $table, string $class): void
+    #[DataProvider('crud_dataProvider')]
+    public function test_crud(AbstractTable&IAutoincrementTable $table, string $class): void
     {
         $table->truncate();
+        $tableConfig = TableConfig::fromEntitySchema($class::schema());
 
         $entity = new $class(
-            name: $this->getUniqueName(),
+            name: Helpers\StringHelper::getUniqueName(),
+            is_test: true,
         );
         $this->assertEntityNotExists($table, PHP_INT_MAX, uniqid());
 
@@ -62,7 +64,81 @@ public function test_crud(IAutoincrementTable $table, string $class): void
         $this->assertEquals($newName, $foundEntity->name);
 
         $table->delete($entity);
-        $this->assertEntityNotExists($table, $entity->id, $entity->name);
+        if ($tableConfig->hasSoftDelete()) {
+            /** @var Entities\TestAutoincrementSdEntity $deletedEntity */
+            $deletedEntity = $table->findByPk($entity->id);
+            $this->assertTrue($deletedEntity->isDeleted());
+        } else {
+            $this->assertEntityNotExists($table, $entity->id, $entity->name);
+        }
+
+        $e1 = new $class(Helpers\StringHelper::getUniqueName());
+        $e2 = new $class(Helpers\StringHelper::getUniqueName());
+
+        $table->save($e1);
+        $table->save($e2);
+        $this->assertEntityExists($table, $e1);
+        $this->assertEntityExists($table, $e2);
+
+        $recentEntities = $table->findRecent(2, 0);
+        $this->assertEquals($e2, $recentEntities[0]);
+        $this->assertEquals($e1, $recentEntities[1]);
+        $preLastEntity = $table->findRecent(1, 1);
+        $this->assertEquals($e1, $preLastEntity[0]);
+
+        if ($tableConfig->hasSoftDelete()) {
+            $e1->name = 'Exception';
+            $exceptionThrown = false;
+            try {
+                $table->deleteMany([$e1, $e2]);
+            } catch (\Exception) {
+                $exceptionThrown = true;
+            }
+            $this->assertTrue($exceptionThrown);
+            $e1->name = Helpers\StringHelper::getUniqueName();
+        }
+
+        $table->deleteMany([$e1, $e2]);
+
+        if ($tableConfig->hasSoftDelete()) {
+            /** @var Entities\TestAutoincrementSdEntity $deletedEntity1 */
+            $deletedEntity1 = $table->findByPk($e1->id);
+            $this->assertTrue($deletedEntity1->isDeleted());
+
+            /** @var Entities\TestAutoincrementSdEntity $deletedEntity2 */
+            $deletedEntity2 = $table->findByPk($e2->id);
+            $this->assertTrue($deletedEntity2->isDeleted());
+        } else {
+            $this->assertEntityNotExists($table, $e1->id, $e1->name);
+            $this->assertEntityNotExists($table, $e2->id, $e2->name);
+        }
+    }
+
+    public function test_getMulti(): void
+    {
+        $table = new Tables\TestAutoincrementTable();
+
+        $e1 = new Entities\TestAutoincrementEntity('name1');
+        $e2 = new Entities\TestAutoincrementEntity('name2');
+        $e3 = new Entities\TestAutoincrementEntity('name3');
+
+        $table->save($e1);
+        $table->save($e2);
+        $table->save($e3);
+
+        $multiResult = $table->findMulti([$e1->id, $e2->id, $e3->id]);
+        $this->assertEquals($e1, $multiResult[$e1->id]);
+        $this->assertEquals($e2, $multiResult[$e2->id]);
+        $this->assertEquals($e3, $multiResult[$e3->id]);
+
+        $this->assertEmpty($table->findMulti([]));
+    }
+
+    public function test_illegalGetMulti(): void
+    {
+        $table = new Tables\TestAutoincrementTable();
+        $this->expectException(DbException::class);
+        $table->findMulti(['a' => 1]);
     }
 
     private function assertEntityExists(IAutoincrementTable $table, Entities\TestAutoincrementEntity $entity): void
diff --git a/tests/Table/CombinedTransactionTest.php b/tests/Table/CombinedTransactionTest.php
index 7108179..f7ba7d9 100644
--- a/tests/Table/CombinedTransactionTest.php
+++ b/tests/Table/CombinedTransactionTest.php
@@ -6,8 +6,10 @@
 use Composite\DB\Exceptions\DbException;
 use Composite\DB\Tests\TestStand\Entities;
 use Composite\DB\Tests\TestStand\Tables;
+use Composite\DB\Tests\Helpers;
+use PHPUnit\Framework\Attributes\DataProvider;
 
-final class CombinedTransactionTest extends BaseTableTest
+final class CombinedTransactionTest extends \PHPUnit\Framework\TestCase
 {
     public function test_transactionCommit(): void
     {
@@ -19,7 +21,7 @@ public function test_transactionCommit(): void
         $e1 = new Entities\TestAutoincrementEntity(name: 'Foo');
         $saveTransaction->save($autoIncrementTable, $e1);
 
-        $e2 = new Entities\TestCompositeEntity(user_id: $e1->id, post_id: mt_rand(1, 1000), message: 'Bar');
+        $e2 = new Entities\TestCompositeEntity(user_id: $e1->id, post_id: mt_rand(1, 1000000), message: 'Bar');
         $saveTransaction->save($compositeTable, $e2);
 
         $saveTransaction->commit();
@@ -36,6 +38,32 @@ public function test_transactionCommit(): void
         $this->assertNull($compositeTable->findOne($e2->user_id, $e2->post_id));
     }
 
+    public function test_saveDeleteMany(): void
+    {
+        $autoIncrementTable = new Tables\TestAutoincrementTable();
+        $compositeTable = new Tables\TestCompositeTable();
+
+        $saveTransaction = new CombinedTransaction();
+
+        $e1 = new Entities\TestAutoincrementEntity(name: 'Foo');
+        $saveTransaction->save($autoIncrementTable, $e1);
+
+        $e2 = new Entities\TestCompositeEntity(user_id: $e1->id, post_id: mt_rand(1, 1000000), message: 'Foo');
+        $e3 = new Entities\TestCompositeEntity(user_id: $e1->id, post_id: mt_rand(1, 1000000), message: 'Bar');
+        $saveTransaction->saveMany($compositeTable, [$e2, $e3]);
+
+        $saveTransaction->commit();
+
+        $this->assertNotNull($autoIncrementTable->findByPk($e1->id));
+        $this->assertNotNull($compositeTable->findOne($e2->user_id, $e2->post_id));
+        $this->assertNotNull($compositeTable->findOne($e3->user_id, $e3->post_id));
+
+        $deleteTransaction = new CombinedTransaction();
+        $deleteTransaction->delete($autoIncrementTable, $e1);
+        $deleteTransaction->deleteMany($compositeTable, [$e2, $e3]);
+        $deleteTransaction->commit();
+    }
+
     public function test_transactionRollback(): void
     {
         $autoIncrementTable = new Tables\TestAutoincrementTable();
@@ -55,7 +83,7 @@ public function test_transactionRollback(): void
         $this->assertNull($compositeTable->findOne($e2->user_id, $e2->post_id));
     }
 
-    public function test_transactionException(): void
+    public function test_failedSave(): void
     {
         $autoIncrementTable = new Tables\TestAutoincrementTable();
         $compositeTable = new Tables\TestCompositeTable();
@@ -69,17 +97,69 @@ public function test_transactionException(): void
         try {
             $transaction->save($compositeTable, $e2);
             $transaction->commit();
-            $this->assertFalse(true, 'This line should not be reached');
+            $this->fail('This line should not be reached');
         } catch (DbException) {}
 
         $this->assertNull($autoIncrementTable->findByPk($e1->id));
         $this->assertNull($compositeTable->findOne($e2->user_id, $e2->post_id));
     }
 
+    public function test_failedDelete(): void
+    {
+        $autoIncrementTable = new Tables\TestAutoincrementTable();
+        $compositeTable = new Tables\TestCompositeTable();
+
+        $aiEntity = new Entities\TestAutoincrementEntity(name: 'Foo');
+        $cEntity = new Entities\TestCompositeEntity(user_id: mt_rand(1, 1000), post_id: mt_rand(1, 1000), message: 'Bar');;
+
+        $autoIncrementTable->save($aiEntity);
+        $compositeTable->save($cEntity);
+
+        $transaction = new CombinedTransaction();
+        try {
+            $aiEntity->name = 'Foo1';
+            $cEntity->message = 'Exception';
+
+            $transaction->save($autoIncrementTable, $aiEntity);
+            $transaction->delete($compositeTable, $cEntity);
+
+            $transaction->commit();
+            $this->fail('This line should not be reached');
+        } catch (DbException) {}
+
+        $this->assertEquals('Foo', $autoIncrementTable->findByPk($aiEntity->id)->name);
+        $this->assertNotNull($compositeTable->findOne($cEntity->user_id, $cEntity->post_id));
+    }
+
+    public function test_try(): void
+    {
+        $compositeTable = new Tables\TestCompositeTable();
+        $entity = new Entities\TestCompositeEntity(user_id: mt_rand(1, 1000), post_id: mt_rand(1, 1000), message: 'Bar');;
+
+        try {
+            $transaction = new CombinedTransaction();
+            $transaction->save($compositeTable, $entity);
+            $transaction->try(fn() => throw new \Exception('test'));
+            $transaction->commit();
+        } catch (DbException) {}
+        $this->assertNull($compositeTable->findOne($entity->user_id, $entity->post_id));
+    }
+
+    public function test_lockFailed(): void
+    {
+        $cache = new Helpers\FalseCache();
+        $keyParts = [uniqid()];
+        $transaction = new CombinedTransaction();
+
+        $this->expectException(DbException::class);
+        $transaction->lock($cache, $keyParts);
+    }
+
     public function test_lock(): void
     {
-        $cache = self::getCache();
+        $cache = Helpers\CacheHelper::getCache();
         $table = new Tables\TestAutoincrementTable();
+
         $e1 = new Entities\TestAutoincrementEntity(name: 'Foo');
         $e2 = new Entities\TestAutoincrementEntity(name: 'Bar');
 
@@ -90,8 +170,10 @@ public function test_lock(): void
         $transaction2 = new CombinedTransaction();
         try {
             $transaction2->lock($cache, $keyParts);
-            $this->assertFalse(false, 'Lock should not be free');
-        } catch (DbException) {}
+            $this->fail('Lock should not be free');
+        } catch (DbException) {
+            $this->assertTrue(true);
+        }
 
         $transaction1->save($table, $e1);
         $transaction1->commit();
@@ -103,4 +185,23 @@ public function test_lock(): void
         $this->assertNotEmpty($table->findByPk($e1->id));
         $this->assertNotEmpty($table->findByPk($e2->id));
     }
+
+    #[DataProvider('buildLockKey_dataProvider')]
+    public function test_buildLockKey($keyParts, $expectedResult)
+    {
+        $reflection = new \ReflectionClass(CombinedTransaction::class);
+        $object = new CombinedTransaction();
+        $result = $reflection->getMethod('buildLockKey')->invoke($object, $keyParts);
+        $this->assertEquals($expectedResult, $result);
+    }
+
+    public static function buildLockKey_dataProvider()
+    {
+        return [
+            'empty array' => [[], 'composite.lock'],
+            'one element' => [['element'], 'composite.lock.element'],
+            'exact length' => [[str_repeat('a', 49)], 'composite.lock.' . str_repeat('a', 49)],
+            'more than max length' => [[str_repeat('a', 55)], sha1('composite.lock.' . str_repeat('a', 55))],
+        ];
+    }
 }
\ No newline at end of file
diff --git a/tests/Table/CompositeTableTest.php b/tests/Table/CompositeTableTest.php
index 87bc767..09f784b 100644
--- a/tests/Table/CompositeTableTest.php
+++ b/tests/Table/CompositeTableTest.php
@@ -2,19 +2,18 @@
 
 namespace Composite\DB\Tests\Table;
 
+use Composite\DB\AbstractTable;
+use Composite\DB\Exceptions\DbException;
+use Composite\DB\TableConfig;
+use Composite\DB\Tests\Helpers;
 use Composite\DB\Tests\TestStand\Tables;
 use Composite\DB\Tests\TestStand\Entities;
 use Composite\DB\Tests\TestStand\Interfaces\ICompositeTable;
+use PHPUnit\Framework\Attributes\DataProvider;
 
-final class CompositeTableTest extends BaseTableTest
+final class CompositeTableTest extends \PHPUnit\Framework\TestCase
 {
-    public static function setUpBeforeClass(): void
-    {
-        (new Tables\TestCompositeTable())->init();
-        (new Tables\TestCompositeSdTable())->init();
-    }
-
-    public function crud_dataProvider(): array
+    public static function crud_dataProvider(): array
     {
         return [
             [
@@ -22,31 +21,25 @@ public function crud_dataProvider(): array
                 Entities\TestCompositeEntity::class,
             ],
             [
-                new Tables\TestCompositeSdTable(),
-                Entities\TestCompositeSdEntity::class,
-            ],
-            [
-                new Tables\TestCompositeCachedTable(self::getCache()),
+                new Tables\TestCompositeCachedTable(Helpers\CacheHelper::getCache()),
                 Entities\TestCompositeEntity::class,
             ],
-            [
-                new Tables\TestCompositeSdCachedTable(self::getCache()),
-                Entities\TestCompositeSdEntity::class,
-            ],
         ];
     }
 
     /**
-     * @dataProvider crud_dataProvider
+     * @param class-string<Entities\TestCompositeEntity> $class
+     * @throws \Throwable
      */
-    public function test_crud(ICompositeTable $table, string $class): void
+    #[DataProvider('crud_dataProvider')]
+    public function test_crud(AbstractTable&ICompositeTable $table, string $class): void
     {
         $table->truncate();
 
         $entity = new $class(
             user_id: mt_rand(1, 1000000),
             post_id: mt_rand(1, 1000000),
-            message: $this->getUniqueName(),
+            message: Helpers\StringHelper::getUniqueName(),
         );
         $this->assertEntityNotExists($table, $entity);
         $table->save($entity);
@@ -59,13 +52,74 @@ public function test_crud(ICompositeTable $table, string $class): void
         $table->delete($entity);
         $this->assertEntityNotExists($table, $entity);
 
-        $newEntity = new $entity(
-            user_id: $entity->user_id,
-            post_id: $entity->post_id,
-            message: 'Hello User',
+        $e1 = new $class(
+            user_id: mt_rand(1, 1000000),
+            post_id: mt_rand(1, 1000000),
+            message: Helpers\StringHelper::getUniqueName(),
+        );
+        $e2 = new $class(
+            user_id: mt_rand(1, 1000000),
+            post_id: mt_rand(1, 1000000),
+            message: Helpers\StringHelper::getUniqueName(),
+        );
+
+        $table->saveMany([$e1, $e2]);
+        $e1->resetChangedColumns();
+        $e2->resetChangedColumns();
+
+        $this->assertEntityExists($table, $e1);
+        $this->assertEntityExists($table, $e2);
+
+        $table->deleteMany([$e1, $e2]);
+
+        $this->assertEntityNotExists($table, $e1);
+        $this->assertEntityNotExists($table, $e2);
+    }
+
+    public function test_getMulti(): void
+    {
+        $table = new Tables\TestCompositeTable();
+        $userId = mt_rand(1, 1000000);
+
+        $e1 = new Entities\TestCompositeEntity(
+            user_id: $userId,
+            post_id: mt_rand(1, 1000000),
+            message: Helpers\StringHelper::getUniqueName(),
         );
-        $table->save($newEntity);
-        $this->assertEntityExists($table, $newEntity);
+
+        $e2 = new Entities\TestCompositeEntity(
+            user_id: $userId,
+            post_id: mt_rand(1, 1000000),
+            message: Helpers\StringHelper::getUniqueName(),
+        );
+
+        $e3 = new Entities\TestCompositeEntity(
+            user_id: $userId,
+            post_id: mt_rand(1, 1000000),
+            message: Helpers\StringHelper::getUniqueName(),
+        );
+
+        $table->saveMany([$e1, $e2, $e3]);
+
+        $e1->resetChangedColumns();
+        $e2->resetChangedColumns();
+        $e3->resetChangedColumns();
+
+        $multiResult = $table->findMulti([
+            ['user_id' => $e1->user_id, 'post_id' => $e1->post_id],
+            ['user_id' => $e2->user_id, 'post_id' => $e2->post_id],
+            ['user_id' => $e3->user_id, 'post_id' => $e3->post_id],
+        ]);
+        $this->assertEquals($e1, $multiResult[$e1->post_id]);
+        $this->assertEquals($e2, $multiResult[$e2->post_id]);
+        $this->assertEquals($e3, $multiResult[$e3->post_id]);
+    }
+
+    public function test_illegalGetMulti(): void
+    {
+        $table = new Tables\TestCompositeTable();
+        $this->expectException(DbException::class);
+        $table->findMulti(['a']);
     }
 
     private function assertEntityExists(ICompositeTable $table, Entities\TestCompositeEntity $entity): void
diff --git a/tests/Table/TableConfigTest.php b/tests/Table/TableConfigTest.php
index b4c558e..e74f318 100644
--- a/tests/Table/TableConfigTest.php
+++ b/tests/Table/TableConfigTest.php
@@ -6,6 +6,7 @@
 use Composite\DB\TableConfig;
 use Composite\DB\Traits;
 use Composite\Entity\AbstractEntity;
+use Composite\Entity\Exceptions\EntityException;
 use Composite\Entity\Schema;
 
 final class TableConfigTest extends \PHPUnit\Framework\TestCase
@@ -26,12 +27,28 @@ public function __construct(
                     private \DateTimeImmutable $dt = new \DateTimeImmutable(),
                 ) {}
         };
-        $schema = Schema::build($class::class);
+        $schema = new Schema($class::class);
         $tableConfig = TableConfig::fromEntitySchema($schema);
         $this->assertNotEmpty($tableConfig->connectionName);
         $this->assertNotEmpty($tableConfig->tableName);
-        $this->assertTrue($tableConfig->isSoftDelete);
+        $this->assertTrue($tableConfig->hasSoftDelete());
         $this->assertCount(1, $tableConfig->primaryKeys);
         $this->assertSame('id', $tableConfig->autoIncrementKey);
     }
+
+    public function test_missingAttribute(): void
+    {
+        $class = new
+        class extends AbstractEntity {
+            #[Attributes\PrimaryKey(autoIncrement: true)]
+            public readonly int $id;
+
+            public function __construct(
+                public string $str = 'abc',
+            ) {}
+        };
+        $schema = new Schema($class::class);
+        $this->expectException(EntityException::class);
+        TableConfig::fromEntitySchema($schema);
+    }
 }
\ No newline at end of file
diff --git a/tests/Table/UniqueTableTest.php b/tests/Table/UniqueTableTest.php
index cb834c3..abeb9ff 100644
--- a/tests/Table/UniqueTableTest.php
+++ b/tests/Table/UniqueTableTest.php
@@ -2,19 +2,17 @@
 
 namespace Composite\DB\Tests\Table;
 
+use Composite\DB\AbstractTable;
+use Composite\DB\Tests\Helpers;
 use Composite\DB\Tests\TestStand\Entities;
 use Composite\DB\Tests\TestStand\Tables;
 use Composite\DB\Tests\TestStand\Interfaces\IUniqueTable;
+use PHPUnit\Framework\Attributes\DataProvider;
+use Ramsey\Uuid\Uuid;
 
-final class UniqueTableTest extends BaseTableTest
+final class UniqueTableTest extends \PHPUnit\Framework\TestCase
 {
-    public static function setUpBeforeClass(): void
-    {
-        (new Tables\TestUniqueTable())->init();
-        (new Tables\TestUniqueSdTable())->init();
-    }
-
-    public function crud_dataProvider(): array
+    public static function crud_dataProvider(): array
     {
         return [
             [
@@ -22,30 +20,23 @@ public function crud_dataProvider(): array
                 Entities\TestUniqueEntity::class,
             ],
             [
-                new Tables\TestUniqueSdTable(),
-                Entities\TestUniqueSdEntity::class,
-            ],
-            [
-                new Tables\TestUniqueCachedTable(self::getCache()),
+                new Tables\TestUniqueCachedTable(Helpers\CacheHelper::getCache()),
                 Entities\TestUniqueEntity::class,
             ],
-            [
-                new Tables\TestUniqueSdCachedTable(self::getCache()),
-                Entities\TestUniqueSdEntity::class,
-            ],
         ];
     }
 
     /**
-     * @dataProvider crud_dataProvider
+     * @param class-string<Entities\TestUniqueEntity> $class
      */
-    public function test_crud(IUniqueTable $table, string $class): void
+    #[DataProvider('crud_dataProvider')]
+    public function test_crud(AbstractTable&IUniqueTable $table, string $class): void
     {
         $table->truncate();
 
         $entity = new $class(
-            id: uniqid(),
-            name: $this->getUniqueName(),
+            id: Uuid::uuid4(),
+            name: Helpers\StringHelper::getUniqueName(),
         );
         $this->assertEntityNotExists($table, $entity);
         $table->save($entity);
@@ -57,16 +48,55 @@ public function test_crud(IUniqueTable $table, string $class): void
 
         $table->delete($entity);
         $this->assertEntityNotExists($table, $entity);
+    }
 
-        $newEntity = new $entity(
-            id: $entity->id,
-            name: $entity->name . ' new',
+    public function test_multiSave(): void
+    {
+        $e1 = new Entities\TestUniqueEntity(
+            id: Uuid::uuid4(),
+            name: Helpers\StringHelper::getUniqueName(),
         );
-        $table->save($newEntity);
-        $this->assertEntityExists($table, $newEntity);
+        $e2 = new Entities\TestUniqueEntity(
+            id: Uuid::uuid4(),
+            name: Helpers\StringHelper::getUniqueName(),
+        );
+        $e3 = new Entities\TestUniqueEntity(
+            id: Uuid::uuid4(),
+            name: Helpers\StringHelper::getUniqueName(),
+        );
+        $e4 = new Entities\TestUniqueEntity(
+            id: Uuid::uuid4(),
+            name: Helpers\StringHelper::getUniqueName(),
+        );
+        $table = new Tables\TestUniqueTable();
+        $table->saveMany([$e1, $e2]);
+
+        $this->assertEntityExists($table, $e1);
+        $this->assertEntityExists($table, $e2);
+
+        $e1->resetChangedColumns();
+        $e2->resetChangedColumns();
+
+        $e1->name = 'Exception';
+
+        $exceptionThrown = false;
+        try {
+            $table->saveMany([$e1, $e2, $e3, $e4]);
+        } catch (\Exception) {
+            $exceptionThrown = true;
+        }
+        $this->assertTrue($exceptionThrown);
+        $this->assertEntityNotExists($table, $e3);
+        $this->assertEntityNotExists($table, $e4);
+
+        $e1->name = 'NonException';
+
+        $table->saveMany([$e1, $e2, $e3, $e4]);
 
-        $table->delete($newEntity);
-        $this->assertEntityNotExists($table, $newEntity);
+        $this->assertEntityExists($table, $e1);
+        $this->assertEntityExists($table, $e2);
+        $this->assertEntityExists($table, $e3);
+        $this->assertEntityExists($table, $e4);
     }
 
     private function assertEntityExists(IUniqueTable $table, Entities\TestUniqueEntity $entity): void
diff --git a/tests/TestStand/Entities/Castable/TestCastableIntObject.php b/tests/TestStand/Entities/Castable/TestCastableIntObject.php
deleted file mode 100644
index 56baddf..0000000
--- a/tests/TestStand/Entities/Castable/TestCastableIntObject.php
+++ /dev/null
@@ -1,32 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Composite\DB\Tests\TestStand\Entities\Castable;
-
-use Composite\DB\Helpers\DateTimeHelper;
-use Composite\Entity\CastableInterface;
-
-class TestCastableIntObject extends \DateTime implements CastableInterface
-{
-    public function __construct(int $unixTime)
-    {
-        parent::__construct(date(DateTimeHelper::DATETIME_FORMAT, $unixTime));
-    }
-
-    public static function cast(mixed $dbValue): ?static
-    {
-        if (!$dbValue || !is_numeric($dbValue) || intval($dbValue) != $dbValue || $dbValue < 0) {
-            return null;
-        }
-        try {
-            return new static((int)$dbValue);
-        } catch (\Exception $e) {
-            return null;
-        }
-    }
-
-    public function uncast(): ?int
-    {
-        $unixTime = (int)$this->format('U');
-        return $unixTime === 0 ? null : $unixTime ;
-    }
-}
\ No newline at end of file
diff --git a/tests/TestStand/Entities/Castable/TestCastableStringObject.php b/tests/TestStand/Entities/Castable/TestCastableStringObject.php
deleted file mode 100644
index 2a09e03..0000000
--- a/tests/TestStand/Entities/Castable/TestCastableStringObject.php
+++ /dev/null
@@ -1,26 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Composite\DB\Tests\TestStand\Entities\Castable;
-
-use Composite\Entity\CastableInterface;
-
-class TestCastableStringObject implements CastableInterface
-{
-    public function __construct(private readonly ?string $value) {}
-
-    public static function cast(mixed $dbValue): ?static
-    {
-        if (is_string($dbValue) || is_numeric($dbValue)) {
-            $dbValue = trim((string)$dbValue);
-            $dbValue = preg_replace('/(^_)|(_$)/', '', $dbValue);
-        } else {
-            $dbValue = null;
-        }
-        return new static($dbValue ?: null);
-    }
-
-    public function uncast(): ?string
-    {
-        return $this->value ? '_' . $this->value . '_' : null;
-    }
-}
\ No newline at end of file
diff --git a/tests/TestStand/Entities/Enums/TestBackedEnum.php b/tests/TestStand/Entities/Enums/TestBackedEnum.php
new file mode 100644
index 0000000..107d79f
--- /dev/null
+++ b/tests/TestStand/Entities/Enums/TestBackedEnum.php
@@ -0,0 +1,12 @@
+<?php declare(strict_types=1);
+
+namespace Composite\DB\Tests\TestStand\Entities\Enums;
+
+use Composite\DB\Attributes\{PrimaryKey};
+use Composite\DB\Attributes\Table;
+
+enum TestBackedEnum: string
+{
+    case ACTIVE = 'Active';
+    case DELETED = 'Deleted';
+}
\ No newline at end of file
diff --git a/tests/TestStand/Entities/Enums/TestBackedIntEnum.php b/tests/TestStand/Entities/Enums/TestBackedIntEnum.php
deleted file mode 100644
index 8244fa3..0000000
--- a/tests/TestStand/Entities/Enums/TestBackedIntEnum.php
+++ /dev/null
@@ -1,9 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Composite\DB\Tests\TestStand\Entities\Enums;
-
-enum TestBackedIntEnum: int
-{
-    case FooInt = 123;
-    case BarInt = 456;
-}
\ No newline at end of file
diff --git a/tests/TestStand/Entities/Enums/TestBackedStringEnum.php b/tests/TestStand/Entities/Enums/TestBackedStringEnum.php
deleted file mode 100644
index bdf0aa6..0000000
--- a/tests/TestStand/Entities/Enums/TestBackedStringEnum.php
+++ /dev/null
@@ -1,9 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Composite\DB\Tests\TestStand\Entities\Enums;
-
-enum TestBackedStringEnum: string
-{
-    case Foo = 'foo';
-    case Bar = 'bar';
-}
\ No newline at end of file
diff --git a/tests/TestStand/Entities/Enums/TestSubEntity.php b/tests/TestStand/Entities/Enums/TestSubEntity.php
deleted file mode 100644
index 7199082..0000000
--- a/tests/TestStand/Entities/Enums/TestSubEntity.php
+++ /dev/null
@@ -1,13 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Composite\DB\Tests\TestStand\Entities\Enums;
-
-use Composite\Entity\AbstractEntity;
-
-class TestSubEntity extends AbstractEntity
-{
-    public function __construct(
-        public string $str = 'foo',
-        public int $number = 123,
-    ) {}
-}
\ No newline at end of file
diff --git a/tests/TestStand/Entities/Enums/TestUnitEnum.php b/tests/TestStand/Entities/Enums/TestUnitEnum.php
index 652a6e6..a787709 100644
--- a/tests/TestStand/Entities/Enums/TestUnitEnum.php
+++ b/tests/TestStand/Entities/Enums/TestUnitEnum.php
@@ -2,8 +2,11 @@
 
 namespace Composite\DB\Tests\TestStand\Entities\Enums;
 
+use Composite\DB\Attributes\{PrimaryKey};
+use Composite\DB\Attributes\Table;
+
 enum TestUnitEnum
 {
-    case Foo;
-    case Bar;
+    case ACTIVE;
+    case DELETED;
 }
\ No newline at end of file
diff --git a/tests/TestStand/Entities/TestAutoincrementEntity.php b/tests/TestStand/Entities/TestAutoincrementEntity.php
index 1d80ec0..f97f1e7 100644
--- a/tests/TestStand/Entities/TestAutoincrementEntity.php
+++ b/tests/TestStand/Entities/TestAutoincrementEntity.php
@@ -13,6 +13,7 @@ class TestAutoincrementEntity extends \Composite\Entity\AbstractEntity
 
     public function __construct(
         public string $name,
+        public bool $is_test = false,
         public readonly \DateTimeImmutable $created_at = new \DateTimeImmutable(),
     ) {}
 }
\ No newline at end of file
diff --git a/tests/TestStand/Entities/TestCompositeEntity.php b/tests/TestStand/Entities/TestCompositeEntity.php
index f624a50..3e1c765 100644
--- a/tests/TestStand/Entities/TestCompositeEntity.php
+++ b/tests/TestStand/Entities/TestCompositeEntity.php
@@ -14,6 +14,7 @@ public function __construct(
         #[PrimaryKey]
         public readonly int $post_id,
         public string $message,
+        public Enums\TestUnitEnum $status = Enums\TestUnitEnum::ACTIVE,
         public readonly \DateTimeImmutable $created_at = new \DateTimeImmutable(),
     ) {}
 }
\ No newline at end of file
diff --git a/tests/TestStand/Entities/TestCompositeSdEntity.php b/tests/TestStand/Entities/TestCompositeSdEntity.php
deleted file mode 100644
index e6d8027..0000000
--- a/tests/TestStand/Entities/TestCompositeSdEntity.php
+++ /dev/null
@@ -1,12 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Composite\DB\Tests\TestStand\Entities;
-
-use Composite\DB\Attributes\Table;
-use Composite\DB\Traits\SoftDelete;
-
-#[Table(connection: 'sqlite', name: 'TestCompositeSoftDelete')]
-class TestCompositeSdEntity extends TestCompositeEntity
-{
-    use SoftDelete;
-}
\ No newline at end of file
diff --git a/tests/TestStand/Entities/TestDiversityEntity.php b/tests/TestStand/Entities/TestDiversityEntity.php
deleted file mode 100644
index 049277a..0000000
--- a/tests/TestStand/Entities/TestDiversityEntity.php
+++ /dev/null
@@ -1,111 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Composite\DB\Tests\TestStand\Entities;
-
-use Composite\DB\Tests\TestStand\Entities\Castable\TestCastableIntObject;
-use Composite\DB\Tests\TestStand\Entities\Castable\TestCastableStringObject;
-use Composite\DB\Tests\TestStand\Entities\Enums\TestBackedIntEnum;
-use Composite\DB\Tests\TestStand\Entities\Enums\TestBackedStringEnum;
-use Composite\DB\Tests\TestStand\Entities\Enums\TestSubEntity;
-use Composite\DB\Tests\TestStand\Entities\Enums\TestUnitEnum;
-use Composite\DB\Attributes;
-use Composite\Entity\AbstractEntity;
-
-#[Attributes\Table(connection: 'sqlite', name: 'Diversity')]
-class TestDiversityEntity extends AbstractEntity
-{
-    #[Attributes\PrimaryKey(autoIncrement: true)]
-    public readonly int $id;
-
-    public string $str1;
-    public ?string $str2;
-    public string $str3 = 'str3 def';
-    public ?string $str4 = '';
-    public ?string $str5 = null;
-    
-    public int $int1;
-    public ?int $int2;
-    public int $int3 = 33;
-    public ?int $int4 = 44;
-    public ?int $int5 = null;
-
-    public float $float1;
-    public ?float $float2;
-    public float $float3 = 3.9;
-    public ?float $float4 = 4.9;
-    public ?float $float5 = null;
-
-    public bool $bool1;
-    public ?bool $bool2;
-    public bool $bool3 = true;
-    public ?bool $bool4 = false;
-    public ?bool $bool5 = null;
-
-    public array $arr1;
-    public ?array $arr2;
-    public array $arr3 = [11, 22, 33];
-    public ?array $arr4 = [];
-    public ?array $arr5 = null;
-
-    public TestBackedStringEnum $benum_str1;
-    public ?TestBackedStringEnum $benum_str2;
-    public TestBackedStringEnum $benum_str3 = TestBackedStringEnum::Foo;
-    public ?TestBackedStringEnum $benum_str4 = TestBackedStringEnum::Bar;
-    public ?TestBackedStringEnum $benum_str5 = null;
-
-    public TestBackedIntEnum $benum_int1;
-    public ?TestBackedIntEnum $benum_int2;
-    public TestBackedIntEnum $benum_int3 = TestBackedIntEnum::FooInt;
-    public ?TestBackedIntEnum $benum_int4 = TestBackedIntEnum::BarInt;
-    public ?TestBackedIntEnum $benum_int5 = null;
-
-    public TestUnitEnum $uenum1;
-    public ?TestUnitEnum $uenum2;
-    public TestUnitEnum $uenum3 = TestUnitEnum::Foo;
-    public ?TestUnitEnum $uenum4 = TestUnitEnum::Bar;
-    public ?TestUnitEnum $uenum5 = null;
-    
-    public function __construct(
-        public \stdClass $obj1,
-        public ?\stdClass $obj2,
-
-        public \DateTime $dt1,
-        public ?\DateTime $dt2,
-
-        public \DateTimeImmutable $dti1,
-        public ?\DateTimeImmutable $dti2,
-
-        public TestSubEntity $entity1,
-        public ?TestSubEntity $entity2,
-
-        public TestCastableIntObject $castable_int1,
-        public ?TestCastableIntObject $castable_int2,
-
-        public TestCastableStringObject $castable_str1,
-        public ?TestCastableStringObject $castable_str2,
-
-        public \stdClass $obj3 = new \stdClass(),
-        public ?\stdClass $obj4 = new \stdClass(),
-        public ?\stdClass $obj5 = null,
-
-        public \DateTime $dt3 = new \DateTime('2000-01-01 00:00:00'),
-        public ?\DateTime $dt4 = new \DateTime(),
-        public ?\DateTime $dt5 = null,
-
-        public \DateTimeImmutable $dti3 = new \DateTimeImmutable('2000-01-01 00:00:00'),
-        public ?\DateTimeImmutable $dti4 = new \DateTimeImmutable(),
-        public ?\DateTimeImmutable $dti5 = null,
-
-        public TestSubEntity $entity3 = new TestSubEntity(),
-        public ?TestSubEntity $entity4 = new TestSubEntity(number: 456),
-        public ?TestSubEntity $entity5 = null,
-
-        public TestCastableIntObject $castable_int3 = new TestCastableIntObject(946684801), //2000-01-01 00:00:01,
-        public ?TestCastableIntObject $castable_int4 = new TestCastableIntObject(946684802), //2000-01-01 00:00:02,
-        public ?TestCastableIntObject $castable_int5 = null,
-
-        public TestCastableStringObject $castable_str3 = new TestCastableStringObject('Hello'),
-        public ?TestCastableStringObject $castable_str4 = new TestCastableStringObject('World'),
-        public ?TestCastableStringObject $castable_str5 = new TestCastableStringObject(null),
-    ) {}
-}
\ No newline at end of file
diff --git a/tests/TestStand/Entities/TestEntity.php b/tests/TestStand/Entities/TestEntity.php
deleted file mode 100644
index d39ad72..0000000
--- a/tests/TestStand/Entities/TestEntity.php
+++ /dev/null
@@ -1,30 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Composite\DB\Tests\TestStand\Entities;
-
-use Composite\DB\Attributes;
-use Composite\DB\Tests\TestStand\Entities\Castable\TestCastableIntObject;
-use Composite\DB\Tests\TestStand\Entities\Enums\TestBackedStringEnum;
-use Composite\DB\Tests\TestStand\Entities\Enums\TestSubEntity;
-use Composite\DB\Tests\TestStand\Entities\Enums\TestUnitEnum;
-use Composite\Entity\AbstractEntity;
-
-#[Attributes\Table(connection: 'sqlite', name: 'Test')]
-class TestEntity extends AbstractEntity
-{
-    public function __construct(
-        #[Attributes\PrimaryKey]
-        public string $str = 'foo',
-        public int $int = 999,
-        public float $float = 9.99,
-        public bool $bool = true,
-        public array $arr = [],
-        public \stdClass $object = new \stdClass(),
-        public \DateTime $date_time = new \DateTime(),
-        public \DateTimeImmutable $date_time_immutable = new \DateTimeImmutable(),
-        public TestBackedStringEnum $backed_enum = TestBackedStringEnum::Foo,
-        public TestUnitEnum $unit_enum = TestUnitEnum::Bar,
-        public TestSubEntity $entity = new TestSubEntity(),
-        public TestCastableIntObject $castable = new TestCastableIntObject(946684801) //2000-01-01 00:00:01
-    ) {}
-}
\ No newline at end of file
diff --git a/tests/TestStand/Entities/TestStrictEntity.php b/tests/TestStand/Entities/TestStrictEntity.php
new file mode 100644
index 0000000..8215aa2
--- /dev/null
+++ b/tests/TestStand/Entities/TestStrictEntity.php
@@ -0,0 +1,17 @@
+<?php declare(strict_types=1);
+
+namespace Composite\DB\Tests\TestStand\Entities;
+
+use Composite\DB\Attributes;
+use Composite\Entity\AbstractEntity;
+
+#[Attributes\Table(connection: 'sqlite', name: 'Strict')]
+class TestStrictEntity extends AbstractEntity
+{
+    #[Attributes\PrimaryKey(autoIncrement: true)]
+    public readonly int $id;
+    
+    public function __construct(
+        public \DateTimeImmutable $dti1,
+    ) {}
+}
\ No newline at end of file
diff --git a/tests/TestStand/Entities/TestUniqueEntity.php b/tests/TestStand/Entities/TestUniqueEntity.php
index 7b676bd..cce8a0b 100644
--- a/tests/TestStand/Entities/TestUniqueEntity.php
+++ b/tests/TestStand/Entities/TestUniqueEntity.php
@@ -5,14 +5,16 @@
 use Composite\DB\Attributes\{PrimaryKey};
 use Composite\DB\Attributes\Table;
 use Composite\Entity\AbstractEntity;
+use Ramsey\Uuid\UuidInterface;
 
 #[Table(connection: 'sqlite', name: 'TestUnique')]
 class TestUniqueEntity extends AbstractEntity
 {
     public function __construct(
         #[PrimaryKey]
-        public readonly string $id,
+        public readonly UuidInterface $id,
         public string $name,
+        public Enums\TestBackedEnum $status = Enums\TestBackedEnum::ACTIVE,
         public readonly \DateTimeImmutable $created_at = new \DateTimeImmutable(),
     ) {}
 }
\ No newline at end of file
diff --git a/tests/TestStand/Entities/TestUniqueSdEntity.php b/tests/TestStand/Entities/TestUniqueSdEntity.php
deleted file mode 100644
index 1a95b5f..0000000
--- a/tests/TestStand/Entities/TestUniqueSdEntity.php
+++ /dev/null
@@ -1,12 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Composite\DB\Tests\TestStand\Entities;
-
-use Composite\DB\Attributes\Table;
-use Composite\DB\Traits\SoftDelete;
-
-#[Table(connection: 'sqlite', name: 'TestUniqueSoftDelete')]
-class TestUniqueSdEntity extends TestUniqueEntity
-{
-    use SoftDelete;
-}
\ No newline at end of file
diff --git a/tests/TestStand/Entities/TestUpdatedAtEntity.php b/tests/TestStand/Entities/TestUpdatedAtEntity.php
new file mode 100644
index 0000000..c47f652
--- /dev/null
+++ b/tests/TestStand/Entities/TestUpdatedAtEntity.php
@@ -0,0 +1,21 @@
+<?php declare(strict_types=1);
+
+namespace Composite\DB\Tests\TestStand\Entities;
+
+use Composite\DB\Traits\UpdatedAt;
+use Composite\DB\Attributes\{PrimaryKey};
+use Composite\DB\Attributes\Table;
+use Composite\Entity\AbstractEntity;
+
+#[Table(connection: 'sqlite', name: 'TestUpdatedAt')]
+class TestUpdatedAtEntity extends AbstractEntity
+{
+    use UpdatedAt;
+    #[PrimaryKey(autoIncrement: true)]
+    public readonly string $id;
+
+    public function __construct(
+        public string $name,
+        public readonly \DateTimeImmutable $created_at = new \DateTimeImmutable(),
+    ) {}
+}
\ No newline at end of file
diff --git a/tests/TestStand/Interfaces/IAutoincrementTable.php b/tests/TestStand/Interfaces/IAutoincrementTable.php
index da6bf1a..e4afcc6 100644
--- a/tests/TestStand/Interfaces/IAutoincrementTable.php
+++ b/tests/TestStand/Interfaces/IAutoincrementTable.php
@@ -13,5 +13,6 @@ public function findOneByName(string $name): ?TestAutoincrementEntity;
      */
     public function findAllByName(string $name): array;
     public function countAllByName(string $name): int;
+    public function findRecent(int $limit, int $offset): array;
     public function truncate(): void;
 }
\ No newline at end of file
diff --git a/tests/TestStand/Interfaces/IUniqueTable.php b/tests/TestStand/Interfaces/IUniqueTable.php
index b0e09dd..197beaf 100644
--- a/tests/TestStand/Interfaces/IUniqueTable.php
+++ b/tests/TestStand/Interfaces/IUniqueTable.php
@@ -4,10 +4,11 @@
 
 use Composite\DB\Tests\TestStand\Entities\TestCompositeEntity;
 use Composite\DB\Tests\TestStand\Entities\TestUniqueEntity;
+use Ramsey\Uuid\UuidInterface;
 
 interface IUniqueTable
 {
-    public function findByPk(string $id): ?TestUniqueEntity;
+    public function findByPk(UuidInterface $id): ?TestUniqueEntity;
     /**
      * @return TestCompositeEntity[]
      */
diff --git a/tests/TestStand/Tables/TestAutoincrementCachedTable.php b/tests/TestStand/Tables/TestAutoincrementCachedTable.php
index 9b65e5c..1fba645 100644
--- a/tests/TestStand/Tables/TestAutoincrementCachedTable.php
+++ b/tests/TestStand/Tables/TestAutoincrementCachedTable.php
@@ -6,10 +6,17 @@
 use Composite\DB\TableConfig;
 use Composite\DB\Tests\TestStand\Entities\TestAutoincrementEntity;
 use Composite\DB\Tests\TestStand\Interfaces\IAutoincrementTable;
+use Composite\DB\Where;
 use Composite\Entity\AbstractEntity;
 
 class TestAutoincrementCachedTable extends AbstractCachedTable implements IAutoincrementTable
 {
+    public function __construct(\Psr\SimpleCache\CacheInterface $cache)
+    {
+        parent::__construct($cache);
+        (new TestAutoincrementTable)->init();
+    }
+
     protected function getConfig(): TableConfig
     {
         return TableConfig::fromEntitySchema(TestAutoincrementEntity::schema());
@@ -19,26 +26,34 @@ protected function getFlushCacheKeys(TestAutoincrementEntity|AbstractEntity $ent
     {
         $keys = [
             $this->getOneCacheKey(['name' => $entity->name]),
-            $this->getListCacheKey('name = :name', ['name' => $entity->name]),
-            $this->getCountCacheKey('name = :name', ['name' => $entity->name]),
+            $this->getListCacheKey(new Where('name = :name', ['name' => $entity->name])),
+            $this->getCountCacheKey(new Where('name = :name', ['name' => $entity->name])),
         ];
         $oldName = $entity->getOldValue('name');
         if (!$entity->isNew() && $oldName !== $entity->name) {
             $keys[] = $this->getOneCacheKey(['name' => $oldName]);
-            $keys[] = $this->getListCacheKey('name = :name', ['name' => $oldName]);
-            $keys[] = $this->getCountCacheKey('name = :name', ['name' => $oldName]);
+            $keys[] = $this->getListCacheKey(new Where('name = :name', ['name' => $oldName]));
+            $keys[] = $this->getCountCacheKey(new Where('name = :name', ['name' => $oldName]));
         }
         return $keys;
     }
 
     public function findByPk(int $id): ?TestAutoincrementEntity
     {
-        return $this->createEntity($this->findByPkCachedInternal($id));
+        return $this->_findByPkCached($id);
     }
 
     public function findOneByName(string $name): ?TestAutoincrementEntity
     {
-        return $this->createEntity($this->findOneCachedInternal(['name' => $name]));
+        return $this->_findOneCached(['name' => $name]);
+    }
+
+    public function delete(TestAutoincrementEntity|AbstractEntity $entity): void
+    {
+        if ($entity->name === 'Exception') {
+            throw new \Exception('Test Exception');
+        }
+        parent::delete($entity);
     }
 
     /**
@@ -46,20 +61,34 @@ public function findOneByName(string $name): ?TestAutoincrementEntity
      */
     public function findAllByName(string $name): array
     {
-        return $this->createEntities($this->findAllCachedInternal(
-            'name = :name',
-            ['name' => $name],
-        ));
+        return $this->_findAllCached(new Where('name = :name', ['name' => $name]));
     }
 
-    public function countAllByName(string $name): int
+    /**
+     * @return TestAutoincrementEntity[]
+     */
+    public function findRecent(int $limit, int $offset): array
     {
-        return $this->countAllCachedInternal(
-            'name = :name',
-            ['name' => $name],
+        return $this->_findAll(
+            orderBy: ['id' => 'DESC'],
+            limit: $limit,
+            offset: $offset,
         );
     }
 
+    public function countAllByName(string $name): int
+    {
+        return $this->_countByAllCached(new Where('name = :name', ['name' => $name]));
+    }
+
+    /**
+     * @return TestAutoincrementEntity[]
+     */
+    public function findMulti(array $ids, ?string $keyColumnName = null): array
+    {
+        return $this->_findMultiCached(ids: $ids, keyColumnName: $keyColumnName);
+    }
+
     public function truncate(): void
     {
         $this->getConnection()->executeStatement("DELETE FROM {$this->getTableName()} WHERE 1");
diff --git a/tests/TestStand/Tables/TestAutoincrementSdCachedTable.php b/tests/TestStand/Tables/TestAutoincrementSdCachedTable.php
index 007a187..07ba91d 100644
--- a/tests/TestStand/Tables/TestAutoincrementSdCachedTable.php
+++ b/tests/TestStand/Tables/TestAutoincrementSdCachedTable.php
@@ -6,10 +6,17 @@
 use Composite\DB\TableConfig;
 use Composite\DB\Tests\TestStand\Entities\TestAutoincrementSdEntity;
 use Composite\DB\Tests\TestStand\Interfaces\IAutoincrementTable;
+use Composite\DB\Where;
 use Composite\Entity\AbstractEntity;
 
 class TestAutoincrementSdCachedTable extends AbstractCachedTable implements IAutoincrementTable
 {
+    public function __construct(\Psr\SimpleCache\CacheInterface $cache)
+    {
+        parent::__construct($cache);
+        (new TestAutoincrementSdTable)->init();
+    }
+
     protected function getConfig(): TableConfig
     {
         return TableConfig::fromEntitySchema(TestAutoincrementSdEntity::schema());
@@ -19,26 +26,34 @@ protected function getFlushCacheKeys(TestAutoincrementSdEntity|AbstractEntity $e
     {
         $keys = [
             $this->getOneCacheKey(['name' => $entity->name]),
-            $this->getListCacheKey('name = :name', ['name' => $entity->name]),
-            $this->getCountCacheKey('name = :name', ['name' => $entity->name]),
+            $this->getListCacheKey(new Where('name = :name', ['name' => $entity->name])),
+            $this->getCountCacheKey(new Where('name = :name', ['name' => $entity->name])),
         ];
         $oldName = $entity->getOldValue('name');
         if ($oldName !== null && $oldName !== $entity->name) {
             $keys[] = $this->getOneCacheKey(['name' => $oldName]);
-            $keys[] = $this->getListCacheKey('name = :name', ['name' => $oldName]);
-            $keys[] = $this->getCountCacheKey('name = :name', ['name' => $oldName]);
+            $keys[] = $this->getListCacheKey(new Where('name = :name', ['name' => $oldName]));
+            $keys[] = $this->getCountCacheKey(new Where('name = :name', ['name' => $oldName]));
         }
         return $keys;
     }
 
     public function findByPk(int $id): ?TestAutoincrementSdEntity
     {
-        return $this->createEntity($this->findByPkInternal($id));
+        return $this->_findByPk($id);
     }
 
     public function findOneByName(string $name): ?TestAutoincrementSdEntity
     {
-        return $this->createEntity($this->findOneCachedInternal(['name' => $name]));
+        return $this->_findOneCached(['name' => $name]);
+    }
+
+    public function delete(TestAutoincrementSdEntity|AbstractEntity $entity): void
+    {
+        if ($entity->name === 'Exception') {
+            throw new \Exception('Test Exception');
+        }
+        parent::delete($entity);
     }
 
     /**
@@ -46,18 +61,27 @@ public function findOneByName(string $name): ?TestAutoincrementSdEntity
      */
     public function findAllByName(string $name): array
     {
-        return $this->createEntities($this->findAllCachedInternal(
-            'name = :name',
-            ['name' => $name],
-        ));
+        return $this->_findAllCached(new Where('name = :name', ['name' => $name, 'deleted_at' => null]));
+    }
+
+    /**
+     * @return TestAutoincrementSdEntity[]
+     */
+    public function findRecent(int $limit, int $offset): array
+    {
+        return $this->_findAll(
+            orderBy: 'id DESC',
+            limit: $limit,
+            offset: $offset,
+        );
     }
 
     public function countAllByName(string $name): int
     {
-        return $this->countAllCachedInternal(
+        return $this->_countByAllCached(new Where(
             'name = :name',
             ['name' => $name],
-        );
+        ));
     }
 
     public function truncate(): void
diff --git a/tests/TestStand/Tables/TestAutoincrementSdTable.php b/tests/TestStand/Tables/TestAutoincrementSdTable.php
index 8f68332..f878db9 100644
--- a/tests/TestStand/Tables/TestAutoincrementSdTable.php
+++ b/tests/TestStand/Tables/TestAutoincrementSdTable.php
@@ -4,9 +4,16 @@
 
 use Composite\DB\TableConfig;
 use Composite\DB\Tests\TestStand\Entities\TestAutoincrementSdEntity;
+use Composite\DB\Where;
 
 class TestAutoincrementSdTable extends TestAutoincrementTable
 {
+    public function __construct()
+    {
+        parent::__construct();
+        $this->init();
+    }
+
     protected function getConfig(): TableConfig
     {
         return TableConfig::fromEntitySchema(TestAutoincrementSdEntity::schema());
@@ -14,12 +21,12 @@ protected function getConfig(): TableConfig
 
     public function findByPk(int $id): ?TestAutoincrementSdEntity
     {
-        return $this->createEntity($this->findByPkInternal($id));
+        return $this->_findByPk($id);
     }
 
     public function findOneByName(string $name): ?TestAutoincrementSdEntity
     {
-        return $this->createEntity($this->findOneInternal(['name' => $name]));
+        return $this->_findOne(['name' => $name, 'deleted_at' => null]);
     }
 
     /**
@@ -27,10 +34,20 @@ public function findOneByName(string $name): ?TestAutoincrementSdEntity
      */
     public function findAllByName(string $name): array
     {
-        return $this->createEntities($this->findAllInternal(
-            'name = :name',
-            ['name' => $name]
-        ));
+        return $this->_findAll(new Where('name = :name', ['name' => $name]));
+    }
+
+    /**
+     * @return TestAutoincrementSdEntity[]
+     */
+    public function findRecent(int $limit, int $offset): array
+    {
+        return $this->_findAll(
+            where: ['deleted_at' => null],
+            orderBy: 'id DESC',
+            limit: $limit,
+            offset: $offset,
+        );
     }
 
     public function init(): bool
@@ -41,6 +58,7 @@ public function init(): bool
             (
                 `id` INTEGER NOT NULL CONSTRAINT TestAutoincrementSd_pk PRIMARY KEY AUTOINCREMENT,
                 `name` VARCHAR(255) NOT NULL,
+                `is_test` INTEGER NOT NULL DEFAULT 0,
                 `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
                 `deleted_at` TIMESTAMP NULL DEFAULT NULL
             );
diff --git a/tests/TestStand/Tables/TestAutoincrementTable.php b/tests/TestStand/Tables/TestAutoincrementTable.php
index 900e576..bef47de 100644
--- a/tests/TestStand/Tables/TestAutoincrementTable.php
+++ b/tests/TestStand/Tables/TestAutoincrementTable.php
@@ -6,9 +6,17 @@
 use Composite\DB\TableConfig;
 use Composite\DB\Tests\TestStand\Entities\TestAutoincrementEntity;
 use Composite\DB\Tests\TestStand\Interfaces\IAutoincrementTable;
+use Composite\DB\Where;
+use Composite\Entity\AbstractEntity;
 
 class TestAutoincrementTable extends AbstractTable implements IAutoincrementTable
 {
+    public function __construct()
+    {
+        parent::__construct();
+        $this->init();
+    }
+
     protected function getConfig(): TableConfig
     {
         return TableConfig::fromEntitySchema(TestAutoincrementEntity::schema());
@@ -16,12 +24,20 @@ protected function getConfig(): TableConfig
 
     public function findByPk(int $id): ?TestAutoincrementEntity
     {
-        return $this->createEntity($this->findByPkInternal($id));
+        return $this->_findByPk($id);
     }
 
     public function findOneByName(string $name): ?TestAutoincrementEntity
     {
-        return $this->createEntity($this->findOneInternal(['name' => $name]));
+        return $this->_findOne(['name' => $name]);
+    }
+
+    public function delete(AbstractEntity|TestAutoincrementEntity $entity): void
+    {
+        if ($entity->name === 'Exception') {
+            throw new \Exception('Test Exception');
+        }
+        parent::delete($entity);
     }
 
     /**
@@ -29,20 +45,39 @@ public function findOneByName(string $name): ?TestAutoincrementEntity
      */
     public function findAllByName(string $name): array
     {
-        return $this->createEntities($this->findAllInternal(
-            'name = :name',
-            ['name' => $name],
-        ));
+        return $this->_findAll(
+            where: new Where('name = :name', ['name' => $name]),
+            orderBy: 'id',
+        );
     }
 
-    public function countAllByName(string $name): int
+    /**
+     * @return TestAutoincrementEntity[]
+     */
+    public function findRecent(int $limit, int $offset): array
     {
-        return $this->countAllInternal(
-            'name = :name',
-            ['name' => $name]
+        return $this->_findAll(
+            orderBy: ['id' => 'DESC'],
+            limit: $limit,
+            offset: $offset,
         );
     }
 
+    public function countAllByName(string $name): int
+    {
+        return $this->_countAll(new Where('name = :name', ['name' => $name]));
+    }
+
+    /**
+     * @param int[] $ids
+     * @return TestAutoincrementEntity[]
+     * @throws \Composite\DB\Exceptions\DbException
+     */
+    public function findMulti(array $ids): array
+    {
+        return $this->_findMulti($ids, 'id');
+    }
+
     public function init(): bool
     {
         $this->getConnection()->executeStatement(
@@ -51,6 +86,7 @@ public function init(): bool
             (
                 `id` INTEGER NOT NULL CONSTRAINT TestAutoincrement_pk PRIMARY KEY AUTOINCREMENT,
                 `name` VARCHAR(255) NOT NULL,
+                `is_test` INTEGER NOT NULL DEFAULT 0,
                 `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
             );
             "
diff --git a/tests/TestStand/Tables/TestCompositeCachedTable.php b/tests/TestStand/Tables/TestCompositeCachedTable.php
index a2519da..6b5996a 100644
--- a/tests/TestStand/Tables/TestCompositeCachedTable.php
+++ b/tests/TestStand/Tables/TestCompositeCachedTable.php
@@ -9,6 +9,12 @@
 
 class TestCompositeCachedTable extends \Composite\DB\AbstractCachedTable implements ICompositeTable
 {
+    public function __construct(\Psr\SimpleCache\CacheInterface $cache)
+    {
+        parent::__construct($cache);
+        (new TestCompositeTable())->init();
+    }
+
     protected function getConfig(): TableConfig
     {
         return TableConfig::fromEntitySchema(TestCompositeEntity::schema());
@@ -17,17 +23,17 @@ protected function getConfig(): TableConfig
     protected function getFlushCacheKeys(TestCompositeEntity|AbstractEntity $entity): array
     {
         return [
-            $this->getListCacheKey('user_id = :user_id', ['user_id' => $entity->user_id]),
-            $this->getCountCacheKey('user_id = :user_id', ['user_id' => $entity->user_id]),
+            $this->getListCacheKey(['user_id' => $entity->user_id]),
+            $this->getCountCacheKey(['user_id' => $entity->user_id]),
         ];
     }
 
     public function findOne(int $user_id, int $post_id): ?TestCompositeEntity
     {
-        return $this->createEntity($this->findOneCachedInternal([
+        return $this->_findOneCached([
             'user_id' => $user_id,
             'post_id' => $post_id,
-        ]));
+        ]);
     }
 
     /**
@@ -35,21 +41,12 @@ public function findOne(int $user_id, int $post_id): ?TestCompositeEntity
      */
     public function findAllByUser(int $userId): array
     {
-        return array_map(
-            fn (array $data) => TestCompositeEntity::fromArray($data),
-            $this->findAllCachedInternal(
-                'user_id = :user_id',
-                ['user_id' => $userId],
-            )
-        );
+        return $this->_findAllCached(['user_id' => $userId]);
     }
 
     public function countAllByUser(int $userId): int
     {
-        return $this->countAllCachedInternal(
-            'user_id = :user_id',
-            ['user_id' => $userId],
-        );
+        return $this->_countByAllCached(['user_id' => $userId]);
     }
 
     public function truncate(): void
diff --git a/tests/TestStand/Tables/TestCompositeSdCachedTable.php b/tests/TestStand/Tables/TestCompositeSdCachedTable.php
deleted file mode 100644
index 97f8512..0000000
--- a/tests/TestStand/Tables/TestCompositeSdCachedTable.php
+++ /dev/null
@@ -1,59 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Composite\DB\Tests\TestStand\Tables;
-
-use Composite\DB\TableConfig;
-use Composite\DB\Tests\TestStand\Entities\TestCompositeSdEntity;
-use Composite\DB\Tests\TestStand\Interfaces\ICompositeTable;
-use Composite\Entity\AbstractEntity;
-
-class TestCompositeSdCachedTable extends \Composite\DB\AbstractCachedTable implements ICompositeTable
-{
-    protected function getConfig(): TableConfig
-    {
-        return TableConfig::fromEntitySchema(TestCompositeSdEntity::schema());
-    }
-
-    protected function getFlushCacheKeys(TestCompositeSdEntity|AbstractEntity $entity): array
-    {
-        return [
-            $this->getListCacheKey('user_id = :user_id', ['user_id' => $entity->user_id]),
-            $this->getCountCacheKey('user_id = :user_id', ['user_id' => $entity->user_id]),
-        ];
-    }
-
-    public function findOne(int $user_id, int $post_id): ?TestCompositeSdEntity
-    {
-        return $this->createEntity($this->findOneCachedInternal([
-            'user_id' => $user_id,
-            'post_id' => $post_id,
-        ]));
-    }
-
-    /**
-     * @return TestCompositeSdEntity[]
-     */
-    public function findAllByUser(int $userId): array
-    {
-        return array_map(
-            fn (array $data) => TestCompositeSdEntity::fromArray($data),
-            $this->findAllCachedInternal(
-                'user_id = :user_id',
-                ['user_id' => $userId],
-            )
-        );
-    }
-
-    public function countAllByUser(int $userId): int
-    {
-        return $this->countAllCachedInternal(
-            'user_id = :user_id',
-            ['user_id' => $userId],
-        );
-    }
-
-    public function truncate(): void
-    {
-        $this->getConnection()->executeStatement("DELETE FROM {$this->getTableName()} WHERE 1");
-    }
-}
\ No newline at end of file
diff --git a/tests/TestStand/Tables/TestCompositeSdTable.php b/tests/TestStand/Tables/TestCompositeSdTable.php
deleted file mode 100644
index a35bd1e..0000000
--- a/tests/TestStand/Tables/TestCompositeSdTable.php
+++ /dev/null
@@ -1,59 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Composite\DB\Tests\TestStand\Tables;
-
-use Composite\DB\TableConfig;
-use Composite\DB\Tests\TestStand\Entities\TestCompositeSdEntity;
-
-class TestCompositeSdTable extends TestCompositeTable
-{
-    protected function getConfig(): TableConfig
-    {
-        return TableConfig::fromEntitySchema(TestCompositeSdEntity::schema());
-    }
-
-    public function findOne(int $user_id, int $post_id): ?TestCompositeSdEntity
-    {
-        return $this->createEntity($this->findOneInternal([
-            'user_id' => $user_id,
-            'post_id' => $post_id,
-        ]));
-    }
-
-    /**
-     * @return TestCompositeSdEntity[]
-     */
-    public function findAllByUser(int $userId): array
-    {
-        return $this->createEntities($this->findAllInternal(
-            'user_id = :user_id',
-            ['user_id' => $userId],
-        ));
-    }
-
-    public function countAllByUser(int $userId): int
-    {
-        return $this->countAllInternal(
-            'user_id = :user_id',
-            ['user_id' => $userId],
-        );
-    }
-
-    public function init(): bool
-    {
-        $this->getConnection()->executeStatement(
-            "
-            CREATE TABLE IF NOT EXISTS {$this->getTableName()}
-            (
-                `user_id` integer not null,
-                `post_id` integer not null,
-                `message` VARCHAR(255) DEFAULT '' NOT NULL,
-                `created_at` TIMESTAMP NOT NULL,
-                `deleted_at` TIMESTAMP NULL DEFAULT NULL,
-                CONSTRAINT TestCompositeSd PRIMARY KEY (`user_id`, `post_id`, `deleted_at`)
-            );
-            "
-        );
-        return true;
-    }
-}
\ No newline at end of file
diff --git a/tests/TestStand/Tables/TestCompositeTable.php b/tests/TestStand/Tables/TestCompositeTable.php
index 931ff55..8a1e9da 100644
--- a/tests/TestStand/Tables/TestCompositeTable.php
+++ b/tests/TestStand/Tables/TestCompositeTable.php
@@ -3,6 +3,7 @@
 namespace Composite\DB\Tests\TestStand\Tables;
 
 use Composite\DB\TableConfig;
+use Composite\DB\Tests\TestStand\Entities\Enums\TestUnitEnum;
 use Composite\DB\Tests\TestStand\Entities\TestCompositeEntity;
 use Composite\DB\Tests\TestStand\Interfaces\ICompositeTable;
 use Composite\Entity\AbstractEntity;
@@ -14,7 +15,7 @@ protected function getConfig(): TableConfig
         return TableConfig::fromEntitySchema(TestCompositeEntity::schema());
     }
 
-    public function save(AbstractEntity|TestCompositeEntity &$entity): void
+    public function save(AbstractEntity|TestCompositeEntity $entity): void
     {
         if ($entity->message === 'Exception') {
             throw new \Exception('Test Exception');
@@ -22,9 +23,17 @@ public function save(AbstractEntity|TestCompositeEntity &$entity): void
         parent::save($entity);
     }
 
+    public function delete(AbstractEntity|TestCompositeEntity $entity): void
+    {
+        if ($entity->message === 'Exception') {
+            throw new \Exception('Test Exception');
+        }
+        parent::delete($entity);
+    }
+
     public function findOne(int $user_id, int $post_id): ?TestCompositeEntity
     {
-        return $this->createEntity($this->findOneInternal(['user_id' => $user_id, 'post_id' => $post_id]));
+        return $this->_findOne(['user_id' => $user_id, 'post_id' => $post_id]);
     }
 
     /**
@@ -32,29 +41,22 @@ public function findOne(int $user_id, int $post_id): ?TestCompositeEntity
      */
     public function findAllByUser(int $userId): array
     {
-        return $this->createEntities($this->findAllInternal(
-            'user_id = :user_id',
-            ['user_id' => $userId],
-        ));
+        return $this->_findAll(['user_id' => $userId, 'status' => TestUnitEnum::ACTIVE]);
     }
 
     public function countAllByUser(int $userId): int
     {
-        return $this->countAllInternal(
-            'user_id = :user_id',
-            ['user_id' => $userId],
-        );
+        return $this->_countAll(['user_id' => $userId]);
     }
 
-    public function test(): array
+    /**
+     * @param array $ids
+     * @return TestCompositeEntity[]
+     * @throws \Composite\DB\Exceptions\DbException
+     */
+    public function findMulti(array $ids): array
     {
-        $rows = $this
-            ->select()
-            ->where()
-            ->orWhere()
-            ->orderBy()
-            ->fetchAllAssociative();
-        return $this->createEntities($rows);
+        return $this->_findMulti($ids, 'post_id');
     }
 
     public function init(): bool
@@ -66,6 +68,7 @@ public function init(): bool
                 `user_id` integer not null,
                 `post_id` integer not null,
                 `message` VARCHAR(255) DEFAULT '' NOT NULL,
+                `status` VARCHAR(16) DEFAULT 'ACTIVE' NOT NULL,
                 `created_at` TIMESTAMP NOT NULL,
                 CONSTRAINT TestComposite PRIMARY KEY (`user_id`, `post_id`)
             );
diff --git a/tests/TestStand/Tables/TestMySQLTable.php b/tests/TestStand/Tables/TestMySQLTable.php
new file mode 100644
index 0000000..e918355
--- /dev/null
+++ b/tests/TestStand/Tables/TestMySQLTable.php
@@ -0,0 +1,25 @@
+<?php declare(strict_types=1);
+
+namespace Composite\DB\Tests\TestStand\Tables;
+
+use Composite\DB\AbstractTable;
+use Composite\DB\TableConfig;
+use Composite\DB\Tests\TestStand\Entities\TestAutoincrementEntity;
+
+class TestMySQLTable extends AbstractTable
+{
+    protected function getConfig(): TableConfig
+    {
+        return new TableConfig(
+            connectionName: 'mysql',
+            tableName: 'Fake',
+            entityClass: TestAutoincrementEntity::class,
+            primaryKeys: [],
+        );
+    }
+
+    public function escapeIdentifierPub(string $key): string
+    {
+        return $this->escapeIdentifier($key);
+    }
+}
\ No newline at end of file
diff --git a/tests/TestStand/Tables/TestOptimisticLockTable.php b/tests/TestStand/Tables/TestOptimisticLockTable.php
index beb6123..1ede3bc 100644
--- a/tests/TestStand/Tables/TestOptimisticLockTable.php
+++ b/tests/TestStand/Tables/TestOptimisticLockTable.php
@@ -8,6 +8,12 @@
 
 class TestOptimisticLockTable extends AbstractTable
 {
+    public function __construct()
+    {
+        parent::__construct();
+        $this->init();
+    }
+
     protected function getConfig(): TableConfig
     {
         return TableConfig::fromEntitySchema(TestOptimisticLockEntity::schema());
@@ -15,7 +21,7 @@ protected function getConfig(): TableConfig
 
     public function findByPk(int $id): ?TestOptimisticLockEntity
     {
-        return $this->createEntity($this->findByPkInternal($id));
+        return $this->_findByPk($id);
     }
 
     public function init(): bool
@@ -26,7 +32,7 @@ public function init(): bool
             (
                 `id` INTEGER NOT NULL CONSTRAINT TestAutoincrement_pk PRIMARY KEY AUTOINCREMENT,
                 `name` VARCHAR(255) NOT NULL,
-                `version` INTEGER NOT NULL DEFAULT 1,
+                `lock_version` INTEGER NOT NULL DEFAULT 1,
                 `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
             );
             "
diff --git a/tests/TestStand/Tables/TestPostgresTable.php b/tests/TestStand/Tables/TestPostgresTable.php
new file mode 100644
index 0000000..94ab839
--- /dev/null
+++ b/tests/TestStand/Tables/TestPostgresTable.php
@@ -0,0 +1,25 @@
+<?php declare(strict_types=1);
+
+namespace Composite\DB\Tests\TestStand\Tables;
+
+use Composite\DB\AbstractTable;
+use Composite\DB\TableConfig;
+use Composite\DB\Tests\TestStand\Entities\TestAutoincrementEntity;
+
+class TestPostgresTable extends AbstractTable
+{
+    protected function getConfig(): TableConfig
+    {
+        return new TableConfig(
+            connectionName: 'postgres',
+            tableName: 'Fake',
+            entityClass: TestAutoincrementEntity::class,
+            primaryKeys: [],
+        );
+    }
+
+    public function escapeIdentifierPub(string $key): string
+    {
+        return $this->escapeIdentifier($key);
+    }
+}
\ No newline at end of file
diff --git a/tests/TestStand/Tables/TestStrictTable.php b/tests/TestStand/Tables/TestStrictTable.php
new file mode 100644
index 0000000..81a2c4b
--- /dev/null
+++ b/tests/TestStand/Tables/TestStrictTable.php
@@ -0,0 +1,28 @@
+<?php declare(strict_types=1);
+
+namespace Composite\DB\Tests\TestStand\Tables;
+
+use Composite\DB\AbstractTable;
+use Composite\DB\TableConfig;
+use Composite\DB\Tests\TestStand\Entities\TestStrictEntity;
+
+class TestStrictTable extends AbstractTable
+{
+    protected function getConfig(): TableConfig
+    {
+        return TableConfig::fromEntitySchema(TestStrictEntity::schema());
+    }
+
+    public function buildEntity(array $data): ?TestStrictEntity
+    {
+        return $this->createEntity($data);
+    }
+
+    /**
+     * @return TestStrictEntity[]
+     */
+    public function buildEntities(mixed $data): array
+    {
+        return $this->createEntities($data);
+    }
+}
\ No newline at end of file
diff --git a/tests/TestStand/Tables/TestUniqueCachedTable.php b/tests/TestStand/Tables/TestUniqueCachedTable.php
index f95d102..8fe714f 100644
--- a/tests/TestStand/Tables/TestUniqueCachedTable.php
+++ b/tests/TestStand/Tables/TestUniqueCachedTable.php
@@ -6,10 +6,18 @@
 use Composite\DB\TableConfig;
 use Composite\DB\Tests\TestStand\Entities\TestUniqueEntity;
 use Composite\DB\Tests\TestStand\Interfaces\IUniqueTable;
+use Composite\DB\Where;
 use Composite\Entity\AbstractEntity;
+use Ramsey\Uuid\UuidInterface;
 
 class TestUniqueCachedTable extends AbstractCachedTable implements IUniqueTable
 {
+    public function __construct(\Psr\SimpleCache\CacheInterface $cache)
+    {
+        parent::__construct($cache);
+        (new TestUniqueTable())->init();
+    }
+
     protected function getConfig(): TableConfig
     {
         return TableConfig::fromEntitySchema(TestUniqueEntity::schema());
@@ -18,14 +26,14 @@ protected function getConfig(): TableConfig
     protected function getFlushCacheKeys(TestUniqueEntity|AbstractEntity $entity): array
     {
         return [
-            $this->getListCacheKey('name = :name', ['name' => $entity->name]),
-            $this->getCountCacheKey('name = :name', ['name' => $entity->name]),
+            $this->getListCacheKey(new Where('name = :name', ['name' => $entity->name])),
+            $this->getCountCacheKey(new Where('name = :name', ['name' => $entity->name])),
         ];
     }
 
-    public function findByPk(string $id): ?TestUniqueEntity
+    public function findByPk(UuidInterface $id): ?TestUniqueEntity
     {
-        return $this->createEntity($this->findByPkInternal($id));
+        return $this->_findByPk($id);
     }
 
     /**
@@ -33,18 +41,12 @@ public function findByPk(string $id): ?TestUniqueEntity
      */
     public function findAllByName(string $name): array
     {
-        return $this->createEntities($this->findAllCachedInternal(
-            'name = :name',
-            ['name' => $name],
-        ));
+        return $this->_findAllCached(new Where('name = :name', ['name' => $name]));
     }
 
     public function countAllByName(string $name): int
     {
-        return $this->countAllCachedInternal(
-            'name = :name',
-            ['name' => $name],
-        );
+        return $this->_countByAllCached(new Where('name = :name', ['name' => $name]));
     }
 
     public function truncate(): void
diff --git a/tests/TestStand/Tables/TestUniqueSdCachedTable.php b/tests/TestStand/Tables/TestUniqueSdCachedTable.php
deleted file mode 100644
index 278cb37..0000000
--- a/tests/TestStand/Tables/TestUniqueSdCachedTable.php
+++ /dev/null
@@ -1,54 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Composite\DB\Tests\TestStand\Tables;
-
-use Composite\DB\AbstractCachedTable;
-use Composite\DB\TableConfig;
-use Composite\DB\Tests\TestStand\Entities\TestUniqueSdEntity;
-use Composite\DB\Tests\TestStand\Interfaces\IUniqueTable;
-use Composite\Entity\AbstractEntity;
-
-class TestUniqueSdCachedTable extends AbstractCachedTable implements IUniqueTable
-{
-    protected function getConfig(): TableConfig
-    {
-        return TableConfig::fromEntitySchema(TestUniqueSdEntity::schema());
-    }
-
-    protected function getFlushCacheKeys(TestUniqueSdEntity|AbstractEntity $entity): array
-    {
-        return [
-            $this->getListCacheKey('name = :name', ['name' => $entity->name]),
-            $this->getCountCacheKey('name = :name', ['name' => $entity->name]),
-        ];
-    }
-
-    public function findByPk(string $id): ?TestUniqueSdEntity
-    {
-        return $this->createEntity($this->findByPkInternal($id));
-    }
-
-    /**
-     * @return TestUniqueSdEntity[]
-     */
-    public function findAllByName(string $name): array
-    {
-        return $this->createEntities($this->findAllCachedInternal(
-            'name = :name',
-            ['name' => $name],
-        ));
-    }
-
-    public function countAllByName(string $name): int
-    {
-        return $this->countAllCachedInternal(
-            'name = :name',
-            ['name' => $name],
-        );
-    }
-
-    public function truncate(): void
-    {
-        $this->getConnection()->executeStatement("DELETE FROM {$this->getTableName()} WHERE 1");
-    }
-}
\ No newline at end of file
diff --git a/tests/TestStand/Tables/TestUniqueSdTable.php b/tests/TestStand/Tables/TestUniqueSdTable.php
deleted file mode 100644
index 6bed15f..0000000
--- a/tests/TestStand/Tables/TestUniqueSdTable.php
+++ /dev/null
@@ -1,47 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Composite\DB\Tests\TestStand\Tables;
-
-use Composite\DB\TableConfig;
-use Composite\DB\Tests\TestStand\Entities\TestUniqueSdEntity;
-
-class TestUniqueSdTable extends TestUniqueTable
-{
-    protected function getConfig(): TableConfig
-    {
-        return TableConfig::fromEntitySchema(TestUniqueSdEntity::schema());
-    }
-
-    public function findByPk(string $id): ?TestUniqueSdEntity
-    {
-        return $this->createEntity($this->findByPkInternal($id));
-    }
-
-    /**
-     * @return TestUniqueSdEntity[]
-     */
-    public function findAllByName(string $name): array
-    {
-        return $this->createEntities($this->findAllInternal(
-            'name = :name',
-            ['name' => $name],
-        ));
-    }
-
-    public function init(): bool
-    {
-        $this->getConnection()->executeStatement(
-            "
-            CREATE TABLE IF NOT EXISTS {$this->getTableName()}
-            (
-                `id` VARCHAR(255) NOT NULL,
-                `name` VARCHAR(255) NOT NULL,
-                `created_at` TIMESTAMP NOT NULL,
-                `deleted_at` TIMESTAMP NULL DEFAULT NULL,
-                CONSTRAINT TestUniqueSd PRIMARY KEY (`id`, `deleted_at`)
-            );
-            "
-        );
-        return true;
-    }
-}
\ No newline at end of file
diff --git a/tests/TestStand/Tables/TestUniqueTable.php b/tests/TestStand/Tables/TestUniqueTable.php
index 317985c..9387104 100644
--- a/tests/TestStand/Tables/TestUniqueTable.php
+++ b/tests/TestStand/Tables/TestUniqueTable.php
@@ -4,19 +4,37 @@
 
 use Composite\DB\AbstractTable;
 use Composite\DB\TableConfig;
+use Composite\DB\Tests\TestStand\Entities\Enums\TestBackedEnum;
 use Composite\DB\Tests\TestStand\Entities\TestUniqueEntity;
 use Composite\DB\Tests\TestStand\Interfaces\IUniqueTable;
+use Composite\DB\Where;
+use Composite\Entity\AbstractEntity;
+use Ramsey\Uuid\UuidInterface;
 
 class TestUniqueTable extends AbstractTable implements IUniqueTable
 {
+    public function __construct()
+    {
+        parent::__construct();
+        $this->init();
+    }
+
+    public function save(AbstractEntity|TestUniqueEntity $entity): void
+    {
+        if ($entity->name === 'Exception') {
+            throw new \Exception('Test Exception');
+        }
+        parent::save($entity);
+    }
+
     protected function getConfig(): TableConfig
     {
         return TableConfig::fromEntitySchema(TestUniqueEntity::schema());
     }
 
-    public function findByPk(string $id): ?TestUniqueEntity
+    public function findByPk(UuidInterface $id): ?TestUniqueEntity
     {
-        return $this->createEntity($this->findByPkInternal($id));
+        return $this->_findByPk($id);
     }
 
     /**
@@ -24,18 +42,12 @@ public function findByPk(string $id): ?TestUniqueEntity
      */
     public function findAllByName(string $name): array
     {
-        return $this->createEntities($this->findAllInternal(
-            'name = :name',
-            ['name' => $name],
-        ));
+        return $this->_findAll(['name' => $name, 'status' => TestBackedEnum::ACTIVE]);
     }
 
     public function countAllByName(string $name): int
     {
-        return $this->countAllInternal(
-            'name = :name',
-            ['name' => $name],
-        );
+        return $this->_countAll(new Where('name = :name', ['name' => $name]));
     }
 
     public function init(): bool
@@ -46,6 +58,7 @@ public function init(): bool
             (
                 `id` VARCHAR(255) NOT NULL CONSTRAINT TestUnique_pk PRIMARY KEY,
                 `name` VARCHAR(255) NOT NULL,
+                `status` VARCHAR(16) DEFAULT 'Active' NOT NULL,
                 `created_at` TIMESTAMP NOT NULL
             );
             "
diff --git a/tests/TestStand/Tables/TestUpdateAtTable.php b/tests/TestStand/Tables/TestUpdateAtTable.php
new file mode 100644
index 0000000..ea114bd
--- /dev/null
+++ b/tests/TestStand/Tables/TestUpdateAtTable.php
@@ -0,0 +1,47 @@
+<?php declare(strict_types=1);
+
+namespace Composite\DB\Tests\TestStand\Tables;
+
+use Composite\DB\AbstractTable;
+use Composite\DB\TableConfig;
+use Composite\DB\Tests\TestStand\Entities\TestUpdatedAtEntity;
+
+class TestUpdateAtTable extends AbstractTable
+{
+    public function __construct()
+    {
+        parent::__construct();
+        $this->init();
+    }
+
+    protected function getConfig(): TableConfig
+    {
+        return TableConfig::fromEntitySchema(TestUpdatedAtEntity::schema());
+    }
+
+    public function findByPk(string $id): ?TestUpdatedAtEntity
+    {
+        return $this->_findByPk($id);
+    }
+
+    public function init(): bool
+    {
+        $this->getConnection()->executeStatement(
+            "
+            CREATE TABLE IF NOT EXISTS {$this->getTableName()}
+            (
+                `id` INTEGER NOT NULL CONSTRAINT TestAutoincrement_pk PRIMARY KEY AUTOINCREMENT,
+                `name` VARCHAR(255) NOT NULL,
+                `created_at` TIMESTAMP NOT NULL,
+                `updated_at` TIMESTAMP NOT NULL
+            );
+            "
+        );
+        return true;
+    }
+
+    public function truncate(): void
+    {
+        $this->getConnection()->executeStatement("DELETE FROM {$this->getTableName()} WHERE 1");
+    }
+}
\ No newline at end of file
diff --git a/tests/TestStand/configs/empty_config.php b/tests/TestStand/configs/empty_config.php
new file mode 100644
index 0000000..e69de29
diff --git a/tests/TestStand/configs/wrong_content_config.php b/tests/TestStand/configs/wrong_content_config.php
new file mode 100644
index 0000000..27cdc70
--- /dev/null
+++ b/tests/TestStand/configs/wrong_content_config.php
@@ -0,0 +1,2 @@
+<?php
+return 'asd';
\ No newline at end of file
diff --git a/tests/TestStand/configs/wrong_doctrine_config.php b/tests/TestStand/configs/wrong_doctrine_config.php
new file mode 100644
index 0000000..a7a4b50
--- /dev/null
+++ b/tests/TestStand/configs/wrong_doctrine_config.php
@@ -0,0 +1,4 @@
+<?php
+return [
+    'db1' => [],
+];
\ No newline at end of file
diff --git a/tests/TestStand/configs/wrong_name_config.php b/tests/TestStand/configs/wrong_name_config.php
new file mode 100644
index 0000000..36b9da1
--- /dev/null
+++ b/tests/TestStand/configs/wrong_name_config.php
@@ -0,0 +1,4 @@
+<?php
+return [
+    123 => [],
+];
\ No newline at end of file
diff --git a/tests/TestStand/configs/wrong_params_config.php b/tests/TestStand/configs/wrong_params_config.php
new file mode 100644
index 0000000..cee852e
--- /dev/null
+++ b/tests/TestStand/configs/wrong_params_config.php
@@ -0,0 +1,7 @@
+<?php
+return [
+    'db1' => [
+        'driver' => 'pdo_nothing',
+        'path' => __DIR__ . '/runtime/sqlite/database.db',
+    ],
+];
\ No newline at end of file
diff --git a/tests/Traits/OptimisticLockTest.php b/tests/Traits/OptimisticLockTest.php
new file mode 100644
index 0000000..9856e25
--- /dev/null
+++ b/tests/Traits/OptimisticLockTest.php
@@ -0,0 +1,66 @@
+<?php declare(strict_types=1);
+
+namespace Composite\DB\Tests\Traits;
+
+use Composite\DB\ConnectionManager;
+use Composite\DB\Exceptions\DbException;
+use Composite\DB\Tests\TestStand\Entities;
+use Composite\DB\Tests\TestStand\Tables;
+
+final class OptimisticLockTest extends \PHPUnit\Framework\TestCase
+{
+    public function test_trait(): void
+    {
+        //checking that problem exists
+        $aiEntity1 = new Entities\TestAutoincrementEntity(name: 'John');
+        $aiTable1 = new Tables\TestAutoincrementTable();
+        $aiTable2 = new Tables\TestAutoincrementTable();
+
+        $aiTable1->save($aiEntity1);
+
+        $aiEntity2 = $aiTable2->findByPk($aiEntity1->id);
+
+        $db = ConnectionManager::getConnection($aiTable1->getConnectionName());
+
+        $db->beginTransaction();
+        $aiEntity1->name = 'John1';
+        $aiTable1->save($aiEntity1);
+
+        $aiEntity2->name = 'John2';
+        $aiTable2->save($aiEntity2);
+
+        $this->assertTrue($db->commit());
+
+        $aiEntity3 = $aiTable1->findByPk($aiEntity1->id);
+        $this->assertEquals('John2', $aiEntity3->name);
+
+        //Checking optimistic lock
+        $olEntity1 = new Entities\TestOptimisticLockEntity(name: 'John');
+        $olTable1 = new Tables\TestOptimisticLockTable();
+        $olTable2 = new Tables\TestOptimisticLockTable();
+
+        $olTable1->save($olEntity1);
+
+        $olEntity2 = $olTable2->findByPk($olEntity1->id);
+
+        $db->beginTransaction();
+        $olEntity1->name = 'John1';
+        $olTable1->save($olEntity1);
+
+        $olEntity2->name = 'John2';
+
+        $exceptionCaught = false;
+        try {
+            $olTable2->save($olEntity2);
+        } catch (DbException) {
+            $exceptionCaught = true;
+        }
+        $this->assertTrue($exceptionCaught);
+
+        $this->assertTrue($db->rollBack());
+
+        $olEntity3 = $olTable1->findByPk($olEntity1->id);
+        $this->assertEquals(1, $olEntity3->getVersion());
+        $this->assertEquals('John', $olEntity3->name);
+    }
+}
\ No newline at end of file
diff --git a/tests/Traits/UpdatedAtTest.php b/tests/Traits/UpdatedAtTest.php
new file mode 100644
index 0000000..bb149c5
--- /dev/null
+++ b/tests/Traits/UpdatedAtTest.php
@@ -0,0 +1,36 @@
+<?php declare(strict_types=1);
+
+namespace Composite\DB\Tests\Traits;
+
+use Composite\DB\Tests\TestStand\Entities\TestUpdatedAtEntity;
+use Composite\DB\Tests\TestStand\Tables\TestUpdateAtTable;
+
+final class UpdatedAtTest extends \PHPUnit\Framework\TestCase
+{
+    public function test_trait(): void
+    {
+        $entity = new TestUpdatedAtEntity('John');
+        $this->assertNull($entity->updated_at);
+
+        $table = new TestUpdateAtTable();
+        $table->save($entity);
+
+        $this->assertNotNull($entity->updated_at);
+
+        $dbEntity = $table->findByPk($entity->id);
+        $this->assertNotNull($dbEntity);
+
+        $this->assertEquals($entity->updated_at, $dbEntity->updated_at);
+
+
+        $entity->name = 'Richard';
+        $table->save($entity);
+
+        $this->assertNotEquals($entity->updated_at, $dbEntity->updated_at);
+        $lastUpdatedAt = $entity->updated_at;
+
+        //should not update entity
+        $table->save($entity);
+        $this->assertEquals($lastUpdatedAt, $entity->updated_at);
+    }
+}
\ No newline at end of file