diff --git a/.gitattributes b/.gitattributes index 433a2de9a..88cfd8800 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,7 @@ .gitattributes export-ignore .github/ export-ignore .gitignore export-ignore +CLAUDE.md export-ignore ncs.* export-ignore phpstan*.neon export-ignore src/**/*.latte export-ignore diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d17174eb8..5ef23f8d6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-22.04 strategy: matrix: - php: ['8.1', '8.2', '8.3', '8.4', '8.5'] + php: ['8.3', '8.4', '8.5'] fail-fast: false @@ -49,7 +49,7 @@ jobs: - uses: actions/checkout@v6 - uses: shivammathur/setup-php@v2 with: - php-version: 8.1 + php-version: 8.3 coverage: none - name: Create databases.ini diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..02f200be5 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,630 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +**Nette Database** is a database abstraction layer for PHP providing two components: +1. **Database Core** - PDO wrapper with advanced query building and parameter substitution +2. **Database Explorer** - ActiveRow pattern implementation (inspired by NotORM) for efficient relationship handling + +Supports PHP 8.1-8.5 and multiple database engines (MySQL, PostgreSQL, SQLite, MS SQL Server, Oracle). + +## Essential Commands + +### Testing + +**Test runner:** always use the wrapper `C:\Tools\cmd\test85.bat` (handles PHP 8.5 / environment setup) — do **not** call `vendor/bin/tester` directly. Use absolute paths for the test directory/file. + +```bash +# Run all tests +C:/Tools/cmd/test85.bat W:/Nette/Database/tests -s -C + +# Run specific test directory +C:/Tools/cmd/test85.bat W:/Nette/Database/tests/Database/Explorer -s -C + +# Run single test file +C:/Tools/cmd/test85.bat W:/Nette/Database/tests/Database/Explorer/Explorer.basic.phpt -s -C +``` + +**Database servers:** most tests connect to MySQL / PostgreSQL / MS SQL via `@dataProvider databases.ini`. Start them with Docker first (Docker Desktop must be running): + +```bash +docker compose -f W:/Nette/Database/docker-compose.yml up -d +# wait until all containers report "healthy", then run the tests +``` + +Without the containers running, those tests fail with `Connection refused` / `could not find driver` — that's an environment issue, not a code failure. + +**Test framework:** Nette Tester with `.phpt` extension +- Tests use `@dataProvider` to run against multiple databases (databases.ini) +- Each test uses `test()` function with descriptive names +- No comments before `test()` calls - the description parameter serves this purpose + +### Static Analysis +```bash +# Run PHPStan +composer run phpstan +``` + +Configuration: PHPStan level 5 with phpstan-nette extension + +## Architecture Overview + +### Two-Layer Design + +**Core Layer** (`src/Database/`) +- `Connection` - PDO connection management, query execution, transactions +- `SqlPreprocessor` - Advanced parameter substitution with multiple modes (ModeAnd, ModeSet, ModeValues, ModeOrder, ModeList) +- `ResultSet` - Iterator-based result sets +- `Driver` interface + database-specific implementations +- `Structure` - Cached database metadata (tables, columns, foreign keys, indexes) +- `Reflection` - On-demand schema reflection with lazy loading + +**Explorer Layer** (`src/Database/Table/`) +- `Explorer` - Entry point for high-level database operations +- `Selection` - Fluent query builder with lazy loading and generic type support (`Selection`) +- `ActiveRow` - Represents database rows with relationship navigation +- `GroupedSelection` - Handles grouped/aggregated queries +- `SqlBuilder` - Constructs SQL from Selection API calls + +### Key Design Patterns + +**ActiveRecord with Automatic N+1 Prevention** +```php +// Automatically batches related queries - constant query count regardless of result size +foreach ($books as $book) { + echo $book->author->name; // First iteration: SELECT * FROM author WHERE id IN (...) +} +``` + +**Convention-based Relationships** +- `DiscoveredConventions` - Auto-discovers from database structure +- `StaticConventions` - Manual configuration +- Forward references: `$book->author` (via foreign key) +- Back-references: `$author->related('book')` (reverse relationship) + +**Relationship Navigation with Colon Notation** +```php +->where('category.slug', $slug) // Forward reference via foreign key +->where(':order_item.quantity >', 1) // Back-reference (reverse relationship) +->where('category.parent.name', 'Root') // Deep relationship traversal +``` + +**Smart Query Optimization** +- Accessed column caching - only SELECTs columns actually used after first query +- Automatic JOIN batching for related data +- Two-level caching: Structure cache (metadata) + accessed columns cache + +### Driver Architecture + +Each driver implements `Driver` interface with database-specific features: +- SQL formatting (delimiters, datetime, LIKE encoding) +- LIMIT/OFFSET application (varies by database) +- Schema reflection +- Exception mapping (PDOException → specific constraint violations) +- Feature detection (sequences, schemas, ungrouped columns support) + +**Exception Hierarchy:** +``` +DriverException +├── ConnectionException +└── ConstraintViolationException + ├── ForeignKeyConstraintViolationException + ├── NotNullConstraintViolationException + └── UniqueConstraintViolationException +``` + +## Coding Standards + +### PHP Standards +- Every file must include `declare(strict_types=1)` +- All parameters, properties, and return values must have types +- Use generic type annotations for IDE support: `@template T of ActiveRow` +- Use single quotes for strings unless containing apostrophes +- Follow Nette Coding Standard (PSR-12 based) + +### Documentation (phpDoc) +- Document class purpose concisely without "Class that..." phrases +- Only document parameters/returns when adding info beyond PHP types +- For arrays, specify contents: `@return string[]` or `@param ActiveRow[] $rows` +- Use single-line format for simple properties: `/** @var string[] */` +- Start method docs with 3rd person singular present tense verb (Returns, Formats, Checks, Creates) + +### Testing Patterns +```php +test('descriptive name of what is being tested', function () use ($explorer) { + $result = $explorer->table('book')->where('id', 1)->fetch(); + Assert::same('expected', $result->title); +}); + +testException('throws exception for invalid input', function () use ($explorer) { + $explorer->table('book')->get(999); +}, Nette\InvalidArgumentException::class, 'Expected message pattern'); +``` + +## Important Implementation Details + +### Generic Type Support +Always use generic annotations for Selection returns: +```php +/** @return Selection */ +public function getProducts(): Selection +{ + return $this->db->table('product'); +} +``` + +### Query Preprocessing Modes +Context-aware array expansion in SqlPreprocessor: +- `ModeAnd` - WHERE/HAVING conditions: `['name' => 'John', 'age >' => 18]` → `name = 'John' AND age > 18` +- `ModeSet` - UPDATE SET clauses: `['name' => 'John', 'age' => 30]` → `name = 'John', age = 30` +- `ModeValues` - INSERT/REPLACE statements +- `ModeOrder` - ORDER BY/GROUP BY clauses +- `ModeList` - IN clauses: `[1, 2, 3]` → `IN (1, 2, 3)` + +### Exception Handling +Catch specific constraint violations, not generic PDOException: +```php +try { + $row->update(['email' => $newEmail]); +} catch (Nette\Database\UniqueConstraintViolationException $e) { + // Handle unique constraint violation +} +``` + +### Relationship Naming Convention +Follow foreign key naming without `_id` suffix: +- Database column: `author_id` +- Relationship property: `$book->author` (not `$book->author_id`) + +### When to Use Selection API vs Raw SQL +**Use Selection API for:** +- Standard CRUD operations +- Simple filtering and sorting +- Queries benefiting from lazy loading +- Dynamic condition chaining + +**Use raw SQL for:** +- Complex analytics and reporting +- Recursive queries (WITH RECURSIVE) +- Performance-critical queries +- Complex joins awkward in fluent API + +### Caching Behavior +- First query fetches all columns: `SELECT * FROM book` +- Subsequent queries use accessed column cache: `SELECT id, title, author_id FROM book` +- Cache persists across requests via Nette\Caching\Storage +- Test environment uses MemoryStorage for isolation + +## Test Organization + +``` +tests/Database/ +├── Connection.*.phpt # Core connection functionality +├── ResultSet.*.phpt # Result fetching methods +├── SqlPreprocessor.phpt # Query preprocessing +├── Structure.phpt # Metadata caching +├── Explorer/ # Explorer layer tests +│ ├── Explorer.*.phpt # Main Explorer functionality +│ ├── Selection.*.phpt # Selection API methods +│ ├── bugs/ # Regression tests +│ └── SqlBuilder.*.phpt # Query building +├── Drivers/ # Database-specific driver tests +├── Conventions/ # Relationship convention tests +└── files/ # SQL fixtures for test databases +``` + +**Database fixtures:** Tests use `@dataProvider databases.ini` to run against multiple database engines. Fixtures in `files/` directory named by pattern: `{driver}-nette_test1.sql` + +## Nette DI Integration + +DatabaseExtension (`src/Bridges/DatabaseDI/DatabaseExtension.php`) provides: +- Nette DI container integration +- Multi-database configuration support +- Auto-wiring for Connection, Structure, and Explorer services +- Tracy debugger integration via ConnectionPanel + +## SQL Placeholders and Hints + +The SQL preprocessor uses special placeholders for different contexts: + +| Placeholder | Purpose | Auto-detected for | +|-------------|---------|-------------------| +| `?` | Standard parameter placeholder | - | +| `?name` | Table/column identifier (properly quoted) | - | +| `?values` | INSERT format: `(col1, col2) VALUES (?, ?)` | `INSERT ... ?`, `REPLACE ... ?` | +| `?set` | UPDATE format: `col1 = ?, col2 = ?` | `SET ?`, `ON DUPLICATE KEY UPDATE ?` | +| `?and` | WHERE format with AND: `col1 = ? AND col2 = ?` | `WHERE ?`, `HAVING ?` | +| `?or` | WHERE format with OR: `col1 = ? OR col2 = ?` | - | +| `?order` | ORDER BY format | `ORDER BY ?`, `GROUP BY ?` | + +**Examples:** +```php +// Dynamic identifiers (use only with trusted values!) +$database->query('SELECT ?name FROM ?name', $column, $table); +// SELECT `name` FROM `users` (MySQL) + +// OR conditions (auto-detection uses AND by default) +$database->query('SELECT * FROM users WHERE ?or', [ + 'name' => 'John', + 'email' => 'john@example.com', +]); +// WHERE `name` = 'John' OR `email` = 'john@example.com' + +// Context-aware array processing +$database->query('INSERT INTO users ?', ['name' => 'John', 'year' => 1994]); +// INSERT INTO users (`name`, `year`) VALUES ('John', 1994) + +$database->query('UPDATE users SET ? WHERE id = ?', ['name' => 'John'], 1); +// UPDATE users SET `name` = 'John' WHERE id = 1 + +$database->query('SELECT * FROM users WHERE', ['active' => true, 'role' => 'admin']); +// WHERE `active` = 1 AND `role` = 'admin' +``` + +**Special value types:** +```php +$database->query('INSERT INTO articles', [ + 'title' => 'My Article', + 'published_at' => new DateTime, // Converted to database format + 'content' => fopen('image.png', 'r'), // Binary file content + 'state' => Status::Draft, // Enum converted to value + 'updated_at' => $database::literal('NOW()'), // SQL literal - not escaped +]); +``` + +## Security Considerations + +### Mass Assignment Vulnerability + +**Never use unvalidated user input directly in arrays for INSERT/UPDATE/WHERE:** +```php +// ❌ DANGEROUS - attacker can set any column +$database->query('INSERT INTO users', $_POST); +$table->insert($_POST); +$table->where($_POST); + +// ❌ DANGEROUS - attacker can use operators and SQL injection +$_POST['salary >'] = 100000; +$table->where($_POST); // WHERE `salary` > 100000 + +$_POST['0) UNION SELECT password FROM users WHERE (1'] = true; +$table->where($_POST); // SQL injection via array keys +``` + +**Always use column whitelist:** +```php +// ✅ Safe - only allowed columns +$allowedColumns = ['name', 'email', 'active']; +$filteredData = array_intersect_key($userData, array_flip($allowedColumns)); + +$database->query('INSERT INTO users', $filteredData); +$table->update($filteredData); +$table->where($filteredData); +``` + +### SQL Injection Prevention + +**Use parameterized queries, never string concatenation:** +```php +// ❌ DANGEROUS +$table->where('name = ' . $_GET['name']); +$table->where("name = '$_GET[name]'"); +$database->query("SELECT * FROM users WHERE name = '$name'"); + +// ✅ Safe +$table->where('name = ?', $name); +$database->query('SELECT * FROM users WHERE name = ?', $name); +``` + +**For dynamic identifiers, use whitelist + ?name:** +```php +// ✅ Safe - validate against whitelist first +$allowedColumns = ['name', 'email', 'created_at']; +if (!in_array($column, $allowedColumns)) { + throw new InvalidArgumentException('Invalid column'); +} +$database->query('SELECT ?name FROM users', $column); +``` + +## Configuration (NEON) + +```neon +database: + # Single connection + dsn: 'mysql:host=127.0.0.1;dbname=test' + user: root + password: password + + # Options + debugger: true # Tracy panel + explain: true # Show EXPLAIN in Tracy + autowired: true # Enable autowiring + conventions: discovered # discovered|static|ClassName + + options: + lazy: false # Connect only when needed + charset: utf8mb4 # MySQL: SET NAMES + sqlmode: '' # MySQL: SET sql_mode + convertBoolean: false # MySQL: TINYINT(1) → bool + newDateTime: false # Return DateTimeImmutable instead of DateTime + formatDateTime: 'U' # SQLite/Oracle: DateTime format (default: Unix timestamp) + +# Multiple connections +database: + main: + dsn: 'mysql:host=127.0.0.1;dbname=test' + user: root + password: password + autowired: true # Only first connection is autowired by default + + another: + dsn: 'sqlite::memory:' + autowired: false + +# DI service names +services: + # Explicitly reference non-autowired connections + - UserFacade(@database.another.connection) +``` + +**Service names:** `database.{name}.connection` and `database.{name}.explorer` + +## Explorer API Methods + +### Filtering and Conditions + +**where() - Automatic operator detection:** +```php +$table->where('id', 1); // WHERE `id` = 1 +$table->where('id', null); // WHERE `id` IS NULL +$table->where('id', [1, 2, 3]); // WHERE `id` IN (1, 2, 3) +$table->where('id NOT', [1, 2, 3]); // WHERE `id` NOT IN (1, 2, 3) +$table->where('id', []); // WHERE `id` IS NULL AND FALSE (finds nothing) + +// Array syntax with explicit operators +$table->where([ + 'age >' => 18, + 'name LIKE' => 'John%', + 'status' => ['active', 'pending'], +]); + +// Subqueries +$table->where('id', $explorer->table('other')->select('foreign_id')); +``` + +**whereOr() - OR conditions:** +```php +$table->whereOr([ + 'status' => 'active', + 'deleted' => false, +]); +// WHERE (`status` = 'active') OR (`deleted` = 0) +``` + +**wherePrimary() - Primary key filtering:** +```php +$table->wherePrimary(123); // WHERE `id` = 123 +$table->wherePrimary([1, 2, 3]); // WHERE `id` IN (1, 2, 3) + +// Composite primary key +$table->wherePrimary(['foo_id' => 1, 'bar_id' => 5]); +// WHERE `foo_id` = 1 AND `bar_id` = 5 +``` + +### Relationships + +**Accessing parent (1:N from child perspective):** +```php +$book = $explorer->table('book')->get(1); +echo $book->author->name; // Via author_id column +echo $book->translator?->name; // Via translator_id column (nullable) + +// Alternative: ref() method +$book->ref('author', 'author_id')->name; +$book->ref('author', 'translator_id')->name; +``` + +**Accessing children (1:N from parent perspective):** +```php +$author = $explorer->table('author')->get(1); + +// Explicit column specification +foreach ($author->related('book.author_id') as $book) { + echo $book->title; +} + +// Auto-detection (uses book.author_id based on parent table name) +foreach ($author->related('book') as $book) { + echo $book->title; +} + +// Translated books +foreach ($author->related('book.translator_id') as $book) { + echo $book->title; +} +``` + +**Many-to-many relationships:** +```php +$book = $explorer->table('book')->get(1); +// Go through junction table first +foreach ($book->related('book_tag') as $bookTag) { + echo $bookTag->tag->name; // Then access final table +} +``` + +**Querying through relationships (automatic JOINs):** +```php +// Dot notation (forward: 1:N from child) +$books->where('author.name LIKE ?', 'Jon%'); +$books->order('author.name DESC'); +$books->select('book.title, author.name'); + +// Colon notation (backward: 1:N from parent) +$authors->where(':book.title LIKE ?', '%PHP%'); +$authors->select('*, COUNT(:book.id) AS book_count')->group('author.id'); + +// Specify joining column explicitly +$authors->where(':book(translator_id).title LIKE ?', '%PHP%'); + +// Chain relationships +$authors->where(':book:book_tag.tag.name', 'PHP'); +``` + +**joinWhere() - Extend JOIN conditions:** +```php +// Adds condition to JOIN ON clause (not WHERE clause) +$books = $explorer->table('book') + ->joinWhere('translator', 'translator.name', 'David'); +// LEFT JOIN author translator ON book.translator_id = translator.id +// AND (translator.name = 'David') + +// With alias for complex queries +$tags = $explorer->table('tag') + ->joinWhere(':book_tag.book.author', 'book_author.born < ?', 1950) + ->alias(':book_tag.book.author', 'book_author'); +``` + +### CRUD Operations + +**insert() - Returns ActiveRow for single insert, int for multi-insert:** +```php +// Single insert +$row = $explorer->table('users')->insert([ + 'name' => 'John', + 'email' => 'john@example.com', +]); +echo $row->id; // Auto-generated ID + +// Multi-insert +$count = $explorer->table('users')->insert([ + ['name' => 'John', 'year' => 1994], + ['name' => 'Jack', 'year' => 1995], +]); +// Returns number of inserted rows + +// Insert from selection +$newUsers = $explorer->table('potential_users') + ->where('approved', 1) + ->select('name, email'); +$explorer->table('users')->insert($newUsers); +``` + +**update() - Returns number of affected rows:** +```php +$affected = $explorer->table('users') + ->where('id', 10) + ->update([ + 'name' => 'John Smith', + 'points+=' => 1, // Increment + 'coins-=' => 1, // Decrement + ]); + +// ActiveRow::update() - updates single row, returns true if changed +$article = $explorer->table('article')->get(1); +$changed = $article->update(['views+=' => 1]); +``` + +**delete() - Returns number of deleted rows:** +```php +$count = $explorer->table('users') + ->where('id', 10) + ->delete(); + +// ActiveRow::delete() +$book = $explorer->table('book')->get(1); +$book->delete(); +``` + +### Aggregation + +```php +$table->count('*'); // SELECT COUNT(*) FROM table +$table->count('DISTINCT column'); // SELECT COUNT(DISTINCT column) +$table->min('price'); // SELECT MIN(price) +$table->max('price'); // SELECT MAX(price) +$table->sum('price * quantity'); // SELECT SUM(price * quantity) + +// Generic aggregation +$avgPrice = $products->where('category_id', 1) + ->aggregation('AVG(price)'); + +// Aggregation over grouped results +$totalPrice = $products + ->select('category_id, SUM(price * stock) AS category_total') + ->group('category_id') + ->aggregation('SUM(category_total)', 'SUM'); +``` + +## Transactions + +**Automatic transaction management:** +```php +// Preferred: automatic commit/rollback +$result = $database->transaction(function ($database) use ($id) { + $database->query('DELETE FROM articles WHERE id = ?', $id); + $database->query('INSERT INTO audit_log', [ + 'article_id' => $id, + 'action' => 'delete', + ]); + return $database->getInsertId(); // Can return values +}); + +// Manual control +$database->beginTransaction(); +try { + $database->query('...'); + $database->commit(); +} catch (\Exception $e) { + $database->rollBack(); + throw $e; +} +``` + +## Reflection API + +**Introspect database structure:** +```php +$reflection = $database->getReflection(); + +// Tables +foreach ($reflection->tables as $name => $table) { + echo $table->name; + echo $table->view ? 'VIEW' : 'TABLE'; + echo $table->fullName; // Including schema if exists +} + +if ($reflection->hasTable('users')) { + $table = $reflection->getTable('users'); +} + +// Columns +foreach ($table->columns as $name => $column) { + echo "$column->name: $column->nativeType"; + echo $column->nullable ? 'NULL' : 'NOT NULL'; + echo $column->default; + echo $column->autoIncrement ? 'AUTO_INCREMENT' : ''; + echo $column->primary ? 'PRIMARY KEY' : ''; +} + +// Indexes +foreach ($table->indexes as $index) { + $columns = implode(', ', array_map(fn($col) => $col->name, $index->columns)); + echo ($index->primary ? 'PRIMARY KEY' : 'INDEX') . ": $columns"; + echo $index->unique ? 'UNIQUE' : ''; +} + +// Primary key +if ($primaryKey = $table->primaryKey) { + $columns = implode(', ', array_map(fn($col) => $col->name, $primaryKey->columns)); +} + +// Foreign keys +foreach ($table->foreignKeys as $fk) { + $localCols = implode(', ', array_map(fn($col) => $col->name, $fk->localColumns)); + $foreignCols = implode(', ', array_map(fn($col) => $col->name, $fk->foreignColumns)); + echo "$localCols -> {$fk->foreignTable->name}($foreignCols)"; +} +``` diff --git a/composer.json b/composer.json index 3b047bcbf..2ce50b288 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": "8.1 - 8.5", + "php": "8.3 - 8.5", "ext-pdo": "*", "nette/caching": "^3.2", "nette/utils": "^4.0" @@ -43,7 +43,7 @@ }, "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "4.0-dev" } }, "config": { diff --git a/readme.md b/readme.md index 0457ab40f..3d01382b1 100644 --- a/readme.md +++ b/readme.md @@ -1,25 +1,26 @@ -Nette Database -============== +[![Nette Database](https://github.com/nette/database/assets/194960/97d8f31b-096c-466c-a76f-f5b9e511ea8d)](https://doc.nette.org/database) [![Downloads this Month](https://img.shields.io/packagist/dm/nette/database.svg)](https://packagist.org/packages/nette/database) -[![Tests](https://github.com/nette/database/actions/workflows/tests.yml/badge.svg?branch=v3.2)](https://github.com/nette/database/actions) +[![Tests](https://github.com/nette/database/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/nette/database/actions) [![Latest Stable Version](https://poser.pugx.org/nette/database/v/stable)](https://github.com/nette/database/releases) [![License](https://img.shields.io/badge/license-New%20BSD-blue.svg)](https://github.com/nette/database/blob/master/license.md) +  Introduction ------------ Nette provides a powerful layer for accessing your database easily. -- composes SQL queries with ease -- easily fetches data -- uses efficient queries and does not transmit unnecessary data +✅ composes SQL queries with ease
+✅ significantly simplifies retrieving data without writing SQL queries
+✅ uses efficient queries and does not transmit unnecessary data -The [Nette Database Core](https://doc.nette.org/database-core) is a wrapper around the PDO and provides core functionality. +The [Nette Database Core](https://doc.nette.org/en/database/core) is a wrapper around the PDO and provides core functionality. -The [Nette Database Explorer](https://doc.nette.org/database-explorer) layer helps you to fetch database data more easily and in a more optimized way. +The [Nette Database Explorer](https://doc.nette.org/en/database/explorer) layer helps you to fetch database data more easily and in a more optimized way. +  [Support Me](https://github.com/sponsors/dg) -------------------------------------------- @@ -30,6 +31,7 @@ Do you like Nette Database? Are you looking forward to the new features? Thank you! +  Installation ------------ @@ -40,8 +42,9 @@ The recommended way to install is via Composer: composer require nette/database ``` -It requires PHP version 8.1 and supports PHP up to 8.5. +It requires PHP version 8.3 and supports PHP up to 8.5. +  Running Tests ------------- @@ -66,6 +69,7 @@ Usage This is just a piece of documentation. [Please see our website](https://doc.nette.org/database). +  Database Core ------------- @@ -73,7 +77,7 @@ Database Core To create a new database connection just create a new instance of `Nette\Database\Connection` class: ```php -$database = new Nette\Database\Connection($dsn, $user, $password); // the same arguments as uses PDO +$database = new Nette\Database\Explorer($dsn, $user, $password); // the same arguments as uses PDO ``` Connection allows you to easily query your database by calling `query` method: @@ -89,6 +93,8 @@ $database->query('UPDATE users SET ? WHERE id=?', $data, $id); $database->query('SELECT * FROM categories WHERE id=?', 123)->dump(); ``` +  + Database Explorer ----------------- diff --git a/src/Bridges/DatabaseDI/DatabaseExtension.php b/src/Bridges/DatabaseDI/DatabaseExtension.php index 5b8560427..b7ac01e4b 100644 --- a/src/Bridges/DatabaseDI/DatabaseExtension.php +++ b/src/Bridges/DatabaseDI/DatabaseExtension.php @@ -8,6 +8,8 @@ namespace Nette\Bridges\DatabaseDI; use Nette; +use Nette\Database\Table\ActiveRow; +use Nette\DI\Definitions\Statement; use Nette\Schema\Expect; use Tracy; use function is_array, is_string; @@ -30,12 +32,17 @@ public function getConfigSchema(): Nette\Schema\Schema Expect::structure([ 'dsn' => Expect::string()->required()->dynamic(), 'user' => Expect::string()->nullable()->dynamic(), + 'username' => Expect::string()->nullable()->dynamic(), 'password' => Expect::string()->nullable()->dynamic(), 'options' => Expect::array(), 'debugger' => Expect::bool(), 'explain' => Expect::bool(true), 'reflection' => Expect::string(), // BC 'conventions' => Expect::string('discovered'), // Nette\Database\Conventions\DiscoveredConventions + 'mapping' => Expect::structure([ + 'convention' => Expect::string(''), + 'tables' => Expect::arrayOf('string', 'string'), + ])->before(fn($v) => is_string($v) ? ['convention' => $v] : $v), 'autowired' => Expect::bool(), ]), )->before(fn($val) => is_array(reset($val)) || reset($val) === null @@ -74,6 +81,10 @@ public function beforeCompile(): void private function setupDatabase(\stdClass $config, string $name): void { + if (!empty($config->reflection)) { + throw new Nette\DeprecatedException('The "reflection" option is deprecated, use "conventions" instead.'); + } + $builder = $this->getContainerBuilder(); foreach ($config->options as $key => $value) { @@ -87,50 +98,63 @@ private function setupDatabase(\stdClass $config, string $name): void } } - $connection = $builder->addDefinition($this->prefix("$name.connection")) - ->setFactory(Nette\Database\Connection::class, [$config->dsn, $config->user, $config->password, $config->options]) - ->setAutowired($config->autowired); + $cacheId = 'Nette.Database.' . hash('xxh128', $name . $config->dsn); + $cache = new Statement(Nette\Caching\Cache::class, [1 => $cacheId]); - $structure = $builder->addDefinition($this->prefix("$name.structure")) - ->setFactory(Nette\Database\Structure::class) - ->setArguments([$connection]) + $explorer = $builder->addDefinition($this->prefix($name)) + ->setFactory(Nette\Database\Explorer::class, [$config->dsn, $config->username ?? $config->user, $config->password, $config->options]) + ->addSetup('setCache', [$cache]) ->setAutowired($config->autowired); - if (!empty($config->reflection)) { - $conventionsServiceName = 'reflection'; - $config->conventions = $config->reflection; - if (is_string($config->conventions) && strtolower($config->conventions) === 'conventional') { - $config->conventions = 'Static'; - } - } else { - $conventionsServiceName = 'conventions'; - } - - if (!$config->conventions) { - $conventions = null; + if (!$config->conventions || $config->conventions === 'discovered') { } elseif (is_string($config->conventions)) { - $conventions = $builder->addDefinition($this->prefix("$name.$conventionsServiceName")) + $conventions = $builder->addDefinition($this->prefix("$name.conventions")) ->setFactory(preg_match('#^[a-z]+$#Di', $config->conventions) ? 'Nette\Database\Conventions\\' . ucfirst($config->conventions) . 'Conventions' : $config->conventions) - ->setArguments(strtolower($config->conventions) === 'discovered' ? [$structure] : []) ->setAutowired($config->autowired); + $explorer->addSetup('setConventions', [$conventions]); } else { - $conventions = Nette\DI\Helpers::filterArguments([$config->conventions])[0]; + $explorer->addSetup('setConventions', [Nette\DI\Helpers::filterArguments([$config->conventions])[0]]); } - $builder->addDefinition($this->prefix("$name.explorer")) - ->setFactory(Nette\Database\Explorer::class, [$connection, $structure, $conventions]) - ->setAutowired($config->autowired); + if ($config->mapping->convention || $config->mapping->tables) { + $explorer->addSetup('setRowMapping', [ + new Statement([self::class, 'createRowMapping'], [ + $config->mapping->convention, + (array) $config->mapping->tables, + ]), + ]); + } - $builder->addAlias($this->prefix("$name.context"), $this->prefix("$name.explorer")); + $builder->addAlias($this->prefix("$name.connection"), $this->prefix($name)); + $builder->addAlias($this->prefix("$name.context"), $this->prefix($name)); + $builder->addAlias($this->prefix("$name.explorer"), $this->prefix($name)); if ($this->name === 'database') { - $builder->addAlias($this->prefix($name), $this->prefix("$name.connection")); $builder->addAlias("nette.database.$name", $this->prefix($name)); $builder->addAlias("nette.database.$name.context", $this->prefix("$name.explorer")); } } + + + public static function createRowMapping(string $convention, array $tables): \Closure + { + return static function (string $table) use ($convention, $tables): string { + if (isset($tables[$table])) { + return $tables[$table]; + } + + if ($convention !== '') { + $class = str_replace('*', str_replace(' ', '', ucwords(strtr($table, '_', ' '))), $convention); + if (class_exists($class)) { + return $class; + } + } + + return ActiveRow::class; + }; + } } diff --git a/src/Bridges/DatabaseTracy/ConnectionPanel.php b/src/Bridges/DatabaseTracy/ConnectionPanel.php index 9c1b27c2e..172235ad8 100644 --- a/src/Bridges/DatabaseTracy/ConnectionPanel.php +++ b/src/Bridges/DatabaseTracy/ConnectionPanel.php @@ -8,8 +8,10 @@ namespace Nette\Bridges\DatabaseTracy; use Nette; -use Nette\Database\Connection; +use Nette\Database\DriverException; +use Nette\Database\Explorer; use Nette\Database\Helpers; +use Nette\Database\Result; use Tracy; use function is_string; @@ -26,9 +28,7 @@ class ConnectionPanel implements Tracy\IBarPanel public float $performanceScale = 0.25; private float $totalTime = 0; private int $count = 0; - - /** @var list, list>, ?float, ?int, ?string}> */ - private array $queries = []; + private array $events = []; private Tracy\BlueScreen $blueScreen; @@ -36,8 +36,8 @@ class ConnectionPanel implements Tracy\IBarPanel * Registers the panel with Tracy. Optionally adds it to the Tracy Bar. */ public static function initialize( - Connection $connection, - bool $addBarPanel = false, + Explorer $explorer, + bool $addBarPanel = true, string $name = '', bool $explain = true, ?Tracy\Bar $bar = null, @@ -48,7 +48,7 @@ public static function initialize( $blueScreen->addPanel(self::renderException(...)); if ($addBarPanel) { - $panel = new self($connection, $blueScreen); + $panel = new self($explorer, $blueScreen); $panel->explain = $explain; $panel->name = $name; $bar ??= Tracy\Debugger::getBar(); @@ -59,14 +59,14 @@ public static function initialize( } - public function __construct(Connection $connection, Tracy\BlueScreen $blueScreen) + public function __construct(Explorer $explorer, Tracy\BlueScreen $blueScreen) { - $connection->onQuery[] = $this->logQuery(...); + $explorer->onQuery[] = $this->logQuery(...); $this->blueScreen = $blueScreen; } - private function logQuery(Connection $connection, Nette\Database\ResultSet|\PDOException $result): void + private function logQuery(Explorer $connection, Result|DriverException $result): void { if ($this->disabled) { return; @@ -74,7 +74,7 @@ private function logQuery(Connection $connection, Nette\Database\ResultSet|\PDOE $this->count++; - $trace = $result instanceof \PDOException + $trace = $result instanceof DriverException ? array_map(fn($row) => array_diff_key($row, ['args' => null]), $result->getTrace()) : debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); @@ -90,13 +90,13 @@ private function logQuery(Connection $connection, Nette\Database\ResultSet|\PDOE array_shift($trace); } - if ($result instanceof Nette\Database\ResultSet) { + if ($result instanceof Result) { $this->totalTime += $result->getTime(); if ($this->count < $this->maxQueries) { - $this->queries[] = [$connection, $result->getQueryString(), $result->getParameters(), $trace, $result->getTime(), $result->getRowCount(), null]; + $this->events[] = [$connection, $result->getQuery(), $trace, $result->getTime(), $result->getRowCount(), null]; } - } elseif ($result instanceof \PDOException && $this->count < $this->maxQueries) { - $this->queries[] = [$connection, $result->queryString, null, $trace, null, null, $result->getMessage()]; + } elseif ($result instanceof DriverException && $this->count < $this->maxQueries) { + $this->events[] = [$connection, $result->getQuery(), $trace, null, null, $result->getMessage()]; } } @@ -104,20 +104,13 @@ private function logQuery(Connection $connection, Nette\Database\ResultSet|\PDOE /** @return array{tab: string, panel: string}|null */ public static function renderException(?\Throwable $e): ?array { - if (!$e instanceof \PDOException) { + if (!$e instanceof DriverException) { return null; } - if (isset($e->queryString)) { - $sql = $e->queryString; - - } elseif (($item = Tracy\Helpers::findTrace($e->getTrace(), 'PDO::prepare')) && isset($item['args'][0])) { - $sql = $item['args'][0]; - } - - return isset($sql) ? [ + return $e->getQuery() ? [ 'tab' => 'SQL', - 'panel' => Helpers::dumpSql($sql, $e->params ?? []), + 'panel' => Helpers::dumpSql($e->getQuery()), ] : null; } @@ -139,11 +132,12 @@ public function getPanel(): ?string return null; } - $queries = []; + $events = []; $connection = null; - foreach ($this->queries as $query) { - [$connection, $sql, $params, , , , $error] = $query; + foreach ($this->events as $event) { + [$connection, $query, , , , $error] = $event; $explain = null; + $sql = $query->getSql(); $command = preg_match('#\s*\(?\s*(SELECT|INSERT|UPDATE|DELETE)\s#iA', $sql, $m) ? strtolower($m[1]) : null; @@ -152,17 +146,18 @@ public function getPanel(): ?string $cmd = is_string($this->explain) ? $this->explain : 'EXPLAIN'; - $explain = (new Nette\Database\ResultSet($connection, "$cmd $sql", $params ?? []))->fetchAll(); - } catch (\PDOException) { + $rows = $connection->getConnection()->query("$cmd $sql", $query->getParameters()); + for ($explain = []; $row = $rows->fetch(); $explain[] = $row); + } catch (DriverException) { } } - $query[] = $command; - $query[] = $explain; - $queries[] = $query; + $event[] = $command; + $event[] = $explain; + $events[] = $event; } - return Nette\Utils\Helpers::capture(function () use ($queries, $connection) { + return Nette\Utils\Helpers::capture(function () use ($events, $connection) { $name = $this->name; $count = $this->count; $totalTime = $this->totalTime; diff --git a/src/Bridges/DatabaseTracy/dist/panel.phtml b/src/Bridges/DatabaseTracy/dist/panel.phtml index 2ba640d30..4ae711974 100644 --- a/src/Bridges/DatabaseTracy/dist/panel.phtml +++ b/src/Bridges/DatabaseTracy/dist/panel.phtml @@ -1,7 +1,7 @@ $queries */ +/** @var list $events */ /** @var string $name */ /** @var int $count */ /** @var float $totalTime */ @@ -15,13 +15,11 @@ echo ' -getDsn())) === null ? '' : ' title="' . Tracy\Helpers::escapeHtml($ʟ_tmp) . '"' /* pos 16:13 */; -echo '>Queries: '; -echo Tracy\Helpers::escapeHtml($count) /* pos 16:46 */; -echo Tracy\Helpers::escapeHtml($totalTime ? sprintf(', time: %0.3f ms', $totalTime * 1000) : '') /* pos 16:54 */; +

Queries: '; +echo Tracy\Helpers::escapeHtml($count) /* pos 16:14 */; +echo Tracy\Helpers::escapeHtml($totalTime ? sprintf(', time: %0.3f ms', $totalTime * 1000) : '') /* pos 16:22 */; echo ', '; -echo Tracy\Helpers::escapeHtml($name) /* pos 16:122 */; +echo Tracy\Helpers::escapeHtml($name) /* pos 16:90 */; echo '

@@ -33,7 +31,7 @@ echo ' '; -foreach ($queries as [$connection, $sql, $params, $trace, $time, $rows, $error, $command, $explain]) /* pos 26:3 */ { +foreach ($events as [$connection, $query, $trace, $time, $rows, $error, $command, $explain]) /* pos 26:3 */ { echo ' '; - echo Nette\Database\Helpers::dumpSql($sql, $params, $connection) /* pos 39:6 */; + echo Nette\Database\Helpers::dumpSql($query, $connection) /* pos 39:6 */; echo ' '; @@ -141,7 +139,7 @@ foreach ($queries as [$connection, $sql, $params, $trace, $time, $rows, $error, echo ' '; -if (count($queries) < $count) /* pos 76:2 */ { +if (count($events) < $count) /* pos 76:2 */ { echo '

...and more

'; } echo ' diff --git a/src/Bridges/DatabaseTracy/panel.latte b/src/Bridges/DatabaseTracy/panel.latte index 2a0d74ea5..d2522ac62 100644 --- a/src/Bridges/DatabaseTracy/panel.latte +++ b/src/Bridges/DatabaseTracy/panel.latte @@ -1,5 +1,5 @@ {varType Nette\Database\Connection $connection} -{varType list $queries} +{varType list $events} {varType string $name} {varType int $count} {varType float $totalTime} @@ -13,7 +13,7 @@ #tracy-debug td.nette-DbConnectionPanel-sql-update { background: #E7FBFF !important } -

Queries: {$count}{$totalTime ? sprintf(', time: %0.3f ms', $totalTime * 1000) : ''}, {$name}

+

Queries: {$count}{$totalTime ? sprintf(', time: %0.3f ms', $totalTime * 1000) : ''}, {$name}

@@ -23,7 +23,7 @@ - {foreach $queries as [$connection, $sql, $params, $trace, $time, $rows, $error, $command, $explain]} + {foreach $events as [$connection, $query, $trace, $time, $rows, $error, $command, $explain]} diff --git a/tests/Database/Connection.exceptions.mysql.phpt b/tests/Database/Connection.exceptions.mysql.phpt index 15cfb9b8c..f4483ae4d 100644 --- a/tests/Database/Connection.exceptions.mysql.phpt +++ b/tests/Database/Connection.exceptions.mysql.phpt @@ -9,20 +9,18 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -$connection = connectToDB()->getConnection(); +$connection = connectToDB(); Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/files/{$driverName}-nette_test1.sql"); test('Exception thrown for invalid database credentials', function () { $options = Tester\Environment::loadData(); $e = Assert::exception( - fn() => new Nette\Database\Connection($options['dsn'], 'unknown', 'unknown'), + fn() => (new Nette\Database\Explorer($options['dsn'], 'unknown', 'unknown'))->connect(), Nette\Database\ConnectionException::class, '%a% Access denied for user %a%', + 1045, ); - - Assert::same(1045, $e->getDriverCode()); Assert::contains($e->getSqlState(), ['HY000', '28000']); - Assert::same($e->getCode(), $e->getSqlState()); }); @@ -33,7 +31,7 @@ test('Exception thrown when calling rollback with no active transaction', functi 'There is no active transaction', 0, ); - Assert::same(null, $e->getDriverCode()); + Assert::null($e->getSqlState()); }); @@ -42,11 +40,9 @@ test('Exception thrown for syntax error in SQL query', function () use ($connect fn() => $connection->query('SELECT'), Nette\Database\DriverException::class, '%a% Syntax error %a%', - '42000', + 1064, ); - - Assert::same(1064, $e->getDriverCode()); - Assert::same($e->getCode(), $e->getSqlState()); + Assert::same('42000', $e->getSqlState()); }); @@ -55,11 +51,9 @@ test('Exception thrown for unique constraint violation', function () use ($conne fn() => $connection->query('INSERT INTO author (id, name, web, born) VALUES (11, "", "", NULL)'), Nette\Database\UniqueConstraintViolationException::class, '%a% Integrity constraint violation: %a%', - '23000', + 1062, ); - - Assert::same(1062, $e->getDriverCode()); - Assert::same($e->getCode(), $e->getSqlState()); + Assert::same('23000', $e->getSqlState()); }); @@ -68,11 +62,9 @@ test('Exception thrown for not null constraint violation', function () use ($con fn() => $connection->query('INSERT INTO author (name, web, born) VALUES (NULL, "", NULL)'), Nette\Database\NotNullConstraintViolationException::class, '%a% Integrity constraint violation: %a%', - '23000', + 1048, ); - - Assert::same(1048, $e->getDriverCode()); - Assert::same($e->getCode(), $e->getSqlState()); + Assert::same('23000', $e->getSqlState()); }); @@ -81,11 +73,9 @@ test('Exception thrown for foreign key constraint violation', function () use ($ fn() => $connection->query('INSERT INTO book (author_id, translator_id, title) VALUES (999, 12, "")'), Nette\Database\ForeignKeyConstraintViolationException::class, '%a% a foreign key constraint fails %a%', - '23000', + 1452, ); - - Assert::same(1452, $e->getDriverCode()); - Assert::same($e->getCode(), $e->getSqlState()); + Assert::same('23000', $e->getSqlState()); }); diff --git a/tests/Database/Connection.exceptions.phpt b/tests/Database/Connection.exceptions.phpt deleted file mode 100644 index 215123cbd..000000000 --- a/tests/Database/Connection.exceptions.phpt +++ /dev/null @@ -1,20 +0,0 @@ - new Nette\Database\Connection('unknown'), - Nette\Database\ConnectionException::class, - '%a%valid data source %a%', - 0, -); - -Assert::same(null, $e->getDriverCode()); -Assert::same(null, $e->getSqlState()); diff --git a/tests/Database/Connection.exceptions.postgre.phpt b/tests/Database/Connection.exceptions.postgre.phpt index b5ca89c35..4650c45e6 100644 --- a/tests/Database/Connection.exceptions.postgre.phpt +++ b/tests/Database/Connection.exceptions.postgre.phpt @@ -9,21 +9,19 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -$connection = connectToDB()->getConnection(); +$connection = connectToDB(); Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/files/{$driverName}-nette_test1.sql"); test('Exception thrown for invalid database credentials', function () { $options = Tester\Environment::loadData(); $e = Assert::exception( - fn() => new Nette\Database\Connection($options['dsn'], 'unknown', 'unknown'), + fn() => (new Nette\Database\Explorer($options['dsn'], 'unknown', 'unknown'))->connect(), Nette\Database\ConnectionException::class, null, - '08006', + 7, ); - - Assert::same(7, $e->getDriverCode()); - Assert::same($e->getCode(), $e->getSqlState()); + Assert::same('08006', $e->getSqlState()); }); @@ -34,8 +32,7 @@ test('Exception thrown when calling rollback with no active transaction', functi 'There is no active transaction', 0, ); - - Assert::same(null, $e->getDriverCode()); + Assert::null($e->getSqlState()); }); @@ -44,11 +41,9 @@ test('Exception thrown for syntax error in SQL query', function () use ($connect fn() => $connection->query('SELECT INTO'), Nette\Database\DriverException::class, '%a% syntax error %A%', - '42601', + 7, ); - - Assert::same(7, $e->getDriverCode()); - Assert::same($e->getCode(), $e->getSqlState()); + Assert::same('42601', $e->getSqlState()); }); @@ -57,11 +52,9 @@ test('Exception thrown for unique constraint violation', function () use ($conne fn() => $connection->query("INSERT INTO author (id, name, web, born) VALUES (11, '', '', NULL)"), Nette\Database\UniqueConstraintViolationException::class, '%a% Unique violation: %A%', - '23505', + 7, ); - - Assert::same(7, $e->getDriverCode()); - Assert::same($e->getCode(), $e->getSqlState()); + Assert::same('23505', $e->getSqlState()); }); @@ -70,11 +63,9 @@ test('Exception thrown for not null constraint violation', function () use ($con fn() => $connection->query("INSERT INTO author (name, web, born) VALUES (NULL, '', NULL)"), Nette\Database\NotNullConstraintViolationException::class, '%a% Not null violation: %A%', - '23502', + 7, ); - - Assert::same(7, $e->getDriverCode()); - Assert::same($e->getCode(), $e->getSqlState()); + Assert::same('23502', $e->getSqlState()); }); @@ -83,11 +74,9 @@ test('Exception thrown for foreign key constraint violation', function () use ($ fn() => $connection->query("INSERT INTO book (author_id, translator_id, title) VALUES (999, 12, '')"), Nette\Database\ForeignKeyConstraintViolationException::class, '%a% Foreign key violation: %A%', - '23503', + 7, ); - - Assert::same(7, $e->getDriverCode()); - Assert::same($e->getCode(), $e->getSqlState()); + Assert::same('23503', $e->getSqlState()); }); diff --git a/tests/Database/Connection.exceptions.sqlite.phpt b/tests/Database/Connection.exceptions.sqlite.phpt index ce94d1a37..1fe2d7f03 100644 --- a/tests/Database/Connection.exceptions.sqlite.phpt +++ b/tests/Database/Connection.exceptions.sqlite.phpt @@ -9,20 +9,18 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -$connection = connectToDB()->getConnection(); +$connection = connectToDB(); Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/files/{$driverName}-nette_test1.sql"); test('Exception thrown for unable to open database file', function () { $e = Assert::exception( - fn() => new Nette\Database\Connection('sqlite:.'), + fn() => (new Nette\Database\Explorer('sqlite:.'))->connect(), Nette\Database\ConnectionException::class, 'SQLSTATE[HY000] [14] unable to open database file', - 'HY000', + 14, ); - - Assert::same(14, $e->getDriverCode()); - Assert::same($e->getCode(), $e->getSqlState()); + Assert::same('HY000', $e->getSqlState()); }); @@ -33,8 +31,7 @@ test('Exception thrown when calling rollback with no active transaction', functi 'There is no active transaction', 0, ); - - Assert::same(null, $e->getDriverCode()); + Assert::null($e->getSqlState()); }); @@ -43,11 +40,9 @@ test('Exception thrown for error in SQL query', function () use ($connection) { fn() => $connection->query('SELECT'), Nette\Database\DriverException::class, '%a% error%a%', - 'HY000', + 1, ); - - Assert::same(1, $e->getDriverCode()); - Assert::same($e->getCode(), $e->getSqlState()); + Assert::same('HY000', $e->getSqlState()); }); @@ -56,11 +51,9 @@ test('Exception thrown for unique constraint violation', function () use ($conne fn() => $connection->query('INSERT INTO author (id, name, web, born) VALUES (11, "", "", NULL)'), Nette\Database\UniqueConstraintViolationException::class, '%a% Integrity constraint violation: %a%', - '23000', + 19, ); - - Assert::same(19, $e->getDriverCode()); - Assert::same($e->getCode(), $e->getSqlState()); + Assert::same('23000', $e->getSqlState()); }); @@ -69,11 +62,9 @@ test('Exception thrown for not null constraint violation', function () use ($con fn() => $connection->query('INSERT INTO author (name, web, born) VALUES (NULL, "", NULL)'), Nette\Database\NotNullConstraintViolationException::class, '%a% Integrity constraint violation: %a%', - '23000', + 19, ); - - Assert::same(19, $e->getDriverCode()); - Assert::same($e->getCode(), $e->getSqlState()); + Assert::same('23000', $e->getSqlState()); }); @@ -81,8 +72,6 @@ test('Exception thrown for foreign key constraint violation', function () use ($ $e = Assert::exception(function () use ($connection) { $connection->query('PRAGMA foreign_keys=true'); $connection->query('INSERT INTO book (author_id, translator_id, title) VALUES (999, 12, "")'); - }, Nette\Database\ForeignKeyConstraintViolationException::class, '%a% Integrity constraint violation: %a%', '23000'); - - Assert::same(19, $e->getDriverCode()); - Assert::same($e->getCode(), $e->getSqlState()); + }, Nette\Database\ForeignKeyConstraintViolationException::class, '%a% Integrity constraint violation: %a%', 19); + Assert::same('23000', $e->getSqlState()); }); diff --git a/tests/Database/Connection.fetch.phpt b/tests/Database/Connection.fetch.phpt index 3c6633958..369423dee 100644 --- a/tests/Database/Connection.fetch.phpt +++ b/tests/Database/Connection.fetch.phpt @@ -9,7 +9,7 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -$connection = connectToDB()->getConnection(); +$connection = connectToDB(); Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/files/{$driverName}-nette_test1.sql"); diff --git a/tests/Database/Connection.getInsertId().mysql.phpt b/tests/Database/Connection.getInsertId().mysql.phpt index fae2e6b44..0d889521f 100644 --- a/tests/Database/Connection.getInsertId().mysql.phpt +++ b/tests/Database/Connection.getInsertId().mysql.phpt @@ -9,7 +9,7 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -$connection = connectToDB()->getConnection(); +$connection = connectToDB(); $connection->query(' CREATE TEMPORARY TABLE noprimarykey ( @@ -17,11 +17,11 @@ $connection->query(' ) ENGINE=InnoDB '); -$connection->query('INSERT INTO noprimarykey (col) VALUES (NULL)'); -Assert::same('0', $connection->getInsertId()); - $connection->query('INSERT INTO noprimarykey (col) VALUES (3)'); -Assert::same('0', $connection->getInsertId()); +Assert::exception( + fn() => $connection->getInsertId(), + Nette\Database\DriverException::class, +); $connection->query(' @@ -32,10 +32,10 @@ $connection->query(' '); $connection->query('INSERT INTO primarykey (prim) VALUES (5)'); -Assert::same('0', $connection->getInsertId()); - -$connection->query('INSERT INTO primarykey (prim) VALUES (6)'); -Assert::same('0', $connection->getInsertId()); +Assert::exception( + fn() => $connection->getInsertId(), + Nette\Database\DriverException::class, +); $connection->query(' @@ -47,13 +47,13 @@ $connection->query(' '); $connection->query('INSERT INTO autoprimarykey (col) VALUES (NULL)'); -Assert::same('1', $connection->getInsertId()); +Assert::same(1, $connection->getInsertId()); $connection->query('INSERT INTO autoprimarykey (col) VALUES (NULL)'); -Assert::same('2', $connection->getInsertId()); +Assert::same(2, $connection->getInsertId()); $connection->query('INSERT INTO autoprimarykey (prim, col) VALUES (10, NULL)'); -Assert::same('10', $connection->getInsertId()); +Assert::same(10, $connection->getInsertId()); $connection->query(' @@ -65,10 +65,10 @@ $connection->query(' '); $connection->query('INSERT INTO multiautoprimarykey (prim2) VALUES (3)'); -Assert::same('1', $connection->getInsertId()); +Assert::same(1, $connection->getInsertId()); $connection->query('INSERT INTO multiautoprimarykey (prim2) VALUES (3)'); -Assert::same('2', $connection->getInsertId()); +Assert::same(2, $connection->getInsertId()); $connection->query('INSERT INTO multiautoprimarykey (prim1, prim2) VALUES (10, 3)'); -Assert::same('10', $connection->getInsertId()); +Assert::same(10, $connection->getInsertId()); diff --git a/tests/Database/Connection.getInsertId().postgre.phpt b/tests/Database/Connection.getInsertId().postgre.phpt index 7c3141543..937243945 100644 --- a/tests/Database/Connection.getInsertId().postgre.phpt +++ b/tests/Database/Connection.getInsertId().postgre.phpt @@ -9,7 +9,7 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -$connection = connectToDB()->getConnection(); +$connection = connectToDB(); $connection->query(' CREATE TEMPORARY TABLE "primarykey" ( @@ -34,13 +34,13 @@ $connection->query(' '); $connection->query('INSERT INTO autoprimarykey (col) VALUES (NULL)'); -Assert::same('1', $connection->getInsertId('autoprimarykey_prim_seq')); +Assert::same(1, $connection->getInsertId('autoprimarykey_prim_seq')); $connection->query('INSERT INTO autoprimarykey (col) VALUES (NULL)'); -Assert::same('2', $connection->getInsertId('autoprimarykey_prim_seq')); +Assert::same(2, $connection->getInsertId('autoprimarykey_prim_seq')); $connection->query('INSERT INTO autoprimarykey (prim, col) VALUES (10, NULL)'); -Assert::same('2', $connection->getInsertId('autoprimarykey_prim_seq')); +Assert::same(2, $connection->getInsertId('autoprimarykey_prim_seq')); $connection->query(' @@ -52,10 +52,10 @@ $connection->query(' '); $connection->query('INSERT INTO multiautoprimarykey (prim2) VALUES (3)'); -Assert::same('1', $connection->getInsertId('multiautoprimarykey_prim1_seq')); +Assert::same(1, $connection->getInsertId('multiautoprimarykey_prim1_seq')); $connection->query('INSERT INTO multiautoprimarykey (prim2) VALUES (3)'); -Assert::same('2', $connection->getInsertId('multiautoprimarykey_prim1_seq')); +Assert::same(2, $connection->getInsertId('multiautoprimarykey_prim1_seq')); $connection->query('INSERT INTO multiautoprimarykey (prim1, prim2) VALUES (10, 3)'); -Assert::same('2', $connection->getInsertId('multiautoprimarykey_prim1_seq')); +Assert::same(2, $connection->getInsertId('multiautoprimarykey_prim1_seq')); diff --git a/tests/Database/Connection.getInsertId().sqlite.phpt b/tests/Database/Connection.getInsertId().sqlite.phpt index 9a4c5361c..60933c7ea 100644 --- a/tests/Database/Connection.getInsertId().sqlite.phpt +++ b/tests/Database/Connection.getInsertId().sqlite.phpt @@ -9,7 +9,7 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -$connection = connectToDB()->getConnection(); +$connection = connectToDB(); $connection->query(' CREATE TABLE [noprimarykey] ( @@ -18,10 +18,10 @@ $connection->query(' '); $connection->query('INSERT INTO noprimarykey (col) VALUES (NULL)'); -Assert::same('1', $connection->getInsertId()); +Assert::same(1, $connection->getInsertId()); $connection->query('INSERT INTO noprimarykey (col) VALUES (3)'); -Assert::same('2', $connection->getInsertId()); +Assert::same(2, $connection->getInsertId()); $connection->query(' @@ -31,10 +31,10 @@ $connection->query(' '); $connection->query('INSERT INTO primarykey (prim) VALUES (5)'); -Assert::same('5', $connection->getInsertId()); +Assert::same(5, $connection->getInsertId()); $connection->query('INSERT INTO primarykey (prim) VALUES (6)'); -Assert::same('6', $connection->getInsertId()); +Assert::same(6, $connection->getInsertId()); $connection->query(' @@ -45,10 +45,10 @@ $connection->query(' '); $connection->query('INSERT INTO autoprimarykey (col) VALUES (NULL)'); -Assert::same('1', $connection->getInsertId()); +Assert::same(1, $connection->getInsertId()); $connection->query('INSERT INTO autoprimarykey (col) VALUES (NULL)'); -Assert::same('2', $connection->getInsertId()); +Assert::same(2, $connection->getInsertId()); $connection->query('INSERT INTO autoprimarykey (prim, col) VALUES (10, NULL)'); -Assert::same('10', $connection->getInsertId()); +Assert::same(10, $connection->getInsertId()); diff --git a/tests/Database/Connection.getInsertId().sqlsrv.phpt b/tests/Database/Connection.getInsertId().sqlsrv.phpt index f7c3d173d..85f87650c 100644 --- a/tests/Database/Connection.getInsertId().sqlsrv.phpt +++ b/tests/Database/Connection.getInsertId().sqlsrv.phpt @@ -9,7 +9,7 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -$connection = connectToDB()->getConnection(); +$connection = connectToDB(); $connection->query("IF OBJECT_ID('noprimarykey', 'U') IS NOT NULL DROP TABLE noprimarykey"); $connection->query(' @@ -19,10 +19,10 @@ $connection->query(' '); $connection->query('INSERT INTO noprimarykey (col) VALUES (NULL)'); -Assert::same('', $connection->getInsertId()); - -$connection->query('INSERT INTO noprimarykey (col) VALUES (NULL)'); -Assert::same('', $connection->getInsertId()); +Assert::exception( + fn() => $connection->getInsertId(), + Nette\Database\DriverException::class, +); $connection->query("IF OBJECT_ID('primarykey', 'U') IS NOT NULL DROP TABLE primarykey"); @@ -34,10 +34,10 @@ $connection->query(' '); $connection->query('INSERT INTO primarykey (prim) VALUES (5)'); -Assert::same('', $connection->getInsertId()); - -$connection->query('INSERT INTO primarykey (prim) VALUES (6)'); -Assert::same('', $connection->getInsertId()); +Assert::exception( + fn() => $connection->getInsertId(), + Nette\Database\DriverException::class, +); $connection->query("IF OBJECT_ID('autoprimarykey', 'U') IS NOT NULL DROP TABLE autoprimarykey"); @@ -50,13 +50,13 @@ $connection->query(' '); $connection->query('INSERT INTO autoprimarykey (col) VALUES (NULL)'); -Assert::same('1', $connection->getInsertId()); +Assert::same(1, $connection->getInsertId()); $connection->query('INSERT INTO autoprimarykey (col) VALUES (NULL)'); -Assert::same('2', $connection->getInsertId()); +Assert::same(2, $connection->getInsertId()); $connection->query('SET IDENTITY_INSERT autoprimarykey ON; INSERT INTO autoprimarykey (prim, col) VALUES (10, NULL)'); -Assert::same('10', $connection->getInsertId()); +Assert::same(10, $connection->getInsertId()); $connection->query("IF OBJECT_ID('multiautoprimarykey', 'U') IS NOT NULL DROP TABLE multiautoprimarykey"); @@ -69,10 +69,10 @@ $connection->query(' '); $connection->query('INSERT INTO multiautoprimarykey (prim2) VALUES (3)'); -Assert::same('1', $connection->getInsertId()); +Assert::same(1, $connection->getInsertId()); $connection->query('INSERT INTO multiautoprimarykey (prim2) VALUES (3)'); -Assert::same('2', $connection->getInsertId()); +Assert::same(2, $connection->getInsertId()); $connection->query('SET IDENTITY_INSERT multiautoprimarykey ON; INSERT INTO multiautoprimarykey (prim1, prim2) VALUES (10, 3)'); -Assert::same('10', $connection->getInsertId()); +Assert::same(10, $connection->getInsertId()); diff --git a/tests/Database/Connection.preprocess.phpt b/tests/Database/Connection.preprocess.phpt index 61ce7f7a4..a6c2e5f9a 100644 --- a/tests/Database/Connection.preprocess.phpt +++ b/tests/Database/Connection.preprocess.phpt @@ -9,7 +9,7 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -$connection = connectToDB()->getConnection(); +$connection = connectToDB(); Assert::same(['SELECT name FROM author', []], $connection->preprocess('SELECT name FROM author')); diff --git a/tests/Database/Connection.query.phpt b/tests/Database/Connection.query.phpt index 17aaf67eb..22eb9fdd1 100644 --- a/tests/Database/Connection.query.phpt +++ b/tests/Database/Connection.query.phpt @@ -9,28 +9,28 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -$connection = connectToDB()->getConnection(); +$connection = connectToDB(); Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/files/{$driverName}-nette_test1.sql"); test('executes parameterized query and returns ResultSet', function () use ($connection) { $res = $connection->query('SELECT id FROM author WHERE id = ?', 11); - Assert::type(Nette\Database\ResultSet::class, $res); - Assert::same('SELECT id FROM author WHERE id = ?', $res->getQueryString()); - Assert::same([11], $res->getParameters()); - Assert::same('SELECT id FROM author WHERE id = ?', $connection->getLastQueryString()); + Assert::type(Nette\Database\Result::class, $res); + Assert::same('SELECT id FROM author WHERE id = ?', $res->getQuery()->getSql()); + Assert::same([11], $res->getQuery()->getParameters()); + Assert::same('SELECT id FROM author WHERE id = ?', $connection->getLastQuery()->getSql()); }); test('multiple query parameters', function () use ($connection) { $res = $connection->query('SELECT id FROM author WHERE id = ? OR id = ?', 11, 12); - Assert::same('SELECT id FROM author WHERE id = ? OR id = ?', $res->getQueryString()); - Assert::same([11, 12], $res->getParameters()); + Assert::same('SELECT id FROM author WHERE id = ? OR id = ?', $res->getQuery()->getSql()); + Assert::same([11, 12], $res->getQuery()->getParameters()); }); test('query with array of parameters', function () use ($connection) { - $res = $connection->queryArgs('SELECT id FROM author WHERE id = ? OR id = ?', [11, 12]); - Assert::same('SELECT id FROM author WHERE id = ? OR id = ?', $res->getQueryString()); - Assert::same([11, 12], $res->getParameters()); + $res = @$connection->queryArgs('SELECT id FROM author WHERE id = ? OR id = ?', [11, 12]); // is deprecated + Assert::same('SELECT id FROM author WHERE id = ? OR id = ?', $res->getQuery()->getSql()); + Assert::same([11, 12], $res->getQuery()->getParameters()); }); diff --git a/tests/Database/Connection.transaction.phpt b/tests/Database/Connection.transaction.phpt index 2e4de1501..cfb1dc1c9 100644 --- a/tests/Database/Connection.transaction.phpt +++ b/tests/Database/Connection.transaction.phpt @@ -5,12 +5,12 @@ * @dataProvider? databases.ini */ -use Nette\Database\Connection; +use Nette\Database\Explorer; use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -$connection = connectToDB()->getConnection(); +$connection = connectToDB(); Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/files/{$driverName}-nette_test1.sql"); @@ -25,7 +25,7 @@ test('rolls back transaction after manual control', function () use ($connection test('rolls back transaction on exception', function () use ($connection) { Assert::exception( - fn() => $connection->transaction(function (Connection $connection) { + fn() => $connection->transaction(function (Explorer $connection) { $connection->query('DELETE FROM book'); throw new Exception('my exception'); }), @@ -50,13 +50,13 @@ test('nested transaction() call fail', function () use ($connection) { $base = (int) $connection->query('SELECT COUNT(*) AS cnt FROM author')->fetchField(); Assert::exception( - fn() => $connection->transaction(function (Connection $connection) { + fn() => $connection->transaction(function (Explorer $connection) { $connection->query('INSERT INTO author', [ 'name' => 'A', 'web' => '', ]); - $connection->transaction(function (Connection $connection2) { + $connection->transaction(function (Explorer $connection2) { $connection2->query('INSERT INTO author', [ 'name' => 'B', 'web' => '', @@ -75,7 +75,7 @@ test('nested transaction() call fail', function () use ($connection) { test('nested transaction() call success', function () use ($connection) { $base = (int) $connection->query('SELECT COUNT(*) AS cnt FROM author')->fetchField(); - $connection->transaction(function (Connection $connection) { + $connection->transaction(function (Explorer $connection) { $connection->query('INSERT INTO author', [ 'name' => 'A', 'web' => '', @@ -95,18 +95,18 @@ test('beginTransaction(), commit() & rollBack() calls are forbidden in transacti Assert::exception( fn() => $connection->transaction(fn() => $connection->beginTransaction()), LogicException::class, - Connection::class . '::beginTransaction() call is forbidden inside a transaction() callback', + Explorer::class . '::beginTransaction() call is forbidden inside a transaction() callback', ); Assert::exception( fn() => $connection->transaction(fn() => $connection->commit()), LogicException::class, - Connection::class . '::commit() call is forbidden inside a transaction() callback', + Explorer::class . '::commit() call is forbidden inside a transaction() callback', ); Assert::exception( fn() => $connection->transaction(fn() => $connection->rollBack()), LogicException::class, - Connection::class . '::rollBack() call is forbidden inside a transaction() callback', + Explorer::class . '::rollBack() call is forbidden inside a transaction() callback', ); }); diff --git a/tests/Database/Conventions/DiscoveredConventions.getBelongsToReference().phpt b/tests/Database/Conventions/DiscoveredConventions.getBelongsToReference().phpt index bd8de938a..88e45ae0e 100644 --- a/tests/Database/Conventions/DiscoveredConventions.getBelongsToReference().phpt +++ b/tests/Database/Conventions/DiscoveredConventions.getBelongsToReference().phpt @@ -11,7 +11,7 @@ require __DIR__ . '/../../bootstrap.php'; test('basic test', function () { - $structure = Mockery::mock(Nette\Database\IStructure::class); + $structure = Mockery::mock(Nette\Database\Structure::class); $structure->shouldReceive('getBelongsToReference')->with('books')->andReturn([ 'author_id' => 'authors', 'translator_id' => 'authors', @@ -24,7 +24,7 @@ test('basic test', function () { }); test('basic test', function () { - $structure = Mockery::mock(Nette\Database\IStructure::class); + $structure = Mockery::mock(Nette\Database\Structure::class); $structure->shouldReceive('getBelongsToReference')->with('public.books')->andReturn([ 'author_id' => 'public.authors', 'translator_id' => 'public.authors', @@ -37,7 +37,7 @@ test('basic test', function () { }); test('tests order of table columns with foreign keys', function () { - $structure = Mockery::mock(Nette\Database\IStructure::class); + $structure = Mockery::mock(Nette\Database\Structure::class); $structure->shouldReceive('getBelongsToReference')->with('books')->andReturn([ 'translator_id' => 'authors', 'author_id' => 'authors', @@ -50,7 +50,7 @@ test('tests order of table columns with foreign keys', function () { test('tests case insensivity', function () { - $structure = Mockery::mock(Nette\Database\IStructure::class); + $structure = Mockery::mock(Nette\Database\Structure::class); $structure->shouldReceive('getBelongsToReference')->with('books')->andReturn([ 'author_id' => 'authors', 'translator_id' => 'authors', @@ -63,7 +63,7 @@ test('tests case insensivity', function () { test('tests case insensivity and prefixes', function () { - $structure = Mockery::mock(Nette\Database\IStructure::class); + $structure = Mockery::mock(Nette\Database\Structure::class); $structure->shouldReceive('getBelongsToReference')->with('nBooks')->andReturn([ 'authorId' => 'nAuthors', 'translatorId' => 'nAuthors', @@ -77,7 +77,7 @@ test('tests case insensivity and prefixes', function () { test('tests rebuilt', function () { - $structure = Mockery::mock(Nette\Database\IStructure::class); + $structure = Mockery::mock(Nette\Database\Structure::class); $structure->shouldReceive('isRebuilt')->andReturn(false); $structure->shouldReceive('rebuild'); $structure->shouldReceive('getBelongsToReference')->andReturn([])->once(); @@ -94,7 +94,7 @@ test('tests rebuilt', function () { test('tests already rebuilt structure', function () { - $structure = Mockery::mock(Nette\Database\IStructure::class); + $structure = Mockery::mock(Nette\Database\Structure::class); $structure->shouldReceive('isRebuilt')->andReturn(true); $structure->shouldReceive('getBelongsToReference')->with('books')->andReturn([])->once(); diff --git a/tests/Database/Conventions/DiscoveredConventions.getHasManyReference().phpt b/tests/Database/Conventions/DiscoveredConventions.getHasManyReference().phpt index ceb35142a..e03b9a305 100644 --- a/tests/Database/Conventions/DiscoveredConventions.getHasManyReference().phpt +++ b/tests/Database/Conventions/DiscoveredConventions.getHasManyReference().phpt @@ -11,7 +11,7 @@ require __DIR__ . '/../../bootstrap.php'; test('basic test singular', function () { - $structure = Mockery::mock(Nette\Database\IStructure::class); + $structure = Mockery::mock(Nette\Database\Structure::class); $structure->shouldReceive('getHasManyReference')->with('author')->andReturn([ 'book' => ['author_id', 'translator_id'], 'book_topics' => ['author_id'], @@ -41,7 +41,7 @@ test('basic test singular', function () { test('basic test singular with schema', function () { - $structure = Mockery::mock(Nette\Database\IStructure::class); + $structure = Mockery::mock(Nette\Database\Structure::class); $structure->shouldReceive('getHasManyReference')->with('public.author')->andReturn([ 'public.book' => ['author_id', 'translator_id'], 'public.book_topics' => ['author_id'], @@ -86,7 +86,7 @@ test('basic test singular with schema', function () { test('basic test plural', function () { - $structure = Mockery::mock(Nette\Database\IStructure::class); + $structure = Mockery::mock(Nette\Database\Structure::class); $structure->shouldReceive('getHasManyReference')->with('authors')->andReturn([ 'books' => ['author_id', 'translator_id'], ])->once(); @@ -110,7 +110,7 @@ test('basic test plural', function () { test('tests column match with source table', function () { - $structure = Mockery::mock(Nette\Database\IStructure::class); + $structure = Mockery::mock(Nette\Database\Structure::class); $structure->shouldReceive('getHasManyReference')->with('author')->andReturn([ 'book' => ['author_id', 'tran_id'], ])->once(); @@ -137,7 +137,7 @@ test('tests column match with source table', function () { test('tests case insensivity and prefixes', function () { - $structure = Mockery::mock(Nette\Database\IStructure::class); + $structure = Mockery::mock(Nette\Database\Structure::class); $structure->shouldReceive('getHasManyReference')->with('nAuthors')->andReturn([ 'nBooks' => ['authorId', 'translatorId'], ])->once(); @@ -149,7 +149,7 @@ test('tests case insensivity and prefixes', function () { test('tests rebuilt', function () { - $structure = Mockery::mock(Nette\Database\IStructure::class); + $structure = Mockery::mock(Nette\Database\Structure::class); $structure->shouldReceive('isRebuilt')->andReturn(false); $structure->shouldReceive('rebuild'); $structure->shouldReceive('getHasManyReference')->with('author')->andReturn([])->once(); @@ -163,7 +163,7 @@ test('tests rebuilt', function () { test('tests already rebuilt structure', function () { - $structure = Mockery::mock(Nette\Database\IStructure::class); + $structure = Mockery::mock(Nette\Database\Structure::class); $structure->shouldReceive('isRebuilt')->andReturn(true); $structure->shouldReceive('getHasManyReference')->with('author')->andReturn([])->once(); diff --git a/tests/Database/Conventions/DiscoveredConventions.getPrimary().phpt b/tests/Database/Conventions/DiscoveredConventions.getPrimary().phpt index d164334e2..4ad22412d 100644 --- a/tests/Database/Conventions/DiscoveredConventions.getPrimary().phpt +++ b/tests/Database/Conventions/DiscoveredConventions.getPrimary().phpt @@ -11,7 +11,7 @@ require __DIR__ . '/../../bootstrap.php'; test('', function () { - $structure = Mockery::mock(Nette\Database\IStructure::class); + $structure = Mockery::mock(Nette\Database\Structure::class); $structure->shouldReceive('getPrimaryKey')->with('books_x_tags')->andReturn(['book_id', 'tag_id']); $conventions = new DiscoveredConventions($structure); diff --git a/tests/Database/Drivers/MsSqlDriver.applyLimit.phpt b/tests/Database/Drivers/MsSqlDriver.applyLimit.phpt deleted file mode 100644 index 247c2cb87..000000000 --- a/tests/Database/Drivers/MsSqlDriver.applyLimit.phpt +++ /dev/null @@ -1,58 +0,0 @@ -applyLimit($query, 10, 20); -}, Nette\NotSupportedException::class, 'Offset is not supported by this database.'); - -Assert::exception(function () use ($driver) { - $query = 'SELECT 1 FROM t'; - $driver->applyLimit($query, 0, 20); -}, Nette\NotSupportedException::class, 'Offset is not supported by this database.'); - -$query = 'SELECT 1 FROM t'; -$driver->applyLimit($query, 10, 0); -Assert::same('SELECT TOP 10 1 FROM t', $query); - -Assert::exception(function () use ($driver) { - $query = 'SELECT 1 FROM t'; - $driver->applyLimit($query, null, 20); -}, Nette\NotSupportedException::class, 'Offset is not supported by this database.'); - -$query = 'SELECT 1 FROM t'; -$driver->applyLimit($query, 10, null); -Assert::same('SELECT TOP 10 1 FROM t', $query); - -$query = ' select distinct 1 FROM t'; -$driver->applyLimit($query, 10, null); -Assert::same(' select distinct TOP 10 1 FROM t', $query); - -$query = 'UPDATE t SET'; -$driver->applyLimit($query, 10, null); -Assert::same('UPDATE TOP 10 t SET', $query); - -$query = 'DELETE FROM t SET'; -$driver->applyLimit($query, 10, null); -Assert::same('DELETE TOP 10 FROM t SET', $query); - -Assert::exception(function () use ($driver) { - $query = 'SET FROM t'; - $driver->applyLimit($query, 10, null); -}, Nette\InvalidArgumentException::class, 'SQL query must begin with SELECT, UPDATE or DELETE command.'); - -Assert::exception(function () use ($driver) { - $query = 'SELECT 1 FROM t'; - $driver->applyLimit($query, -1, null); -}, Nette\InvalidArgumentException::class, 'Negative offset or limit.'); - -Assert::exception(function () use ($driver) { - $query = 'SELECT 1 FROM t'; - $driver->applyLimit($query, null, -1); -}, Nette\NotSupportedException::class, 'Offset is not supported by this database.'); diff --git a/tests/Database/Drivers/MySqlDriver.applyLimit.phpt b/tests/Database/Drivers/MySqlDriver.applyLimit.phpt deleted file mode 100644 index 521033bbe..000000000 --- a/tests/Database/Drivers/MySqlDriver.applyLimit.phpt +++ /dev/null @@ -1,42 +0,0 @@ -getConnection(); -$driver = $connection->getDriver(); - -$query = 'SELECT 1 FROM t'; -$driver->applyLimit($query, 10, 20); -Assert::same('SELECT 1 FROM t LIMIT 10 OFFSET 20', $query); - -$query = 'SELECT 1 FROM t'; -$driver->applyLimit($query, 0, 20); -Assert::same('SELECT 1 FROM t LIMIT 0 OFFSET 20', $query); - -$query = 'SELECT 1 FROM t'; -$driver->applyLimit($query, 10, 0); -Assert::same('SELECT 1 FROM t LIMIT 10', $query); - -$query = 'SELECT 1 FROM t'; -$driver->applyLimit($query, null, 20); -Assert::same('SELECT 1 FROM t LIMIT 18446744073709551615 OFFSET 20', $query); - -$query = 'SELECT 1 FROM t'; -$driver->applyLimit($query, 10, null); -Assert::same('SELECT 1 FROM t LIMIT 10', $query); - -Assert::exception(function () use ($driver) { - $query = 'SELECT 1 FROM t'; - $driver->applyLimit($query, -1, null); -}, Nette\InvalidArgumentException::class, 'Negative offset or limit.'); - -Assert::exception(function () use ($driver) { - $query = 'SELECT 1 FROM t'; - $driver->applyLimit($query, null, -1); -}, Nette\InvalidArgumentException::class, 'Negative offset or limit.'); diff --git a/tests/Database/Drivers/MySqlDriver.formatLike.phpt b/tests/Database/Drivers/MySqlDriver.formatLike.phpt deleted file mode 100644 index f9312a162..000000000 --- a/tests/Database/Drivers/MySqlDriver.formatLike.phpt +++ /dev/null @@ -1,30 +0,0 @@ -getConnection(); -$driver = $connection->getDriver(); - -Assert::same(0, $connection->query("SELECT 'AAxBB' LIKE", $connection::literal($driver->formatLike('A_B', 0)))->fetchField()); -Assert::same(1, $connection->query("SELECT 'AA_BB' LIKE", $connection::literal($driver->formatLike('A_B', 0)))->fetchField()); - -Assert::same(0, $connection->query("SELECT 'AAxBB' LIKE", $connection::literal($driver->formatLike('A%B', 0)))->fetchField()); -Assert::same(1, $connection->query("SELECT 'AA%BB' LIKE", $connection::literal($driver->formatLike('A%B', 0)))->fetchField()); - -Assert::same(0, $connection->query("SELECT 'AAxBB' LIKE", $connection::literal($driver->formatLike("A'B", 0)))->fetchField()); -Assert::same(1, $connection->query("SELECT 'AA''BB' LIKE", $connection::literal($driver->formatLike("A'B", 0)))->fetchField()); - -Assert::same(0, $connection->query("SELECT 'AAxBB' LIKE", $connection::literal($driver->formatLike('A"B', 0)))->fetchField()); -Assert::same(1, $connection->query("SELECT 'AA\"BB' LIKE", $connection::literal($driver->formatLike('A"B', 0)))->fetchField()); - -Assert::same(0, $connection->query("SELECT 'AAxBB' LIKE", $connection::literal($driver->formatLike('A\B', 0)))->fetchField()); -Assert::same(1, $connection->query("SELECT 'AA\\\\BB' LIKE", $connection::literal($driver->formatLike('A\B', 0)))->fetchField()); - -Assert::same(0, $connection->query("SELECT 'AAxBB' LIKE", $connection::literal($driver->formatLike('A\%B', 0)))->fetchField()); -Assert::same(1, $connection->query("SELECT 'AA\\\\%BB' LIKE", $connection::literal($driver->formatLike('A\%B', 0)))->fetchField()); diff --git a/tests/Database/Drivers/OciDriver.applyLimit.phpt b/tests/Database/Drivers/OciDriver.applyLimit.phpt deleted file mode 100644 index ad0b866ee..000000000 --- a/tests/Database/Drivers/OciDriver.applyLimit.phpt +++ /dev/null @@ -1,38 +0,0 @@ -applyLimit($query, 10, 20); -Assert::same('SELECT * FROM (SELECT t.*, ROWNUM AS "__rnum" FROM (SELECT 1 FROM t) t ) WHERE "__rnum" > 20 AND "__rnum" <= 30', $query); - -$query = 'SELECT 1 FROM t'; -$driver->applyLimit($query, 0, 20); -Assert::same('SELECT * FROM (SELECT t.*, ROWNUM AS "__rnum" FROM (SELECT 1 FROM t) t ) WHERE "__rnum" > 20 AND "__rnum" <= 20', $query); - -$query = 'SELECT 1 FROM t'; -$driver->applyLimit($query, 10, 0); -Assert::same('SELECT * FROM (SELECT 1 FROM t) WHERE ROWNUM <= 10', $query); - -$query = 'SELECT 1 FROM t'; -$driver->applyLimit($query, null, 20); -Assert::same('SELECT * FROM (SELECT t.*, ROWNUM AS "__rnum" FROM (SELECT 1 FROM t) t ) WHERE "__rnum" > 20', $query); - -$query = 'SELECT 1 FROM t'; -$driver->applyLimit($query, 10, null); -Assert::same('SELECT * FROM (SELECT 1 FROM t) WHERE ROWNUM <= 10', $query); - -Assert::exception(function () use ($driver) { - $query = 'SELECT 1 FROM t'; - $driver->applyLimit($query, -1, null); -}, Nette\InvalidArgumentException::class, 'Negative offset or limit.'); - -Assert::exception(function () use ($driver) { - $query = 'SELECT 1 FROM t'; - $driver->applyLimit($query, null, -1); -}, Nette\InvalidArgumentException::class, 'Negative offset or limit.'); diff --git a/tests/Database/Drivers/OdbcDriver.applyLimit.phpt b/tests/Database/Drivers/OdbcDriver.applyLimit.phpt deleted file mode 100644 index b9480efe5..000000000 --- a/tests/Database/Drivers/OdbcDriver.applyLimit.phpt +++ /dev/null @@ -1,58 +0,0 @@ -applyLimit($query, 10, 20); -}, Nette\NotSupportedException::class, 'Offset is not supported by this database.'); - -Assert::exception(function () use ($driver) { - $query = 'SELECT 1 FROM t'; - $driver->applyLimit($query, 0, 20); -}, Nette\NotSupportedException::class, 'Offset is not supported by this database.'); - -$query = 'SELECT 1 FROM t'; -$driver->applyLimit($query, 10, 0); -Assert::same('SELECT TOP 10 1 FROM t', $query); - -Assert::exception(function () use ($driver) { - $query = 'SELECT 1 FROM t'; - $driver->applyLimit($query, null, 20); -}, Nette\NotSupportedException::class, 'Offset is not supported by this database.'); - -$query = 'SELECT 1 FROM t'; -$driver->applyLimit($query, 10, null); -Assert::same('SELECT TOP 10 1 FROM t', $query); - -$query = ' select distinct 1 FROM t'; -$driver->applyLimit($query, 10, null); -Assert::same(' select distinct TOP 10 1 FROM t', $query); - -$query = 'UPDATE t SET'; -$driver->applyLimit($query, 10, null); -Assert::same('UPDATE TOP 10 t SET', $query); - -$query = 'DELETE FROM t SET'; -$driver->applyLimit($query, 10, null); -Assert::same('DELETE TOP 10 FROM t SET', $query); - -Assert::exception(function () use ($driver) { - $query = 'SET FROM t'; - $driver->applyLimit($query, 10, null); -}, Nette\InvalidArgumentException::class, 'SQL query must begin with SELECT, UPDATE or DELETE command.'); - -Assert::exception(function () use ($driver) { - $query = 'SELECT 1 FROM t'; - $driver->applyLimit($query, -1, null); -}, Nette\InvalidArgumentException::class, 'Negative offset or limit.'); - -Assert::exception(function () use ($driver) { - $query = 'SELECT 1 FROM t'; - $driver->applyLimit($query, null, -1); -}, Nette\NotSupportedException::class, 'Offset is not supported by this database.'); diff --git a/tests/Database/Drivers/PgSqlDriver.applyLimit.phpt b/tests/Database/Drivers/PgSqlDriver.applyLimit.phpt deleted file mode 100644 index 3e365a105..000000000 --- a/tests/Database/Drivers/PgSqlDriver.applyLimit.phpt +++ /dev/null @@ -1,38 +0,0 @@ -applyLimit($query, 10, 20); -Assert::same('SELECT 1 FROM t LIMIT 10 OFFSET 20', $query); - -$query = 'SELECT 1 FROM t'; -$driver->applyLimit($query, 0, 20); -Assert::same('SELECT 1 FROM t LIMIT 0 OFFSET 20', $query); - -$query = 'SELECT 1 FROM t'; -$driver->applyLimit($query, 10, 0); -Assert::same('SELECT 1 FROM t LIMIT 10', $query); - -$query = 'SELECT 1 FROM t'; -$driver->applyLimit($query, null, 20); -Assert::same('SELECT 1 FROM t OFFSET 20', $query); - -$query = 'SELECT 1 FROM t'; -$driver->applyLimit($query, 10, null); -Assert::same('SELECT 1 FROM t LIMIT 10', $query); - -Assert::exception(function () use ($driver) { - $query = 'SELECT 1 FROM t'; - $driver->applyLimit($query, -1, null); -}, Nette\InvalidArgumentException::class, 'Negative offset or limit.'); - -Assert::exception(function () use ($driver) { - $query = 'SELECT 1 FROM t'; - $driver->applyLimit($query, null, -1); -}, Nette\InvalidArgumentException::class, 'Negative offset or limit.'); diff --git a/tests/Database/Drivers/PgSqlDriver.formatLike.phpt b/tests/Database/Drivers/PgSqlDriver.formatLike.phpt deleted file mode 100644 index c12cd5123..000000000 --- a/tests/Database/Drivers/PgSqlDriver.formatLike.phpt +++ /dev/null @@ -1,40 +0,0 @@ -getConnection(); - -$tests = function ($connection) { - $driver = $connection->getDriver(); - - Assert::false($connection->query("SELECT 'AAxBB' LIKE", $connection::literal($driver->formatLike('A_B', 0)))->fetchField()); - Assert::true($connection->query("SELECT 'AA_BB' LIKE", $connection::literal($driver->formatLike('A_B', 0)))->fetchField()); - - Assert::false($connection->query("SELECT 'AAxBB' LIKE", $connection::literal($driver->formatLike('A%B', 0)))->fetchField()); - Assert::true($connection->query("SELECT 'AA%BB' LIKE", $connection::literal($driver->formatLike('A%B', 0)))->fetchField()); - - Assert::false($connection->query("SELECT 'AAxBB' LIKE", $connection::literal($driver->formatLike("A'B", 0)))->fetchField()); - Assert::true($connection->query("SELECT 'AA''BB' LIKE", $connection::literal($driver->formatLike("A'B", 0)))->fetchField()); - - Assert::false($connection->query("SELECT 'AAxBB' LIKE", $connection::literal($driver->formatLike('A"B', 0)))->fetchField()); - Assert::true($connection->query("SELECT 'AA\"BB' LIKE", $connection::literal($driver->formatLike('A"B', 0)))->fetchField()); -}; - -$driver = $connection->getDriver(); -$connection->query('SET escape_string_warning TO off'); // do not log warnings - -$connection->query('SET standard_conforming_strings TO on'); -$tests($connection); -Assert::false($connection->query("SELECT 'AAxBB' LIKE", $connection::literal($driver->formatLike('A\B', 0)))->fetchField()); -Assert::true($connection->query("SELECT 'AA\\BB' LIKE", $connection::literal($driver->formatLike('A\B', 0)))->fetchField()); - -$connection->query('SET standard_conforming_strings TO off'); -$tests($connection); -Assert::false($connection->query("SELECT 'AAxBB' LIKE", $connection::literal($driver->formatLike('A\B', 0)))->fetchField()); -Assert::true($connection->query("SELECT 'AA\\\\BB' LIKE", $connection::literal($driver->formatLike('A\B', 0)))->fetchField()); diff --git a/tests/Database/Drivers/SqliteDriver.applyLimit.phpt b/tests/Database/Drivers/SqliteDriver.applyLimit.phpt deleted file mode 100644 index 3bd86e28d..000000000 --- a/tests/Database/Drivers/SqliteDriver.applyLimit.phpt +++ /dev/null @@ -1,38 +0,0 @@ -applyLimit($query, 10, 20); -Assert::same('SELECT 1 FROM t LIMIT 10 OFFSET 20', $query); - -$query = 'SELECT 1 FROM t'; -$driver->applyLimit($query, 0, 20); -Assert::same('SELECT 1 FROM t LIMIT 0 OFFSET 20', $query); - -$query = 'SELECT 1 FROM t'; -$driver->applyLimit($query, 10, 0); -Assert::same('SELECT 1 FROM t LIMIT 10', $query); - -$query = 'SELECT 1 FROM t'; -$driver->applyLimit($query, null, 20); -Assert::same('SELECT 1 FROM t LIMIT -1 OFFSET 20', $query); - -$query = 'SELECT 1 FROM t'; -$driver->applyLimit($query, 10, null); -Assert::same('SELECT 1 FROM t LIMIT 10', $query); - -Assert::exception(function () use ($driver) { - $query = 'SELECT 1 FROM t'; - $driver->applyLimit($query, -1, null); -}, Nette\InvalidArgumentException::class, 'Negative offset or limit.'); - -Assert::exception(function () use ($driver) { - $query = 'SELECT 1 FROM t'; - $driver->applyLimit($query, null, -1); -}, Nette\InvalidArgumentException::class, 'Negative offset or limit.'); diff --git a/tests/Database/Drivers/SqliteDriver.formatLike.phpt b/tests/Database/Drivers/SqliteDriver.formatLike.phpt deleted file mode 100644 index 6510f0999..000000000 --- a/tests/Database/Drivers/SqliteDriver.formatLike.phpt +++ /dev/null @@ -1,30 +0,0 @@ -getConnection(); -$driver = $connection->getDriver(); - -Assert::same(0, $connection->query("SELECT 'AAxBB' LIKE", $connection::literal($driver->formatLike('A_B', 0)))->fetchField()); -Assert::same(1, $connection->query("SELECT 'AA_BB' LIKE", $connection::literal($driver->formatLike('A_B', 0)))->fetchField()); - -Assert::same(0, $connection->query("SELECT 'AAxBB' LIKE", $connection::literal($driver->formatLike('A%B', 0)))->fetchField()); -Assert::same(1, $connection->query("SELECT 'AA%BB' LIKE", $connection::literal($driver->formatLike('A%B', 0)))->fetchField()); - -Assert::same(0, $connection->query("SELECT 'AAxBB' LIKE", $connection::literal($driver->formatLike("A'B", 0)))->fetchField()); -Assert::same(1, $connection->query("SELECT 'AA''BB' LIKE", $connection::literal($driver->formatLike("A'B", 0)))->fetchField()); - -Assert::same(0, $connection->query("SELECT 'AAxBB' LIKE", $connection::literal($driver->formatLike('A"B', 0)))->fetchField()); -Assert::same(1, $connection->query("SELECT 'AA\"BB' LIKE", $connection::literal($driver->formatLike('A"B', 0)))->fetchField()); - -Assert::same(0, $connection->query("SELECT 'AAxBB' LIKE", $connection::literal($driver->formatLike('A\B', 0)))->fetchField()); -Assert::same(1, $connection->query("SELECT 'AA\\BB' LIKE", $connection::literal($driver->formatLike('A\B', 0)))->fetchField()); - -Assert::same(0, $connection->query("SELECT 'AAxBB' LIKE", $connection::literal($driver->formatLike('A\%B', 0)))->fetchField()); -Assert::same(1, $connection->query("SELECT 'AA\\%BB' LIKE", $connection::literal($driver->formatLike('A\%B', 0)))->fetchField()); diff --git a/tests/Database/Drivers/SqlsrvDriver.applyLimit.phpt b/tests/Database/Drivers/SqlsrvDriver.applyLimit.phpt deleted file mode 100644 index 34668674f..000000000 --- a/tests/Database/Drivers/SqlsrvDriver.applyLimit.phpt +++ /dev/null @@ -1,40 +0,0 @@ -applyLimit($query, 10, 20); -Assert::same('SELECT 1 FROM t OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY', $query); - -$query = 'SELECT 1 FROM t'; -$driver->applyLimit($query, 0, 20); -Assert::same('SELECT 1 FROM t OFFSET 20 ROWS FETCH NEXT 0 ROWS ONLY', $query); - -$query = 'SELECT 1 FROM t'; -$driver->applyLimit($query, 10, 0); -Assert::same('SELECT 1 FROM t OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY', $query); - -$query = 'SELECT 1 FROM t'; -$driver->applyLimit($query, null, 20); -Assert::same('SELECT 1 FROM t OFFSET 20 ROWS FETCH NEXT 0 ROWS ONLY', $query); - -$query = 'SELECT 1 FROM t'; -$driver->applyLimit($query, 10, null); -Assert::same('SELECT 1 FROM t OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY', $query); - - - -Assert::exception(function () use ($driver) { - $query = 'SELECT 1 FROM t'; - $driver->applyLimit($query, -1, null); -}, Nette\InvalidArgumentException::class, 'Negative offset or limit.'); - -Assert::exception(function () use ($driver) { - $query = 'SELECT 1 FROM t'; - $driver->applyLimit($query, null, -1); -}, Nette\InvalidArgumentException::class, 'Negative offset or limit.'); diff --git a/tests/Database/Drivers/SqlsrvDriver.formatLike.phpt b/tests/Database/Drivers/SqlsrvDriver.formatLike.phpt deleted file mode 100644 index 869ef8ed5..000000000 --- a/tests/Database/Drivers/SqlsrvDriver.formatLike.phpt +++ /dev/null @@ -1,33 +0,0 @@ -getConnection(); -$driver = $connection->getDriver(); - -Assert::same(0, $connection->query("SELECT CASE WHEN 'AAxBB' LIKE", $connection::literal($driver->formatLike('A_B', 0)), 'THEN 1 ELSE 0 END AS col')->fetchField()); -Assert::same(1, $connection->query("SELECT CASE WHEN 'AA_BB' LIKE", $connection::literal($driver->formatLike('A_B', 0)), 'THEN 1 ELSE 0 END AS col')->fetchField()); - -Assert::same(0, $connection->query("SELECT CASE WHEN 'AAxBB' LIKE", $connection::literal($driver->formatLike('A%B', 0)), 'THEN 1 ELSE 0 END AS col')->fetchField()); -Assert::same(1, $connection->query("SELECT CASE WHEN 'AA%BB' LIKE", $connection::literal($driver->formatLike('A%B', 0)), 'THEN 1 ELSE 0 END AS col')->fetchField()); - -Assert::same(0, $connection->query("SELECT CASE WHEN 'AAxBB' LIKE", $connection::literal($driver->formatLike("A'B", 0)), 'THEN 1 ELSE 0 END AS col')->fetchField()); -Assert::same(1, $connection->query("SELECT CASE WHEN 'AA''BB' LIKE", $connection::literal($driver->formatLike("A'B", 0)), 'THEN 1 ELSE 0 END AS col')->fetchField()); - -Assert::same(0, $connection->query("SELECT CASE WHEN 'AAxBB' LIKE", $connection::literal($driver->formatLike('A"B', 0)), 'THEN 1 ELSE 0 END AS col')->fetchField()); -Assert::same(1, $connection->query("SELECT CASE WHEN 'AA\"BB' LIKE", $connection::literal($driver->formatLike('A"B', 0)), 'THEN 1 ELSE 0 END AS col')->fetchField()); - -Assert::same(0, $connection->query("SELECT CASE WHEN 'AAxBB' LIKE", $connection::literal($driver->formatLike('A\B', 0)), 'THEN 1 ELSE 0 END AS col')->fetchField()); -Assert::same(1, $connection->query("SELECT CASE WHEN 'AA\\BB' LIKE", $connection::literal($driver->formatLike('A\B', 0)), 'THEN 1 ELSE 0 END AS col')->fetchField()); - -Assert::same(0, $connection->query("SELECT CASE WHEN 'AAxBB' LIKE", $connection::literal($driver->formatLike('A\%B', 0)), 'THEN 1 ELSE 0 END AS col')->fetchField()); -Assert::same(1, $connection->query("SELECT CASE WHEN 'AA\\%BB' LIKE", $connection::literal($driver->formatLike('A\%B', 0)), 'THEN 1 ELSE 0 END AS col')->fetchField()); - -Assert::same(0, $connection->query("SELECT CASE WHEN 'AAxBB' LIKE", $connection::literal($driver->formatLike('A[a-z]B', 0)), 'THEN 1 ELSE 0 END AS col')->fetchField()); -Assert::same(1, $connection->query("SELECT CASE WHEN 'AA[a-z]BB' LIKE", $connection::literal($driver->formatLike('A[a-z]B', 0)), 'THEN 1 ELSE 0 END AS col')->fetchField()); diff --git a/tests/Database/Reflection.postgre.10.phpt b/tests/Database/Engine.postgre.10.phpt similarity index 77% rename from tests/Database/Reflection.postgre.10.phpt rename to tests/Database/Engine.postgre.10.phpt index 56f511045..b1f4eaf59 100644 --- a/tests/Database/Reflection.postgre.10.phpt +++ b/tests/Database/Engine.postgre.10.phpt @@ -10,7 +10,7 @@ use Tester\Environment; require __DIR__ . '/../bootstrap.php'; -$connection = connectToDB()->getConnection(); +$connection = connectToDB(); $ver = $connection->query('SHOW server_version')->fetchField(); if (version_compare($ver, '10') < 0) { @@ -22,13 +22,13 @@ function shortInfo(array $columns): array { return array_map(fn(array $col): array => [ 'name' => $col['name'], - 'autoincrement' => $col['autoincrement'], + 'autoIncrement' => $col['autoIncrement'], 'sequence' => $col['vendor']['sequence'], ], $columns); } -test('SERIAL and IDENTITY imply autoincrement on primary keys', function () use ($connection) { +test('SERIAL and IDENTITY imply autoIncrement on primary keys', function () use ($connection) { Nette\Database\Helpers::loadFromFile($connection, Tester\FileMock::create(' DROP SCHEMA IF EXISTS "reflection_10" CASCADE; CREATE SCHEMA "reflection_10"; @@ -43,53 +43,53 @@ test('SERIAL and IDENTITY imply autoincrement on primary keys', function () use CREATE TABLE "reflection_10"."identity_by_default_pk" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY); ')); - $driver = $connection->getDriver(); + $engine = $connection->getDatabaseEngine(); $connection->query('SET search_path TO reflection_10'); $columns = [ - 'serial' => shortInfo($driver->getColumns('serial')), - 'serial_pk' => shortInfo($driver->getColumns('serial_pk')), - 'identity_always' => shortInfo($driver->getColumns('identity_always')), - 'identity_always_pk' => shortInfo($driver->getColumns('identity_always_pk')), - 'identity_by_default' => shortInfo($driver->getColumns('identity_by_default')), - 'identity_by_default_pk' => shortInfo($driver->getColumns('identity_by_default_pk')), + 'serial' => shortInfo($engine->getColumns('serial')), + 'serial_pk' => shortInfo($engine->getColumns('serial_pk')), + 'identity_always' => shortInfo($engine->getColumns('identity_always')), + 'identity_always_pk' => shortInfo($engine->getColumns('identity_always_pk')), + 'identity_by_default' => shortInfo($engine->getColumns('identity_by_default')), + 'identity_by_default_pk' => shortInfo($engine->getColumns('identity_by_default_pk')), ]; Assert::same([ 'serial' => [[ 'name' => 'id', - 'autoincrement' => false, + 'autoIncrement' => false, 'sequence' => 'serial_id_seq', ]], 'serial_pk' => [[ 'name' => 'id', - 'autoincrement' => true, + 'autoIncrement' => true, 'sequence' => 'serial_pk_id_seq', ]], 'identity_always' => [[ 'name' => 'id', - 'autoincrement' => false, + 'autoIncrement' => false, 'sequence' => 'identity_always_id_seq', ]], 'identity_always_pk' => [[ 'name' => 'id', - 'autoincrement' => true, + 'autoIncrement' => true, 'sequence' => 'identity_always_pk_id_seq', ]], 'identity_by_default' => [[ 'name' => 'id', - 'autoincrement' => false, + 'autoIncrement' => false, 'sequence' => 'identity_by_default_id_seq', ]], 'identity_by_default_pk' => [[ 'name' => 'id', - 'autoincrement' => true, + 'autoIncrement' => true, 'sequence' => 'identity_by_default_pk_id_seq', ]], ], $columns); @@ -109,18 +109,18 @@ test('Materialized view columns', function () use ($connection) { CREATE MATERIALIZED VIEW "reflection_10"."source_mt" AS SELECT "name", "id" FROM "reflection_10"."source"; ')); - $driver = $connection->getDriver(); + $engine = $connection->getDatabaseEngine(); $connection->query('SET search_path TO reflection_10'); Assert::same([ ['name' => 'source', 'view' => false, 'fullName' => 'reflection_10.source', 'comment' => ''], ['name' => 'source_mt', 'view' => true, 'fullName' => 'reflection_10.source_mt', 'comment' => ''], - ], $driver->getTables()); + ], $engine->getTables()); Assert::same( ['name', 'id'], - array_column($driver->getColumns('source_mt'), 'name'), + array_column($engine->getColumns('source_mt'), 'name'), ); }); @@ -138,22 +138,22 @@ test('Partitioned table', function () use ($connection) { CREATE TABLE "reflection_10"."part_1" PARTITION OF "reflection_10"."parted" FOR VALUES FROM (1) TO (10); ')); - $driver = $connection->getDriver(); + $engine = $connection->getDatabaseEngine(); $connection->query('SET search_path TO reflection_10'); Assert::same([ ['name' => 'part_1', 'view' => false, 'fullName' => 'reflection_10.part_1', 'comment' => ''], ['name' => 'parted', 'view' => false, 'fullName' => 'reflection_10.parted', 'comment' => ''], - ], $driver->getTables()); + ], $engine->getTables()); - Assert::same(['id', 'value'], array_column($driver->getColumns('parted'), 'name')); - Assert::same(['id', 'value'], array_column($driver->getColumns('part_1'), 'name')); + Assert::same(['id', 'value'], array_column($engine->getColumns('parted'), 'name')); + Assert::same(['id', 'value'], array_column($engine->getColumns('part_1'), 'name')); Assert::same([[ 'name' => 'parted_pkey', 'unique' => true, 'primary' => true, 'columns' => ['id'], - ]], $driver->getIndexes('parted')); + ]], $engine->getIndexes('parted')); }); diff --git a/tests/Database/Reflection.postgre.phpt b/tests/Database/Engine.postgre.phpt similarity index 66% rename from tests/Database/Reflection.postgre.phpt rename to tests/Database/Engine.postgre.phpt index b3788e82a..8f120c898 100644 --- a/tests/Database/Reflection.postgre.phpt +++ b/tests/Database/Engine.postgre.phpt @@ -9,7 +9,7 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -$connection = connectToDB()->getConnection(); +$connection = connectToDB(); function names($columns): array @@ -41,38 +41,38 @@ test('Tables in schema', function () use ($connection) { ALTER TABLE "two"."slave" ADD CONSTRAINT "two_slave_fk" FOREIGN KEY ("two_id") REFERENCES "two"."master"("two_id"); ')); - $driver = $connection->getDriver(); + $engine = $connection->getDatabaseEngine(); // Reflection for tables with the same name but different schema $connection->query('SET search_path TO one, two'); - Assert::same(['master', 'slave'], names($driver->getTables())); - Assert::same(['one_id'], names($driver->getColumns('master'))); - Assert::same(['one_master_pkey'], names($driver->getIndexes('master'))); - Assert::same(['one_slave_fk'], names($driver->getForeignKeys('slave'))); + Assert::same(['master', 'slave'], names($engine->getTables())); + Assert::same(['one_id'], names($engine->getColumns('master'))); + Assert::same(['one_master_pkey'], names($engine->getIndexes('master'))); + Assert::same(['one_slave_fk'], names($engine->getForeignKeys('slave'))); $connection->query('SET search_path TO two, one'); - Assert::same(['master', 'slave'], names($driver->getTables())); - Assert::same(['two_id'], names($driver->getColumns('master'))); - Assert::same(['two_master_pkey'], names($driver->getIndexes('master'))); - Assert::same(['two_slave_fk'], names($driver->getForeignKeys('slave'))); + Assert::same(['master', 'slave'], names($engine->getTables())); + Assert::same(['two_id'], names($engine->getColumns('master'))); + Assert::same(['two_master_pkey'], names($engine->getIndexes('master'))); + Assert::same(['two_slave_fk'], names($engine->getForeignKeys('slave'))); // Reflection for FQN - Assert::same(['one_id'], names($driver->getColumns('one.master'))); - Assert::same(['one_master_pkey'], names($driver->getIndexes('one.master'))); - $foreign = $driver->getForeignKeys('one.slave'); + Assert::same(['one_id'], names($engine->getColumns('one.master'))); + Assert::same(['one_master_pkey'], names($engine->getIndexes('one.master'))); + $foreign = $engine->getForeignKeys('one.slave'); Assert::same([ 'name' => 'one_slave_fk', - 'local' => 'one_id', + 'local' => ['one_id'], 'table' => 'one.master', - 'foreign' => 'one_id', + 'foreign' => ['one_id'], ], (array) $foreign[0]); // Limit foreign keys for current schemas only $connection->query('ALTER TABLE "one"."slave" ADD CONSTRAINT "one_two_fk" FOREIGN KEY ("one_id") REFERENCES "two"."master"("two_id")'); $connection->query('SET search_path TO one'); - Assert::same(['one_slave_fk'], names($driver->getForeignKeys('slave'))); + Assert::same(['one_slave_fk'], names($engine->getForeignKeys('slave'))); $connection->query('SET search_path TO one, two'); - Assert::same(['one_slave_fk', 'one_two_fk'], names($driver->getForeignKeys('slave'))); + Assert::same(['one_slave_fk', 'one_two_fk'], names($engine->getForeignKeys('slave'))); }); diff --git a/tests/Database/Reflection.driver.phpt b/tests/Database/Engine.reflection.phpt similarity index 82% rename from tests/Database/Reflection.driver.phpt rename to tests/Database/Engine.reflection.phpt index 6d0c79060..40c8ffad3 100644 --- a/tests/Database/Reflection.driver.phpt +++ b/tests/Database/Engine.reflection.phpt @@ -5,23 +5,22 @@ * @dataProvider? databases.ini */ -use Nette\Database\Driver; +use Nette\Database\Drivers\Engine; use Tester\Assert; require __DIR__ . '/../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/files/{$driverName}-nette_test1.sql"); -$driver = $connection->getDriver(); -$tables = $driver->getTables(); +$engine = $explorer->getDatabaseEngine(); +$tables = $engine->getTables(); $tables = array_filter($tables, fn($t) => in_array($t['name'], ['author', 'book', 'book_tag', 'tag'], true)); usort($tables, fn($a, $b) => strcmp($a['name'], $b['name'])); -if ($driver->isSupported(Driver::SupportSchema)) { +if ($engine->isSupported(Engine::SupportSchema)) { Assert::same( [ ['name' => 'author', 'view' => false, 'fullName' => 'public.author', 'comment' => 'Table containing book authors'], @@ -48,7 +47,7 @@ if ($driver->isSupported(Driver::SupportSchema)) { } -$columns = $driver->getColumns('author'); +$columns = $engine->getColumns('author'); array_walk($columns, function (&$item) { Assert::type('array', $item['vendor']); unset($item['vendor']); @@ -58,44 +57,48 @@ $expectedColumns = [ [ 'name' => 'id', 'table' => 'author', - 'nativetype' => 'INT', + 'nativeType' => 'INT', 'size' => 11, + 'scale' => null, 'nullable' => false, 'default' => null, - 'autoincrement' => true, + 'autoIncrement' => true, 'primary' => true, 'comment' => '', ], [ 'name' => 'name', 'table' => 'author', - 'nativetype' => 'VARCHAR', + 'nativeType' => 'VARCHAR', 'size' => 30, + 'scale' => null, 'nullable' => false, 'default' => null, - 'autoincrement' => false, + 'autoIncrement' => false, 'primary' => false, 'comment' => '', ], [ 'name' => 'web', 'table' => 'author', - 'nativetype' => 'VARCHAR', + 'nativeType' => 'VARCHAR', 'size' => 100, + 'scale' => null, 'nullable' => false, 'default' => null, - 'autoincrement' => false, + 'autoIncrement' => false, 'primary' => false, 'comment' => 'Author\'s website URL', ], [ 'name' => 'born', 'table' => 'author', - 'nativetype' => 'DATE', + 'nativeType' => 'DATE', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, - 'autoincrement' => false, + 'autoIncrement' => false, 'primary' => false, 'comment' => '', ], @@ -103,25 +106,25 @@ $expectedColumns = [ switch ($driverName) { case 'mysql': - $version = $connection->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION); + $version = $explorer->getServerVersion(); if (version_compare($version, '8.0', '>=')) { $expectedColumns[0]['size'] = null; } break; case 'pgsql': - $expectedColumns[0]['nativetype'] = 'INT4'; + $expectedColumns[0]['nativeType'] = 'INT4'; $expectedColumns[0]['default'] = "nextval('author_id_seq'::regclass)"; $expectedColumns[0]['size'] = 4; $expectedColumns[3]['size'] = 4; break; case 'sqlite': - $expectedColumns[0]['nativetype'] = 'INTEGER'; + $expectedColumns[0]['nativeType'] = 'INTEGER'; $expectedColumns[0]['size'] = null; + $expectedColumns[1]['nativeType'] = 'TEXT'; $expectedColumns[0]['comment'] = null; - $expectedColumns[1]['nativetype'] = 'TEXT'; $expectedColumns[1]['size'] = null; $expectedColumns[1]['comment'] = null; - $expectedColumns[2]['nativetype'] = 'TEXT'; + $expectedColumns[2]['nativeType'] = 'TEXT'; $expectedColumns[2]['size'] = null; $expectedColumns[2]['comment'] = null; $expectedColumns[3]['comment'] = null; @@ -137,7 +140,7 @@ switch ($driverName) { Assert::same($expectedColumns, $columns); -$indexes = $driver->getIndexes('book_tag'); +$indexes = $engine->getIndexes('book_tag'); switch ($driverName) { case 'pgsql': Assert::same([ diff --git a/tests/Database/Engines/MSSQLEngine.applyLimit.phpt b/tests/Database/Engines/MSSQLEngine.applyLimit.phpt new file mode 100644 index 000000000..fff93be50 --- /dev/null +++ b/tests/Database/Engines/MSSQLEngine.applyLimit.phpt @@ -0,0 +1,69 @@ + $engine->applyLimit('SELECT 1 FROM t', 10, 20), + Nette\NotSupportedException::class, + 'Offset is not supported by this database.', +); + +Assert::exception( + fn() => $engine->applyLimit('SELECT 1 FROM t', 0, 20), + Nette\NotSupportedException::class, + 'Offset is not supported by this database.', +); + +Assert::same( + $engine->applyLimit('SELECT 1 FROM t', 10, 0), + 'SELECT TOP 10 1 FROM t', +); + +Assert::exception( + fn() => $engine->applyLimit('SELECT 1 FROM t', null, 20), + Nette\NotSupportedException::class, + 'Offset is not supported by this database.', +); + +Assert::same( + $engine->applyLimit('SELECT 1 FROM t', 10, null), + 'SELECT TOP 10 1 FROM t', +); + +Assert::same( + $engine->applyLimit(' select distinct 1 FROM t', 10, null), + ' select distinct TOP 10 1 FROM t', +); + +Assert::same( + $engine->applyLimit('UPDATE t SET', 10, null), + 'UPDATE TOP 10 t SET', +); + +Assert::same( + $engine->applyLimit('DELETE FROM t SET', 10, null), + 'DELETE TOP 10 FROM t SET', +); + +Assert::exception( + fn() => $engine->applyLimit('SET FROM t', 10, null), + Nette\InvalidArgumentException::class, + 'SQL query must begin with SELECT, UPDATE or DELETE command.', +); + +Assert::exception( + fn() => $engine->applyLimit('SELECT 1 FROM t', -1, null), + Nette\InvalidArgumentException::class, + 'Negative offset or limit.', +); + +Assert::exception( + fn() => $engine->applyLimit('SELECT 1 FROM t', null, -1), + Nette\NotSupportedException::class, + 'Offset is not supported by this database.', +); diff --git a/tests/Database/Engines/MySQLEngine.applyLimit.phpt b/tests/Database/Engines/MySQLEngine.applyLimit.phpt new file mode 100644 index 000000000..76bc37135 --- /dev/null +++ b/tests/Database/Engines/MySQLEngine.applyLimit.phpt @@ -0,0 +1,49 @@ +applyLimit('SELECT 1 FROM t', 10, 20), + 'SELECT 1 FROM t LIMIT 10 OFFSET 20', +); + +Assert::same( + $engine->applyLimit('SELECT 1 FROM t', 0, 20), + 'SELECT 1 FROM t LIMIT 0 OFFSET 20', +); + +Assert::same( + $engine->applyLimit('SELECT 1 FROM t', 10, 0), + 'SELECT 1 FROM t LIMIT 10', +); + +Assert::same( + $engine->applyLimit('SELECT 1 FROM t', null, 20), + 'SELECT 1 FROM t LIMIT 18446744073709551615 OFFSET 20', +); + +Assert::same( + $engine->applyLimit('SELECT 1 FROM t', 10, null), + 'SELECT 1 FROM t LIMIT 10', +); + +Assert::exception( + fn() => $engine->applyLimit('SELECT 1 FROM t', -1, null), + Nette\InvalidArgumentException::class, + 'Negative offset or limit.', +); + +Assert::exception( + fn() => $engine->applyLimit('SELECT 1 FROM t', null, -1), + Nette\InvalidArgumentException::class, + 'Negative offset or limit.', +); diff --git a/tests/Database/Engines/ODBCEngine.applyLimit.phpt b/tests/Database/Engines/ODBCEngine.applyLimit.phpt new file mode 100644 index 000000000..bf01f8733 --- /dev/null +++ b/tests/Database/Engines/ODBCEngine.applyLimit.phpt @@ -0,0 +1,69 @@ + $engine->applyLimit('SELECT 1 FROM t', 10, 20), + Nette\NotSupportedException::class, + 'Offset is not supported by this database.', +); + +Assert::exception( + fn() => $engine->applyLimit('SELECT 1 FROM t', 0, 20), + Nette\NotSupportedException::class, + 'Offset is not supported by this database.', +); + +Assert::same( + $engine->applyLimit('SELECT 1 FROM t', 10, 0), + 'SELECT TOP 10 1 FROM t', +); + +Assert::exception( + fn() => $engine->applyLimit('SELECT 1 FROM t', null, 20), + Nette\NotSupportedException::class, + 'Offset is not supported by this database.', +); + +Assert::same( + $engine->applyLimit('SELECT 1 FROM t', 10, null), + 'SELECT TOP 10 1 FROM t', +); + +Assert::same( + $engine->applyLimit(' select distinct 1 FROM t', 10, null), + ' select distinct TOP 10 1 FROM t', +); + +Assert::same( + $engine->applyLimit('UPDATE t SET', 10, null), + 'UPDATE TOP 10 t SET', +); + +Assert::same( + $engine->applyLimit('DELETE FROM t SET', 10, null), + 'DELETE TOP 10 FROM t SET', +); + +Assert::exception( + fn() => $engine->applyLimit('SET FROM t', 10, null), + Nette\InvalidArgumentException::class, + 'SQL query must begin with SELECT, UPDATE or DELETE command.', +); + +Assert::exception( + fn() => $engine->applyLimit('SELECT 1 FROM t', -1, null), + Nette\InvalidArgumentException::class, + 'Negative offset or limit.', +); + +Assert::exception( + fn() => $engine->applyLimit('SELECT 1 FROM t', null, -1), + Nette\NotSupportedException::class, + 'Offset is not supported by this database.', +); diff --git a/tests/Database/Engines/OracleEngine.applyLimit.phpt b/tests/Database/Engines/OracleEngine.applyLimit.phpt new file mode 100644 index 000000000..4ca785103 --- /dev/null +++ b/tests/Database/Engines/OracleEngine.applyLimit.phpt @@ -0,0 +1,45 @@ +applyLimit('SELECT 1 FROM t', 10, 20), + 'SELECT * FROM (SELECT t.*, ROWNUM AS "__rnum" FROM (SELECT 1 FROM t) t ) WHERE "__rnum" > 20 AND "__rnum" <= 30', +); + +Assert::same( + $engine->applyLimit('SELECT 1 FROM t', 0, 20), + 'SELECT * FROM (SELECT t.*, ROWNUM AS "__rnum" FROM (SELECT 1 FROM t) t ) WHERE "__rnum" > 20 AND "__rnum" <= 20', +); + +Assert::same( + $engine->applyLimit('SELECT 1 FROM t', 10, 0), + 'SELECT * FROM (SELECT 1 FROM t) WHERE ROWNUM <= 10', +); + +Assert::same( + $engine->applyLimit('SELECT 1 FROM t', null, 20), + 'SELECT * FROM (SELECT t.*, ROWNUM AS "__rnum" FROM (SELECT 1 FROM t) t ) WHERE "__rnum" > 20', +); + +Assert::same( + $engine->applyLimit('SELECT 1 FROM t', 10, null), + 'SELECT * FROM (SELECT 1 FROM t) WHERE ROWNUM <= 10', +); + +Assert::exception( + fn() => $engine->applyLimit('SELECT 1 FROM t', -1, null), + Nette\InvalidArgumentException::class, + 'Negative offset or limit.', +); + +Assert::exception( + fn() => $engine->applyLimit('SELECT 1 FROM t', null, -1), + Nette\InvalidArgumentException::class, + 'Negative offset or limit.', +); diff --git a/tests/Database/Engines/PostgreSQLEngine.applyLimit.phpt b/tests/Database/Engines/PostgreSQLEngine.applyLimit.phpt new file mode 100644 index 000000000..a9877a94e --- /dev/null +++ b/tests/Database/Engines/PostgreSQLEngine.applyLimit.phpt @@ -0,0 +1,45 @@ +applyLimit('SELECT 1 FROM t', 10, 20), + 'SELECT 1 FROM t LIMIT 10 OFFSET 20', +); + +Assert::same( + $engine->applyLimit('SELECT 1 FROM t', 0, 20), + 'SELECT 1 FROM t LIMIT 0 OFFSET 20', +); + +Assert::same( + $engine->applyLimit('SELECT 1 FROM t', 10, 0), + 'SELECT 1 FROM t LIMIT 10', +); + +Assert::same( + $engine->applyLimit('SELECT 1 FROM t', null, 20), + 'SELECT 1 FROM t OFFSET 20', +); + +Assert::same( + $engine->applyLimit('SELECT 1 FROM t', 10, null), + 'SELECT 1 FROM t LIMIT 10', +); + +Assert::exception( + fn() => $engine->applyLimit('SELECT 1 FROM t', -1, null), + Nette\InvalidArgumentException::class, + 'Negative offset or limit.', +); + +Assert::exception( + fn() => $engine->applyLimit('SELECT 1 FROM t', null, -1), + Nette\InvalidArgumentException::class, + 'Negative offset or limit.', +); diff --git a/tests/Database/Engines/SQLServerEngine.applyLimit.phpt b/tests/Database/Engines/SQLServerEngine.applyLimit.phpt new file mode 100644 index 000000000..5e415df44 --- /dev/null +++ b/tests/Database/Engines/SQLServerEngine.applyLimit.phpt @@ -0,0 +1,45 @@ +applyLimit('SELECT 1 FROM t', 10, 20), + 'SELECT 1 FROM t OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY', +); + +Assert::same( + $engine->applyLimit('SELECT 1 FROM t', 0, 20), + 'SELECT 1 FROM t OFFSET 20 ROWS FETCH NEXT 0 ROWS ONLY', +); + +Assert::same( + $engine->applyLimit('SELECT 1 FROM t', 10, 0), + 'SELECT 1 FROM t OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY', +); + +Assert::same( + $engine->applyLimit('SELECT 1 FROM t', null, 20), + 'SELECT 1 FROM t OFFSET 20 ROWS FETCH NEXT 0 ROWS ONLY', +); + +Assert::same( + $engine->applyLimit('SELECT 1 FROM t', 10, null), + 'SELECT 1 FROM t OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY', +); + +Assert::exception( + fn() => $engine->applyLimit('SELECT 1 FROM t', -1, null), + Nette\InvalidArgumentException::class, + 'Negative offset or limit.', +); + +Assert::exception( + fn() => $engine->applyLimit('SELECT 1 FROM t', null, -1), + Nette\InvalidArgumentException::class, + 'Negative offset or limit.', +); diff --git a/tests/Database/Engines/SQLiteEngine.applyLimit.phpt b/tests/Database/Engines/SQLiteEngine.applyLimit.phpt new file mode 100644 index 000000000..3cb055ae6 --- /dev/null +++ b/tests/Database/Engines/SQLiteEngine.applyLimit.phpt @@ -0,0 +1,45 @@ +applyLimit('SELECT 1 FROM t', 10, 20), + 'SELECT 1 FROM t LIMIT 10 OFFSET 20', +); + +Assert::same( + $engine->applyLimit('SELECT 1 FROM t', 0, 20), + 'SELECT 1 FROM t LIMIT 0 OFFSET 20', +); + +Assert::same( + $engine->applyLimit('SELECT 1 FROM t', 10, 0), + 'SELECT 1 FROM t LIMIT 10', +); + +Assert::same( + $engine->applyLimit('SELECT 1 FROM t', null, 20), + 'SELECT 1 FROM t LIMIT -1 OFFSET 20', +); + +Assert::same( + $engine->applyLimit('SELECT 1 FROM t', 10, null), + 'SELECT 1 FROM t LIMIT 10', +); + +Assert::exception( + fn() => $engine->applyLimit('SELECT 1 FROM t', -1, null), + Nette\InvalidArgumentException::class, + 'Negative offset or limit.', +); + +Assert::exception( + fn() => $engine->applyLimit('SELECT 1 FROM t', null, -1), + Nette\InvalidArgumentException::class, + 'Negative offset or limit.', +); diff --git a/tests/Database/Explorer.fetch.phpt b/tests/Database/Explorer.fetch.phpt index 97c5aa97a..4b6ac422d 100644 --- a/tests/Database/Explorer.fetch.phpt +++ b/tests/Database/Explorer.fetch.phpt @@ -10,9 +10,8 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/files/{$driverName}-nette_test1.sql"); test('fetch', function () use ($explorer) { diff --git a/tests/Database/Explorer.query.phpt b/tests/Database/Explorer.query.phpt index edb8aef36..6fea8ccf6 100644 --- a/tests/Database/Explorer.query.phpt +++ b/tests/Database/Explorer.query.phpt @@ -10,28 +10,27 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/files/{$driverName}-nette_test1.sql"); test('parameterized query through explorer', function () use ($explorer) { $res = $explorer->query('SELECT id FROM author WHERE id = ?', 11); - Assert::type(Nette\Database\ResultSet::class, $res); - Assert::same('SELECT id FROM author WHERE id = ?', $res->getQueryString()); - Assert::same([11], $res->getParameters()); + Assert::type(Nette\Database\Result::class, $res); + Assert::same('SELECT id FROM author WHERE id = ?', $res->getQuery()->getSql()); + Assert::same([11], $res->getQuery()->getParameters()); }); test('multiple parameters in explorer query', function () use ($explorer) { $res = $explorer->query('SELECT id FROM author WHERE id = ? OR id = ?', 11, 12); - Assert::same('SELECT id FROM author WHERE id = ? OR id = ?', $res->getQueryString()); - Assert::same([11, 12], $res->getParameters()); + Assert::same('SELECT id FROM author WHERE id = ? OR id = ?', $res->getQuery()->getSql()); + Assert::same([11, 12], $res->getQuery()->getParameters()); }); test('explorer query with array of parameters', function () use ($explorer) { - $res = $explorer->queryArgs('SELECT id FROM author WHERE id = ? OR id = ?', [11, 12]); - Assert::same('SELECT id FROM author WHERE id = ? OR id = ?', $res->getQueryString()); - Assert::same([11, 12], $res->getParameters()); + $res = @$explorer->queryArgs('SELECT id FROM author WHERE id = ? OR id = ?', [11, 12]); // is deprecated + Assert::same('SELECT id FROM author WHERE id = ? OR id = ?', $res->getQuery()->getSql()); + Assert::same([11, 12], $res->getQuery()->getParameters()); }); diff --git a/tests/Database/Explorer.transaction.phpt b/tests/Database/Explorer.transaction.phpt index d23732cec..47a47a8b3 100644 --- a/tests/Database/Explorer.transaction.phpt +++ b/tests/Database/Explorer.transaction.phpt @@ -11,9 +11,8 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/files/{$driverName}-nette_test1.sql"); test('rolls back explorer transaction after manual control', function () use ($explorer) { diff --git a/tests/Database/Explorer/ActiveRow.__toString().phpt b/tests/Database/Explorer/ActiveRow.__toString().phpt index 03c408fbc..e0a5ee24d 100644 --- a/tests/Database/Explorer/ActiveRow.__toString().phpt +++ b/tests/Database/Explorer/ActiveRow.__toString().phpt @@ -10,9 +10,8 @@ use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); test('', function () use ($explorer) { diff --git a/tests/Database/Explorer/Explorer.aggregation.phpt b/tests/Database/Explorer/Explorer.aggregation.phpt index dee750846..4e7a89e3a 100644 --- a/tests/Database/Explorer/Explorer.aggregation.phpt +++ b/tests/Database/Explorer/Explorer.aggregation.phpt @@ -10,9 +10,8 @@ use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); test('basic count aggregation', function () use ($explorer) { diff --git a/tests/Database/Explorer/Explorer.backjoin.phpt b/tests/Database/Explorer/Explorer.backjoin.phpt index 557a60f3c..ad16fa4ab 100644 --- a/tests/Database/Explorer/Explorer.backjoin.phpt +++ b/tests/Database/Explorer/Explorer.backjoin.phpt @@ -5,16 +5,15 @@ * @dataProvider? ../databases.ini */ -use Nette\Database\Driver; +use Nette\Database\Drivers\Engine; use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); -$driver = $connection->getDriver(); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +$engine = $explorer->getDatabaseEngine(); test('backward join aggregation with having', function () use ($explorer) { @@ -37,10 +36,10 @@ test('backward join aggregation with having', function () use ($explorer) { }); -test('left join SQL structure verification', function () use ($explorer, $driver) { +test('left join SQL structure verification', function () use ($explorer, $engine) { $authorsSelection = $explorer->table('author')->where(':book.translator_id IS NOT NULL')->wherePrimary(12); - if ($driver->isSupported(Driver::SupportSchema)) { + if ($engine->isSupported(Engine::SupportSchema)) { Assert::same( reformat('SELECT [author].* FROM [author] LEFT JOIN [public].[book] [book] ON [author].[id] = [book].[author_id] WHERE ([book].[translator_id] IS NOT NULL) AND ([author].[id] = ?)'), $authorsSelection->getSql(), diff --git a/tests/Database/Explorer/Explorer.basic.camelCase.phpt b/tests/Database/Explorer/Explorer.basic.camelCase.phpt index a1b79975e..239910d00 100644 --- a/tests/Database/Explorer/Explorer.basic.camelCase.phpt +++ b/tests/Database/Explorer/Explorer.basic.camelCase.phpt @@ -10,9 +10,8 @@ use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test2.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test2.sql"); test('', function () use ($explorer) { diff --git a/tests/Database/Explorer/Explorer.basic.phpt b/tests/Database/Explorer/Explorer.basic.phpt index 865327d2f..ec872fdd1 100644 --- a/tests/Database/Explorer/Explorer.basic.phpt +++ b/tests/Database/Explorer/Explorer.basic.phpt @@ -10,9 +10,8 @@ use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); test('select specific columns', function () use ($explorer) { diff --git a/tests/Database/Explorer/Explorer.cache.observer.phpt b/tests/Database/Explorer/Explorer.cache.observer.phpt index 4aedbcf18..d2fd2f45d 100644 --- a/tests/Database/Explorer/Explorer.cache.observer.phpt +++ b/tests/Database/Explorer/Explorer.cache.observer.phpt @@ -5,27 +5,30 @@ * @dataProvider? ../databases.ini */ -use Nette\Database\ResultSet; +use Nette\Database\Result; use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); -$cacheStorage = Mockery::mock(Nette\Caching\Storage::class); -$cacheStorage->shouldReceive('read')->withAnyArgs()->once()->andReturn(['id' => true]); -$cacheStorage->shouldReceive('read')->withAnyArgs()->times(2)->andReturn(['id' => true, 'author_id' => true]); -$cacheStorage->shouldReceive('write')->with(Mockery::any(), ['id' => true, 'author_id' => true, 'title' => true], []); - -$explorer = new Nette\Database\Explorer($connection, $explorer->getStructure(), $explorer->getConventions(), $cacheStorage); +$cache = Mockery::mock(Nette\Caching\Cache::class); +$cache->shouldReceive('load')->withAnyArgs()->once()->andReturn([]); +$cache->shouldReceive('load')->withAnyArgs()->once()->andReturn(['id' => true]); +$cache->shouldReceive('load')->withAnyArgs()->times(2)->andReturn(['id' => true, 'author_id' => true]); +$cache->shouldReceive('save')->with('structure', Mockery::any()); +$cache->shouldReceive('save')->with(Mockery::any(), ['id' => true, 'author_id' => true, 'title' => true]); +$explorer->setCache($cache); $queries = 0; -$connection->onQuery[] = function ($dao, ResultSet $result) use (&$queries) { - if (!preg_match('#SHOW|CONSTRAINT_NAME|information_schema|pg_catalog|sys\.|SET|PRAGMA|FROM sqlite_#i', $result->getQueryString())) { +$explorer->onQuery[] = function ($explorer, $result) use (&$queries) { + if ( + $result instanceof Result + && !preg_match('#SHOW|CONSTRAINT_NAME|information_schema|pg_catalog|sys\.|SET|PRAGMA|FROM sqlite_#i', $result->getQuery()->getSql()) + ) { $queries++; } }; diff --git a/tests/Database/Explorer/Explorer.cache.observer2.phpt b/tests/Database/Explorer/Explorer.cache.observer2.phpt index 71f4fffc6..e4aa42c13 100644 --- a/tests/Database/Explorer/Explorer.cache.observer2.phpt +++ b/tests/Database/Explorer/Explorer.cache.observer2.phpt @@ -5,31 +5,31 @@ * @dataProvider? ../databases.ini */ +use Nette\Caching\Cache; use Nette\Caching\Storages\MemoryStorage; use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); -class CacheMock extends MemoryStorage +class CacheMock extends Cache { public int $writes = 0; - public function write(string $key, $data, array $dependencies): void + public function save(mixed $key, mixed $data, ?array $dependencies = null): mixed { $this->writes++; - parent::write($key, $data, $dependencies); + return parent::save($key, $data, $dependencies); } } -$cacheStorage = new CacheMock; -$explorer = new Nette\Database\Explorer($connection, $explorer->getStructure(), $explorer->getConventions(), $cacheStorage); +$cache = new CacheMock(new MemoryStorage); +$explorer->setCache($cache); for ($i = 0; $i < 2; ++$i) { $authors = $explorer->table('author'); @@ -50,4 +50,4 @@ for ($i = 0; $i < 2; ++$i) { } Assert::same(reformat('SELECT [id], [name] FROM [author]'), $sql); -Assert::same(2, $cacheStorage->writes); +Assert::same(3, $cache->writes); // Structure + 2x Selection diff --git a/tests/Database/Explorer/Explorer.cache.phpt b/tests/Database/Explorer/Explorer.cache.phpt index 64b863a9b..15505eb31 100644 --- a/tests/Database/Explorer/Explorer.cache.phpt +++ b/tests/Database/Explorer/Explorer.cache.phpt @@ -10,9 +10,8 @@ use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); test('column access caching across queries', function () use ($explorer) { @@ -195,7 +194,7 @@ test('incremental column access tracking', function () use ($explorer) { test('SQL query logging with caching', function () use ($explorer) { $sql = []; - $explorer->getConnection()->onQuery[] = function ($_, $result) use (&$sql) { + $explorer->onQuery[] = function ($_, $result) use (&$sql) { $sql[] = $result->getQueryString(); }; diff --git a/tests/Database/Explorer/Explorer.cache.rows.phpt b/tests/Database/Explorer/Explorer.cache.rows.phpt index 75682d0c9..07ed66c8e 100644 --- a/tests/Database/Explorer/Explorer.cache.rows.phpt +++ b/tests/Database/Explorer/Explorer.cache.rows.phpt @@ -10,9 +10,8 @@ use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); $selections = []; diff --git a/tests/Database/Explorer/Explorer.cache2.phpt b/tests/Database/Explorer/Explorer.cache2.phpt index 0a2745f4e..a0392e5c2 100644 --- a/tests/Database/Explorer/Explorer.cache2.phpt +++ b/tests/Database/Explorer/Explorer.cache2.phpt @@ -10,9 +10,8 @@ use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); $res = []; diff --git a/tests/Database/Explorer/Explorer.columnRefetch.phpt b/tests/Database/Explorer/Explorer.columnRefetch.phpt index 9b8398af7..bf6158ccc 100644 --- a/tests/Database/Explorer/Explorer.columnRefetch.phpt +++ b/tests/Database/Explorer/Explorer.columnRefetch.phpt @@ -10,9 +10,8 @@ use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); $books = $explorer->table('book')->order('id DESC')->limit(2); diff --git a/tests/Database/Explorer/Explorer.discoveredReflection.phpt b/tests/Database/Explorer/Explorer.discoveredReflection.phpt index 6b70da8cd..491b9fa7c 100644 --- a/tests/Database/Explorer/Explorer.discoveredReflection.phpt +++ b/tests/Database/Explorer/Explorer.discoveredReflection.phpt @@ -10,9 +10,8 @@ use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); test('reflection-based related data fetching', function () use ($explorer) { @@ -88,10 +87,10 @@ test('property existence and empty checks', function () use ($explorer) { }); -test('case-insensitive table reflection', function () use ($connection, $explorer, $driverName) { +test('case-insensitive table reflection', function () use ($explorer, $driverName) { if ( $driverName === 'mysql' && - ($lowerCase = $connection->query('SHOW VARIABLES LIKE "lower_case_table_names"')->fetch()) && + ($lowerCase = $explorer->query('SHOW VARIABLES LIKE "lower_case_table_names"')->fetch()) && $lowerCase->Value != 0 ) { // tests case-insensitive reflection diff --git a/tests/Database/Explorer/Explorer.join-condition.phpt b/tests/Database/Explorer/Explorer.join-condition.phpt index aa061ef90..71e79e7f5 100644 --- a/tests/Database/Explorer/Explorer.join-condition.phpt +++ b/tests/Database/Explorer/Explorer.join-condition.phpt @@ -5,18 +5,18 @@ * @dataProvider? ../databases.ini */ -use Nette\Database\Driver; +use Nette\Database\Drivers\Engine; use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); -$driver = $connection->getDriver(); -test('', function () use ($explorer, $driver) { - $schema = $driver->isSupported(Driver::SupportSchema) +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +$engine = $explorer->getDatabaseEngine(); + +test('', function () use ($explorer, $engine) { + $schema = $engine->isSupported(Engine::SupportSchema) ? '[public].' : ''; $sql = $explorer->table('book')->joinWhere('translator', 'translator.name', 'Geek')->select('book.*')->getSql(); @@ -27,7 +27,7 @@ test('', function () use ($explorer, $driver) { ), $sql); }); -test('', function () use ($explorer, $driver) { +test('', function () use ($explorer, $engine) { $sql = $explorer->table('tag') ->select('tag.name, COUNT(:book_tag.book.id) AS count_of_next_volume_written_by_younger_author') ->joinWhere(':book_tag.book.author', ':book_tag.book.author.born < next_volume_author.born') @@ -35,7 +35,7 @@ test('', function () use ($explorer, $driver) { ->where('tag.name', 'PHP') ->group('tag.name') ->getSql(); - if ($driver->isSupported(Driver::SupportSchema)) { + if ($engine->isSupported(Engine::SupportSchema)) { Assert::same( reformat( 'SELECT [tag].[name], COUNT([book].[id]) AS [count_of_next_volume_written_by_younger_author] FROM [tag] ' . diff --git a/tests/Database/Explorer/Explorer.join.phpt b/tests/Database/Explorer/Explorer.join.phpt index 8dfe3f1c1..1a76ac0ce 100644 --- a/tests/Database/Explorer/Explorer.join.phpt +++ b/tests/Database/Explorer/Explorer.join.phpt @@ -5,16 +5,15 @@ * @dataProvider? ../databases.ini */ -use Nette\Database\Driver; +use Nette\Database\Drivers\Engine; use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); -$driver = $connection->getDriver(); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +$engine = $explorer->getDatabaseEngine(); test('join with order by related column', function () use ($explorer) { @@ -32,10 +31,10 @@ test('join with order by related column', function () use ($explorer) { }); -test('join SQL structure verification', function () use ($explorer, $driver) { +test('join SQL structure verification', function () use ($explorer, $engine) { $joinSql = $explorer->table('book_tag')->where('book_id', 1)->select('tag.*')->getSql(); - if ($driver->isSupported(Driver::SupportSchema)) { + if ($engine->isSupported(Engine::SupportSchema)) { Assert::same( reformat('SELECT [tag].* FROM [book_tag] LEFT JOIN [public].[tag] [tag] ON [book_tag].[tag_id] = [tag].[id] WHERE ([book_id] = ?)'), $joinSql, @@ -49,10 +48,10 @@ test('join SQL structure verification', function () use ($explorer, $driver) { }); -test('aliased column selection in join', function () use ($explorer, $driver) { +test('aliased column selection in join', function () use ($explorer, $engine) { $joinSql = $explorer->table('book_tag')->where('book_id', 1)->select('Tag.id')->getSql(); - if ($driver->isSupported(Driver::SupportSchema)) { + if ($engine->isSupported(Engine::SupportSchema)) { Assert::same( reformat('SELECT [Tag].[id] FROM [book_tag] LEFT JOIN [public].[tag] [Tag] ON [book_tag].[tag_id] = [Tag].[id] WHERE ([book_id] = ?)'), $joinSql, diff --git a/tests/Database/Explorer/Explorer.limit.phpt b/tests/Database/Explorer/Explorer.limit.phpt index cc5b3bca0..04b4236f3 100644 --- a/tests/Database/Explorer/Explorer.limit.phpt +++ b/tests/Database/Explorer/Explorer.limit.phpt @@ -10,9 +10,8 @@ use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); Assert::same( diff --git a/tests/Database/Explorer/Explorer.limit.sqlsrv.phpt b/tests/Database/Explorer/Explorer.limit.sqlsrv.phpt index 8d7756e24..3fd80e940 100644 --- a/tests/Database/Explorer/Explorer.limit.sqlsrv.phpt +++ b/tests/Database/Explorer/Explorer.limit.sqlsrv.phpt @@ -10,11 +10,10 @@ use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); -$version2008 = $connection->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION) < 11; +$version2008 = $explorer->getServerVersion() < 11; Assert::same( $version2008 diff --git a/tests/Database/Explorer/Explorer.multi-primary-key.phpt b/tests/Database/Explorer/Explorer.multi-primary-key.phpt index 9d76650ae..43acca106 100644 --- a/tests/Database/Explorer/Explorer.multi-primary-key.phpt +++ b/tests/Database/Explorer/Explorer.multi-primary-key.phpt @@ -10,9 +10,8 @@ use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); test('deletion through multi-key relation', function () use ($explorer) { diff --git a/tests/Database/Explorer/Explorer.placeholders.phpt b/tests/Database/Explorer/Explorer.placeholders.phpt index 0ecedc80c..0e133e370 100644 --- a/tests/Database/Explorer/Explorer.placeholders.phpt +++ b/tests/Database/Explorer/Explorer.placeholders.phpt @@ -11,9 +11,8 @@ use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); test('dynamic SQL function placeholders', function () use ($explorer, $driverName) { diff --git a/tests/Database/Explorer/Explorer.ref().phpt b/tests/Database/Explorer/Explorer.ref().phpt index 9e2043b52..c449a1b2c 100644 --- a/tests/Database/Explorer/Explorer.ref().phpt +++ b/tests/Database/Explorer/Explorer.ref().phpt @@ -10,9 +10,8 @@ use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); Assert::same('Jakub Vrana', $explorer->table('book')->get(1)->ref('author')->name); @@ -34,10 +33,10 @@ test('null reference handling', function () use ($explorer) { Assert::null($explorer->table('book')->get(2)->ref('author', 'translator_id')); }); -test('query count on reference access', function () use ($explorer, $connection) { +test('query count on reference access', function () use ($explorer) { $counter = 0; - $connection->onQuery[] = function ($connection, $result) use (&$counter) { + $explorer->onQuery[] = function ($explorer, $result) use (&$counter) { $counter++; }; diff --git a/tests/Database/Explorer/Explorer.related().caching.phpt b/tests/Database/Explorer/Explorer.related().caching.phpt index 1f526457d..a2a531a3e 100644 --- a/tests/Database/Explorer/Explorer.related().caching.phpt +++ b/tests/Database/Explorer/Explorer.related().caching.phpt @@ -10,9 +10,8 @@ use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); test('cached related data consistency', function () use ($explorer) { diff --git a/tests/Database/Explorer/Explorer.related().phpt b/tests/Database/Explorer/Explorer.related().phpt index 148a28cc9..1199d53d8 100644 --- a/tests/Database/Explorer/Explorer.related().phpt +++ b/tests/Database/Explorer/Explorer.related().phpt @@ -10,9 +10,8 @@ use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); test('various related method usages', function () use ($explorer) { diff --git a/tests/Database/Explorer/Explorer.self-reference.phpt b/tests/Database/Explorer/Explorer.self-reference.phpt index 7190184bd..c1cf1b2d1 100644 --- a/tests/Database/Explorer/Explorer.self-reference.phpt +++ b/tests/Database/Explorer/Explorer.self-reference.phpt @@ -10,15 +10,14 @@ use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); $explorer->query('UPDATE book SET next_volume = 3 WHERE id IN (2,4)'); -test('resolve self-reference using volume and next_volume relations', function () use ($connection, $explorer) { +test('resolve self-reference using volume and next_volume relations', function () use ($explorer) { $book = $explorer->table('book')->get(4); Assert::same('Nette', $book->volume->title); Assert::same('Nette', $book->ref('book', 'next_volume')->title); diff --git a/tests/Database/Explorer/Explorer.subquery.phpt b/tests/Database/Explorer/Explorer.subquery.phpt index 0b2d7131a..5ae45104a 100644 --- a/tests/Database/Explorer/Explorer.subquery.phpt +++ b/tests/Database/Explorer/Explorer.subquery.phpt @@ -10,9 +10,8 @@ use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); test('', function () use ($explorer) { diff --git a/tests/Database/Explorer/Explorer.update().phpt b/tests/Database/Explorer/Explorer.update().phpt index 0c804a04a..3d48b03ff 100644 --- a/tests/Database/Explorer/Explorer.update().phpt +++ b/tests/Database/Explorer/Explorer.update().phpt @@ -10,9 +10,8 @@ use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); $author = $explorer->table('author')->get(12); // SELECT * FROM `author` WHERE (`id` = ?) @@ -77,7 +76,7 @@ $tag2 = $explorer->table('tag')->insert([ 'name' => 'PS4 Game', ]); // INSERT INTO `tag` (`name`) VALUES ('PS4 Game') -// SQL Server throw PDOException because does not allow to update identity column +// SQL Server throw exception because does not allow to update identity column if ($driverName !== 'sqlsrv') { $tag2->update([ 'id' => 1, diff --git a/tests/Database/Explorer/GroupedSelection.insert().phpt b/tests/Database/Explorer/GroupedSelection.insert().phpt index 067cf8b79..b4000c431 100644 --- a/tests/Database/Explorer/GroupedSelection.insert().phpt +++ b/tests/Database/Explorer/GroupedSelection.insert().phpt @@ -10,9 +10,8 @@ use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); test('grouped selection insert increases count', function () use ($explorer) { diff --git a/tests/Database/Explorer/Selection.delete().phpt b/tests/Database/Explorer/Selection.delete().phpt index 53ea7d1ae..30af3fa7d 100644 --- a/tests/Database/Explorer/Selection.delete().phpt +++ b/tests/Database/Explorer/Selection.delete().phpt @@ -10,9 +10,8 @@ use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); test('delete records by condition in book_tag', function () use ($explorer) { diff --git a/tests/Database/Explorer/Selection.fetch().phpt b/tests/Database/Explorer/Selection.fetch().phpt index 38d10f3e7..fc1792179 100644 --- a/tests/Database/Explorer/Selection.fetch().phpt +++ b/tests/Database/Explorer/Selection.fetch().phpt @@ -10,9 +10,8 @@ use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); test('', function () use ($explorer) { diff --git a/tests/Database/Explorer/Selection.fetchAssoc().phpt b/tests/Database/Explorer/Selection.fetchAssoc().phpt index 6cc285432..e5f336adb 100644 --- a/tests/Database/Explorer/Selection.fetchAssoc().phpt +++ b/tests/Database/Explorer/Selection.fetchAssoc().phpt @@ -11,9 +11,8 @@ use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); test('', function () use ($explorer) { diff --git a/tests/Database/Explorer/Selection.fetchField().phpt b/tests/Database/Explorer/Selection.fetchField().phpt index 91d0cb2d1..1f5ed10da 100644 --- a/tests/Database/Explorer/Selection.fetchField().phpt +++ b/tests/Database/Explorer/Selection.fetchField().phpt @@ -10,9 +10,8 @@ use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); test('fetch specific field value or null if absent', function () use ($explorer) { diff --git a/tests/Database/Explorer/Selection.fetchPairs().phpt b/tests/Database/Explorer/Selection.fetchPairs().phpt index d5eded48f..3c76b27ac 100644 --- a/tests/Database/Explorer/Selection.fetchPairs().phpt +++ b/tests/Database/Explorer/Selection.fetchPairs().phpt @@ -10,9 +10,8 @@ use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); test('fetch id-title pairs ordered by title', function () use ($explorer) { diff --git a/tests/Database/Explorer/Selection.get().phpt b/tests/Database/Explorer/Selection.get().phpt index 27d45f169..57680dc21 100644 --- a/tests/Database/Explorer/Selection.get().phpt +++ b/tests/Database/Explorer/Selection.get().phpt @@ -10,9 +10,8 @@ use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); test('', function () use ($explorer) { diff --git a/tests/Database/Explorer/Selection.group().phpt b/tests/Database/Explorer/Selection.group().phpt index 1996c73b6..f9992eaaa 100644 --- a/tests/Database/Explorer/Selection.group().phpt +++ b/tests/Database/Explorer/Selection.group().phpt @@ -10,9 +10,8 @@ use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); test('', function () use ($explorer) { diff --git a/tests/Database/Explorer/Selection.insert().multi.phpt b/tests/Database/Explorer/Selection.insert().multi.phpt index b560b268e..f868eac4a 100644 --- a/tests/Database/Explorer/Selection.insert().multi.phpt +++ b/tests/Database/Explorer/Selection.insert().multi.phpt @@ -10,14 +10,13 @@ use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); test('', function () use ($explorer) { Assert::same(3, $explorer->table('author')->count()); - $explorer->table('author')->insert([ + $result = $explorer->table('author')->insert([ [ 'name' => 'Catelyn Stark', 'web' => 'http://example.com', @@ -29,15 +28,19 @@ test('', function () use ($explorer) { 'born' => new DateTime('2021-11-11'), ], ]); // INSERT INTO `author` (`name`, `web`, `born`) VALUES ('Catelyn Stark', 'http://example.com', '2011-11-11 00:00:00'), ('Sansa Stark', 'http://example.com', '2021-11-11 00:00:00') + Assert::type('int', $result); + Assert::same(2, $result); Assert::same(5, $explorer->table('author')->count()); $explorer->table('book_tag')->where('book_id', 1)->delete(); // DELETE FROM `book_tag` WHERE (`book_id` = ?) Assert::same(4, $explorer->table('book_tag')->count()); - $explorer->table('book')->get(1)->related('book_tag')->insert([ // SELECT * FROM `book` WHERE (`id` = ?) + $result = $explorer->table('book')->get(1)->related('book_tag')->insert([ // SELECT * FROM `book` WHERE (`id` = ?) ['tag_id' => 21], ['tag_id' => 22], ['tag_id' => 23], ]); // INSERT INTO `book_tag` (`tag_id`, `book_id`) VALUES (21, 1), (22, 1), (23, 1) + Assert::type('int', $result); + Assert::same(3, $result); Assert::same(7, $explorer->table('book_tag')->count()); }); diff --git a/tests/Database/Explorer/Selection.insert().phpt b/tests/Database/Explorer/Selection.insert().phpt index 910be3d2c..4d1d67471 100644 --- a/tests/Database/Explorer/Selection.insert().phpt +++ b/tests/Database/Explorer/Selection.insert().phpt @@ -10,9 +10,8 @@ use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); $book = $explorer->table('author')->insert([ @@ -39,7 +38,7 @@ $book2 = $books->insert([ Assert::same('eddard stark', $book2->author->name); // SELECT * FROM `author` WHERE (`author`.`id` IN (11, 15)) -// SQL Server throw PDOException because does not allow insert explicit value for IDENTITY column. +// SQL Server throw exception because does not allow insert explicit value for IDENTITY column. // This exception is about primary key violation. if ($driverName !== 'sqlsrv') { Assert::exception( @@ -48,7 +47,7 @@ if ($driverName !== 'sqlsrv') { 'name' => 'Jon Snow', 'web' => 'http://example.com', ]), - PDOException::class, + Nette\Database\UniqueConstraintViolationException::class, ); } diff --git a/tests/Database/Explorer/Selection.insert().primaryKeys.phpt b/tests/Database/Explorer/Selection.insert().primaryKeys.phpt index 85fe3ebe7..169b0d550 100644 --- a/tests/Database/Explorer/Selection.insert().primaryKeys.phpt +++ b/tests/Database/Explorer/Selection.insert().primaryKeys.phpt @@ -10,9 +10,8 @@ use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test4.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test4.sql"); test('insert into table with simple primary index (autoincrement)', function () use ($explorer) { $simplePkAutoincrementResult = $explorer->table('simple_pk_autoincrement')->insert([ diff --git a/tests/Database/Explorer/Selection.order().phpt b/tests/Database/Explorer/Selection.order().phpt index 47a6c20be..c8074ee31 100644 --- a/tests/Database/Explorer/Selection.order().phpt +++ b/tests/Database/Explorer/Selection.order().phpt @@ -10,9 +10,8 @@ use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); test('', function () use ($explorer) { diff --git a/tests/Database/Explorer/Selection.page().phpt b/tests/Database/Explorer/Selection.page().phpt index 858ba5bdb..95647a75f 100644 --- a/tests/Database/Explorer/Selection.page().phpt +++ b/tests/Database/Explorer/Selection.page().phpt @@ -10,13 +10,12 @@ use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -if ($driverName === 'sqlsrv' && $connection->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION) < 11) { +if ($driverName === 'sqlsrv' && $explorer->getServerVersion() < 11) { Tester\Environment::skip('Offset is supported since SQL Server 2012'); } -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); //public function page($page, $itemsPerPage, &$numOfPages = null) diff --git a/tests/Database/Explorer/Selection.whereOr().phpt b/tests/Database/Explorer/Selection.whereOr().phpt index 7fe0a5ef9..76d0e9fe2 100644 --- a/tests/Database/Explorer/Selection.whereOr().phpt +++ b/tests/Database/Explorer/Selection.whereOr().phpt @@ -10,9 +10,8 @@ use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); test('without question mark', function () use ($explorer) { $count = $explorer->table('book')->whereOr([ diff --git a/tests/Database/Explorer/SqlBuilder.addAlias().phpt b/tests/Database/Explorer/SqlBuilder.addAlias().phpt index 13e1f0782..0b7a9891f 100644 --- a/tests/Database/Explorer/SqlBuilder.addAlias().phpt +++ b/tests/Database/Explorer/SqlBuilder.addAlias().phpt @@ -5,16 +5,15 @@ * @dataProvider? ../databases.ini */ -use Nette\Database\Driver; +use Nette\Database\Drivers\Engine; use Nette\Database\Table\SqlBuilder; use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); class SqlBuilderMock extends SqlBuilder { @@ -30,11 +29,11 @@ class SqlBuilderMock extends SqlBuilder } } -$driver = $connection->getDriver(); +$engine = $explorer->getDatabaseEngine(); -test('test duplicated table names throw exception', function () use ($explorer, $driver) { - $authorTable = ($driver->isSupported(Driver::SupportSchema) ? 'public.' : '') . 'author'; +test('test duplicated table names throw exception', function () use ($explorer, $engine) { + $authorTable = ($engine->isSupported(Engine::SupportSchema) ? 'public.' : '') . 'author'; $sqlBuilder = new SqlBuilderMock($authorTable, $explorer); $sqlBuilder->addAlias(':book(translator)', 'book1'); $sqlBuilder->addAlias(':book:book_tag', 'book2'); @@ -71,7 +70,7 @@ test('test duplicated table names throw exception', function () use ($explorer, }); -test('test same table chain with another alias', function () use ($explorer, $driver) { +test('test same table chain with another alias', function () use ($explorer, $engine) { $sqlBuilder = new SqlBuilderMock('author', $explorer); $sqlBuilder->addAlias(':book(translator)', 'translated_book'); $sqlBuilder->addAlias(':book(translator)', 'translated_book2'); @@ -88,8 +87,8 @@ test('test same table chain with another alias', function () use ($explorer, $dr }); -test('test nested alias', function () use ($explorer, $driver) { - $sqlBuilder = $driver->isSupported(Driver::SupportSchema) +test('test nested alias', function () use ($explorer, $engine) { + $sqlBuilder = $engine->isSupported(Engine::SupportSchema) ? new SqlBuilderMock('public.author', $explorer) : new SqlBuilderMock('author', $explorer); $sqlBuilder->addAlias(':book(translator)', 'translated_book'); @@ -98,7 +97,7 @@ test('test nested alias', function () use ($explorer, $driver) { $joins = []; $sqlBuilder->parseJoins($joins, $query); $join = $sqlBuilder->buildQueryJoins($joins); - if ($driver->isSupported(Driver::SupportSchema)) { + if ($engine->isSupported(Engine::SupportSchema)) { Assert::same( 'LEFT JOIN book translated_book ON author.id = translated_book.translator_id ' . 'LEFT JOIN public.book next ON translated_book.next_volume = next.id', diff --git a/tests/Database/Explorer/SqlBuilder.addWhere().phpt b/tests/Database/Explorer/SqlBuilder.addWhere().phpt index 3c07a3756..9174313b6 100644 --- a/tests/Database/Explorer/SqlBuilder.addWhere().phpt +++ b/tests/Database/Explorer/SqlBuilder.addWhere().phpt @@ -5,7 +5,7 @@ * @dataProvider? ../databases.ini */ -use Nette\Database\Driver; +use Nette\Database\Drivers\Engine; use Nette\Database\SqlLiteral; use Nette\Database\Table\SqlBuilder; use Tester\Assert; @@ -13,9 +13,8 @@ use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); test('combine duplicate where conditions, ignoring repetition', function () use ($explorer) { @@ -71,7 +70,7 @@ test('build where condition with direct values from selection', function () use test('where with HAVING subquery and parameter handling', function () use ($explorer) { $sqlBuilder = new SqlBuilder('book', $explorer); $sqlBuilder->addWhere('id', $explorer->table('book')->having('COUNT(:book_tag.tag_id) >', 1)); - $schemaSupported = $explorer->getConnection()->getDriver()->isSupported(Driver::SupportSchema); + $schemaSupported = $explorer->getDatabaseEngine()->isSupported(Engine::SupportSchema); Assert::same(reformat('SELECT * FROM [book] WHERE ([id] IN (SELECT [id] FROM [book] LEFT JOIN ' . ($schemaSupported ? '[public].[book_tag] ' : '') . '[book_tag] ON [book].[id] = [book_tag].[book_id] HAVING COUNT([book_tag].[tag_id]) > ?))'), $sqlBuilder->buildSelectQuery()); Assert::count(1, $sqlBuilder->getParameters()); }); @@ -220,7 +219,7 @@ Assert::exception(function () use ($explorer) { }, Nette\InvalidArgumentException::class, 'Column operator does not accept array argument.'); -test('missing primary key triggers selection argument exception', function () use ($driverName, $explorer, $connection) { +test('missing primary key triggers selection argument exception', function () use ($driverName, $explorer) { $structure = $explorer->getStructure(); switch ($driverName) { case 'mysql': diff --git a/tests/Database/Explorer/SqlBuilder.parseJoinConditions().phpt b/tests/Database/Explorer/SqlBuilder.parseJoinConditions().phpt index a6f777f8a..75be1cfaa 100644 --- a/tests/Database/Explorer/SqlBuilder.parseJoinConditions().phpt +++ b/tests/Database/Explorer/SqlBuilder.parseJoinConditions().phpt @@ -5,16 +5,15 @@ * @dataProvider? ../databases.ini */ -use Nette\Database\Driver; +use Nette\Database\Drivers\Engine; use Nette\Database\Table\SqlBuilder; use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); class SqlBuilderMock extends SqlBuilder { @@ -42,7 +41,7 @@ class SqlBuilderMock extends SqlBuilder } } -$driver = $connection->getDriver(); +$engine = $explorer->getDatabaseEngine(); test('detect circular join reference in nested conditions', function () use ($explorer) { $sqlBuilder = new SqlBuilderMock('author', $explorer); @@ -73,7 +72,7 @@ test('detect circular join reference in nested conditions', function () use ($ex ); }); -test('combine join conditions with extra filters and parameters', function () use ($explorer, $driver) { +test('combine join conditions with extra filters and parameters', function () use ($explorer, $engine) { $sqlBuilder = new SqlBuilderMock('author', $explorer); $sqlBuilder->addJoinCondition(':book(translator)', ':book(translator).id > ?', 2); $sqlBuilder->addJoinCondition(':book(translator):book_tag_alt', ':book(translator):book_tag_alt.state ?', 'private'); @@ -81,7 +80,7 @@ test('combine join conditions with extra filters and parameters', function () us $leftJoinConditions = $sqlBuilder->parseJoinConditions($joins, $sqlBuilder->buildJoinConditions()); $join = $sqlBuilder->buildQueryJoins($joins, $leftJoinConditions); - if ($driver->isSupported(Driver::SupportSchema)) { + if ($engine->isSupported(Engine::SupportSchema)) { Assert::same( 'LEFT JOIN book ON author.id = book.translator_id AND (book.id > ?) ' . 'LEFT JOIN public.book_tag_alt book_tag_alt ON book.id = book_tag_alt.book_id AND (book_tag_alt.state = ?)', diff --git a/tests/Database/Explorer/SqlBuilder.parseJoins().phpt b/tests/Database/Explorer/SqlBuilder.parseJoins().phpt index c7033d425..6161f48b3 100644 --- a/tests/Database/Explorer/SqlBuilder.parseJoins().phpt +++ b/tests/Database/Explorer/SqlBuilder.parseJoins().phpt @@ -6,16 +6,15 @@ */ use Nette\Database\Conventions\DiscoveredConventions; -use Nette\Database\Driver; +use Nette\Database\Drivers\Engine; use Nette\Database\Table\SqlBuilder; use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test2.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test2.sql"); class SqlBuilderMock extends SqlBuilder @@ -35,7 +34,7 @@ class SqlBuilderMock extends SqlBuilder $structure = $explorer->getStructure(); $conventions = new DiscoveredConventions($structure); $sqlBuilder = new SqlBuilderMock('nUsers', $explorer); -$driver = $connection->getDriver(); +$engine = $explorer->getDatabaseEngine(); $joins = []; @@ -44,9 +43,9 @@ $sqlBuilder->parseJoins($joins, $query); $join = $sqlBuilder->buildQueryJoins($joins); Assert::same('WHERE priorit.id IS NULL', $query); -$tables = $connection->getDriver()->getTables(); +$tables = $explorer->getDatabaseEngine()->getTables(); if (!in_array($tables[0]['name'], ['npriorities', 'ntopics', 'nusers', 'nusers_ntopics', 'nusers_ntopics_alt'], true)) { - if ($driver->isSupported(Driver::SupportSchema)) { + if ($engine->isSupported(Engine::SupportSchema)) { Assert::same( 'LEFT JOIN public.nUsers_nTopics nusers_ntopics ON nUsers.nUserId = nusers_ntopics.nUserId ' . 'LEFT JOIN public.nTopics topic ON nusers_ntopics.nTopicId = topic.nTopicId ' . @@ -73,7 +72,7 @@ if (!in_array($tables[0]['name'], ['npriorities', 'ntopics', 'nusers', 'nusers_n } -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); $structure->rebuild(); $sqlBuilder = new SqlBuilderMock('author', $explorer); @@ -89,7 +88,7 @@ Assert::same( ); -$sqlBuilder = $driver->isSupported(Driver::SupportSchema) +$sqlBuilder = $engine->isSupported(Engine::SupportSchema) ? new SqlBuilderMock('public.book', $explorer) : new SqlBuilderMock('book', $explorer); @@ -99,7 +98,7 @@ $sqlBuilder->parseJoins($joins, $query); $join = $sqlBuilder->buildQueryJoins($joins); Assert::same('WHERE book_ref.translator_id IS NULL AND book_ref_ref.translator_id IS NULL', $query); -if ($driver->isSupported(Driver::SupportSchema)) { +if ($engine->isSupported(Engine::SupportSchema)) { Assert::same( 'LEFT JOIN public.book book_ref ON book.id = book_ref.next_volume ' . 'LEFT JOIN public.book book_ref_ref ON book_ref.id = book_ref_ref.next_volume', diff --git a/tests/Database/Explorer/SqlBuilder.tryDelimit().phpt b/tests/Database/Explorer/SqlBuilder.tryDelimit().phpt new file mode 100644 index 000000000..a2930edf2 --- /dev/null +++ b/tests/Database/Explorer/SqlBuilder.tryDelimit().phpt @@ -0,0 +1,29 @@ +getMethod('tryDelimit'); +$tryDelimit->setAccessible(true); + +Assert::same(reformat('[hello]'), $tryDelimit->invoke($sqlBuilder, 'hello')); +Assert::same(reformat(' [hello] '), $tryDelimit->invoke($sqlBuilder, ' hello ')); +Assert::same(reformat('HELLO'), $tryDelimit->invoke($sqlBuilder, 'HELLO')); +Assert::same(reformat('[HellO]'), $tryDelimit->invoke($sqlBuilder, 'HellO')); +Assert::same(reformat('[hello].[world]'), $tryDelimit->invoke($sqlBuilder, 'hello.world')); +Assert::same(reformat('[hello] [world]'), $tryDelimit->invoke($sqlBuilder, 'hello world')); +Assert::same(reformat('HELLO([world])'), $tryDelimit->invoke($sqlBuilder, 'HELLO(world)')); +Assert::same(reformat('hello([world])'), $tryDelimit->invoke($sqlBuilder, 'hello(world)')); +Assert::same('[hello]', $tryDelimit->invoke($sqlBuilder, '[hello]')); +Assert::same(reformat('::int'), $tryDelimit->invoke($sqlBuilder, '::int')); diff --git a/tests/Database/Explorer/SqlBuilder.tryDelimite().phpt b/tests/Database/Explorer/SqlBuilder.tryDelimite().phpt deleted file mode 100644 index 66dae9397..000000000 --- a/tests/Database/Explorer/SqlBuilder.tryDelimite().phpt +++ /dev/null @@ -1,26 +0,0 @@ -getMethod('tryDelimite'); - -Assert::same(reformat('[hello]'), $tryDelimite->invoke($sqlBuilder, 'hello')); -Assert::same(reformat(' [hello] '), $tryDelimite->invoke($sqlBuilder, ' hello ')); -Assert::same(reformat('HELLO'), $tryDelimite->invoke($sqlBuilder, 'HELLO')); -Assert::same(reformat('[HellO]'), $tryDelimite->invoke($sqlBuilder, 'HellO')); -Assert::same(reformat('[hello].[world]'), $tryDelimite->invoke($sqlBuilder, 'hello.world')); -Assert::same(reformat('[hello] [world]'), $tryDelimite->invoke($sqlBuilder, 'hello world')); -Assert::same(reformat('HELLO([world])'), $tryDelimite->invoke($sqlBuilder, 'HELLO(world)')); -Assert::same(reformat('hello([world])'), $tryDelimite->invoke($sqlBuilder, 'hello(world)')); -Assert::same('[hello]', $tryDelimite->invoke($sqlBuilder, '[hello]')); -Assert::same(reformat('::int'), $tryDelimite->invoke($sqlBuilder, '::int')); diff --git a/tests/Database/Explorer/bugs/ActiveRow.__isset().phpt b/tests/Database/Explorer/bugs/ActiveRow.__isset().phpt index d727ec986..b29285c19 100644 --- a/tests/Database/Explorer/bugs/ActiveRow.__isset().phpt +++ b/tests/Database/Explorer/bugs/ActiveRow.__isset().phpt @@ -9,9 +9,8 @@ use Tester\Assert; require __DIR__ . '/../../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../../files/{$driverName}-nette_test1.sql"); test('', function () use ($explorer) { diff --git a/tests/Database/Explorer/bugs/Selection.emptyResultSet.phpt b/tests/Database/Explorer/bugs/Selection.emptyResultSet.phpt index f5be720cc..ff767313c 100644 --- a/tests/Database/Explorer/bugs/Selection.emptyResultSet.phpt +++ b/tests/Database/Explorer/bugs/Selection.emptyResultSet.phpt @@ -9,9 +9,8 @@ use Tester\Assert; require __DIR__ . '/../../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../../files/{$driverName}-nette_test1.sql"); test('', function () use ($explorer) { diff --git a/tests/Database/Explorer/bugs/Selection.getReferencingTable.pkTypes.phpt b/tests/Database/Explorer/bugs/Selection.getReferencingTable.pkTypes.phpt index 8e6c6f2b3..ff91bb59a 100644 --- a/tests/Database/Explorer/bugs/Selection.getReferencingTable.pkTypes.phpt +++ b/tests/Database/Explorer/bugs/Selection.getReferencingTable.pkTypes.phpt @@ -10,9 +10,8 @@ use Tester\Assert; require __DIR__ . '/../../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../../files/{$driverName}-nette_test5.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../../files/{$driverName}-nette_test5.sql"); test('referencing table with integer primary key', function () use ($explorer) { diff --git a/tests/Database/Explorer/bugs/bug1356.phpt b/tests/Database/Explorer/bugs/bug1356.phpt index 50c046f71..070fca8f2 100644 --- a/tests/Database/Explorer/bugs/bug1356.phpt +++ b/tests/Database/Explorer/bugs/bug1356.phpt @@ -10,9 +10,8 @@ use Tester\Assert; require __DIR__ . '/../../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../../files/{$driverName}-nette_test1.sql"); $books = $explorer->table('book')->limit(1); @@ -29,7 +28,7 @@ foreach ($books as $book) { } Assert::same(reformat([ - 'sqlsrv' => $connection->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION) < 11 + 'sqlsrv' => $explorer->getServerVersion() < 11 ? 'SELECT TOP 1 * FROM [book] ORDER BY [book].[id]' : 'SELECT * FROM [book] ORDER BY [book].[id] OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY', 'SELECT * FROM [book] ORDER BY [book].[id] LIMIT 1', diff --git a/tests/Database/Explorer/bugs/bug170.phpt b/tests/Database/Explorer/bugs/bug170.phpt index 1591d0853..450920997 100644 --- a/tests/Database/Explorer/bugs/bug170.phpt +++ b/tests/Database/Explorer/bugs/bug170.phpt @@ -10,9 +10,8 @@ use Tester\Assert; require __DIR__ . '/../../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../../files/{$driverName}-bug170.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../../files/{$driverName}-bug170.sql"); Assert::noError(function () use ($explorer) { // this bug is about picking the right foreign key to specified table regardless FKs definition order diff --git a/tests/Database/Explorer/bugs/bug187.phpt b/tests/Database/Explorer/bugs/bug187.phpt index 340f57c78..92a285c07 100644 --- a/tests/Database/Explorer/bugs/bug187.phpt +++ b/tests/Database/Explorer/bugs/bug187.phpt @@ -11,9 +11,8 @@ use Tester\Assert; require __DIR__ . '/../../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../../files/{$driverName}-bug187.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../../files/{$driverName}-bug187.sql"); foreach ([true, false] as $published) { $where = $published diff --git a/tests/Database/Explorer/bugs/bug216.phpt b/tests/Database/Explorer/bugs/bug216.phpt index 42ebfe2bb..99c94c281 100644 --- a/tests/Database/Explorer/bugs/bug216.phpt +++ b/tests/Database/Explorer/bugs/bug216.phpt @@ -10,9 +10,8 @@ use Tester\Assert; require __DIR__ . '/../../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../../files/{$driverName}-nette_test1.sql"); $book = $explorer->table('author')->insert([ 'name' => $explorer->literal('LOWER(?)', 'Eddard Stark'), diff --git a/tests/Database/Explorer/bugs/deleteCacheBug.phpt b/tests/Database/Explorer/bugs/deleteCacheBug.phpt index 60e37b92c..f5c32d1b9 100644 --- a/tests/Database/Explorer/bugs/deleteCacheBug.phpt +++ b/tests/Database/Explorer/bugs/deleteCacheBug.phpt @@ -9,9 +9,8 @@ use Tester\Assert; require __DIR__ . '/../../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../../files/{$driverName}-nette_test1.sql"); test('', function () use ($explorer) { for ($i = 0; $i < 2; $i++) { @@ -39,7 +38,7 @@ test('', function () use ($explorer) { $book->id; if ($i === 1) { - $book->getTable()->createSelectionInstance() + $explorer->table('book') ->wherePrimary($book->id) ->delete(); diff --git a/tests/Database/Explorer/bugs/query.count.phpt b/tests/Database/Explorer/bugs/query.count.phpt index 728dd1eab..08ce3fc81 100644 --- a/tests/Database/Explorer/bugs/query.count.phpt +++ b/tests/Database/Explorer/bugs/query.count.phpt @@ -9,20 +9,19 @@ use Tester\Assert; require __DIR__ . '/../../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../../files/{$driverName}-nette_test1.sql"); // add additional tags (not relevant to other tests) $explorer->query("INSERT INTO book_tag_alt (book_id, tag_id, state) VALUES (1, 24, 'private');"); $explorer->query("INSERT INTO book_tag_alt (book_id, tag_id, state) VALUES (2, 24, 'private');"); $explorer->query("INSERT INTO book_tag_alt (book_id, tag_id, state) VALUES (2, 22, 'private');"); -test('', function () use ($connection, $explorer) { +test('', function () use ($explorer) { $explorer->table('author')->get(11); // have to build cache first $count = 0; - $connection->onQuery[] = function () use (&$count) { + $explorer->onQuery[] = function () use (&$count) { $count++; }; diff --git a/tests/Database/Explorer/bugs/staticReflection.undeclaredColumn.phpt b/tests/Database/Explorer/bugs/staticReflection.undeclaredColumn.phpt index bc9d1b059..224acd6b7 100644 --- a/tests/Database/Explorer/bugs/staticReflection.undeclaredColumn.phpt +++ b/tests/Database/Explorer/bugs/staticReflection.undeclaredColumn.phpt @@ -10,13 +10,10 @@ use Tester\Assert; require __DIR__ . '/../../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -$conventions = new Nette\Database\Conventions\StaticConventions; -$cacheStorage = new Nette\Caching\Storages\MemoryStorage; -$explorer = new Nette\Database\Explorer($explorer->getConnection(), $explorer->getStructure(), $conventions, $cacheStorage); +$explorer->setConventions(new Nette\Database\Conventions\StaticConventions); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../../files/{$driverName}-nette_test1.sql"); test('', function () use ($explorer) { diff --git a/tests/Database/Explorer/bugs/view.bug.phpt b/tests/Database/Explorer/bugs/view.bug.phpt index 4b583f45e..fe332db58 100644 --- a/tests/Database/Explorer/bugs/view.bug.phpt +++ b/tests/Database/Explorer/bugs/view.bug.phpt @@ -9,9 +9,8 @@ use Tester\Assert; require __DIR__ . '/../../../bootstrap.php'; $explorer = connectToDB(); -$connection = $explorer->getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../../files/{$driverName}-nette_test1.sql"); +Nette\Database\Helpers::loadFromFile($explorer, __DIR__ . "/../../files/{$driverName}-nette_test1.sql"); $explorer->query('CREATE VIEW books_view AS SELECT * FROM book'); @@ -20,9 +19,9 @@ test('', function () use ($explorer) { Assert::same(1, $selection->count()); }); -test('', function () use ($connection) { - $driver = $connection->getDriver(); - $columns = $driver->getColumns('books_view'); +test('', function () use ($explorer) { + $engine = $explorer->getDatabaseEngine(); + $columns = $engine->getColumns('books_view'); $columnsNames = array_map(fn($item) => $item['name'], $columns); Assert::same(['id', 'author_id', 'translator_id', 'title', 'next_volume'], $columnsNames); }); diff --git a/tests/Database/Helpers.dumpSql.phpt b/tests/Database/Helpers.dumpSql.phpt index 8db50fbc9..0411c8c53 100644 --- a/tests/Database/Helpers.dumpSql.phpt +++ b/tests/Database/Helpers.dumpSql.phpt @@ -5,58 +5,62 @@ * @dataProvider? databases.ini mysql */ +use Nette\Database\SqlLiteral; use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -$connection = connectToDB()->getConnection(); +$connection = connectToDB(); Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/files/{$driverName}-nette_test1.sql"); test('int check', function () use ($connection) { Assert::same( "
SELECT id \nFROM author \nWHERE id = 10 OR id = 11
\n", - Nette\Database\Helpers::dumpSql('SELECT id FROM author WHERE id = ? OR id = ?', [10, 11], $connection), + Nette\Database\Helpers::dumpSql(new SqlLiteral('SELECT id FROM author WHERE id = ? OR id = ?', [10, 11]), $connection), ); }); test('bool check', function () use ($connection) { Assert::same( "
SELECT id \nFROM author \nWHERE deleted = 0
\n", - Nette\Database\Helpers::dumpSql('SELECT id FROM author WHERE deleted = ?', [false], $connection), + Nette\Database\Helpers::dumpSql(new SqlLiteral('SELECT id FROM author WHERE deleted = ?', [false]), $connection), ); }); test('string check', function () use ($connection) { Assert::same( "
SELECT id \nFROM author \nWHERE name = 'Alexej Chruščev'
\n", - Nette\Database\Helpers::dumpSql('SELECT id FROM author WHERE name = ?', ['Alexej Chruščev'], $connection), + Nette\Database\Helpers::dumpSql(new SqlLiteral('SELECT id FROM author WHERE name = ?', ['Alexej Chruščev']), $connection), ); }); test('string check with \'', function () use ($connection) { Assert::same( "
SELECT id \nFROM author \nWHERE name = 'Alexej Ch\\'ruščev'
\n", - Nette\Database\Helpers::dumpSql('SELECT id FROM author WHERE name = ?', ["Alexej Ch'ruščev"], $connection), + Nette\Database\Helpers::dumpSql(new SqlLiteral('SELECT id FROM author WHERE name = ?', ["Alexej Ch'ruščev"]), $connection), ); }); test('string check without connection', function () { Assert::same( "
SELECT id \nFROM author \nWHERE name = 'Alexej Ch'ruščev'
\n", - Nette\Database\Helpers::dumpSql('SELECT id FROM author WHERE name = ?', ["Alexej Ch'ruščev"]), + Nette\Database\Helpers::dumpSql(new SqlLiteral('SELECT id FROM author WHERE name = ?', ["Alexej Ch'ruščev"])), ); }); test('string compare with $connection vs without', function () use ($connection) { - Assert::notSame(Nette\Database\Helpers::dumpSql('SELECT id FROM author WHERE name = ?', ["Alexej Ch'ruščev"], $connection), Nette\Database\Helpers::dumpSql('SELECT id FROM author WHERE name = ?', ["Alexej Ch'ruščev"])); + Assert::notSame( + Nette\Database\Helpers::dumpSql(new SqlLiteral('SELECT id FROM author WHERE name = ?', ["Alexej Ch'ruščev"]), $connection), + Nette\Database\Helpers::dumpSql(new SqlLiteral('SELECT id FROM author WHERE name = ?', ["Alexej Ch'ruščev"])), + ); }); test('string check with \'', function () use ($connection) { Nette\Database\Helpers::$maxLength = 10; Assert::same( "
SELECT id \nFROM author \nWHERE name = 'Alexej Ch…'
\n", - Nette\Database\Helpers::dumpSql('SELECT id FROM author WHERE name = ?', ["Alexej Ch'ruščev"], $connection), + Nette\Database\Helpers::dumpSql(new SqlLiteral('SELECT id FROM author WHERE name = ?', ["Alexej Ch'ruščev"]), $connection), ); }); diff --git a/tests/Database/Helpers.loadFromFile.phpt b/tests/Database/Helpers.loadFromFile.phpt index 80678a623..2c8c2cee1 100644 --- a/tests/Database/Helpers.loadFromFile.phpt +++ b/tests/Database/Helpers.loadFromFile.phpt @@ -9,7 +9,7 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -$connection = connectToDB()->getConnection(); +$connection = connectToDB(); Nette\Database\Helpers::loadFromFile($connection, __DIR__ . '/files/mysql-delimiter.sql'); $arr = $connection->query('SELECT name, id FROM author ORDER BY id')->fetchAll(); diff --git a/tests/Database/Helpers.parseColumnType.phpt b/tests/Database/Helpers.parseColumnType.phpt index 935e074ba..e3272ffe3 100644 --- a/tests/Database/Helpers.parseColumnType.phpt +++ b/tests/Database/Helpers.parseColumnType.phpt @@ -13,20 +13,20 @@ require __DIR__ . '/../bootstrap.php'; // Test basic type $result = Helpers::parseColumnType('UNSIGNED INT'); -Assert::same(['type' => 'UNSIGNED INT', 'length' => null, 'scale' => null, 'parameters' => null], $result); +Assert::same(['type' => 'UNSIGNED INT', 'size' => null, 'scale' => null, 'parameters' => null], $result); -// Test type with length +// Test type with size $result = Helpers::parseColumnType('VARCHAR(255)'); -Assert::same(['type' => 'VARCHAR', 'length' => 255, 'scale' => null, 'parameters' => null], $result); +Assert::same(['type' => 'VARCHAR', 'size' => 255, 'scale' => null, 'parameters' => null], $result); // Test type with precision and scale $result = Helpers::parseColumnType('DECIMAL(10,2)'); -Assert::same(['type' => 'DECIMAL', 'length' => 10, 'scale' => 2, 'parameters' => null], $result); +Assert::same(['type' => 'DECIMAL', 'size' => 10, 'scale' => 2, 'parameters' => null], $result); // Test type with additional parameters $result = Helpers::parseColumnType("ENUM('value1','value2')"); -Assert::same(['type' => 'ENUM', 'length' => null, 'scale' => null, 'parameters' => "'value1','value2'"], $result); +Assert::same(['type' => 'ENUM', 'size' => null, 'scale' => null, 'parameters' => "'value1','value2'"], $result); // Test omitted type $result = Helpers::parseColumnType(''); -Assert::same(['type' => null, 'length' => null, 'scale' => null, 'parameters' => null], $result); +Assert::same(['type' => null, 'size' => null, 'scale' => null, 'parameters' => null], $result); diff --git a/tests/Database/Reflection.columns.mysql.phpt b/tests/Database/Reflection.columns.mysql.phpt index aea563ff8..4e0a55eed 100644 --- a/tests/Database/Reflection.columns.mysql.phpt +++ b/tests/Database/Reflection.columns.mysql.phpt @@ -9,11 +9,11 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -$connection = connectToDB()->getConnection(); +$connection = connectToDB(); Nette\Database\Helpers::loadFromFile($connection, __DIR__ . '/files/mysql-nette_test3.sql'); -$version80 = version_compare($connection->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION), '8.0', '>='); +$version80 = version_compare($connection->getServerVersion(), '8.0', '>='); $reflection = $connection->getReflection(); $columns = $reflection->getTable('types')->columns; @@ -23,6 +23,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => $version80 ? 'INT UNSIGNED' : 'INT', 'size' => $version80 ? null : 11, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -33,6 +34,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'INT', 'size' => $version80 ? null : 11, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -43,6 +45,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'SMALLINT', 'size' => $version80 ? null : 6, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -53,6 +56,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'TINYINT', 'size' => $version80 ? null : 4, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -63,6 +67,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'MEDIUMINT', 'size' => $version80 ? null : 9, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -73,6 +78,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'BIGINT', 'size' => $version80 ? null : 20, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -83,6 +89,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'TINYINT', 'size' => 1, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -93,6 +100,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'BIT', 'size' => 1, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -103,6 +111,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'DECIMAL', 'size' => 10, + 'scale' => 0, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -113,6 +122,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'DECIMAL', 'size' => 10, + 'scale' => 2, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -123,6 +133,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'FLOAT', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -133,6 +144,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'DOUBLE', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -143,6 +155,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'DATE', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -153,6 +166,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'TIME', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -163,6 +177,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'DATETIME', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -173,6 +188,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'TIMESTAMP', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -183,6 +199,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'YEAR', 'size' => $version80 ? null : 4, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -193,6 +210,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'CHAR', 'size' => 1, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -203,6 +221,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'VARCHAR', 'size' => 30, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -213,6 +232,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'BINARY', 'size' => 1, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -223,6 +243,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'VARBINARY', 'size' => 30, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -233,6 +254,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'BLOB', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -243,6 +265,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'TINYBLOB', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -253,6 +276,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'MEDIUMBLOB', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -263,6 +287,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'LONGBLOB', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -273,6 +298,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'TEXT', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -283,6 +309,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'TINYTEXT', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -293,6 +320,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'MEDIUMTEXT', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -303,6 +331,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'LONGTEXT', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -313,6 +342,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'ENUM', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -323,6 +353,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'SET', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -337,6 +368,7 @@ Assert::same( 'table' => $c->table->name, 'nativeType' => $c->nativeType, 'size' => $c->size, + 'scale' => $c->scale, 'nullable' => $c->nullable, 'default' => $c->default, 'autoIncrement' => $c->autoIncrement, diff --git a/tests/Database/Reflection.columns.postgre.phpt b/tests/Database/Reflection.columns.postgre.phpt index cede867a6..527a3284c 100644 --- a/tests/Database/Reflection.columns.postgre.phpt +++ b/tests/Database/Reflection.columns.postgre.phpt @@ -9,7 +9,7 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -$connection = connectToDB()->getConnection(); +$connection = connectToDB(); Nette\Database\Helpers::loadFromFile($connection, __DIR__ . '/files/pgsql-nette_test3.sql'); @@ -22,6 +22,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'INT2', 'size' => 2, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -32,6 +33,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'INT4', 'size' => 4, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -42,6 +44,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'INT8', 'size' => 8, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -52,6 +55,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'NUMERIC', 'size' => 3, + 'scale' => 2, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -62,6 +66,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'FLOAT4', 'size' => 4, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -72,6 +77,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'FLOAT8', 'size' => 8, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -82,6 +88,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'MONEY', 'size' => 8, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -92,6 +99,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'BOOL', 'size' => 1, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -102,6 +110,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'DATE', 'size' => 4, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -112,6 +121,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'TIME', 'size' => 8, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -122,6 +132,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'TIMESTAMP', 'size' => 8, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -132,6 +143,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'TIMESTAMPTZ', 'size' => 8, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -142,6 +154,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'INTERVAL', 'size' => 16, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -152,6 +165,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'BPCHAR', 'size' => 30, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -162,6 +176,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'VARCHAR', 'size' => 30, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -172,6 +187,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'TEXT', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -182,6 +198,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'TSQUERY', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -192,6 +209,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'TSVECTOR', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -202,6 +220,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'UUID', 'size' => 16, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -212,6 +231,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'XML', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -222,6 +242,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'CIDR', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -232,6 +253,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'INET', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -242,6 +264,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'MACADDR', 'size' => 6, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -252,6 +275,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'BIT', 'size' => -3, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -262,6 +286,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'VARBIT', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -272,6 +297,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'BYTEA', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -282,6 +308,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'BOX', 'size' => 32, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -292,6 +319,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'CIRCLE', 'size' => 24, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -302,6 +330,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'LSEG', 'size' => 32, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -312,6 +341,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'PATH', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -322,6 +352,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'POINT', 'size' => 16, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -332,6 +363,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'POLYGON', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -346,6 +378,7 @@ Assert::same( 'table' => $c->table->name, 'nativeType' => $c->nativeType, 'size' => $c->size, + 'scale' => $c->scale, 'nullable' => $c->nullable, 'default' => $c->default, 'autoIncrement' => $c->autoIncrement, diff --git a/tests/Database/Reflection.columns.sqlite.phpt b/tests/Database/Reflection.columns.sqlite.phpt index f88fe1f45..34935f198 100644 --- a/tests/Database/Reflection.columns.sqlite.phpt +++ b/tests/Database/Reflection.columns.sqlite.phpt @@ -9,7 +9,7 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -$connection = connectToDB()->getConnection(); +$connection = connectToDB(); Nette\Database\Helpers::loadFromFile($connection, __DIR__ . '/files/sqlite-nette_test3.sql'); @@ -22,6 +22,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'INT', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -32,6 +33,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'INTEGER', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -42,6 +44,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'TINYINT', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -52,6 +55,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'SMALLINT', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -62,6 +66,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'MEDIUMINT', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -72,6 +77,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'BIGINT', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -82,6 +88,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'UNSIGNED BIG INT', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -92,6 +99,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'INT2', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -102,6 +110,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'INT8', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -112,6 +121,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'CHARACTER', 'size' => 20, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -122,6 +132,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'VARCHAR', 'size' => 255, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -132,6 +143,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'VARYING CHARACTER', 'size' => 255, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -142,6 +154,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'NCHAR', 'size' => 55, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -152,6 +165,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'NATIVE CHARACTER', 'size' => 70, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -162,6 +176,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'NVARCHAR', 'size' => 100, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -172,6 +187,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'TEXT', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -182,6 +198,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'CLOB', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -192,6 +209,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'BLOB', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -202,6 +220,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'REAL', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -212,6 +231,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'DOUBLE', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -222,6 +242,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'DOUBLE PRECISION', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -232,6 +253,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'FLOAT', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -242,6 +264,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'NUMERIC', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -252,6 +275,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'DECIMAL', 'size' => 10, + 'scale' => 5, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -262,6 +286,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'BOOLEAN', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -272,6 +297,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'DATE', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -282,6 +308,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'DATETIME', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -292,6 +319,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'BLOB', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -306,6 +334,7 @@ Assert::same( 'table' => $c->table->name, 'nativeType' => $c->nativeType, 'size' => $c->size, + 'scale' => $c->scale, 'nullable' => $c->nullable, 'default' => $c->default, 'autoIncrement' => $c->autoIncrement, diff --git a/tests/Database/Reflection.columns.sqlsrv.phpt b/tests/Database/Reflection.columns.sqlsrv.phpt index f2878f06b..9e513823e 100644 --- a/tests/Database/Reflection.columns.sqlsrv.phpt +++ b/tests/Database/Reflection.columns.sqlsrv.phpt @@ -9,7 +9,7 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -$connection = connectToDB()->getConnection(); +$connection = connectToDB(); Nette\Database\Helpers::loadFromFile($connection, __DIR__ . '/files/sqlsrv-nette_test3.sql'); @@ -22,6 +22,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'BIGINT', 'size' => 19, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -32,6 +33,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'BINARY', 'size' => 3, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -42,6 +44,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'BIT', 'size' => 1, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -52,6 +55,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'CHAR', 'size' => 5, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -62,6 +66,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'DATE', 'size' => 10, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -72,6 +77,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'DATETIME', 'size' => 23, + 'scale' => 3, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -82,6 +88,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'DATETIME2', 'size' => 27, + 'scale' => 7, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -92,6 +99,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'DECIMAL', 'size' => 18, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -102,6 +110,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'FLOAT', 'size' => 53, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -112,6 +121,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'GEOGRAPHY', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -122,6 +132,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'GEOMETRY', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -132,6 +143,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'HIERARCHYID', 'size' => 892, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -142,6 +154,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'INT', 'size' => 10, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -152,6 +165,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'MONEY', 'size' => 19, + 'scale' => 4, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -162,6 +176,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'NCHAR', 'size' => 2, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -172,6 +187,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'NTEXT', 'size' => 16, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -182,6 +198,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'NUMERIC', 'size' => 10, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -192,6 +209,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'NUMERIC', 'size' => 10, + 'scale' => 2, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -202,6 +220,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'NVARCHAR', 'size' => 2, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -212,6 +231,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'REAL', 'size' => 24, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -222,6 +242,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'SMALLDATETIME', 'size' => 16, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -232,6 +253,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'SMALLINT', 'size' => 5, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -242,6 +264,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'SMALLMONEY', 'size' => 10, + 'scale' => 4, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -252,6 +275,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'TEXT', 'size' => 16, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -262,6 +286,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'TIME', 'size' => 16, + 'scale' => 7, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -272,6 +297,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'TINYINT', 'size' => 3, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -282,6 +308,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'UNIQUEIDENTIFIER', 'size' => 16, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -292,6 +319,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'VARBINARY', 'size' => 1, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -302,6 +330,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'VARCHAR', 'size' => 1, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -312,6 +341,7 @@ $expectedColumns = [ 'table' => 'types', 'nativeType' => 'XML', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -326,6 +356,7 @@ Assert::same( 'table' => $c->table->name, 'nativeType' => $c->nativeType, 'size' => $c->size, + 'scale' => $c->scale, 'nullable' => $c->nullable, 'default' => $c->default, 'autoIncrement' => $c->autoIncrement, diff --git a/tests/Database/Reflection.phpt b/tests/Database/Reflection.phpt index fe5e8a6e7..c914b1480 100644 --- a/tests/Database/Reflection.phpt +++ b/tests/Database/Reflection.phpt @@ -5,17 +5,17 @@ * @dataProvider? databases.ini */ -use Nette\Database\Driver; +use Nette\Database\Drivers\Engine; use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -$connection = connectToDB()->getConnection(); +$connection = connectToDB(); Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/files/{$driverName}-nette_test1.sql"); $reflection = $connection->getReflection(); -$schemaSupported = $connection->getDriver()->isSupported(Driver::SupportSchema); +$schemaSupported = $connection->getDatabaseEngine()->isSupported(Engine::SupportSchema); // table names $tableNames = array_keys($reflection->tables); @@ -81,6 +81,7 @@ $expectedColumns = [ 'table' => 'author', 'nativeType' => 'INT', 'size' => 11, + 'scale' => null, 'nullable' => false, 'default' => null, 'autoIncrement' => true, @@ -91,6 +92,7 @@ $expectedColumns = [ 'table' => 'author', 'nativeType' => 'VARCHAR', 'size' => 30, + 'scale' => null, 'nullable' => false, 'default' => null, 'autoIncrement' => false, @@ -101,6 +103,7 @@ $expectedColumns = [ 'table' => 'author', 'nativeType' => 'VARCHAR', 'size' => 100, + 'scale' => null, 'nullable' => false, 'default' => null, 'autoIncrement' => false, @@ -111,6 +114,7 @@ $expectedColumns = [ 'table' => 'author', 'nativeType' => 'DATE', 'size' => null, + 'scale' => null, 'nullable' => true, 'default' => null, 'autoIncrement' => false, @@ -120,7 +124,7 @@ $expectedColumns = [ switch ($driverName) { case 'mysql': - $version = $connection->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION); + $version = $connection->getServerVersion(); if (version_compare($version, '8.0', '>=')) { $expectedColumns['id']['size'] = null; } @@ -155,6 +159,7 @@ Assert::same( 'table' => $c->table->name, 'nativeType' => $c->nativeType, 'size' => $c->size, + 'scale' => $c->scale, 'nullable' => $c->nullable, 'default' => $c->default, 'autoIncrement' => $c->autoIncrement, diff --git a/tests/Database/ResultSet.fetch().phpt b/tests/Database/Result.fetch().phpt similarity index 85% rename from tests/Database/ResultSet.fetch().phpt rename to tests/Database/Result.fetch().phpt index 57e6c2344..e838fa561 100644 --- a/tests/Database/ResultSet.fetch().phpt +++ b/tests/Database/Result.fetch().phpt @@ -1,7 +1,7 @@ getConnection(); +$connection = connectToDB(); Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/files/{$driverName}-nette_test1.sql"); test('detects duplicate column names in simple query', function () use ($connection, $driverName) { $res = $connection->query('SELECT name, name FROM author'); $message = match ($driverName) { - 'mysql' => "Found duplicate columns in database result set: 'name' (from author).", + 'mysql' => "Found duplicate columns in database result set: 'name'.", 'pgsql' => "Found duplicate columns in database result set: 'name'%a%", - 'sqlite' => "Found duplicate columns in database result set: 'name' (from author).", + 'sqlite' => "Found duplicate columns in database result set: 'name'.", 'sqlsrv' => "Found duplicate columns in database result set: 'name'.", default => Assert::fail("Unsupported driver $driverName"), }; @@ -36,7 +36,7 @@ test('detects duplicate column names in simple query', function () use ($connect test('handles cursor management in stored procedures', function () use ($connection, $driverName) { if ($driverName === 'mysql') { $connection->query('CREATE DEFINER = CURRENT_USER PROCEDURE `testProc`(IN param int(10) unsigned) BEGIN SELECT * FROM book WHERE id != param; END;;'); - $connection->getPdo()->setAttribute(PHP_VERSION_ID < 80500 ? PDO::MYSQL_ATTR_USE_BUFFERED_QUERY : Pdo\Mysql::ATTR_USE_BUFFERED_QUERY, false); + $connection->getConnection()->getNativeConnection()->setAttribute(PHP_VERSION_ID < 80500 ? PDO::MYSQL_ATTR_USE_BUFFERED_QUERY : Pdo\Mysql::ATTR_USE_BUFFERED_QUERY, false); $res = $connection->query('CALL testProc(1)'); foreach ($res as $row) { @@ -52,9 +52,9 @@ test('handles cursor management in stored procedures', function () use ($connect test('detects duplicate columns in JOINed tables', function () use ($connection, $driverName) { $res = $connection->query('SELECT book.id, author.id, author.name, translator.name FROM book JOIN author ON (author.id = book.author_id) JOIN author translator ON (translator.id = book.translator_id)'); $message = match ($driverName) { - 'mysql' => "Found duplicate columns in database result set: 'id' (from book, author), 'name' (from author, translator).", + 'mysql' => "Found duplicate columns in database result set: 'id', 'name'.", 'pgsql' => "Found duplicate columns in database result set: 'id'%a% 'name'%a%", - 'sqlite' => "Found duplicate columns in database result set: 'id' (from book, author), 'name' (from author).", + 'sqlite' => "Found duplicate columns in database result set: 'id', 'name'.", 'sqlsrv' => "Found duplicate columns in database result set: 'id', 'name'.", default => Assert::fail("Unsupported driver $driverName"), }; diff --git a/tests/Database/ResultSet.fetchAll().phpt b/tests/Database/Result.fetchAll().phpt similarity index 87% rename from tests/Database/ResultSet.fetchAll().phpt rename to tests/Database/Result.fetchAll().phpt index af7d99fbb..b8cc54262 100644 --- a/tests/Database/ResultSet.fetchAll().phpt +++ b/tests/Database/Result.fetchAll().phpt @@ -1,7 +1,7 @@ getConnection(); +$connection = connectToDB(); Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/files/{$driverName}-nette_test1.sql"); @@ -22,7 +22,7 @@ match ($driverName) { }; Assert::same(1, $res->getColumnCount()); -Assert::same('SELECT id FROM book ORDER BY id', $res->getQueryString()); +Assert::same('SELECT id FROM book ORDER BY id', $res->getQuery()->getSql()); Assert::equal([ Nette\Database\Row::from(['id' => 1]), diff --git a/tests/Database/ResultSet.fetchAssoc().phpt b/tests/Database/Result.fetchAssoc().phpt similarity index 94% rename from tests/Database/ResultSet.fetchAssoc().phpt rename to tests/Database/Result.fetchAssoc().phpt index 92f517513..435141179 100644 --- a/tests/Database/ResultSet.fetchAssoc().phpt +++ b/tests/Database/Result.fetchAssoc().phpt @@ -1,7 +1,7 @@ getConnection(); +$connection = connectToDB(); Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/files/{$driverName}-nette_test1.sql"); diff --git a/tests/Database/ResultSet.fetchField().phpt b/tests/Database/Result.fetchField().phpt similarity index 86% rename from tests/Database/ResultSet.fetchField().phpt rename to tests/Database/Result.fetchField().phpt index bec07b207..e4dfd6cb6 100644 --- a/tests/Database/ResultSet.fetchField().phpt +++ b/tests/Database/Result.fetchField().phpt @@ -1,7 +1,7 @@ getConnection(); +$connection = connectToDB(); Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/files/{$driverName}-nette_test1.sql"); diff --git a/tests/Database/ResultSet.fetchList().phpt b/tests/Database/Result.fetchList().phpt similarity index 86% rename from tests/Database/ResultSet.fetchList().phpt rename to tests/Database/Result.fetchList().phpt index e69efa5e2..638adb886 100644 --- a/tests/Database/ResultSet.fetchList().phpt +++ b/tests/Database/Result.fetchList().phpt @@ -1,7 +1,7 @@ getConnection(); +$connection = connectToDB(); Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/files/{$driverName}-nette_test1.sql"); diff --git a/tests/Database/ResultSet.fetchPairs().phpt b/tests/Database/Result.fetchPairs().phpt similarity index 98% rename from tests/Database/ResultSet.fetchPairs().phpt rename to tests/Database/Result.fetchPairs().phpt index 9ba247de9..0fd75ca79 100644 --- a/tests/Database/ResultSet.fetchPairs().phpt +++ b/tests/Database/Result.fetchPairs().phpt @@ -1,7 +1,7 @@ getConnection(); +$connection = connectToDB(); Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/files/{$driverName}-nette_test1.sql"); diff --git a/tests/Database/ResultSet.customNormalizer.phpt b/tests/Database/ResultSet.customNormalizer.phpt deleted file mode 100644 index 925a6f623..000000000 --- a/tests/Database/ResultSet.customNormalizer.phpt +++ /dev/null @@ -1,51 +0,0 @@ -getConnection(); -Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/files/{$driverName}-nette_test1.sql"); - -$connection->query('UPDATE author SET born=?', new DateTime('2022-01-23')); - - -test('disabled normalization', function () use ($connection) { - $driverName = $GLOBALS['driverName']; - - $connection->setRowNormalizer(null); - $res = $connection->query('SELECT * FROM author'); - $asInt = $driverName === 'pgsql' || $driverName !== 'sqlsrv'; - Assert::same([ - 'id' => $asInt ? 11 : '11', - 'name' => 'Jakub Vrana', - 'web' => 'http://www.vrana.cz/', - 'born' => $driverName === 'sqlite' ? ($asInt ? 1_642_892_400 : '1642892400') : '2022-01-23', - ], (array) $res->fetch()); -}); - - -test('custom normalization', function () use ($connection) { - $driverName = $GLOBALS['driverName']; - - $connection->setRowNormalizer(function (array $row, Nette\Database\ResultSet $resultSet) { - foreach ($row as $key => $value) { - unset($row[$key]); - $row['_' . $key . '_'] = (string) $value; - } - - return $row; - }); - - $res = $connection->query('SELECT * FROM author'); - Assert::same([ - '_id_' => '11', - '_name_' => 'Jakub Vrana', - '_web_' => 'http://www.vrana.cz/', - '_born_' => $driverName === 'sqlite' ? '1642892400' : '2022-01-23', - ], (array) $res->fetch()); -}); diff --git a/tests/Database/ResultSet.normalizeRow.mysql.phpt b/tests/Database/ResultSet.normalizeRow.mysql.phpt index 9da74c1c0..d35ad98f5 100644 --- a/tests/Database/ResultSet.normalizeRow.mysql.phpt +++ b/tests/Database/ResultSet.normalizeRow.mysql.phpt @@ -10,7 +10,7 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -$connection = connectToDB()->getConnection(); +$connection = connectToDB(); Nette\Database\Helpers::loadFromFile($connection, __DIR__ . '/files/mysql-nette_test3.sql'); @@ -149,7 +149,7 @@ Assert::same( ); -$connection->getPdo()->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); +$connection->getConnection()->getNativeConnection()->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); $res = $connection->query('SELECT `int`, `decimal`, `decimal2`, `float`, `double` FROM types'); Assert::same([ 'int' => 1, diff --git a/tests/Database/ResultSet.normalizeRow.postgre.phpt b/tests/Database/ResultSet.normalizeRow.postgre.phpt index 75cfc0262..9b297a0f4 100644 --- a/tests/Database/ResultSet.normalizeRow.postgre.phpt +++ b/tests/Database/ResultSet.normalizeRow.postgre.phpt @@ -10,7 +10,7 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -$connection = connectToDB()->getConnection(); +$connection = connectToDB(); Nette\Database\Helpers::loadFromFile($connection, __DIR__ . '/files/pgsql-nette_test3.sql'); diff --git a/tests/Database/ResultSet.normalizeRow.sqlite.phpt b/tests/Database/ResultSet.normalizeRow.sqlite.phpt index 0e180397d..4452822bc 100644 --- a/tests/Database/ResultSet.normalizeRow.sqlite.phpt +++ b/tests/Database/ResultSet.normalizeRow.sqlite.phpt @@ -10,7 +10,7 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -$connection = connectToDB()->getConnection(); +$connection = connectToDB(); Nette\Database\Helpers::loadFromFile($connection, __DIR__ . '/files/sqlite-nette_test3.sql'); diff --git a/tests/Database/ResultSet.normalizeRow.sqlsrv.phpt b/tests/Database/ResultSet.normalizeRow.sqlsrv.phpt index a75a05e4f..db8dd7aae 100644 --- a/tests/Database/ResultSet.normalizeRow.sqlsrv.phpt +++ b/tests/Database/ResultSet.normalizeRow.sqlsrv.phpt @@ -10,7 +10,7 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -$connection = connectToDB()->getConnection(); +$connection = connectToDB(); Nette\Database\Helpers::loadFromFile($connection, __DIR__ . '/files/sqlsrv-nette_test3.sql'); @@ -19,12 +19,12 @@ $res = $connection->query('SELECT * FROM types'); Assert::equal([ 'bigint' => 1, 'binary_3' => "\x00\x00\xFF", - 'bit' => '1', + 'bit' => true, 'char_5' => 'a ', 'date' => new DateTime('2012-10-13 00:00:00'), 'datetime' => new DateTime('2012-10-13 10:10:10'), 'datetime2' => new DateTime('2012-10-13 10:10:10'), - 'decimal' => 1.0, + 'decimal' => 1, 'float' => 1.1, 'geography' => "\xe6\x10\x00\x00\x01\x14\x87\x16\xd9\xce\xf7\xd3G@\xd7\xa3p=\n\x97^\xc0\x87\x16\xd9\xce\xf7\xd3G@\xcb\xa1E\xb6\xf3\x95^\xc0", 'geometry' => "\x00\x00\x00\x00\x01\x04\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00Y@\x00\x00\x00\x00\x00\x00Y@\x00\x00\x00\x00\x00\x004@\x00\x00\x00\x00\x00\x80f@\x00\x00\x00\x00\x00\x80f@\x00\x00\x00\x00\x00\x80f@\x01\x00\x00\x00\x01\x00\x00\x00\x00\x01\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x02", @@ -33,7 +33,7 @@ Assert::equal([ 'money' => 1111.1, 'nchar' => 'a', 'ntext' => 'a', - 'numeric_10_0' => 1.0, + 'numeric_10_0' => 1, 'numeric_10_2' => 1.1, 'nvarchar' => 'a', 'real' => 1.1, @@ -52,12 +52,12 @@ Assert::equal([ Assert::equal([ 'bigint' => 0, 'binary_3' => "\x00\x00\x00", - 'bit' => '0', + 'bit' => false, 'char_5' => ' ', 'date' => new DateTime('0001-01-01 00:00:00'), 'datetime' => new DateTime('1753-01-01 00:00:00'), 'datetime2' => new DateTime('0001-01-01 00:00:00'), - 'decimal' => 0.0, + 'decimal' => 0, 'float' => 0.5, 'geography' => null, 'geometry' => null, @@ -66,7 +66,7 @@ Assert::equal([ 'money' => 0.0, 'nchar' => ' ', 'ntext' => '', - 'numeric_10_0' => 0.0, + 'numeric_10_0' => 0, 'numeric_10_2' => 0.5, 'nvarchar' => '', 'real' => 0.0, diff --git a/tests/Database/Row.phpt b/tests/Database/Row.phpt index 32c386ea1..f54c85357 100644 --- a/tests/Database/Row.phpt +++ b/tests/Database/Row.phpt @@ -9,10 +9,10 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -$connection = connectToDB()->getConnection(); +$connection = connectToDB(); test('numeric field', function () use ($connection) { - $row = $connection->fetch("SELECT 123 AS {$connection->getDriver()->delimite('123')}, NULL as nullcol"); + $row = $connection->fetch("SELECT 123 AS {$connection->getDatabaseEngine()->delimit('123')}, NULL as nullcol"); Assert::same(123, $row->{123}); Assert::same(123, $row->{'123'}); Assert::true(isset($row->{123})); diff --git a/tests/Database/SqlPreprocessor.enum.phpt b/tests/Database/SqlPreprocessor.enum.phpt index 3eb8ffeb8..ed1c9bd66 100644 --- a/tests/Database/SqlPreprocessor.enum.phpt +++ b/tests/Database/SqlPreprocessor.enum.phpt @@ -8,7 +8,7 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -$connection = connectToDB()->getConnection(); +$connection = connectToDB(); enum EnumInt: int { diff --git a/tests/Database/SqlPreprocessor.phpt b/tests/Database/SqlPreprocessor.phpt index ad386f4c1..397e6657c 100644 --- a/tests/Database/SqlPreprocessor.phpt +++ b/tests/Database/SqlPreprocessor.phpt @@ -10,7 +10,7 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -$connection = connectToDB()->getConnection(); +$connection = connectToDB(); $preprocessor = new Nette\Database\SqlPreprocessor($connection); test('Processes basic SQL query with single parameter', function () use ($preprocessor) { diff --git a/tests/Database/Structure.phpt b/tests/Database/Structure.phpt index 6fd7e50ca..df884e822 100644 --- a/tests/Database/Structure.phpt +++ b/tests/Database/Structure.phpt @@ -4,6 +4,7 @@ * Test: Nette\Database\Structure */ +use Mockery\MockInterface; use Nette\Database\Structure; use Tester\Assert; use Tester\TestCase; @@ -27,61 +28,56 @@ class StructureMock extends Structure */ class StructureTestCase extends TestCase { - private Nette\Database\Connection $connection; - private Nette\Database\Driver $driver; - private Nette\Caching\Storage $storage; - private Structure $structure; + private Nette\Database\Drivers\Engine|MockInterface $engine; + private Nette\Caching\Cache|MockInterface $cache; + private Structure|MockInterface $structure; protected function setUp() { parent::setUp(); - $this->driver = Mockery::mock(Nette\Database\Driver::class); - $this->connection = Mockery::mock(Nette\Database\Connection::class); - $this->storage = Mockery::mock(Nette\Caching\Storage::class); + $this->engine = Mockery::mock(Nette\Database\Drivers\Engine::class); + $this->cache = Mockery::mock(Nette\Caching\Cache::class); - $this->connection->shouldReceive('getDsn')->once()->andReturn(''); - $this->connection->shouldReceive('getDriver')->once()->andReturn($this->driver); - $this->driver->shouldReceive('getTables')->once()->andReturn([ + $this->engine->shouldReceive('getTables')->once()->andReturn([ ['name' => 'authors', 'view' => false], ['name' => 'Books', 'view' => false], ['name' => 'tags', 'view' => false], ['name' => 'books_x_tags', 'view' => false], ['name' => 'books_view', 'view' => true], ]); - $this->driver->shouldReceive('getColumns')->with('authors')->once()->andReturn([ - ['name' => 'id', 'primary' => true, 'autoincrement' => true, 'vendor' => ['sequence' => '"public"."authors_id_seq"']], - ['name' => 'name', 'primary' => false, 'autoincrement' => false, 'vendor' => []], + $this->engine->shouldReceive('getColumns')->with('authors')->once()->andReturn([ + ['name' => 'id', 'primary' => true, 'autoIncrement' => true, 'vendor' => ['sequence' => '"public"."authors_id_seq"']], + ['name' => 'name', 'primary' => false, 'autoIncrement' => false, 'vendor' => []], ]); - $this->driver->shouldReceive('getColumns')->with('Books')->once()->andReturn([ - ['name' => 'id', 'primary' => true, 'autoincrement' => true, 'vendor' => ['sequence' => '"public"."Books_id_seq"']], - ['name' => 'title', 'primary' => false, 'autoincrement' => false, 'vendor' => []], + $this->engine->shouldReceive('getColumns')->with('Books')->once()->andReturn([ + ['name' => 'id', 'primary' => true, 'autoIncrement' => true, 'vendor' => ['sequence' => '"public"."Books_id_seq"']], + ['name' => 'title', 'primary' => false, 'autoIncrement' => false, 'vendor' => []], ]); - $this->driver->shouldReceive('getColumns')->with('tags')->once()->andReturn([ - ['name' => 'id', 'primary' => true, 'autoincrement' => false, 'vendor' => []], - ['name' => 'name', 'primary' => false, 'autoincrement' => false, 'vendor' => []], + $this->engine->shouldReceive('getColumns')->with('tags')->once()->andReturn([ + ['name' => 'id', 'primary' => true, 'autoIncrement' => false, 'vendor' => []], + ['name' => 'name', 'primary' => false, 'autoIncrement' => false, 'vendor' => []], ]); - $this->driver->shouldReceive('getColumns')->with('books_x_tags')->once()->andReturn([ - ['name' => 'book_id', 'primary' => true, 'autoincrement' => false, 'vendor' => []], - ['name' => 'tag_id', 'primary' => true, 'autoincrement' => false, 'vendor' => []], + $this->engine->shouldReceive('getColumns')->with('books_x_tags')->once()->andReturn([ + ['name' => 'book_id', 'primary' => true, 'autoIncrement' => false, 'vendor' => []], + ['name' => 'tag_id', 'primary' => true, 'autoIncrement' => false, 'vendor' => []], ]); - $this->driver->shouldReceive('getColumns')->with('books_view')->once()->andReturn([ - ['name' => 'id', 'primary' => false, 'autoincrement' => false, 'vendor' => []], - ['name' => 'title', 'primary' => false, 'autoincrement' => false, 'vendor' => []], + $this->engine->shouldReceive('getColumns')->with('books_view')->once()->andReturn([ + ['name' => 'id', 'primary' => false, 'autoIncrement' => false, 'vendor' => []], + ['name' => 'title', 'primary' => false, 'autoIncrement' => false, 'vendor' => []], ]); - $this->connection->shouldReceive('getDriver')->times(4)->andReturn($this->driver); - $this->driver->shouldReceive('getForeignKeys')->with('authors')->once()->andReturn([]); - $this->driver->shouldReceive('getForeignKeys')->with('Books')->once()->andReturn([ - ['local' => 'author_id', 'table' => 'authors', 'foreign' => 'id', 'name' => 'authors_fk1'], - ['local' => 'translator_id', 'table' => 'authors', 'foreign' => 'id', 'name' => 'authors_fk2'], + $this->engine->shouldReceive('getForeignKeys')->with('authors')->once()->andReturn([]); + $this->engine->shouldReceive('getForeignKeys')->with('Books')->once()->andReturn([ + ['local' => ['author_id'], 'table' => 'authors', 'foreign' => ['id'], 'name' => 'authors_fk1'], + ['local' => ['translator_id'], 'table' => 'authors', 'foreign' => ['id'], 'name' => 'authors_fk2'], ]); - $this->driver->shouldReceive('getForeignKeys')->with('tags')->once()->andReturn([]); - $this->driver->shouldReceive('getForeignKeys')->with('books_x_tags')->once()->andReturn([ - ['local' => 'book_id', 'table' => 'Books', 'foreign' => 'id', 'name' => 'books_x_tags_fk1'], - ['local' => 'tag_id', 'table' => 'tags', 'foreign' => 'id', 'name' => 'books_x_tags_fk2'], + $this->engine->shouldReceive('getForeignKeys')->with('tags')->once()->andReturn([]); + $this->engine->shouldReceive('getForeignKeys')->with('books_x_tags')->once()->andReturn([ + ['local' => ['book_id'], 'table' => 'Books', 'foreign' => ['id'], 'name' => 'books_x_tags_fk1'], + ['local' => ['tag_id'], 'table' => 'tags', 'foreign' => ['id'], 'name' => 'books_x_tags_fk2'], ]); - $this->structure = new StructureMock($this->connection, $this->storage); + $this->structure = new StructureMock($this->engine, $this->cache); } @@ -100,8 +96,8 @@ class StructureTestCase extends TestCase public function testGetColumns() { $columns = [ - ['name' => 'id', 'primary' => true, 'autoincrement' => false, 'vendor' => []], - ['name' => 'name', 'primary' => false, 'autoincrement' => false, 'vendor' => []], + ['name' => 'id', 'primary' => true, 'autoIncrement' => false, 'vendor' => []], + ['name' => 'name', 'primary' => false, 'autoIncrement' => false, 'vendor' => []], ]; Assert::same($columns, $this->structure->getColumns('tags')); @@ -130,9 +126,8 @@ class StructureTestCase extends TestCase public function testGetPrimaryKeySequence() { - $this->connection->shouldReceive('getDriver')->times(4)->andReturn($this->driver); - $this->driver->shouldReceive('isSupported')->with('sequence')->once()->andReturn(false); - $this->driver->shouldReceive('isSupported')->with('sequence')->times(3)->andReturn(true); + $this->engine->shouldReceive('isSupported')->with('sequence')->once()->andReturn(false); + $this->engine->shouldReceive('isSupported')->with('sequence')->times(3)->andReturn(true); Assert::null($this->structure->getPrimaryKeySequence('Authors')); Assert::same('"public"."authors_id_seq"', $this->structure->getPrimaryKeySequence('Authors')); diff --git a/tests/Database/Structure.schemas.phpt b/tests/Database/Structure.schemas.phpt index 389166542..07648e2dd 100644 --- a/tests/Database/Structure.schemas.phpt +++ b/tests/Database/Structure.schemas.phpt @@ -4,6 +4,7 @@ * Test: Nette\Database\Structure */ +use Mockery\MockInterface; use Nette\Database\Structure; use Tester\Assert; use Tester\TestCase; @@ -27,42 +28,37 @@ class StructureMock extends Structure */ class StructureSchemasTestCase extends TestCase { - private Nette\Database\Connection $connection; - private Nette\Database\Driver $driver; - private Nette\Caching\Storage $storage; - private Structure $structure; + private Nette\Database\Drivers\Engine|MockInterface $engine; + private Nette\Caching\Cache|MockInterface $cache; + private Structure|MockInterface $structure; protected function setUp() { parent::setUp(); - $this->driver = Mockery::mock(Nette\Database\Driver::class); - $this->connection = Mockery::mock(Nette\Database\Connection::class); - $this->storage = Mockery::mock(Nette\Caching\Storage::class); + $this->engine = Mockery::mock(Nette\Database\Drivers\Engine::class); + $this->cache = Mockery::mock(Nette\Caching\Cache::class); - $this->connection->shouldReceive('getDsn')->once()->andReturn(''); - $this->connection->shouldReceive('getDriver')->once()->andReturn($this->driver); - $this->driver->shouldReceive('getTables')->once()->andReturn([ + $this->engine->shouldReceive('getTables')->once()->andReturn([ ['name' => 'authors', 'view' => false, 'fullName' => 'authors.authors'], ['name' => 'books', 'view' => false, 'fullName' => 'books.books'], ]); - $this->driver->shouldReceive('getColumns')->with('authors.authors')->once()->andReturn([ + $this->engine->shouldReceive('getColumns')->with('authors.authors')->once()->andReturn([ ['name' => 'id', 'primary' => true, 'vendor' => ['sequence' => '"authors"."authors_id_seq"']], ['name' => 'name', 'primary' => false, 'vendor' => []], ]); - $this->driver->shouldReceive('getColumns')->with('books.books')->once()->andReturn([ + $this->engine->shouldReceive('getColumns')->with('books.books')->once()->andReturn([ ['name' => 'id', 'primary' => true, 'vendor' => ['sequence' => '"books"."books_id_seq"']], ['name' => 'title', 'primary' => false, 'vendor' => []], ]); - $this->connection->shouldReceive('getDriver')->times(2)->andReturn($this->driver); - $this->driver->shouldReceive('getForeignKeys')->with('authors.authors')->once()->andReturn([]); - $this->driver->shouldReceive('getForeignKeys')->with('books.books')->once()->andReturn([ - ['local' => 'author_id', 'table' => 'authors.authors', 'foreign' => 'id', 'name' => 'authors_authors_fk1'], - ['local' => 'translator_id', 'table' => 'authors.authors', 'foreign' => 'id', 'name' => 'authors_authors_fk2'], + $this->engine->shouldReceive('getForeignKeys')->with('authors.authors')->once()->andReturn([]); + $this->engine->shouldReceive('getForeignKeys')->with('books.books')->once()->andReturn([ + ['local' => ['author_id'], 'table' => 'authors.authors', 'foreign' => ['id'], 'name' => 'authors_authors_fk1'], + ['local' => ['translator_id'], 'table' => 'authors.authors', 'foreign' => ['id'], 'name' => 'authors_authors_fk2'], ]); - $this->structure = new StructureMock($this->connection, $this->storage); + $this->structure = new StructureMock($this->engine, $this->cache); } diff --git a/tests/Database/connection.disconnect.phpt b/tests/Database/connection.disconnect.phpt new file mode 100644 index 000000000..f989f0053 --- /dev/null +++ b/tests/Database/connection.disconnect.phpt @@ -0,0 +1,53 @@ + null, 'password' => null]; + $connections = 1; + + $connection = new Nette\Database\Explorer($options['dsn'], $options['username'], $options['password']); + try { + $connection->connect(); + } catch (Nette\Database\DriverException $e) { + Tester\Environment::skip("Connection to '$options[dsn]' failed. Reason: " . $e->getMessage()); + } + + $connection->onConnect[] = function () use (&$connections) { + $connections++; + }; + + // first connection + $native = $connection->getConnection()->getNativeConnection(); + $driver = $connection->getConnection(); + Assert::same(1, $connections); + + // still first connection + $connection->connect(); + Assert::same($native, $connection->getConnection()->getNativeConnection()); + Assert::same($driver, $connection->getConnection()); + Assert::same(1, $connections); + + // second connection + $connection->reconnect(); + $native2 = $connection->getConnection()->getNativeConnection(); + $driver2 = $connection->getConnection(); + + Assert::notSame($native, $native2); + Assert::notSame($driver, $driver2); + Assert::same(2, $connections); + + // third connection + $connection->disconnect(); + Assert::notSame($native2, $connection->getConnection()->getNativeConnection()); + Assert::notSame($driver2, $connection->getConnection()); + Assert::same(3, $connections); +}); diff --git a/tests/Database/connection.option.lazy.phpt b/tests/Database/connection.option.lazy.phpt deleted file mode 100644 index a8b48300c..000000000 --- a/tests/Database/connection.option.lazy.phpt +++ /dev/null @@ -1,84 +0,0 @@ - new Nette\Database\Connection('dsn', 'user', 'password'), - Nette\Database\DriverException::class, - '%a%valid data source %a%', - ); -}); - - -test('lazy', function () { - $connection = new Nette\Database\Connection('dsn', 'user', 'password', ['lazy' => true]); - $explorer = new Nette\Database\Explorer($connection, new Structure($connection, new DevNullStorage)); - Assert::exception( - fn() => $explorer->query('SELECT ?', 10), - Nette\Database\DriverException::class, - '%a%valid data source %a%', - ); -}); - - -test('', function () { - $connection = new Nette\Database\Connection('dsn', 'user', 'password', ['lazy' => true]); - Assert::exception( - fn() => $connection->quote('x'), - Nette\Database\DriverException::class, - '%a%valid data source %a%', - ); -}); - - -test('connect & disconnect', function () { - $options = Tester\Environment::loadData() + ['username' => null, 'password' => null]; - $connections = 1; - - try { - $connection = new Nette\Database\Connection($options['dsn'], $options['username'], $options['password']); - } catch (PDOException $e) { - Tester\Environment::skip("Connection to '$options[dsn]' failed. Reason: " . $e->getMessage()); - } - - $connection->onConnect[] = function () use (&$connections) { - $connections++; - }; - - // first connection - $pdo = $connection->getPdo(); - $driver = $connection->getDriver(); - Assert::same(1, $connections); - - // still first connection - $connection->connect(); - Assert::same($pdo, $connection->getPdo()); - Assert::same($driver, $connection->getDriver()); - Assert::same(1, $connections); - - // second connection - $connection->reconnect(); - $pdo2 = $connection->getPdo(); - $driver2 = $connection->getDriver(); - - Assert::notSame($pdo, $pdo2); - Assert::notSame($driver, $driver2); - Assert::same(2, $connections); - - // third connection - $connection->disconnect(); - Assert::notSame($pdo2, $connection->getPdo()); - Assert::notSame($driver2, $connection->getDriver()); - Assert::same(3, $connections); -}); diff --git a/tests/Database/connection.options.mysql.phpt b/tests/Database/connection.options.mysql.phpt index ba067e81f..820229257 100644 --- a/tests/Database/connection.options.mysql.phpt +++ b/tests/Database/connection.options.mysql.phpt @@ -11,13 +11,13 @@ require __DIR__ . '/../bootstrap.php'; test('default charset', function () { - $connection = connectToDB(['charset' => null])->getConnection(); + $connection = connectToDB(['charset' => null]); $row = $connection->fetch("SHOW VARIABLES LIKE 'character_set_client'"); Assert::same('utf8mb4', $row->Value); }); test('custom charset', function () { - $connection = connectToDB(['charset' => 'latin2'])->getConnection(); + $connection = connectToDB(['charset' => 'latin2']); $row = $connection->fetch("SHOW VARIABLES LIKE 'character_set_client'"); Assert::same('latin2', $row->Value); }); @@ -25,28 +25,28 @@ test('custom charset', function () { test('custom sqlmode', function () { $desiredMode = 'STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION'; - $connection = connectToDB(['sqlmode' => $desiredMode])->getConnection(); + $connection = connectToDB(['sqlmode' => $desiredMode]); $field = $connection->fetchField('SELECT @@sql_mode'); Assert::same($desiredMode, $field); }); test('default convertBoolean', function () { - $connection = connectToDB(['convertBoolean' => null])->getConnection(); + $connection = connectToDB(['convertBoolean' => null]); Nette\Database\Helpers::loadFromFile($connection, __DIR__ . '/files/mysql-nette_test3.sql'); $row = $connection->fetch('SELECT * FROM types'); - Assert::same(1, $row->bool); + Assert::same(true, $row->bool); }); test('convertBoolean = true', function () { - $connection = connectToDB(['convertBoolean' => true])->getConnection(); + $connection = connectToDB(['convertBoolean' => true]); Nette\Database\Helpers::loadFromFile($connection, __DIR__ . '/files/mysql-nette_test3.sql'); $row = $connection->fetch('SELECT * FROM types'); Assert::same(true, $row->bool); }); test('convertBoolean = false', function () { - $connection = connectToDB(['convertBoolean' => false])->getConnection(); + $connection = connectToDB(['convertBoolean' => false]); Nette\Database\Helpers::loadFromFile($connection, __DIR__ . '/files/mysql-nette_test3.sql'); $row = $connection->fetch('SELECT * FROM types'); Assert::same(1, $row->bool); @@ -54,19 +54,72 @@ test('convertBoolean = false', function () { test('default newDateTime', function () { - $connection = connectToDB(['newDateTime' => null])->getConnection(); + $connection = connectToDB(['newDateTime' => null]); $field = $connection->fetchField('SELECT NOW()'); - Assert::type(Nette\Utils\DateTime::class, $field); + Assert::type(Nette\Database\DateTime::class, $field); }); test('newDateTime = false', function () { - $connection = connectToDB(['newDateTime' => false])->getConnection(); + $connection = connectToDB(['newDateTime' => false]); $field = $connection->fetchField('SELECT NOW()'); Assert::type(Nette\Utils\DateTime::class, $field); }); test('newDateTime = true', function () { - $connection = connectToDB(['newDateTime' => true])->getConnection(); + $connection = connectToDB(['newDateTime' => true]); + $field = $connection->fetchField('SELECT NOW()'); + Assert::type(Nette\Database\DateTime::class, $field); +}); + + +test('default convertDateTime', function () { + $connection = connectToDB(['convertDateTime' => null]); + $field = $connection->fetchField('SELECT NOW()'); + Assert::type(Nette\Database\DateTime::class, $field); +}); + +test('convertDateTime = false', function () { + $connection = connectToDB(['convertDateTime' => false]); + $field = $connection->fetchField('SELECT NOW()'); + Assert::type('string', $field); +}); + +test('convertDateTime = true', function () { + $connection = connectToDB(['convertDateTime' => true]); $field = $connection->fetchField('SELECT NOW()'); Assert::type(Nette\Database\DateTime::class, $field); }); + + +test('default convertDecimal', function () { + $connection = connectToDB(['convertDecimal' => null]); + Nette\Database\Helpers::loadFromFile($connection, __DIR__ . '/files/mysql-nette_test3.sql'); + $row = $connection->fetch('SELECT * FROM types'); + Assert::same(1, $row->decimal); + Assert::same(1.1, $row->decimal2); + + $fields = $connection->fetchFields('SELECT 10, 10.5'); + Assert::same([10, 10.5], $fields); +}); + +test('convertDecimal = false', function () { + $connection = connectToDB(['convertDecimal' => false]); + Nette\Database\Helpers::loadFromFile($connection, __DIR__ . '/files/mysql-nette_test3.sql'); + $row = $connection->fetch('SELECT * FROM types'); + Assert::same('1', $row->decimal); + Assert::same('1.10', $row->decimal2); + + $fields = $connection->fetchFields('SELECT 10, 10.5'); + Assert::same([10, '10.5'], $fields); +}); + +test('convertDecimal = true', function () { + $connection = connectToDB(['convertDecimal' => true]); + Nette\Database\Helpers::loadFromFile($connection, __DIR__ . '/files/mysql-nette_test3.sql'); + $row = $connection->fetch('SELECT * FROM types'); + Assert::same(1, $row->decimal); + Assert::same(1.1, $row->decimal2); + + $fields = $connection->fetchFields('SELECT 10, 10.5'); + Assert::same([10, 10.5], $fields); +}); diff --git a/tests/Database/connection.options.sqlite.phpt b/tests/Database/connection.options.sqlite.phpt index f70bc148a..f517a4b46 100644 --- a/tests/Database/connection.options.sqlite.phpt +++ b/tests/Database/connection.options.sqlite.phpt @@ -11,13 +11,37 @@ require __DIR__ . '/../bootstrap.php'; test('formatDateTime', function () { - $connection = connectToDB(['formatDateTime' => 'U'])->getConnection(); - $driver = $connection->getDriver(); - Assert::same('254358000', $driver->formatDateTime(new DateTime('1978-01-23 00:00:00'))); + $connection = connectToDB(['formatDateTime' => 'U']); + $engine = $connection->getDatabaseEngine(); + Assert::same('254358000', $engine->formatDateTime(new DateTime('1978-01-23 00:00:00'))); }); test('formatDateTime', function () { - $connection = connectToDB(['formatDateTime' => 'Y-m-d'])->getConnection(); - $driver = $connection->getDriver(); - Assert::same('1978-01-23', $driver->formatDateTime(new DateTime('1978-01-23 00:00:00'))); + $connection = connectToDB(['formatDateTime' => 'Y-m-d']); + $engine = $connection->getDatabaseEngine(); + Assert::same('1978-01-23', $engine->formatDateTime(new DateTime('1978-01-23 00:00:00'))); +}); + + +test('default convertDateTime', function () { + $connection = connectToDB(['convertDateTime' => null]); + Nette\Database\Helpers::loadFromFile($connection, __DIR__ . '/files/sqlite-nette_test3.sql'); + $row = $connection->fetch('SELECT * FROM types'); + Assert::type(Nette\Database\DateTime::class, $row->date); + Assert::type(Nette\Database\DateTime::class, $row->datetime); +}); + +test('convertDateTime = false', function () { + $connection = connectToDB(['convertDateTime' => false]); + Nette\Database\Helpers::loadFromFile($connection, __DIR__ . '/files/sqlite-nette_test3.sql'); + $row = $connection->fetch('SELECT * FROM types'); + Assert::type('int', $row->date); + Assert::type('int', $row->datetime); +}); + +test('convertDateTime = true', function () { + $connection = connectToDB(['convertDateTime' => true]); + Nette\Database\Helpers::loadFromFile($connection, __DIR__ . '/files/sqlite-nette_test3.sql'); + $row = $connection->fetch('SELECT * FROM types'); + Assert::type(Nette\Database\DateTime::class, $row->date); }); diff --git a/tests/Database/connection.options.sqlsrv.phpt b/tests/Database/connection.options.sqlsrv.phpt new file mode 100644 index 000000000..39369c69a --- /dev/null +++ b/tests/Database/connection.options.sqlsrv.phpt @@ -0,0 +1,62 @@ + null]); + Nette\Database\Helpers::loadFromFile($connection, __DIR__ . '/files/sqlsrv-nette_test3.sql'); + $row = $connection->fetch('SELECT * FROM types'); + Assert::same(1, $row->decimal); + Assert::same(1, $row->numeric_10_0); + Assert::same(1.1, $row->numeric_10_2); +}); + +test('convertDecimal = true', function () { + $connection = connectToDB(['convertDecimal' => true]); + Nette\Database\Helpers::loadFromFile($connection, __DIR__ . '/files/sqlsrv-nette_test3.sql'); + $row = $connection->fetch('SELECT * FROM types'); + Assert::same(1, $row->decimal); + Assert::same(1, $row->numeric_10_0); + Assert::same(1.1, $row->numeric_10_2); +}); + +test('convertDecimal = false', function () { + $connection = connectToDB(['convertDecimal' => false]); + Nette\Database\Helpers::loadFromFile($connection, __DIR__ . '/files/sqlsrv-nette_test3.sql'); + $row = $connection->fetch('SELECT * FROM types'); + Assert::same('1', $row->decimal); + Assert::same('1', $row->numeric_10_0); + Assert::same('1.10', $row->numeric_10_2); +}); + + +test('default convertBoolean', function () { + $connection = connectToDB(['convertBoolean' => null]); + Nette\Database\Helpers::loadFromFile($connection, __DIR__ . '/files/sqlsrv-nette_test3.sql'); + $row = $connection->fetch('SELECT * FROM types'); + Assert::equal(true, $row->bit); +}); + +test('convertBoolean = true', function () { + $connection = connectToDB(['convertBoolean' => true]); + Nette\Database\Helpers::loadFromFile($connection, __DIR__ . '/files/sqlsrv-nette_test3.sql'); + $row = $connection->fetch('SELECT * FROM types'); + Assert::equal(true, $row->bit); +}); + +test('convertBoolean = false', function () { + $connection = connectToDB(['convertBoolean' => false]); + Nette\Database\Helpers::loadFromFile($connection, __DIR__ . '/files/sqlsrv-nette_test3.sql'); + $row = $connection->fetch('SELECT * FROM types'); + Assert::equal(1, $row->bit); +}); diff --git a/tests/Database/files/sqlsrv-loadFromFile.sql b/tests/Database/files/sqlsrv-loadFromFile.sql new file mode 100644 index 000000000..d4336c2a0 --- /dev/null +++ b/tests/Database/files/sqlsrv-loadFromFile.sql @@ -0,0 +1,13 @@ +IF OBJECT_ID('products', 'U') IS NOT NULL DROP TABLE products; + +CREATE TABLE products ( + product_id int NOT NULL IDENTITY(11,1), + title varchar(50) NOT NULL, + PRIMARY KEY(product_id) +); + +SET IDENTITY_INSERT products ON; +INSERT INTO products (product_id, title) VALUES (1, 'Chair'); +INSERT INTO products (product_id, title) VALUES (2, 'Table'); +INSERT INTO products (product_id, title) VALUES (3, 'Computer'); +SET IDENTITY_INSERT products OFF; diff --git a/tests/Database/sqlsrv-loadFromFile.phpt b/tests/Database/sqlsrv-loadFromFile.phpt new file mode 100644 index 000000000..623710823 --- /dev/null +++ b/tests/Database/sqlsrv-loadFromFile.phpt @@ -0,0 +1,18 @@ + Nette\Database\Helpers::loadFromFile($connection, __DIR__ . '/files/sqlsrv-loadFromFile.sql'), +); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 9386a542a..1b555fd92 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -32,13 +32,10 @@ function connectToDB(array $options = []): Nette\Database\Explorer Tester\Environment::lock($args['dsn'], getTempDir()); } - $connection = new Nette\Database\Connection($args['dsn'], $args['username'], $args['password'], $args['options']); - $driverName = $connection->getPdo()->getAttribute(PDO::ATTR_DRIVER_NAME); - $cacheMemoryStorage = new Nette\Caching\Storages\MemoryStorage; - - $structure = new Nette\Database\Structure($connection, $cacheMemoryStorage); - $conventions = new Nette\Database\Conventions\DiscoveredConventions($structure); - $explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $cacheMemoryStorage); + $explorer = new Nette\Database\Explorer($args['dsn'], $args['username'], $args['password'], $args['options']); + $connection->connect(); + $driverName = $explorer->getConnection()->getNativeConnection()->getAttribute(PDO::ATTR_DRIVER_NAME); + $explorer->setCache(new Nette\Caching\Cache(new Nette\Caching\Storages\MemoryStorage)); echo "Driver: $driverName\n"; $GLOBALS['driverName'] = $driverName; diff --git a/tests/databases.sqlite.ini b/tests/databases.sqlite.ini index 2e076f7e8..2d89c58ce 100644 --- a/tests/databases.sqlite.ini +++ b/tests/databases.sqlite.ini @@ -1,3 +1,2 @@ [sqlite] dsn = "sqlite::memory:" -options[newDateTime] = yes diff --git a/tests/types/database-types.php b/tests/types/database-types.php index d553a9282..e4170d4f6 100644 --- a/tests/types/database-types.php +++ b/tests/types/database-types.php @@ -72,7 +72,7 @@ function testSelectionFluentMethods(Selection $selection): void function testParseColumnType(): void { $result = Helpers::parseColumnType('varchar(255)'); - assertType('array{type: string|null, length: int|null, scale: int|null, parameters: string|null}', $result); + assertType('array{type: string|null, size: int|null, scale: int|null, parameters: string|null}', $result); } @@ -112,6 +112,12 @@ function testResultSetFetchAll(ResultSet $resultSet): void } +function testResultSetFetchAllAssoc(ResultSet $resultSet): void +{ + assertType('list', $resultSet->fetchAllAssoc()); +} + + function testResultSetFetchPairs(ResultSet $resultSet): void { assertType('array', $resultSet->fetchPairs());
Rows
{if $error} @@ -36,7 +36,7 @@ - {Nette\Database\Helpers::dumpSql($sql, $params, $connection)|noescape} + {Nette\Database\Helpers::dumpSql($query, $connection)|noescape} {if $explain} @@ -73,5 +73,5 @@ {/foreach}
- {if count($queries) < $count}

...and more

{/if} + {if count($events) < $count}

...and more

{/if} diff --git a/src/Database/Connection.php b/src/Database/Connection.php deleted file mode 100644 index 537702be7..000000000 --- a/src/Database/Connection.php +++ /dev/null @@ -1,385 +0,0 @@ - Occurs after connection is established */ - public array $onConnect = []; - - /** @var array Occurs after query is executed */ - public array $onQuery = []; - private Driver $driver; - private SqlPreprocessor $preprocessor; - private ?PDO $pdo = null; - - /** @var ?\Closure(array, ResultSet): array */ - private ?\Closure $rowNormalizer; - private ?string $sql = null; - private int $transactionDepth = 0; - - - /** @param array $options */ - public function __construct( - private readonly string $dsn, - #[\SensitiveParameter] - private readonly ?string $user = null, - #[\SensitiveParameter] - private readonly ?string $password = null, - private readonly array $options = [], - ) { - $this->rowNormalizer = !empty($options['newDateTime']) - ? fn(array $row, ResultSet $resultSet): array => Helpers::normalizeRow($row, $resultSet, DateTime::class) - : Helpers::normalizeRow(...); - if (empty($options['lazy'])) { - $this->connect(); - } - } - - - /** - * Connects to the database server if not already connected. - * @throws ConnectionException - */ - public function connect(): void - { - if ($this->pdo) { - return; - } - - try { - $this->pdo = new PDO($this->dsn, $this->user, $this->password, $this->options); - } catch (PDOException $e) { - throw ConnectionException::from($e); - } - - $class = empty($this->options['driverClass']) - ? 'Nette\Database\Drivers\\' . ucfirst(str_replace('sql', 'Sql', $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME))) . 'Driver' - : $this->options['driverClass']; - $driver = new $class; - if (!$driver instanceof Driver) { - throw new Nette\InvalidStateException("Driver class '$class' does not implement " . Driver::class . '.'); - } - $this->driver = $driver; - $this->preprocessor = new SqlPreprocessor($this); - $this->driver->initialize($this, $this->options); - Arrays::invoke($this->onConnect, $this); - } - - - /** - * Disconnects and connects to database again. - */ - public function reconnect(): void - { - $this->disconnect(); - $this->connect(); - } - - - /** - * Disconnects from database. - */ - public function disconnect(): void - { - $this->pdo = null; - } - - - public function getDsn(): string - { - return $this->dsn; - } - - - public function getPdo(): PDO - { - $this->connect(); - return $this->pdo ?? throw new Nette\ShouldNotHappenException; - } - - - public function getDriver(): Driver - { - $this->connect(); - return $this->driver; - } - - - /** @deprecated use getDriver() */ - public function getSupplementalDriver(): Driver - { - $this->connect(); - return $this->driver; - } - - - public function getReflection(): Reflection - { - return new Reflection($this->getDriver()); - } - - - /** - * Sets a callback for normalizing each result row (e.g., type conversion). Pass null to disable. - * @param ?(callable(array, ResultSet): array) $normalizer - */ - public function setRowNormalizer(?callable $normalizer): static - { - $this->rowNormalizer = $normalizer ? $normalizer(...) : null; - return $this; - } - - - /** - * Returns the ID of the last inserted row, or the last value from a sequence. - */ - public function getInsertId(?string $sequence = null): string - { - try { - $res = $this->getPdo()->lastInsertId($sequence); - return $res === false ? '0' : $res; - } catch (PDOException $e) { - throw $this->driver->convertException($e); - } - } - - - /** - * Quotes string for use in SQL. - */ - public function quote(string $string, int $type = PDO::PARAM_STR): string - { - try { - return $this->getPdo()->quote($string, $type); - } catch (PDOException $e) { - throw DriverException::from($e); - } - } - - - /** - * Starts a transaction. - * @throws \LogicException when called inside a transaction - */ - public function beginTransaction(): void - { - if ($this->transactionDepth !== 0) { - throw new \LogicException(__METHOD__ . '() call is forbidden inside a transaction() callback'); - } - - $this->query('::beginTransaction'); - } - - - /** - * Commits current transaction. - * @throws \LogicException when called inside a transaction - */ - public function commit(): void - { - if ($this->transactionDepth !== 0) { - throw new \LogicException(__METHOD__ . '() call is forbidden inside a transaction() callback'); - } - - $this->query('::commit'); - } - - - /** - * Rolls back current transaction. - * @throws \LogicException when called inside a transaction - */ - public function rollBack(): void - { - if ($this->transactionDepth !== 0) { - throw new \LogicException(__METHOD__ . '() call is forbidden inside a transaction() callback'); - } - - $this->query('::rollBack'); - } - - - /** - * Executes callback inside a transaction. Supports nesting. - * @param callable(static): mixed $callback - */ - public function transaction(callable $callback): mixed - { - if ($this->transactionDepth === 0) { - $this->beginTransaction(); - } - - $this->transactionDepth++; - try { - $res = $callback($this); - } catch (\Throwable $e) { - $this->transactionDepth--; - if ($this->transactionDepth === 0) { - $this->rollBack(); - } - - throw $e; - } - - $this->transactionDepth--; - if ($this->transactionDepth === 0) { - $this->commit(); - } - - return $res; - } - - - /** - * Generates and executes SQL query. - * @param literal-string $sql - */ - public function query(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): ResultSet - { - [$this->sql, $params] = $this->preprocess($sql, ...$params); - try { - $result = new ResultSet($this, $this->sql, $params, $this->rowNormalizer); - } catch (PDOException $e) { - Arrays::invoke($this->onQuery, $this, $e); - throw $e; - } - - Arrays::invoke($this->onQuery, $this, $result); - return $result; - } - - - /** - * @deprecated use query() - * @param literal-string $sql - * @param array $params - */ - public function queryArgs(string $sql, array $params): ResultSet - { - return $this->query($sql, ...$params); - } - - - /** - * Preprocesses SQL query with parameter substitution and returns the resulting SQL and bound parameters. - * @param literal-string $sql - * @return array{string, array} - */ - public function preprocess(string $sql, mixed ...$params): array - { - $this->connect(); - return $params - ? $this->preprocessor->process(func_get_args()) - : [$sql, []]; - } - - - public function getLastQueryString(): ?string - { - return $this->sql; - } - - - /********************* shortcuts ****************d*g**/ - - - /** - * Executes SQL query and returns the first row, or null if no rows were returned. - * @param literal-string $sql - */ - public function fetch(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): ?Row - { - return $this->query($sql, ...$params)->fetch(); - } - - - /** - * Executes SQL query and returns the first row as an associative array, or null. - * @param literal-string $sql - * @return ?array - */ - public function fetchAssoc(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): ?array - { - return $this->query($sql, ...$params)->fetchAssoc(); - } - - - /** - * Executes SQL query and returns the first field of the first row, or null. - * @param literal-string $sql - */ - public function fetchField(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): mixed - { - return $this->query($sql, ...$params)->fetchField(); - } - - - /** - * Executes SQL query and returns the first row as an indexed array, or null. - * @param literal-string $sql - * @return ?list - */ - public function fetchList(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): ?array - { - return $this->query($sql, ...$params)->fetchList(); - } - - - /** - * Executes SQL query and returns the first row as an indexed array, or null. - * @param literal-string $sql - * @return ?list - */ - public function fetchFields(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): ?array - { - return $this->query($sql, ...$params)->fetchList(); - } - - - /** - * Executes SQL query and returns rows as key-value pairs. - * @param literal-string $sql - * @return array - */ - public function fetchPairs(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): array - { - return $this->query($sql, ...$params)->fetchPairs(); - } - - - /** - * Executes SQL query and returns all rows as an array of Row objects. - * @param literal-string $sql - * @return list - */ - public function fetchAll(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): array - { - return $this->query($sql, ...$params)->fetchAll(); - } - - - /** - * Creates SQL literal value. - */ - public static function literal(string $value, mixed ...$params): SqlLiteral - { - return new SqlLiteral($value, $params); - } -} diff --git a/src/Database/Conventions.php b/src/Database/Conventions.php index 8ed7aaf6c..e853163c3 100644 --- a/src/Database/Conventions.php +++ b/src/Database/Conventions.php @@ -40,6 +40,3 @@ function getHasManyReference(string $table, string $key): ?array; */ function getBelongsToReference(string $table, string $key): ?array; } - - -interface_exists(IConventions::class); diff --git a/src/Database/Conventions/DiscoveredConventions.php b/src/Database/Conventions/DiscoveredConventions.php index 352e4d9fc..bd012f304 100644 --- a/src/Database/Conventions/DiscoveredConventions.php +++ b/src/Database/Conventions/DiscoveredConventions.php @@ -8,7 +8,8 @@ namespace Nette\Database\Conventions; use Nette\Database\Conventions; -use Nette\Database\IStructure; +use Nette\Database\Structure; +use function count, preg_replace, strcmp, stripos, strtolower; /** @@ -17,7 +18,7 @@ class DiscoveredConventions implements Conventions { public function __construct( - protected readonly IStructure $structure, + protected readonly Structure $structure, ) { } @@ -90,7 +91,7 @@ public function getBelongsToReference(string $table, string $key): ?array $tableColumns = $this->structure->getBelongsToReference($table) ?? []; foreach ($tableColumns as $column => $targetTable) { - if (stripos($column, $key) !== false) { + if ($column === $key . '_id' || $column === $key . 'Id' || $column === 'id_' . $key) { return [$targetTable, $column]; } } diff --git a/src/Database/Database.php b/src/Database/Database.php new file mode 100644 index 000000000..172e81fa9 --- /dev/null +++ b/src/Database/Database.php @@ -0,0 +1,581 @@ + Drivers\PDO\MSSQL\Driver::class, + 'pdo-mysql' => Drivers\PDO\MySQL\Driver::class, + 'pdo-oci' => Drivers\PDO\OCI\Driver::class, + 'pdo-odbc' => Drivers\PDO\ODBC\Driver::class, + 'pdo-pgsql' => Drivers\PDO\PgSQL\Driver::class, + 'pdo-sqlite' => Drivers\PDO\SQLite\Driver::class, + 'pdo-sqlsrv' => Drivers\PDO\SQLSrv\Driver::class, + ]; + private const TypeConverterOptions = ['convertBoolean', 'convertDateTime', 'convertDecimal', 'newDateTime']; + + /** @var array Occurs after connection is established */ + public array $onConnect = []; + + /** @var array Occurs after query is executed */ + public array $onQuery = []; + private Drivers\Driver $driver; + private ?Drivers\Connection $connection = null; + private Drivers\Engine $engine; + private SqlPreprocessor $preprocessor; + private TypeConverter $typeConverter; + private ?SqlLiteral $lastQuery = null; + private int $transactionDepth = 0; + private bool $autoCommit = true; + private ?Cache $cache = null; + private ?Conventions $conventions = null; + private ?Structure $structure = null; + + + public static function createFromParameters( + #[\SensitiveParameter] + ...$params, + ): self + { + $params = count($params) === 1 && is_array($params[0] ?? null) ? $params[0] : $params; + + if ($class = $params['driverClass'] ?? null) { + if (!is_subclass_of($class, Drivers\Driver::class)) { + throw new \LogicException("Driver class '$class' is not subclass of " . Drivers\Driver::class); + } + unset($params['driverClass']); + + } elseif ($driver = $params['driver'] ?? null) { + $class = self::Drivers[$driver] ?? throw new \LogicException("Unknown driver '$driver'."); + unset($params['driver']); + + } elseif ($dsn = $params['dsn'] ?? null) { + $driver = explode(':', $dsn)[0]; + $class = self::Drivers['pdo-' . $driver] ?? throw new \LogicException("Unknown PDO driver '$driver'."); + + } else { + throw new \LogicException("Missing options 'driver', 'driverClass' or 'dsn'."); + } + + $args = array_diff_key($params, array_flip(self::TypeConverterOptions)); + $explorer = new self(new $class(...$args)); + array_map(fn($opt) => isset($params[$opt]) && ($explorer->typeConverter->$opt = (bool) $params[$opt]), self::TypeConverterOptions); + $this->autoCommit = (bool) ($params['autoCommit'] ?? true); + return $explorer; + } + + + public static function createFromDsn( + string $dsn, + ?string $username = null, + #[\SensitiveParameter] + ?string $password = null, + array $options = [], + ): self + { + $params = compact('dsn', 'username', 'password', 'options'); + foreach ($options as $key => $value) { + if (!is_int($key) && $value !== null) { + $params[$key] = $value; + unset($params['options'][$key]); + } + } + unset($params['lazy']); + return self::createFromParameters($params); + } + + + public function __construct( + Drivers\Driver|string $driver, + ) { + if (is_string($driver)) { // back compatibility with version 3.x + $explorer = self::createFromDsn(...func_get_args()); + [$this->driver, $this->typeConverter] = [$explorer->driver, $explorer->typeConverter]; + } else { + $this->driver = $driver; + $this->typeConverter = new TypeConverter; + } + } + + + /** + * Connects to the database server if not already connected. + * @throws ConnectionException + */ + public function connect(): void + { + if ($this->connection) { + return; + } + + try { + $this->connection = $this->driver->connect(); + } catch (DriverException $e) { + throw ConnectionException::from($e); + } + + if (!$this->autoCommit) { + $this->beginTransaction(); + } + Arrays::invoke($this->onConnect, $this); + } + + + /** + * Disconnects and connects to database again. + */ + public function reconnect(): void + { + $this->disconnect(); + $this->connect(); + } + + + /** + * Disconnects from database. + */ + public function disconnect(): void + { + $this->connection = null; + } + + + #[\Deprecated] + public function getDsn(): string + { + throw new Nette\DeprecatedException(__METHOD__ . '() is deprecated.'); + } + + + #[\Deprecated('use getConnection()->getNativeConnection()')] + public function getPdo(): \PDO + { + trigger_error(__METHOD__ . '() is deprecated, use getConnection()->getNativeConnection()', E_USER_DEPRECATED); + return $this->getConnection()->getNativeConnection(); + } + + + public function getConnection(): Drivers\Connection + { + $this->connect(); + return $this->connection; + } + + + #[\Deprecated('use getConnection()')] + public function getSupplementalDriver(): Drivers\Connection + { + trigger_error(__METHOD__ . '() is deprecated, use getConnection()', E_USER_DEPRECATED); + return $this->getConnection(); + } + + + public function getDatabaseEngine(): Drivers\Engine + { + return $this->engine ??= $this->driver->createEngine(new Drivers\Accessory\LazyConnection($this->getConnection(...))); + } + + + public function getServerVersion(): string + { + return $this->getConnection()->getServerVersion(); + } + + + public function getReflection(): Reflection + { + return new Reflection($this->getDatabaseEngine()); + } + + + public function getTypeConverter(): TypeConverter + { + return $this->typeConverter; + } + + + #[\Deprecated] + public function setRowNormalizer(?callable $normalizer): static + { + throw new Nette\DeprecatedException(__METHOD__ . "() is deprecated, configure 'convert*' options instead."); + } + + + /** + * Returns the ID of the last inserted row, or the last value from a sequence. + */ + public function getInsertId(?string $sequence = null): int|string + { + try { + return $this->getConnection()->getInsertId($sequence); + } catch (DriverException $e) { + throw $this->convertException($e); + } + } + + + /** + * Quotes string for use in SQL. + */ + public function quote(string $string): string + { + return $this->getConnection()->quote($string); + } + + + /** + * Starts a transaction. + * @throws \LogicException when called inside a transaction + */ + public function beginTransaction(): void + { + if ($this->transactionDepth === 0) { + $this->logOperation($this->getConnection()->beginTransaction(...), new SqlLiteral('BEGIN TRANSACTION')); + } else { + $this->createSavepoint($this->transactionDepth - 1); + } + + $this->transactionDepth++; + } + + + /** + * Commits current transaction. + * @throws \LogicException when called inside a transaction + */ + public function commit(): void + { + if ($this->transactionDepth === 0) { + throw new \LogicException('No active transaction to commit.'); + + } elseif (--$this->transactionDepth === 0) { + $this->logOperation($this->getConnection()->commit(...), new SqlLiteral('COMMIT')); + if (!$this->autoCommit) { + $this->beginTransaction(); + } + } else { + $this->releaseSavepoint($this->transactionDepth); + } + } + + + /** + * Rolls back current transaction. + * @throws \LogicException when called inside a transaction + */ + public function rollBack(): void + { + if ($this->transactionDepth === 0) { + throw new \LogicException('No active transaction to roll back.'); + + } elseif (--$this->transactionDepth === 0) { + $this->logOperation($this->getConnection()->rollBack(...), new SqlLiteral('ROLLBACK')); + if (!$this->autoCommit) { + $this->beginTransaction(); + } + } else { + $this->rollbackToSavepoint($this->transactionDepth); + } + } + + + /** + * Executes callback inside a transaction. Supports nesting. + * @param callable(static): mixed $callback + */ + public function transaction(callable $callback): mixed + { + $this->beginTransaction(); + try { + $res = $callback($this); + $this->commit(); + return $res; + } catch (\Throwable $e) { + $this->rollBack(); + throw $e; + } + } + + + public function setAutoCommit(bool $state): void + { + if ($this->autoCommit === $state) { + return; + } + + while ($this->transactionDepth > 0) { + if (--$this->transactionDepth === 0) { + $this->query('::commit'); + } else { + $this->releaseSavepoint($this->transactionDepth); + } + } + + $this->autoCommit = $state; + if (!$state && $this->connection) { + $this->beginTransaction(); + } + } + + + public function createSavepoint(int $level): void + { + $this->query('SAVEPOINT LEVEL' . $level); // TODO: to driver, logOperation + } + + + public function releaseSavepoint(int $level): void + { + $this->query('RELEASE SAVEPOINT LEVEL' . $level); + } + + + /** + * Generates and executes SQL query. + * @param literal-string $sql + */ + public function query(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): Result + { + [$sql, $params] = $this->preprocess($sql, ...$params); + return $this->logOperation( + fn() => $this->connection->query($sql, $params), + $this->lastQuery = new SqlLiteral($sql, $params), + ); + } + + + /** + * @param literal-string $sql + * @param array $params + */ + #[\Deprecated('use query()')] + public function queryArgs(string $sql, array $params): Result + { + trigger_error(__METHOD__ . '() is deprecated, use query()', E_USER_DEPRECATED); + return $this->query($sql, ...$params); + } + + + /** + * Preprocesses SQL query with parameter substitution and returns the resulting SQL and bound parameters. + * @param literal-string $sql + * @return array{string, array} + */ + public function preprocess(string $sql, mixed ...$params): array + { + $this->connect(); + $this->preprocessor ??= new SqlPreprocessor($this); + return $params + ? $this->preprocessor->process(func_get_args()) + : [$sql, []]; + } + + + /** @param \Closure(): (Drivers\Result|void) $callback */ + private function logOperation(\Closure $callback, SqlLiteral $query): Result + { + try { + $time = microtime(as_float: true); + $result = $callback(); + $time = microtime(as_float: true) - $time; + } catch (DriverException $e) { + $e = $this->convertException($e); + Arrays::invoke($this->onQuery, $this, $e); + throw $e; + } + + $result = new Result($this, $query, $result, $time); + Arrays::invoke($this->onQuery, $this, $result); + return $result; + } + + + public function getLastQuery(): ?SqlLiteral + { + return $this->lastQuery; + } + + + #[\Deprecated('use getLastQuery()->getSql()')] + public function getLastQueryString(): ?string + { + trigger_error(__METHOD__ . '() is deprecated, use getLastQuery()->getSql()', E_USER_DEPRECATED); + return $this->lastQuery?->getSql(); + } + + + private function convertException(DriverException $e): DriverException + { + $class = $this->getDatabaseEngine()->classifyException($e); + return $class ? $class::from($e) : $e; + } + + + /********************* shortcuts ****************d*g**/ + + + /** + * Executes SQL query and returns the first row, or null if no rows were returned. + * @param literal-string $sql + */ + public function fetch(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): ?Row + { + return $this->query($sql, ...$params)->fetch(); + } + + + /** + * Executes SQL query and returns the first row as an associative array, or null. + * @param literal-string $sql + * @return ?array + */ + public function fetchAssoc(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): ?array + { + return $this->query($sql, ...$params)->fetchAssoc(); + } + + + /** + * Executes SQL query and returns the first field of the first row, or null. + * @param literal-string $sql + */ + public function fetchField(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): mixed + { + return $this->query($sql, ...$params)->fetchField(); + } + + + /** + * Executes SQL query and returns the first row as an indexed array, or null. + * @param literal-string $sql + * @return ?list + */ + public function fetchList(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): ?array + { + return $this->query($sql, ...$params)->fetchList(); + } + + + /** + * Executes SQL query and returns the first row as an indexed array, or null. + * @param literal-string $sql + * @return ?list + */ + public function fetchFields(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): ?array + { + return $this->query($sql, ...$params)->fetchList(); + } + + + /** + * Executes SQL query and returns rows as key-value pairs. + * @param literal-string $sql + * @return array + */ + public function fetchPairs(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): array + { + return $this->query($sql, ...$params)->fetchPairs(); + } + + + /** + * Executes SQL query and returns all rows as an array of Row objects. + * @param literal-string $sql + * @return list + */ + public function fetchAll(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): array + { + return $this->query($sql, ...$params)->fetchAll(); + } + + + /** + * Executes SQL query and returns all rows as an array of arrays. + * Useful because the rows can be typed using @var. + * @param literal-string $sql + * @return list + */ + public function fetchAllAssoc(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): array + { + return $this->query($sql, ...$params)->fetchAllAssoc(); + } + + + /** + * Creates SQL literal value. + */ + public static function literal(string $value, mixed ...$params): SqlLiteral + { + return new SqlLiteral($value, $params); + } + + + /********************* active row ****************d*g**/ + + + public function table(string $table): Table\Selection + { + return new Table\Selection($this, $table); + } + + + public function setCache(Cache $cache): static + { + if (isset($this->structure)) { + throw new \LogicException('Cannot set cache after structure is created.'); + } + $this->cache = $cache; + return $this; + } + + + /** @internal */ + public function getCache(): ?Cache + { + return $this->cache; + } + + + public function setConventions(Conventions $conventions): static + { + if (isset($this->conventions)) { + throw new \LogicException('Conventions are already set.'); + } + $this->conventions = $conventions; + return $this; + } + + + /** @internal */ + public function getConventions(): Conventions + { + return $this->conventions ??= new Conventions\DiscoveredConventions($this->getStructure()); + } + + + /** @internal */ + public function getStructure(): Structure + { + return $this->structure ??= new Structure($this->getDatabaseEngine(), $this->getCache()); + } +} + + +class_exists(Connection::class); diff --git a/src/Database/DriverException.php b/src/Database/DriverException.php index 41a29d47d..8fce7298f 100644 --- a/src/Database/DriverException.php +++ b/src/Database/DriverException.php @@ -7,48 +7,34 @@ namespace Nette\Database; -use function array_slice; /** * Base class for all errors in the driver or SQL server. */ -class DriverException extends \PDOException +class DriverException extends \Exception { - public ?string $queryString = null; - - /** @var array|null */ - public ?array $params = null; + public static function from(self $e): static + { + return new static($e->getMessage(), $e->sqlState, $e->getCode(), $e->query, $e); + } - /** - * Creates a DriverException from a PDOException, preserving error info and stack trace location. - */ - public static function from(\PDOException $src): static - { - $e = new static($src->message, 0, $src); - $e->file = $src->file; - $e->line = $src->line; - if (!$src->errorInfo && preg_match('#SQLSTATE\[(.*?)\] \[(.*?)\] (.*)#A', $src->message, $m)) { - $m[2] = (int) $m[2]; - $e->errorInfo = array_slice($m, 1); - $e->code = $m[1]; - } else { - $e->errorInfo = $src->errorInfo; - $e->code = $src->code; - $e->code = $e->errorInfo[0] ?? $src->code; - } - - return $e; + public function __construct( + string $message, + private readonly ?string $sqlState = null, + int $code = 0, + private readonly ?SqlLiteral $query = null, + ?\Throwable $previous = null, + ) { + parent::__construct($message, $code, $previous); } - /** - * Returns the driver-specific error code, or null if not available. - */ - public function getDriverCode(): int|string|null + /** @deprecated use getCode() */ + public function getDriverCode(): int { - return $this->errorInfo[1] ?? null; + return $this->getCode(); } @@ -57,19 +43,26 @@ public function getDriverCode(): int|string|null */ public function getSqlState(): ?string { - return $this->errorInfo[0] ?? null; + return $this->sqlState; + } + + + public function getQuery(): ?SqlLiteral + { + return $this->query; } + /** @deprecated use getQuery()->getSql() */ public function getQueryString(): ?string { - return $this->queryString; + return $this->query?->getSql(); } - /** @return array|null */ + #[\Deprecated('use getQuery()->getParameters()')] public function getParameters(): ?array { - return $this->params; + return $this->query?->getParameters(); } } diff --git a/src/Database/Drivers/Accessory/LazyConnection.php b/src/Database/Drivers/Accessory/LazyConnection.php new file mode 100644 index 000000000..78d26b3e7 --- /dev/null +++ b/src/Database/Drivers/Accessory/LazyConnection.php @@ -0,0 +1,82 @@ +callback)(); + } + + + public function query(string $sql, array $params = []): Drivers\Result + { + return $this->getConnection()->query($sql, $params); + } + + + public function execute(string $sql): int + { + return $this->getConnection()->execute($sql); + } + + + public function getNativeConnection(): mixed + { + return $this->getConnection()->getNativeConnection(); + } + + + public function beginTransaction(): void + { + $this->getConnection()->beginTransaction(); + } + + + public function commit(): void + { + $this->getConnection()->commit(); + } + + + public function rollBack(): void + { + $this->getConnection()->rollBack(); + } + + + public function getInsertId(?string $sequence = null): int|string + { + return $this->getConnection()->getInsertId($sequence); + } + + + public function quote(string $string): string + { + return $this->getConnection()->quote($string); + } + + + public function getServerVersion(): string + { + return $this->getConnection()->getServerVersion(); + } +} diff --git a/src/Database/Drivers/Connection.php b/src/Database/Drivers/Connection.php new file mode 100644 index 000000000..03dd4d412 --- /dev/null +++ b/src/Database/Drivers/Connection.php @@ -0,0 +1,45 @@ + $options + * Maps a driver exception to an appropriate exception class. + * @return ?class-string */ - function initialize(Connection $connection, array $options): void; + function classifyException(DriverException $e): ?string; - /** - * Converts PDOException to DriverException or its descendant. - */ - function convertException(\PDOException $e): DriverException; + /** Converts a value from the database to a PHP value. */ + function convertToPhp(mixed $value, array $meta, TypeConverter $converter): mixed; /********************* SQL utilities ****************d*g**/ /** Adds delimiters around database identifier. */ - function delimite(string $name): string; + function delimit(string $name): string; /** Formats a date-time value for use in an SQL statement. */ function formatDateTime(\DateTimeInterface $value): string; @@ -58,11 +49,8 @@ function formatDateTime(\DateTimeInterface $value): string; /** Formats a date-time interval for use in an SQL statement. */ function formatDateInterval(\DateInterval $value): string; - /** Encodes string for use in a LIKE statement. */ - function formatLike(string $value, int $pos): string; - /** Applies LIMIT and OFFSET clauses to an SQL query. */ - function applyLimit(string &$sql, ?int $limit, ?int $offset): void; + function applyLimit(string $sql, ?int $limit, ?int $offset): string; /********************* reflection ****************d*g**/ @@ -74,7 +62,7 @@ function getTables(): array; /** * Returns metadata for all columns in a table. - * @return list}> + * @return list}> */ function getColumns(string $table): array; @@ -86,16 +74,7 @@ function getIndexes(string $table): array; /** * Returns metadata for all foreign keys in a table. - * @return list + * @return list, table: string, foreign: list}> */ function getForeignKeys(string $table): array; - - /** - * Returns associative array of detected types (IStructure::FIELD_*) in result set. - * @return array - */ - function getColumnTypes(\PDOStatement $statement): array; } - - -interface_exists(ISupplementalDriver::class); diff --git a/src/Database/Drivers/MsSqlDriver.php b/src/Database/Drivers/Engines/MSSQLEngine.php similarity index 75% rename from src/Database/Drivers/MsSqlDriver.php rename to src/Database/Drivers/Engines/MSSQLEngine.php index b3f6e04c0..de1f6d747 100644 --- a/src/Database/Drivers/MsSqlDriver.php +++ b/src/Database/Drivers/Engines/MSSQLEngine.php @@ -5,23 +5,23 @@ * Copyright (c) 2004 David Grudl (https://davidgrudl.com) */ -namespace Nette\Database\Drivers; +namespace Nette\Database\Drivers\Engines; use Nette; -use function array_values, explode, preg_replace, str_replace, strtoupper, strtr; +use Nette\Database\Drivers\Connection; +use Nette\Database\Drivers\Engine; +use Nette\Database\TypeConverter; +use function array_values, explode, preg_replace, str_replace, strtoupper; /** - * Supplemental MS SQL database driver. + * MS SQL database platform. */ -class MsSqlDriver implements Nette\Database\Driver +class MSSQLEngine implements Engine { - private Nette\Database\Connection $connection; - - - public function initialize(Nette\Database\Connection $connection, array $options): void - { - $this->connection = $connection; + public function __construct( + private readonly Connection $connection, + ) { } @@ -31,24 +31,16 @@ public function isSupported(string $feature): bool } - public function convertException(\PDOException $e): Nette\Database\DriverException + public function classifyException(Nette\Database\DriverException $e): ?string { - $code = $e->errorInfo[1] ?? null; - if ($code === 1205) { - return Nette\Database\DeadlockException::from($e); - - } elseif ($code === 1222) { - return Nette\Database\LockTimeoutException::from($e); - } - - return Nette\Database\DriverException::from($e); + return null; } /********************* SQL ****************d*g**/ - public function delimite(string $name): string + public function delimit(string $name): string { // @see https://msdn.microsoft.com/en-us/library/ms176027.aspx return '[' . str_replace(['[', ']'], ['[[', ']]'], $name) . ']'; @@ -67,14 +59,7 @@ public function formatDateInterval(\DateInterval $value): string } - public function formatLike(string $value, int $pos): string - { - $value = strtr($value, ["'" => "''", '%' => '[%]', '_' => '[_]', '[' => '[[]']); - return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'"); - } - - - public function applyLimit(string &$sql, ?int $limit, ?int $offset): void + public function applyLimit(string $sql, ?int $limit, ?int $offset): string { if ($offset) { throw new Nette\NotSupportedException('Offset is not supported by this database.'); @@ -88,6 +73,8 @@ public function applyLimit(string &$sql, ?int $limit, ?int $offset): void throw new Nette\InvalidArgumentException('SQL query must begin with SELECT, UPDATE or DELETE command.'); } } + + return $sql; } @@ -134,6 +121,7 @@ public function getColumns(string $table): array c.DATA_TYPE, c.CHARACTER_MAXIMUM_LENGTH, c.NUMERIC_PRECISION, + c.NUMERIC_SCALE, c.IS_NULLABLE, c.COLUMN_DEFAULT, c.DOMAIN_NAME, @@ -147,21 +135,22 @@ public function getColumns(string $table): array WHERE c.TABLE_SCHEMA = ? AND c.TABLE_NAME = ? - X, $table_schema, $table_name); + X, [$table_schema, $table_name]); while ($row = $rows->fetch()) { $columns[] = [ 'name' => $row['COLUMN_NAME'], 'table' => $table, - 'nativetype' => strtoupper($row['DATA_TYPE']), + 'nativeType' => strtoupper($row['DATA_TYPE']), 'size' => $row['CHARACTER_MAXIMUM_LENGTH'] ?? $row['NUMERIC_PRECISION'], + 'scale' => $row['NUMERIC_SCALE'], 'unsigned' => false, 'nullable' => $row['IS_NULLABLE'] === 'YES', 'default' => $row['COLUMN_DEFAULT'], - 'autoincrement' => $row['DOMAIN_NAME'] === 'COUNTER', + 'autoIncrement' => $row['DOMAIN_NAME'] === 'COUNTER', 'primary' => $row['COLUMN_NAME'] === 'ID', 'comment' => $row['comment'] ?? '', - 'vendor' => (array) $row, + 'vendor' => $row, ]; } @@ -188,8 +177,8 @@ public function getIndexes(string $table): array WHERE t.name = ? ORDER BY - ind.name, ic.index_column_id - X, $table_name); + t.name, ind.name, ind.index_id, ic.index_column_id + X, [$table_name]); while ($row = $rows->fetch()) { $id = (string) $row['name']; @@ -233,23 +222,22 @@ public function getForeignKeys(string $table): array ON col2.column_id = referenced_column_id AND col2.object_id = tab2.object_id WHERE tab1.name = ? - X, $table_name); + X, [$table_name]); while ($row = $rows->fetch()) { - $keys[] = [ - 'name' => (string) $row['name'], - 'local' => (string) $row['local'], - 'table' => $table_schema . '.' . $row['table'], - 'foreign' => (string) $row['foreign'], - ]; + $id = $row['fk_name']; + $keys[$id]['name'] = $id; + $keys[$id]['local'][] = $row['column']; + $keys[$id]['table'] = $table_schema . '.' . $row['referenced_table']; + $keys[$id]['foreign'][] = $row['referenced_column']; } - return $keys; + return array_values($keys); } - public function getColumnTypes(\PDOStatement $statement): array + public function convertToPhp(mixed $value, array $meta, TypeConverter $converter): mixed { - return Nette\Database\Helpers::detectTypes($statement); + return $converter->convertToPhp($value, $meta); } } diff --git a/src/Database/Drivers/Engines/MySQLEngine.php b/src/Database/Drivers/Engines/MySQLEngine.php new file mode 100644 index 000000000..1969da413 --- /dev/null +++ b/src/Database/Drivers/Engines/MySQLEngine.php @@ -0,0 +1,199 @@ +getCode(); + return match (true) { + in_array($code, [1216, 1217, 1451, 1452, 1701], strict: true) => Nette\Database\ForeignKeyConstraintViolationException::class, + in_array($code, [1062, 1557, 1569, 1586], strict: true) => Nette\Database\UniqueConstraintViolationException::class, + $code >= 2001 && $code <= 2028 => Nette\Database\ConnectionException::class, + in_array($code, [1048, 1121, 1138, 1171, 1252, 1263, 1566], strict: true) => Nette\Database\NotNullConstraintViolationException::class, + default => null, + }; + } + + + /********************* SQL ****************d*g**/ + + + public function delimit(string $name): string + { + // @see http://dev.mysql.com/doc/refman/5.0/en/identifiers.html + return '`' . str_replace('`', '``', $name) . '`'; + } + + + public function formatDateTime(\DateTimeInterface $value): string + { + return $value->format("'Y-m-d H:i:s'"); + } + + + public function formatDateInterval(\DateInterval $value): string + { + return $value->format("'%r%h:%I:%S'"); + } + + + public function applyLimit(string $sql, ?int $limit, ?int $offset): string + { + if ($limit < 0 || $offset < 0) { + throw new Nette\InvalidArgumentException('Negative offset or limit.'); + + } elseif ($limit !== null || $offset) { + // see http://dev.mysql.com/doc/refman/5.0/en/select.html + $sql .= ' LIMIT ' . ($limit ?? '18446744073709551615') + . ($offset ? ' OFFSET ' . $offset : ''); + } + + return $sql; + } + + + /********************* reflection ****************d*g**/ + + + public function getTables(): array + { + $tables = []; + $query = $this->connection->query(<<<'XX' + SELECT TABLE_NAME, TABLE_TYPE, TABLE_COMMENT + FROM information_schema.TABLES + WHERE TABLE_SCHEMA = DATABASE() + XX); + + while ($row = $query->fetch()) { + $row = array_values($row); + $tables[] = [ + 'name' => (string) $row['TABLE_NAME'], + 'view' => $row['TABLE_TYPE'] === 'VIEW', + 'comment' => (string) $row['TABLE_COMMENT'], + ]; + } + + return $tables; + } + + + public function getColumns(string $table): array + { + $columns = []; + $rows = $this->connection->query('SHOW FULL COLUMNS FROM ' . $this->delimit($table)); + while ($row = $rows->fetch()) { + $row = array_change_key_case($row); + $typeInfo = Nette\Database\Helpers::parseColumnType($row['type']); + $columns[] = [ + 'name' => $row['field'], + 'table' => $table, + 'nativeType' => strtoupper($typeInfo['type'] ?? ''), + 'size' => $typeInfo['size'], + 'scale' => $typeInfo['scale'], + 'nullable' => $row['null'] === 'YES', + 'default' => $row['default'], + 'autoIncrement' => $row['extra'] === 'auto_increment', + 'primary' => $row['key'] === 'PRI', + 'comment' => $row['comment'], + 'vendor' => $row, + ]; + } + + return $columns; + } + + + public function getIndexes(string $table): array + { + $indexes = []; + $rows = $this->connection->query('SHOW INDEX FROM ' . $this->delimit($table)); + while ($row = $rows->fetch()) { + $id = (string) $row['Key_name']; + $indexes[$id] ??= [ + 'name' => $id, + 'unique' => !$row['Non_unique'], + 'primary' => $id === 'PRIMARY', + 'columns' => [], + ]; + $indexes[$id]['columns'][(int) $row['Seq_in_index'] - 1] = (string) $row['Column_name']; + } + + foreach ($indexes as &$index) { + ksort($index['columns']); + $index['columns'] = array_values($index['columns']); + } + + return array_values($indexes); + } + + + public function getForeignKeys(string $table): array + { + $keys = []; + $rows = $this->connection->query(<<<'X' + SELECT CONSTRAINT_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME + FROM information_schema.KEY_COLUMN_USAGE + WHERE TABLE_SCHEMA = DATABASE() + AND REFERENCED_TABLE_NAME IS NOT NULL + AND TABLE_NAME = ? + X, [$table]); + + while ($row = $rows->fetch()) { + $id = $row['CONSTRAINT_NAME']; + $keys[$id]['name'] = $id; + $keys[$id]['local'][] = $row['COLUMN_NAME']; + $keys[$id]['table'] = $row['REFERENCED_TABLE_NAME']; + $keys[$id]['foreign'][] = $row['REFERENCED_COLUMN_NAME']; + } + + return array_values($keys); + } + + + public function convertToPhp(mixed $value, array $meta, TypeConverter $converter): mixed + { + return match ($meta['nativeType']) { + 'TINY' => $meta['size'] === 1 && $converter->convertBoolean + ? $converter->toBool($value) + : $converter->toInt($value), + 'TIME' => $converter->convertDateTime ? $converter->toInterval($value) : $value, + 'DATE', 'DATETIME', 'TIMESTAMP' => $converter->convertDateTime + ? (str_starts_with($value, '0000-00') ? null : $converter->toDateTime($value)) + : $value, + default => $converter->convertToPhp($value, $meta), + }; + } +} diff --git a/src/Database/Drivers/OdbcDriver.php b/src/Database/Drivers/Engines/ODBCEngine.php similarity index 65% rename from src/Database/Drivers/OdbcDriver.php rename to src/Database/Drivers/Engines/ODBCEngine.php index 9b9403398..00d5ad408 100644 --- a/src/Database/Drivers/OdbcDriver.php +++ b/src/Database/Drivers/Engines/ODBCEngine.php @@ -5,38 +5,35 @@ * Copyright (c) 2004 David Grudl (https://davidgrudl.com) */ -namespace Nette\Database\Drivers; +namespace Nette\Database\Drivers\Engines; use Nette; -use function preg_replace, str_replace, strtr; +use Nette\Database\Drivers\Engine; +use Nette\Database\TypeConverter; +use function preg_replace, str_replace; /** - * Supplemental ODBC database driver. + * Microsoft ODBC database platform. */ -class OdbcDriver implements Nette\Database\Driver +class ODBCEngine implements Engine { - public function initialize(Nette\Database\Connection $connection, array $options): void - { - } - - public function isSupported(string $feature): bool { return false; } - public function convertException(\PDOException $e): Nette\Database\DriverException + public function classifyException(Nette\Database\DriverException $e): ?string { - return Nette\Database\DriverException::from($e); + return null; } /********************* SQL ****************d*g**/ - public function delimite(string $name): string + public function delimit(string $name): string { return '[' . str_replace(['[', ']'], ['[[', ']]'], $name) . ']'; } @@ -54,14 +51,7 @@ public function formatDateInterval(\DateInterval $value): string } - public function formatLike(string $value, int $pos): string - { - $value = strtr($value, ["'" => "''", '%' => '[%]', '_' => '[_]', '[' => '[[]']); - return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'"); - } - - - public function applyLimit(string &$sql, ?int $limit, ?int $offset): void + public function applyLimit(string $sql, ?int $limit, ?int $offset): string { if ($offset) { throw new Nette\NotSupportedException('Offset is not supported by this database.'); @@ -75,6 +65,8 @@ public function applyLimit(string &$sql, ?int $limit, ?int $offset): void throw new Nette\InvalidArgumentException('SQL query must begin with SELECT, UPDATE or DELETE command.'); } } + + return $sql; } @@ -105,8 +97,8 @@ public function getForeignKeys(string $table): array } - public function getColumnTypes(\PDOStatement $statement): array + public function convertToPhp(mixed $value, array $meta, TypeConverter $converter): mixed { - return []; + return $converter->convertToPhp($value, $meta); } } diff --git a/src/Database/Drivers/OciDriver.php b/src/Database/Drivers/Engines/OracleEngine.php similarity index 58% rename from src/Database/Drivers/OciDriver.php rename to src/Database/Drivers/Engines/OracleEngine.php index 7587dd572..3927eb183 100644 --- a/src/Database/Drivers/OciDriver.php +++ b/src/Database/Drivers/Engines/OracleEngine.php @@ -5,25 +5,26 @@ * Copyright (c) 2004 David Grudl (https://davidgrudl.com) */ -namespace Nette\Database\Drivers; +namespace Nette\Database\Drivers\Engines; use Nette; +use Nette\Database\Drivers\Connection; +use Nette\Database\Drivers\Engine; +use Nette\Database\TypeConverter; use function in_array, str_replace; /** - * Supplemental Oracle database driver. + * Oracle database platform. */ -class OciDriver implements Nette\Database\Driver +class OracleEngine implements Engine { - private Nette\Database\Connection $connection; - private string $fmtDateTime; + public string $formatDateTime = 'U'; - public function initialize(Nette\Database\Connection $connection, array $options): void - { - $this->connection = $connection; - $this->fmtDateTime = $options['formatDateTime'] ?? 'U'; + public function __construct( + private readonly Connection $connection, + ) { } @@ -33,31 +34,22 @@ public function isSupported(string $feature): bool } - public function convertException(\PDOException $e): Nette\Database\DriverException + public function classifyException(Nette\Database\DriverException $e): ?string { - $code = $e->errorInfo[1] ?? null; - if (in_array($code, [1, 2299, 38911], strict: true)) { - return Nette\Database\UniqueConstraintViolationException::from($e); - - } elseif (in_array($code, [1400], strict: true)) { - return Nette\Database\NotNullConstraintViolationException::from($e); - - } elseif (in_array($code, [2266, 2291, 2292], strict: true)) { - return Nette\Database\ForeignKeyConstraintViolationException::from($e); - - } elseif ($code === 60) { - return Nette\Database\DeadlockException::from($e); - - } else { - return Nette\Database\DriverException::from($e); - } + $code = $e->getCode(); + return match (true) { + in_array($code, [1, 2299, 38911], strict: true) => Nette\Database\UniqueConstraintViolationException::class, + in_array($code, [1400], strict: true) => Nette\Database\NotNullConstraintViolationException::class, + in_array($code, [2266, 2291, 2292], strict: true) => Nette\Database\ForeignKeyConstraintViolationException::class, + default => null, + }; } /********************* SQL ****************d*g**/ - public function delimite(string $name): string + public function delimit(string $name): string { // @see http://download.oracle.com/docs/cd/B10500_01/server.920/a96540/sql_elements9a.htm return '"' . str_replace('"', '""', $name) . '"'; @@ -66,7 +58,7 @@ public function delimite(string $name): string public function formatDateTime(\DateTimeInterface $value): string { - return $value->format($this->fmtDateTime); + return $value->format($this->formatDateTime); } @@ -76,13 +68,7 @@ public function formatDateInterval(\DateInterval $value): string } - public function formatLike(string $value, int $pos): string - { - throw new Nette\NotImplementedException; - } - - - public function applyLimit(string &$sql, ?int $limit, ?int $offset): void + public function applyLimit(string $sql, ?int $limit, ?int $offset): string { if ($limit < 0 || $offset < 0) { throw new Nette\InvalidArgumentException('Negative offset or limit.'); @@ -96,6 +82,8 @@ public function applyLimit(string &$sql, ?int $limit, ?int $offset): void } elseif ($limit !== null) { $sql = 'SELECT * FROM (' . $sql . ') WHERE ROWNUM <= ' . $limit; } + + return $sql; } @@ -107,6 +95,7 @@ public function getTables(): array $tables = []; $rows = $this->connection->query('SELECT * FROM cat'); while ($row = $rows->fetch()) { + $row = array_values($row); if ($row[1] === 'TABLE' || $row[1] === 'VIEW') { $tables[] = [ 'name' => (string) $row[0], @@ -138,8 +127,8 @@ public function getForeignKeys(string $table): array } - public function getColumnTypes(\PDOStatement $statement): array + public function convertToPhp(mixed $value, array $meta, TypeConverter $converter): mixed { - return []; + return $converter->convertToPhp($value, $meta); } } diff --git a/src/Database/Drivers/PgSqlDriver.php b/src/Database/Drivers/Engines/PostgreSQLEngine.php similarity index 62% rename from src/Database/Drivers/PgSqlDriver.php rename to src/Database/Drivers/Engines/PostgreSQLEngine.php index 4024359f9..683e6f6a6 100644 --- a/src/Database/Drivers/PgSqlDriver.php +++ b/src/Database/Drivers/Engines/PostgreSQLEngine.php @@ -5,23 +5,23 @@ * Copyright (c) 2004 David Grudl (https://davidgrudl.com) */ -namespace Nette\Database\Drivers; +namespace Nette\Database\Drivers\Engines; use Nette; -use function array_map, array_values, explode, implode, str_contains, str_replace, strtr, substr; +use Nette\Database\Drivers\Connection; +use Nette\Database\Drivers\Engine; +use Nette\Database\TypeConverter; +use function array_map, array_values, explode, implode, str_contains, str_replace; /** - * Supplemental PostgreSQL database driver. + * PostgreSQL database platform. */ -class PgSqlDriver implements Nette\Database\Driver +class PostgreSQLEngine implements Engine { - private Nette\Database\Connection $connection; - - - public function initialize(Nette\Database\Connection $connection, array $options): void - { - $this->connection = $connection; + public function __construct( + private readonly Connection $connection, + ) { } @@ -31,47 +31,23 @@ public function isSupported(string $feature): bool } - public function convertException(\PDOException $e): Nette\Database\DriverException + public function classifyException(Nette\Database\DriverException $e): ?string { - $code = $e->errorInfo[0] ?? null; - if ($code === '0A000' && str_contains($e->getMessage(), 'truncate')) { - return Nette\Database\ForeignKeyConstraintViolationException::from($e); - - } elseif ($code === '23502') { - return Nette\Database\NotNullConstraintViolationException::from($e); - - } elseif ($code === '23503') { - return Nette\Database\ForeignKeyConstraintViolationException::from($e); - - } elseif ($code === '23505') { - return Nette\Database\UniqueConstraintViolationException::from($e); - - } elseif ($code === '23514') { - return Nette\Database\CheckConstraintViolationException::from($e); - - } elseif ($code === '40001' || $code === '40P01') { - return Nette\Database\DeadlockException::from($e); - - } elseif ($code === '55P03') { - return Nette\Database\LockTimeoutException::from($e); - - } elseif ( - $code === '08003' - || $code === '08006' - || ($code === 'HY000' && str_contains($e->getMessage(), 'server closed the connection unexpectedly')) - ) { - return Nette\Database\ConnectionLostException::from($e); - - } else { - return Nette\Database\DriverException::from($e); - } + return match ($e->getSqlState()) { + '0A000' => str_contains($e->getMessage(), 'truncate') ? Nette\Database\ForeignKeyConstraintViolationException::class : null, + '23502' => Nette\Database\NotNullConstraintViolationException::class, + '23503' => Nette\Database\ForeignKeyConstraintViolationException::class, + '23505' => Nette\Database\UniqueConstraintViolationException::class, + '08006' => Nette\Database\ConnectionException::class, + default => null, + }; } /********************* SQL ****************d*g**/ - public function delimite(string $name): string + public function delimit(string $name): string { // @see http://www.postgresql.org/docs/8.2/static/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS return '"' . str_replace('"', '""', $name) . '"'; @@ -90,16 +66,7 @@ public function formatDateInterval(\DateInterval $value): string } - public function formatLike(string $value, int $pos): string - { - $bs = substr($this->connection->quote('\\'), 1, -1); // standard_conforming_strings = on/off - $value = substr($this->connection->quote($value), 1, -1); - $value = strtr($value, ['%' => $bs . '%', '_' => $bs . '_', '\\' => '\\\\']); - return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'"); - } - - - public function applyLimit(string &$sql, ?int $limit, ?int $offset): void + public function applyLimit(string $sql, ?int $limit, ?int $offset): string { if ($limit < 0 || $offset < 0) { throw new Nette\InvalidArgumentException('Negative offset or limit.'); @@ -112,6 +79,8 @@ public function applyLimit(string &$sql, ?int $limit, ?int $offset): void if ($offset) { $sql .= ' OFFSET ' . $offset; } + + return $sql; } @@ -139,12 +108,7 @@ public function getTables(): array X); while ($row = $rows->fetch()) { - $tables[] = [ - 'name' => (string) $row['name'], - 'view' => (bool) $row['view'], - 'fullName' => (string) $row['fullName'], - 'comment' => (string) $row['comment'], - ]; + $tables[] = $row; } return $tables; @@ -158,16 +122,20 @@ public function getColumns(string $table): array SELECT a.attname::varchar AS name, c.relname::varchar AS table, - upper(t.typname) AS nativetype, + upper(t.typname) AS "nativeType", CASE WHEN a.atttypid IN (1700, 1231) THEN ((a.atttypmod - 4) >> 16) & 65535 -- precision for numeric/decimal WHEN a.atttypmod > 0 THEN a.atttypmod - 4 -- length for varchar etc. WHEN t.typlen > 0 THEN t.typlen -- length for fixed-length types ELSE NULL END AS size, + CASE + WHEN a.atttypid IN (1700, 1231) THEN (a.atttypmod - 4) & 65535 + ELSE null + END AS scale, NOT (a.attnotnull OR t.typtype = 'd' AND t.typnotnull) AS nullable, pg_catalog.pg_get_expr(ad.adbin, 'pg_catalog.pg_attrdef'::regclass)::varchar AS default, - coalesce(co.contype = 'p' AND (seq.relname IS NOT NULL OR strpos(pg_catalog.pg_get_expr(ad.adbin, ad.adrelid), 'nextval') = 1), FALSE) AS autoincrement, + coalesce(co.contype = 'p' AND (seq.relname IS NOT NULL OR strpos(pg_catalog.pg_get_expr(ad.adbin, ad.adrelid), 'nextval') = 1), FALSE) AS "autoIncrement", coalesce(co.contype = 'p', FALSE) AS primary, coalesce(col_description(c.oid, a.attnum)::varchar, '') AS comment, coalesce(seq.relname, substring(pg_catalog.pg_get_expr(ad.adbin, 'pg_catalog.pg_attrdef'::regclass) from 'nextval[(]''"?([^''"]+)')) AS sequence @@ -186,22 +154,14 @@ public function getColumns(string $table): array AND NOT a.attisdropped ORDER BY a.attnum - X, $this->delimiteFQN($table)); + X, [$this->delimitFQN($table)]); while ($row = $rows->fetch()) { - $vendor = (array) $row; - $columns[] = [ - 'name' => (string) $row['name'], - 'table' => (string) $row['table'], - 'nativetype' => (string) $row['nativetype'], - 'size' => $row['size'] !== null ? (int) $row['size'] : null, - 'nullable' => (bool) $row['nullable'], - 'default' => $row['default'], - 'autoincrement' => (bool) $row['autoincrement'], - 'primary' => (bool) $row['primary'], - 'comment' => (string) $row['comment'], - 'vendor' => $vendor, - ]; + $column = $row; + $column['vendor'] = $column; + unset($column['sequence']); + + $columns[] = $column; } return $columns; @@ -225,7 +185,7 @@ public function getIndexes(string $table): array WHERE c1.relkind IN ('r', 'p') AND c1.oid = ?::regclass - X, $this->delimiteFQN($table)); + X, [$this->delimitFQN($table)]); while ($row = $rows->fetch()) { $id = (string) $row['name']; @@ -263,35 +223,33 @@ public function getForeignKeys(string $table): array co.contype = 'f' AND cl.oid = ?::regclass AND nf.nspname = ANY (pg_catalog.current_schemas(FALSE)) - X, $this->delimiteFQN($table)); + X, [$this->delimitFQN($table)]); while ($row = $rows->fetch()) { - $keys[] = [ - 'name' => (string) $row['name'], - 'local' => (string) $row['local'], - 'table' => (string) $row['table'], - 'foreign' => (string) $row['foreign'], - ]; + $id = $row['name']; + $keys[$id]['name'] = $id; + $keys[$id]['local'][] = $row['local']; + $keys[$id]['table'] = $row['table']; + $keys[$id]['foreign'][] = $row['foreign']; } - return $keys; + return array_values($keys); } - public function getColumnTypes(\PDOStatement $statement): array + public function convertToPhp(mixed $value, array $meta, TypeConverter $converter): mixed { - static $cache; - $item = &$cache[$statement->queryString]; - $item ??= Nette\Database\Helpers::detectTypes($statement); - return $item; + return $meta['nativeType'] === 'bool' + ? ($value && $value !== 'f' && $value !== 'F') + : $converter->convertToPhp($value, $meta); } /** * Converts: schema.name => "schema"."name" */ - private function delimiteFQN(string $name): string + private function delimitFQN(string $name): string { - return implode('.', array_map($this->delimite(...), explode('.', $name))); + return implode('.', array_map($this->delimit(...), explode('.', $name))); } } diff --git a/src/Database/Drivers/SqlsrvDriver.php b/src/Database/Drivers/Engines/SQLServerEngine.php similarity index 64% rename from src/Database/Drivers/SqlsrvDriver.php rename to src/Database/Drivers/Engines/SQLServerEngine.php index 143f182b6..57208ac2c 100644 --- a/src/Database/Drivers/SqlsrvDriver.php +++ b/src/Database/Drivers/Engines/SQLServerEngine.php @@ -5,23 +5,23 @@ * Copyright (c) 2004 David Grudl (https://davidgrudl.com) */ -namespace Nette\Database\Drivers; +namespace Nette\Database\Drivers\Engines; use Nette; -use function array_values, str_replace, strtr; +use Nette\Database\Drivers\Connection; +use Nette\Database\Drivers\Engine; +use Nette\Database\TypeConverter; +use function array_values, str_replace; /** - * Supplemental SQL Server 2005 and later database driver. + * Microsoft SQL Server database platform. */ -class SqlsrvDriver implements Nette\Database\Driver +class SQLServerEngine implements Engine { - private Nette\Database\Connection $connection; - - - public function initialize(Nette\Database\Connection $connection, array $options): void - { - $this->connection = $connection; + public function __construct( + private readonly Connection $connection, + ) { } @@ -31,24 +31,21 @@ public function isSupported(string $feature): bool } - public function convertException(\PDOException $e): Nette\Database\DriverException + public function classifyException(Nette\Database\DriverException $e): ?string { - $code = $e->errorInfo[1] ?? null; - if ($code === 1205) { - return Nette\Database\DeadlockException::from($e); - - } elseif ($code === 1222) { - return Nette\Database\LockTimeoutException::from($e); - } - - return Nette\Database\DriverException::from($e); + $code = $e->getCode(); + return match (true) { + $code === 1205 => Nette\Database\DeadlockException::class, + $code === 1222 => Nette\Database\LockTimeoutException::class, + default => null, + }; } /********************* SQL ****************d*g**/ - public function delimite(string $name): string + public function delimit(string $name): string { /** @see https://msdn.microsoft.com/en-us/library/ms176027.aspx */ return '[' . str_replace(']', ']]', $name) . ']'; @@ -68,15 +65,7 @@ public function formatDateInterval(\DateInterval $value): string } - public function formatLike(string $value, int $pos): string - { - /** @see https://msdn.microsoft.com/en-us/library/ms179859.aspx */ - $value = strtr($value, ["'" => "''", '%' => '[%]', '_' => '[_]', '[' => '[[]']); - return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'"); - } - - - public function applyLimit(string &$sql, ?int $limit, ?int $offset): void + public function applyLimit(string $sql, ?int $limit, ?int $offset): string { if ($limit < 0 || $offset < 0) { throw new Nette\InvalidArgumentException('Negative offset or limit.'); @@ -86,6 +75,8 @@ public function applyLimit(string &$sql, ?int $limit, ?int $offset): void $sql .= ' OFFSET ' . (int) $offset . ' ROWS ' . 'FETCH NEXT ' . (int) $limit . ' ROWS ONLY'; } + + return $sql; } @@ -132,15 +123,16 @@ public function getColumns(string $table): array SELECT c.name AS name, o.name AS [table], - UPPER(t.name) AS nativetype, + UPPER(t.name) AS nativeType, CASE WHEN c.precision <> 0 THEN c.precision WHEN c.max_length <> -1 THEN c.max_length ELSE NULL END AS size, + NULLIF(c.scale, 0) AS scale, c.is_nullable AS nullable, OBJECT_DEFINITION(c.default_object_id) AS [default], - c.is_identity AS autoincrement, + c.is_identity AS autoIncrement, CASE WHEN i.index_id IS NULL THEN 0 ELSE 1 @@ -159,21 +151,18 @@ public function getColumns(string $table): array WHERE o.type IN ('U', 'V') AND o.name = ? - X, $table); + X, [$table]); while ($row = $rows->fetch()) { - $columns[] = [ - 'name' => (string) $row['name'], - 'table' => (string) $row['table'], - 'nativetype' => (string) $row['nativetype'], - 'size' => $row['size'] !== null ? (int) $row['size'] : null, - 'nullable' => (bool) $row['nullable'], - 'default' => $row['default'], - 'autoincrement' => (bool) $row['autoincrement'], - 'primary' => (bool) $row['primary'], - 'comment' => (string) ($row['comment'] ?? ''), - 'vendor' => (array) $row, - ]; + $row['vendor'] = $row; + $row['size'] = $row['size'] ? (int) $row['size'] : null; + $row['scale'] = $row['scale'] ? (int) $row['scale'] : null; + $row['nullable'] = (bool) $row['nullable']; + $row['autoIncrement'] = (bool) $row['autoIncrement']; + $row['primary'] = (bool) $row['primary']; + $row['comment'] ??= ''; + + $columns[] = $row; } return $columns; @@ -202,7 +191,7 @@ public function getIndexes(string $table): array ORDER BY i.index_id, ic.index_column_id - X, $table); + X, [$table]); while ($row = $rows->fetch()) { $id = (string) $row['name']; @@ -237,39 +226,26 @@ public function getForeignKeys(string $table): array JOIN sys.columns cf ON fkc.referenced_object_id = cf.object_id AND fkc.referenced_column_id = cf.column_id WHERE tl.name = ? - ORDER BY - fk.name, fkc.constraint_column_id - X, $table); + X, [$table]); while ($row = $rows->fetch()) { - $keys[] = [ - 'name' => (string) $row['name'], - 'local' => (string) $row['local'], - 'table' => (string) $row['table'], - 'foreign' => (string) $row['foreign'], - ]; + $id = $row['name']; + $keys[$id]['name'] = $id; + $keys[$id]['local'][] = $row['local']; + $keys[$id]['table'] = $row['table']; + $keys[$id]['foreign'][] = $row['foreign']; } return $keys; } - public function getColumnTypes(\PDOStatement $statement): array + public function convertToPhp(mixed $value, array $meta, TypeConverter $converter): mixed { - $types = []; - $count = $statement->columnCount(); - for ($col = 0; $col < $count; $col++) { - $meta = $statement->getColumnMeta($col); - if ( - isset($meta['sqlsrv:decl_type']) - && $meta['sqlsrv:decl_type'] !== 'timestamp' - ) { // timestamp does not mean time in sqlsrv - $types[$meta['name']] = Nette\Database\Helpers::detectType($meta['sqlsrv:decl_type']); - } elseif (isset($meta['native_type'])) { - $types[$meta['name']] = Nette\Database\Helpers::detectType($meta['native_type']); - } - } - - return $types; + return match ($meta['nativeType']) { + 'timestamp' => $value, // timestamp does not mean time in sqlsrv + 'bit' => $converter->convertBoolean ? $converter->toBool($value) : $converter->toInt($value), + default => $converter->convertToPhp($value, $meta), + }; } } diff --git a/src/Database/Drivers/Engines/SQLiteEngine.php b/src/Database/Drivers/Engines/SQLiteEngine.php new file mode 100644 index 000000000..a05835afc --- /dev/null +++ b/src/Database/Drivers/Engines/SQLiteEngine.php @@ -0,0 +1,244 @@ +getMessage(); + if ($e->getCode() !== 19) { + return null; + + } elseif ( + str_contains($message, 'must be unique') + || str_contains($message, 'is not unique') + || str_contains($message, 'UNIQUE constraint failed') + ) { + return Nette\Database\UniqueConstraintViolationException::class; + + } elseif ( + str_contains($message, 'may not be null') + || str_contains($message, 'NOT NULL constraint failed') + ) { + return Nette\Database\NotNullConstraintViolationException::class; + + } elseif ( + str_contains($message, 'foreign key constraint failed') + || str_contains($message, 'FOREIGN KEY constraint failed') + ) { + return Nette\Database\ForeignKeyConstraintViolationException::class; + + } else { + return Nette\Database\ConstraintViolationException::class; + } + } + + + /********************* SQL ****************d*g**/ + + + public function delimit(string $name): string + { + return '[' . strtr($name, '[]', ' ') . ']'; + } + + + public function formatDateTime(\DateTimeInterface $value): string + { + return $value->format($this->formatDateTime); + } + + + public function formatDateInterval(\DateInterval $value): string + { + throw new Nette\NotSupportedException; + } + + + public function applyLimit(string $sql, ?int $limit, ?int $offset): string + { + if ($limit < 0 || $offset < 0) { + throw new Nette\InvalidArgumentException('Negative offset or limit.'); + + } elseif ($limit !== null || $offset) { + $sql .= ' LIMIT ' . ($limit ?? '-1') + . ($offset ? ' OFFSET ' . $offset : ''); + } + + return $sql; + } + + + /********************* reflection ****************d*g**/ + + + public function getTables(): array + { + $tables = []; + $rows = $this->connection->query(<<<'X' + SELECT name, type = 'view' as view + FROM sqlite_master + WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%' + UNION ALL + SELECT name, type = 'view' as view + FROM sqlite_temp_master + WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%' + ORDER BY name + X); + + while ($row = $rows->fetch()) { + $tables[] = [ + 'name' => (string) $row['name'], + 'view' => (bool) $row['view'], + 'comment' => null, + ]; + } + + return $tables; + } + + + public function getColumns(string $table): array + { + $createSql = $this->connection->query(<<<'X' + SELECT sql + FROM sqlite_master + WHERE type = 'table' AND name = ? + UNION ALL + SELECT sql + FROM sqlite_temp_master + WHERE type = 'table' AND name = ? + X, [$table, $table])->fetch(); + + $columns = []; + $rows = $this->connection->query("PRAGMA table_info({$this->delimit($table)})"); + while ($row = $rows->fetch()) { + $column = $row['name']; + $pattern = "/(\"$column\"|`$column`|\\[$column\\]|$column)\\s+[^,]+\\s+PRIMARY\\s+KEY\\s+AUTOINCREMENT/Ui"; + $typeInfo = Nette\Database\Helpers::parseColumnType($row['type']); + $columns[] = [ + 'name' => $column, + 'table' => $table, + 'nativeType' => strtoupper($typeInfo['type'] ?? 'BLOB'), + 'size' => $typeInfo['size'], + 'scale' => $typeInfo['scale'], + 'nullable' => $row['notnull'] == 0, + 'default' => $row['dflt_value'], + 'autoIncrement' => $createSql && preg_match($pattern, $createSql['sql']), + 'primary' => $row['pk'] > 0, + 'comment' => null, + 'vendor' => $row, + ]; + } + + return $columns; + } + + + public function getIndexes(string $table): array + { + $indexes = []; + $rows = $this->connection->query("PRAGMA index_list({$this->delimit($table)})"); + while ($row = $rows->fetch()) { + $id = (string) $row['name']; + $indexes[$id] = [ + 'name' => $id, + 'unique' => (bool) $row['unique'], + 'primary' => false, + 'columns' => [], + ]; + } + + $columns = $this->getColumns($table); + foreach ($indexes as $id => &$index) { + $res = $this->connection->query("PRAGMA index_info({$this->delimit($id)})"); + while ($row = $res->fetch()) { + $index['columns'][] = (string) $row['name']; + } + + foreach ($columns as $info) { + if (($index['columns'][0] ?? null) === $info['name']) { + $index['primary'] = (bool) $info['primary']; + break; + } + } + } + + unset($index); + + if (!$indexes) { // @see http://www.sqlite.org/lang_createtable.html#rowid + foreach ($columns as $column) { + if ($column['vendor']['pk']) { + $indexes[] = [ + 'name' => 'ROWID', + 'unique' => true, + 'primary' => true, + 'columns' => [$column['name']], + ]; + break; + } + } + } + + return array_values($indexes); + } + + + public function getForeignKeys(string $table): array + { + $keys = []; + $rows = $this->connection->query("PRAGMA foreign_key_list({$this->delimit($table)})"); + while ($row = $rows->fetch()) { + $id = $row['id']; + $keys[$id]['name'] = $id; + $keys[$id]['local'][] = $row['from']; + $keys[$id]['table'] = $row['table']; + $keys[$id]['foreign'][] = $row['to']; + if ($keys[$id]['foreign'][0] == null) { + $keys[$id]['foreign'] = []; + } + } + + return array_values($keys); + } + + + public function convertToPhp(mixed $value, array $meta, TypeConverter $converter): mixed + { + return $converter->convertDateTime && in_array($meta['nativeType'], ['DATE', 'DATETIME'], strict: true) + ? (is_int($value) ? (new DateTime)->setTimestamp($value) : new DateTime($value)) + : $converter->convertToPhp($value, $meta); + } +} diff --git a/src/Database/Drivers/MySqlDriver.php b/src/Database/Drivers/MySqlDriver.php deleted file mode 100644 index 8ed8cc6fb..000000000 --- a/src/Database/Drivers/MySqlDriver.php +++ /dev/null @@ -1,251 +0,0 @@ - character encoding to set (default is utf8mb4) - * - sqlmode => see http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html - * - convertBoolean => converts INT(1) to boolean - */ - public function initialize(Nette\Database\Connection $connection, array $options): void - { - $this->connection = $connection; - $charset = $options['charset'] ?? 'utf8mb4'; - if ($charset) { - $connection->query('SET NAMES ?', $charset); - } - - if (isset($options['sqlmode'])) { - $connection->query('SET sql_mode=?', $options['sqlmode']); - } - - $this->convertBoolean = (bool) ($options['convertBoolean'] ?? $options['supportBooleans'] ?? false); - } - - - public function isSupported(string $feature): bool - { - // MULTI_COLUMN_AS_OR_COND due to mysql bugs: - // - http://bugs.mysql.com/bug.php?id=31188 - // - http://bugs.mysql.com/bug.php?id=35819 - // and more. - return $feature === self::SupportSelectUngroupedColumns || $feature === self::SupportMultiColumnAsOrCondition; - } - - - public function convertException(\PDOException $e): Nette\Database\DriverException - { - $code = $e->errorInfo[1] ?? null; - if (in_array($code, [1216, 1217, 1451, 1452, 1701], strict: true)) { - return Nette\Database\ForeignKeyConstraintViolationException::from($e); - - } elseif (in_array($code, [1062, 1557, 1569, 1586], strict: true)) { - return Nette\Database\UniqueConstraintViolationException::from($e); - - } elseif ($code === 3819) { - return Nette\Database\CheckConstraintViolationException::from($e); - - } elseif ($code === 1213) { - return Nette\Database\DeadlockException::from($e); - - } elseif ($code === 1205) { - return Nette\Database\LockTimeoutException::from($e); - - } elseif ($code === 2006 || $code === 2013) { - return Nette\Database\ConnectionLostException::from($e); - - } elseif ($code >= 2001 && $code <= 2028) { - return Nette\Database\ConnectionException::from($e); - - } elseif (in_array($code, [1048, 1121, 1138, 1171, 1252, 1263, 1566], strict: true)) { - return Nette\Database\NotNullConstraintViolationException::from($e); - - } else { - return Nette\Database\DriverException::from($e); - } - } - - - /********************* SQL ****************d*g**/ - - - public function delimite(string $name): string - { - // @see http://dev.mysql.com/doc/refman/5.0/en/identifiers.html - return '`' . str_replace('`', '``', $name) . '`'; - } - - - public function formatDateTime(\DateTimeInterface $value): string - { - return $value->format("'Y-m-d H:i:s'"); - } - - - public function formatDateInterval(\DateInterval $value): string - { - return $value->format("'%r%h:%I:%S'"); - } - - - public function formatLike(string $value, int $pos): string - { - $value = str_replace('\\', '\\\\', $value); - $value = addcslashes(substr($this->connection->quote($value), 1, -1), '%_'); - return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'"); - } - - - public function applyLimit(string &$sql, ?int $limit, ?int $offset): void - { - if ($limit < 0 || $offset < 0) { - throw new Nette\InvalidArgumentException('Negative offset or limit.'); - - } elseif ($limit !== null || $offset) { - // see http://dev.mysql.com/doc/refman/5.0/en/select.html - $sql .= ' LIMIT ' . ($limit ?? '18446744073709551615') - . ($offset ? ' OFFSET ' . $offset : ''); - } - } - - - /********************* reflection ****************d*g**/ - - - public function getTables(): array - { - $tables = []; - $query = $this->connection->query(<<<'XX' - SELECT TABLE_NAME, TABLE_TYPE, TABLE_COMMENT - FROM information_schema.TABLES - WHERE TABLE_SCHEMA = DATABASE() - XX); - - while ($row = $query->fetch()) { - $tables[] = [ - 'name' => (string) $row['TABLE_NAME'], - 'view' => $row['TABLE_TYPE'] === 'VIEW', - 'comment' => (string) $row['TABLE_COMMENT'], - ]; - } - - return $tables; - } - - - public function getColumns(string $table): array - { - $columns = []; - $rows = $this->connection->query('SHOW FULL COLUMNS FROM ?name', $table); - while ($row = $rows->fetch()) { - $row = array_change_key_case((array) $row); - $typeInfo = Nette\Database\Helpers::parseColumnType($row['type']); - $columns[] = [ - 'name' => $row['field'], - 'table' => $table, - 'nativetype' => strtoupper($typeInfo['type'] ?? ''), - 'size' => $typeInfo['length'], - 'nullable' => $row['null'] === 'YES', - 'default' => $row['default'], - 'autoincrement' => $row['extra'] === 'auto_increment', - 'primary' => $row['key'] === 'PRI', - 'comment' => $row['comment'], - 'vendor' => $row, - ]; - } - - return $columns; - } - - - public function getIndexes(string $table): array - { - $indexes = []; - $rows = $this->connection->query('SHOW INDEX FROM ?name', $table); - while ($row = $rows->fetch()) { - $id = (string) $row['Key_name']; - $indexes[$id] ??= [ - 'name' => $id, - 'unique' => !$row['Non_unique'], - 'primary' => $id === 'PRIMARY', - 'columns' => [], - ]; - $indexes[$id]['columns'][(int) $row['Seq_in_index'] - 1] = (string) $row['Column_name']; - } - - foreach ($indexes as &$index) { - ksort($index['columns']); - $index['columns'] = array_values($index['columns']); - } - - return array_values($indexes); - } - - - public function getForeignKeys(string $table): array - { - $keys = []; - $rows = $this->connection->query(<<<'X' - SELECT CONSTRAINT_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME - FROM information_schema.KEY_COLUMN_USAGE - WHERE TABLE_SCHEMA = DATABASE() - AND REFERENCED_TABLE_NAME IS NOT NULL - AND TABLE_NAME = ? - X, $table); - - while ($row = $rows->fetch()) { - $keys[] = [ - 'name' => (string) $row['CONSTRAINT_NAME'], - 'local' => (string) $row['COLUMN_NAME'], - 'table' => (string) $row['REFERENCED_TABLE_NAME'], - 'foreign' => (string) $row['REFERENCED_COLUMN_NAME'], - ]; - } - - return $keys; - } - - - public function getColumnTypes(\PDOStatement $statement): array - { - $types = []; - $count = $statement->columnCount(); - for ($col = 0; $col < $count; $col++) { - $meta = $statement->getColumnMeta($col); - if (isset($meta['native_type'])) { - $types[$meta['name']] = match (true) { - $meta['native_type'] === 'NEWDECIMAL' && $meta['precision'] === 0 => Nette\Database\IStructure::FIELD_INTEGER, - $meta['native_type'] === 'TINY' && $meta['len'] === 1 && $this->convertBoolean => Nette\Database\IStructure::FIELD_BOOL, - $meta['native_type'] === 'TIME' => Nette\Database\IStructure::FIELD_TIME_INTERVAL, - default => Nette\Database\Helpers::detectType($meta['native_type']), - }; - } - } - - return $types; - } -} diff --git a/src/Database/Drivers/PDO/Connection.php b/src/Database/Drivers/PDO/Connection.php new file mode 100644 index 000000000..540b725d5 --- /dev/null +++ b/src/Database/Drivers/PDO/Connection.php @@ -0,0 +1,121 @@ + PDO::PARAM_BOOL, 'integer' => PDO::PARAM_INT, 'resource' => PDO::PARAM_LOB, 'NULL' => PDO::PARAM_NULL]; + + try { + $statement = $this->pdo->prepare($sql); + foreach ($params as $key => $value) { + $statement->bindValue(is_int($key) ? $key + 1 : $key, $value, $types[gettype($value)] ?? PDO::PARAM_STR); + } + $statement->execute(); + return new ($this->resultClass)($statement, $this); + + } catch (PDOException $e) { + throw new DriverException(...Driver::exceptionArgs($e, new SqlLiteral($sql, $params))); + } + } + + + public function execute(string $sql): int + { + try { + return $this->pdo->exec($sql); + } catch (PDOException $e) { + throw new DriverException(...Driver::exceptionArgs($e, new SqlLiteral($sql))); + } + } + + + public function beginTransaction(): void + { + try { + $this->pdo->beginTransaction(); + } catch (PDOException $e) { + throw new DriverException(...Driver::exceptionArgs($e)); + } + } + + + public function commit(): void + { + try { + $this->pdo->commit(); + } catch (PDOException $e) { + throw new DriverException(...Driver::exceptionArgs($e)); + } + } + + + public function rollBack(): void + { + try { + $this->pdo->rollBack(); + } catch (PDOException $e) { + throw new DriverException(...Driver::exceptionArgs($e)); + } + } + + + public function getInsertId(?string $sequence = null): int|string + { + try { + $id = $this->pdo->lastInsertId($sequence); + if ($id === '0' || $id === '' || $id === false) { + throw new DriverException('Cannot retrieve last generated ID.'); + } + $int = (int) $id; + return $id === (string) $int ? $int : $id; + + } catch (PDOException $e) { + throw new DriverException(...Driver::exceptionArgs($e)); + } + } + + + public function quote(string $string): string + { + return $this->pdo->quote($string); + } + + + public function getServerVersion(): string + { + return $this->pdo->getAttribute(PDO::ATTR_SERVER_VERSION); + } + + + public function getNativeConnection(): PDO + { + return $this->pdo; + } +} diff --git a/src/Database/Drivers/PDO/Driver.php b/src/Database/Drivers/PDO/Driver.php new file mode 100644 index 000000000..21b2f1139 --- /dev/null +++ b/src/Database/Drivers/PDO/Driver.php @@ -0,0 +1,48 @@ +dsn, $this->username, $this->password, $this->options); + } catch (PDOException $e) { + throw new DriverException(...self::exceptionArgs($e)); + } + return new Connection($pdo); + } + + + public static function exceptionArgs(PDOException $e, ?SqlLiteral $query = null): array + { + return [$e->getMessage(), $e->errorInfo[0] ?? null, $e->errorInfo[1] ?? 0, $query, $e]; + } +} diff --git a/src/Database/Drivers/PDO/MSSQL/Driver.php b/src/Database/Drivers/PDO/MSSQL/Driver.php new file mode 100644 index 000000000..4c3acd205 --- /dev/null +++ b/src/Database/Drivers/PDO/MSSQL/Driver.php @@ -0,0 +1,25 @@ +charset) { + $connection->execute('SET NAMES ' . $connection->quote($this->charset)); + } + if ($this->sqlmode) { + $connection->execute('SET sql_mode=' . $connection->quote($this->sqlmode)); + } + return $connection; + } + + + public function createEngine(Drivers\Connection $connection): MySQLEngine + { + return new MySQLEngine($connection); + } +} diff --git a/src/Database/Drivers/PDO/OCI/Driver.php b/src/Database/Drivers/PDO/OCI/Driver.php new file mode 100644 index 000000000..606f7df6a --- /dev/null +++ b/src/Database/Drivers/PDO/OCI/Driver.php @@ -0,0 +1,40 @@ +formatDateTime) { + $engine->formatDateTime = $this->formatDateTime; + } + return $engine; + } +} diff --git a/src/Database/Drivers/PDO/ODBC/Driver.php b/src/Database/Drivers/PDO/ODBC/Driver.php new file mode 100644 index 000000000..47371287e --- /dev/null +++ b/src/Database/Drivers/PDO/ODBC/Driver.php @@ -0,0 +1,25 @@ +resultClass = Result::class; + return $connection; + } + + + public function createEngine(Drivers\Connection $connection): PostgreSQLEngine + { + return new PostgreSQLEngine($connection); + } +} diff --git a/src/Database/Drivers/PDO/PgSQL/Result.php b/src/Database/Drivers/PDO/PgSQL/Result.php new file mode 100644 index 000000000..482161829 --- /dev/null +++ b/src/Database/Drivers/PDO/PgSQL/Result.php @@ -0,0 +1,24 @@ +result->queryString] ??= parent::collectColumnsInfo(); + } +} diff --git a/src/Database/Drivers/PDO/Result.php b/src/Database/Drivers/PDO/Result.php new file mode 100644 index 000000000..f17b88cef --- /dev/null +++ b/src/Database/Drivers/PDO/Result.php @@ -0,0 +1,107 @@ +fetchList(); + if (!$row) { + return null; + } + + $res = []; + foreach ($this->getColumnsInfo() as $i => $meta) { + $res[$meta['name']] = $row[$i]; + } + return $res; + } + + + public function fetchList(): ?array + { + try { + $row = $this->result->fetch(\PDO::FETCH_NUM); + if (!$row) { + $this->free(); + return null; + } + return $row; + + } catch (PDOException $e) { + throw new DriverException(...Driver::exceptionArgs($e)); + } + } + + + public function getColumnCount(): int + { + try { + return $this->result->columnCount(); + } catch (PDOException $e) { + throw new DriverException(...Driver::exceptionArgs($e)); + } + } + + + public function getRowCount(): int + { + try { + return $this->result->rowCount(); + } catch (PDOException $e) { + throw new DriverException(...Driver::exceptionArgs($e)); + } + } + + + public function getColumnsInfo(): array + { + return $this->columns ??= $this->collectColumnsInfo(); + } + + + protected function collectColumnsInfo(): array + { + $res = []; + $count = $this->result->columnCount(); + for ($i = 0; $i < $count; $i++) { + $meta = $this->result->getColumnMeta($i) ?: throw new DriverException('Cannot fetch column metadata'); + $res[] = [ + 'name' => $meta['name'], + 'nativeType' => $meta[$this->connection->metaTypeKey] ?? null, + 'size' => $meta['len'], + 'scale' => $meta['precision'], + ]; + } + return $res; + } + + + public function free(): void + { + $this->result->closeCursor(); + } +} diff --git a/src/Database/Drivers/PDO/SQLSrv/Driver.php b/src/Database/Drivers/PDO/SQLSrv/Driver.php new file mode 100644 index 000000000..46909e970 --- /dev/null +++ b/src/Database/Drivers/PDO/SQLSrv/Driver.php @@ -0,0 +1,35 @@ + converts BIT to boolean + */ +class Driver extends Drivers\PDO\Driver +{ + public function connect(): Drivers\PDO\Connection + { + $connection = parent::connect(); + $connection->metaTypeKey = 'sqlsrv:decl_type'; + return $connection; + } + + + public function createEngine(Drivers\Connection $connection): SQLServerEngine + { + return new SQLServerEngine($connection); + } +} diff --git a/src/Database/Drivers/PDO/SQLite/Driver.php b/src/Database/Drivers/PDO/SQLite/Driver.php new file mode 100644 index 000000000..f73619b0c --- /dev/null +++ b/src/Database/Drivers/PDO/SQLite/Driver.php @@ -0,0 +1,48 @@ +metaTypeKey = 'sqlite:decl_type'; + return $connection; + } + + + public function createEngine(Drivers\Connection $connection): SQLiteEngine + { + $engine = new SQLiteEngine($connection); + if ($this->formatDateTime) { + $engine->formatDateTime = $this->formatDateTime; + } + return $engine; + } +} diff --git a/src/Database/Drivers/Result.php b/src/Database/Drivers/Result.php new file mode 100644 index 000000000..e17895e1f --- /dev/null +++ b/src/Database/Drivers/Result.php @@ -0,0 +1,38 @@ + + */ + function getColumnsInfo(): array; + + /** Frees the result set. */ + function free(): void; +} diff --git a/src/Database/Drivers/SqliteDriver.php b/src/Database/Drivers/SqliteDriver.php deleted file mode 100644 index 40abcb71b..000000000 --- a/src/Database/Drivers/SqliteDriver.php +++ /dev/null @@ -1,257 +0,0 @@ -connection = $connection; - $this->fmtDateTime = $options['formatDateTime'] ?? 'U'; - } - - - public function isSupported(string $feature): bool - { - return $feature === self::SupportMultiInsertAsSelect || $feature === self::SupportMultiColumnAsOrCondition; - } - - - public function convertException(\PDOException $e): Nette\Database\DriverException - { - $code = $e->errorInfo[1] ?? null; - $msg = $e->getMessage(); - if ($code !== 19) { - return Nette\Database\DriverException::from($e); - - } elseif ( - str_contains($msg, 'must be unique') - || str_contains($msg, 'is not unique') - || str_contains($msg, 'UNIQUE constraint failed') - ) { - return Nette\Database\UniqueConstraintViolationException::from($e); - - } elseif ( - str_contains($msg, 'may not be null') - || str_contains($msg, 'NOT NULL constraint failed') - ) { - return Nette\Database\NotNullConstraintViolationException::from($e); - - } elseif ( - str_contains($msg, 'foreign key constraint failed') - || str_contains($msg, 'FOREIGN KEY constraint failed') - ) { - return Nette\Database\ForeignKeyConstraintViolationException::from($e); - - } else { - return Nette\Database\ConstraintViolationException::from($e); - } - } - - - /********************* SQL ****************d*g**/ - - - public function delimite(string $name): string - { - return '[' . strtr($name, '[]', ' ') . ']'; - } - - - public function formatDateTime(\DateTimeInterface $value): string - { - return $value->format($this->fmtDateTime); - } - - - public function formatDateInterval(\DateInterval $value): string - { - throw new Nette\NotSupportedException; - } - - - public function formatLike(string $value, int $pos): string - { - $value = addcslashes(substr($this->connection->quote($value), 1, -1), '%_\\'); - return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'") . " ESCAPE '\\'"; - } - - - public function applyLimit(string &$sql, ?int $limit, ?int $offset): void - { - if ($limit < 0 || $offset < 0) { - throw new Nette\InvalidArgumentException('Negative offset or limit.'); - - } elseif ($limit !== null || $offset) { - $sql .= ' LIMIT ' . ($limit ?? '-1') - . ($offset ? ' OFFSET ' . $offset : ''); - } - } - - - /********************* reflection ****************d*g**/ - - - public function getTables(): array - { - $tables = []; - $rows = $this->connection->query(<<<'X' - SELECT name, type = 'view' as view - FROM sqlite_master - WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%' - UNION ALL - SELECT name, type = 'view' as view - FROM sqlite_temp_master - WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%' - ORDER BY name - X); - - while ($row = $rows->fetch()) { - $tables[] = [ - 'name' => (string) $row['name'], - 'view' => (bool) $row['view'], - 'comment' => null, - ]; - } - - return $tables; - } - - - public function getColumns(string $table): array - { - $createSql = $this->connection->query(<<<'X' - SELECT sql - FROM sqlite_master - WHERE type = 'table' AND name = ? - UNION ALL - SELECT sql - FROM sqlite_temp_master - WHERE type = 'table' AND name = ? - X, $table, $table)->fetch(); - - $columns = []; - $rows = $this->connection->query('PRAGMA table_info(?name)', $table); - while ($row = $rows->fetch()) { - $column = $row['name']; - $pattern = "/(\"$column\"|`$column`|\\[$column\\]|$column)\\s+[^,]+\\s+PRIMARY\\s+KEY\\s+AUTOINCREMENT/Ui"; - $typeInfo = Nette\Database\Helpers::parseColumnType($row['type']); - $columns[] = [ - 'name' => $column, - 'table' => $table, - 'nativetype' => strtoupper($typeInfo['type'] ?? 'BLOB'), - 'size' => $typeInfo['length'], - 'nullable' => $row['notnull'] == 0, - 'default' => $row['dflt_value'], - 'autoincrement' => $createSql && preg_match($pattern, $createSql['sql']), - 'primary' => $row['pk'] > 0, - 'comment' => null, - 'vendor' => (array) $row, - ]; - } - - return $columns; - } - - - public function getIndexes(string $table): array - { - $indexes = []; - $rows = $this->connection->query('PRAGMA index_list(?name)', $table); - while ($row = $rows->fetch()) { - $id = (string) $row['name']; - $indexes[$id] = [ - 'name' => $id, - 'unique' => (bool) $row['unique'], - 'primary' => false, - 'columns' => [], - ]; - } - - foreach ($indexes as $index => $values) { - $res = $this->connection->query('PRAGMA index_info(?name)', $index); - while ($row = $res->fetch()) { - $indexes[$index]['columns'][] = (string) $row['name']; - } - } - - $columns = $this->getColumns($table); - foreach ($indexes as $index => $values) { - $column = $values['columns'][0]; - foreach ($columns as $info) { - if ($column === $info['name']) { - $indexes[$index]['primary'] = (bool) $info['primary']; - break; - } - } - } - - if (!$indexes) { // @see http://www.sqlite.org/lang_createtable.html#rowid - foreach ($columns as $column) { - if ($column['vendor']['pk']) { - $indexes[] = [ - 'name' => 'ROWID', - 'unique' => true, - 'primary' => true, - 'columns' => [$column['name']], - ]; - break; - } - } - } - - return array_values($indexes); - } - - - public function getForeignKeys(string $table): array - { - $keys = []; - $rows = $this->connection->query('PRAGMA foreign_key_list(?name)', $table); - while ($row = $rows->fetch()) { - $keys[] = [ - 'name' => (string) $row['id'], // SQLite does not expose FK names via PRAGMA, synthetic id is used - 'local' => (string) $row['from'], - 'table' => (string) $row['table'], - 'foreign' => (string) $row['to'], - ]; - } - - return $keys; - } - - - public function getColumnTypes(\PDOStatement $statement): array - { - $types = []; - $count = $statement->columnCount(); - for ($col = 0; $col < $count; $col++) { - $meta = $statement->getColumnMeta($col); - if (isset($meta['sqlite:decl_type'])) { - $types[$meta['name']] = $this->fmtDateTime === 'U' && in_array($meta['sqlite:decl_type'], ['DATE', 'DATETIME'], strict: true) - ? Nette\Database\IStructure::FIELD_UNIX_TIMESTAMP - : Nette\Database\Helpers::detectType($meta['sqlite:decl_type']); - } elseif (isset($meta['native_type'])) { - $types[$meta['name']] = Nette\Database\Helpers::detectType($meta['native_type']); - } - } - - return $types; - } -} diff --git a/src/Database/Explorer.php b/src/Database/Explorer.php index f030813b4..b6cdc6b62 100644 --- a/src/Database/Explorer.php +++ b/src/Database/Explorer.php @@ -7,225 +7,44 @@ namespace Nette\Database; -use JetBrains\PhpStorm\Language; -use Nette; -use Nette\Database\Conventions\StaticConventions; -use function class_exists; +use Closure; /** - * Provides high-level database layer with ActiveRow pattern. + * Manages database connection and executes SQL queries. */ -class Explorer +class Explorer extends Database { - private readonly Conventions $conventions; + /** @var ?(Closure(string $table): class-string) */ + private ?Closure $rowMapping = null; - public function __construct( - private readonly Connection $connection, - private readonly IStructure $structure, - ?Conventions $conventions = null, - private readonly ?Nette\Caching\Storage $cacheStorage = null, - ) { - $this->conventions = $conventions ?: new StaticConventions; - } - - - public function beginTransaction(): void - { - $this->connection->beginTransaction(); - } - - - public function commit(): void - { - $this->connection->commit(); - } - - - public function rollBack(): void - { - $this->connection->rollBack(); - } - - - /** - * Executes callback inside a transaction. - * @param callable(static): mixed $callback - */ - public function transaction(callable $callback): mixed - { - return $this->connection->transaction(fn() => $callback($this)); - } - - - public function getInsertId(?string $sequence = null): string - { - return $this->connection->getInsertId($sequence); - } - - - /** - * Generates and executes SQL query. - * @param literal-string $sql - */ - public function query(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): ResultSet - { - return $this->connection->query($sql, ...$params); - } - - - /** - * @deprecated use query() - * @param literal-string $sql - * @param array $params - */ - public function queryArgs(string $sql, array $params): ResultSet - { - return $this->connection->query($sql, ...$params); - } - - - /** - * Returns table selection. - * @return Table\Selection - */ - public function table(string $table): Table\Selection - { - return new Table\Selection($this, $this->conventions, $table, $this->cacheStorage); - } - - - public function getConnection(): Connection - { - return $this->connection; - } - - - public function getStructure(): IStructure + public function setRowMapping(?Closure $factory): void { - return $this->structure; + $this->rowMapping = $factory; } - public function getConventions(): Conventions + /** @internal */ + public function createActiveRow(Table\Selection $selection, array $row): Table\ActiveRow { - return $this->conventions; + $class = $this->rowMapping + ? ($this->rowMapping)($selection->getName()) + : Table\ActiveRow::class; + return new $class($row, $selection); } - /** - * Creates an ActiveRow instance, using the configured row mapping class if available. - * @param array $data - * @param Table\Selection $selection - */ - public function createActiveRow(array $data, Table\Selection $selection): Table\ActiveRow - { - return new Table\ActiveRow($data, $selection); - } - - - /** - * @internal - * @param Table\Selection $refSelection - * @return Table\GroupedSelection - */ - public function createGroupedSelection( - Table\Selection $refSelection, + /** @internal */ + public function createGroupedSelectionInstance( + Table\Selection $selection, string $table, string $column, ): Table\GroupedSelection { - return new Table\GroupedSelection($this, $this->conventions, $table, $column, $refSelection, $this->cacheStorage); - } - - - /********************* shortcuts ****************d*g**/ - - - /** - * Executes SQL query and returns the first row, or null if no rows were returned. - * @param literal-string $sql - */ - public function fetch(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): ?Row - { - return $this->connection->query($sql, ...$params)->fetch(); - } - - - /** - * Executes SQL query and returns the first row as an associative array, or null. - * @param literal-string $sql - * @return ?array - */ - public function fetchAssoc(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): ?array - { - return $this->connection->query($sql, ...$params)->fetchAssoc(); - } - - - /** - * Executes SQL query and returns the first field of the first row, or null. - * @param literal-string $sql - */ - public function fetchField(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): mixed - { - return $this->connection->query($sql, ...$params)->fetchField(); - } - - - /** - * Executes SQL query and returns the first row as an indexed array, or null. - * @param literal-string $sql - * @return ?list - */ - public function fetchList(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): ?array - { - return $this->connection->query($sql, ...$params)->fetchList(); - } - - - /** - * Executes SQL query and returns the first row as an indexed array, or null. - * @param literal-string $sql - * @return ?list - */ - public function fetchFields(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): ?array - { - return $this->connection->query($sql, ...$params)->fetchList(); - } - - - /** - * Executes SQL query and returns rows as key-value pairs. - * @param literal-string $sql - * @return array - */ - public function fetchPairs(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): array - { - return $this->connection->query($sql, ...$params)->fetchPairs(); - } - - - /** - * Executes SQL query and returns all rows as an array of Row objects. - * @param literal-string $sql - * @return list - */ - public function fetchAll(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): array - { - return $this->connection->query($sql, ...$params)->fetchAll(); - } - - - /** - * Creates SQL literal value. - */ - public static function literal(string $value, mixed ...$params): SqlLiteral - { - return new SqlLiteral($value, $params); + return new Table\GroupedSelection($this, $table, $column, $selection); } } -class_exists(Context::class); +class_exists(Connection::class); diff --git a/src/Database/Helpers.php b/src/Database/Helpers.php index cef1af1f8..6a6e37726 100644 --- a/src/Database/Helpers.php +++ b/src/Database/Helpers.php @@ -10,7 +10,6 @@ use Nette; use Nette\Bridges\DatabaseTracy\ConnectionPanel; use Tracy; -use function array_filter, array_keys, array_unique, count, fclose, fgets, fopen, fstat, get_resource_type, htmlspecialchars, implode, is_bool, is_float, is_resource, is_string, preg_last_error, preg_match, preg_replace, preg_replace_callback, reset, rtrim, set_time_limit, str_ends_with, str_starts_with, stream_get_meta_data, strlen, strncasecmp, substr, trim, wordwrap; /** @@ -23,26 +22,13 @@ class Helpers /** maximum SQL length */ public static int $maxLength = 100; - /** @var array */ - public static array $typePatterns = [ - '^_' => IStructure::FIELD_TEXT, // PostgreSQL arrays - '(TINY|SMALL|SHORT|MEDIUM|BIG|LONG)(INT)?|INT(EGER|\d+| IDENTITY| UNSIGNED)?|(SMALL|BIG|)SERIAL\d*|COUNTER|YEAR|BYTE|LONGLONG|UNSIGNED BIG INT' => IStructure::FIELD_INTEGER, - '(NEW)?DEC(IMAL)?(\(.*)?|NUMERIC|(SMALL)?MONEY|CURRENCY|NUMBER' => IStructure::FIELD_DECIMAL, - 'REAL|DOUBLE( PRECISION)?|FLOAT\d*' => IStructure::FIELD_FLOAT, - 'BOOL(EAN)?' => IStructure::FIELD_BOOL, - 'TIME' => IStructure::FIELD_TIME, - 'DATE' => IStructure::FIELD_DATE, - '(SMALL)?DATETIME(OFFSET)?\d*|TIME(STAMP.*)?' => IStructure::FIELD_DATETIME, - 'BYTEA|(TINY|MEDIUM|LONG|)BLOB|(LONG )?(VAR)?BINARY|IMAGE' => IStructure::FIELD_BINARY, - ]; - /** * Displays result set as HTML table. */ - public static function dumpResult(ResultSet $result): void + public static function dumpResult(Result $result): void { - echo "\n\n\n"; + echo "\n
" . htmlspecialchars($result->getQueryString(), ENT_IGNORE, 'UTF-8') . "
\n\n"; if (!$result->getColumnCount()) { echo "\t\n\t\t\n\t\t\n\t\n
" . htmlspecialchars($result->getQuery()->getSql(), ENT_IGNORE, 'UTF-8') . "
Affected rows:", $result->getRowCount(), "
\n"; return; @@ -88,12 +74,13 @@ public static function dumpResult(ResultSet $result): void * Returns syntax-highlighted SQL query as an HTML string. * @param ?array $params */ - public static function dumpSql(string $sql, ?array $params = null, ?Connection $connection = null): string + public static function dumpSql(SqlLiteral $query, ?Explorer $explorer = null): string { $keywords1 = 'SELECT|(?:ON\s+DUPLICATE\s+KEY)?UPDATE|INSERT(?:\s+INTO)?|REPLACE(?:\s+INTO)?|DELETE|CALL|UNION|FROM|WHERE|HAVING|GROUP\s+BY|ORDER\s+BY|LIMIT|OFFSET|SET|VALUES|LEFT\s+JOIN|INNER\s+JOIN|TRUNCATE'; $keywords2 = 'ALL|DISTINCT|DISTINCTROW|IGNORE|AS|USING|ON|AND|OR|IN|IS|NOT|NULL|[RI]?LIKE|REGEXP|TRUE|FALSE'; // insert new lines + $sql = $query->getSql(); $sql = " $sql "; $sql = preg_replace("#(?<=[\\s,(])($keywords1)(?=[\\s,)])#i", "\n\$1", $sql); @@ -121,14 +108,14 @@ public static function dumpSql(string $sql, ?array $params = null, ?Connection $ }, $sql); // parameters - $sql = preg_replace_callback('#\?#', function () use ($params, $connection): string { + $params = $query->getParameters(); + $sql = preg_replace_callback('#\?#', function () use ($params, $explorer): string { static $i = 0; - if (!isset($params[$i])) { + $param = $params[$i++] ?? null; + if ($param === null) { return '?'; - } - $param = $params[$i++]; - if ( + } elseif ( is_string($param) && ( preg_match('#[^\x09\x0A\x0D\x20-\x7E\xA0-\x{10FFFF}]#u', $param) @@ -140,7 +127,7 @@ public static function dumpSql(string $sql, ?array $params = null, ?Connection $ } elseif (is_string($param)) { $length = Nette\Utils\Strings::length($param); $truncated = Nette\Utils\Strings::truncate($param, self::$maxLength); - $text = htmlspecialchars($connection ? $connection->quote($truncated) : '\'' . $truncated . '\'', ENT_NOQUOTES, 'UTF-8'); + $text = htmlspecialchars($explorer ? $explorer->quote($truncated) : '\'' . $truncated . '\'', ENT_NOQUOTES, 'UTF-8'); return '' . $text . ''; } elseif (is_resource($param)) { @@ -164,101 +151,13 @@ public static function dumpSql(string $sql, ?array $params = null, ?Connection $ } - /** - * Detects column types from a PDO statement using column metadata. - * @return array column name => IStructure::FIELD_* type - */ - public static function detectTypes(\PDOStatement $statement): array - { - $types = []; - $count = $statement->columnCount(); // driver must be meta-aware, see PHP bugs #53782, #54695 - for ($col = 0; $col < $count; $col++) { - $meta = $statement->getColumnMeta($col); - if (isset($meta['native_type'])) { - $types[$meta['name']] = self::detectType($meta['native_type']); - } - } - - return $types; - } - - - /** - * Maps a native column type string to an IStructure::FIELD_* constant. - * @internal - */ - public static function detectType(string $type): string - { - static $cache; - if (!isset($cache[$type])) { - $cache[$type] = 'string'; - foreach (self::$typePatterns as $s => $val) { - if (preg_match("#^($s)$#i", $type)) { - return $cache[$type] = $val; - } - } - } - - return $cache[$type]; - } - - - /** - * Converts raw column values to PHP types based on column type metadata. - * @internal - * @param array $row - * @param class-string<\DateTime|\DateTimeImmutable> $dateTimeClass - * @return array - */ - public static function normalizeRow( - array $row, - ResultSet $resultSet, - string $dateTimeClass = Nette\Utils\DateTime::class, - ): array - { - foreach ($resultSet->getColumnTypes() as $key => $type) { - $value = $row[$key]; - if ($value === null || $value === false || $type === IStructure::FIELD_TEXT) { - // do nothing - } elseif ($type === IStructure::FIELD_INTEGER) { - $row[$key] = is_float($tmp = $value * 1) ? $value : $tmp; - - } elseif ($type === IStructure::FIELD_FLOAT || $type === IStructure::FIELD_DECIMAL) { - $row[$key] = (float) $value; - - } elseif ($type === IStructure::FIELD_BOOL) { - $row[$key] = $value && $value !== 'f' && $value !== 'F'; - - } elseif ($type === IStructure::FIELD_DATETIME || $type === IStructure::FIELD_DATE) { - $row[$key] = str_starts_with($value, '0000-00') - ? null - : new $dateTimeClass($value); - - } elseif ($type === IStructure::FIELD_TIME) { - $row[$key] = (new $dateTimeClass($value))->setDate(1, 1, 1); - - } elseif ($type === IStructure::FIELD_TIME_INTERVAL) { - preg_match('#^(-?)(\d+)\D(\d+)\D(\d+)(\.\d+)?$#D', $value, $m); - $row[$key] = new \DateInterval("PT$m[2]H$m[3]M$m[4]S"); - $row[$key]->f = isset($m[5]) ? (float) $m[5] : 0.0; - $row[$key]->invert = (int) (bool) $m[1]; - - } elseif ($type === IStructure::FIELD_UNIX_TIMESTAMP) { - $row[$key] = (new $dateTimeClass)->setTimestamp($value); - } - } - - return $row; - } - - /** * Imports SQL dump from file. * @param ?(callable(int, ?float): void) $onProgress Called after each query * @return int Number of executed commands * @throws Nette\FileNotFoundException */ - public static function loadFromFile(Connection $connection, string $file, ?callable $onProgress = null): int + public static function loadFromFile(Explorer $explorer, string $file, ?callable $onProgress = null): int { @set_time_limit(0); // @ function may be disabled @@ -271,7 +170,7 @@ public static function loadFromFile(Connection $connection, string $file, ?calla $count = $size = 0; $delimiter = ';'; $sql = ''; - $pdo = $connection->getPdo(); // native query without logging + $connection = $explorer->getConnection(); // native query without logging while (($s = fgets($handle)) !== false) { $size += strlen($s); if (!strncasecmp($s, 'DELIMITER ', 10)) { @@ -279,7 +178,7 @@ public static function loadFromFile(Connection $connection, string $file, ?calla } elseif (str_ends_with($ts = rtrim($s), $delimiter)) { $sql .= substr($ts, 0, -strlen($delimiter)); - $pdo->exec($sql); + $connection->execute($sql); $sql = ''; $count++; if ($onProgress) { @@ -291,7 +190,7 @@ public static function loadFromFile(Connection $connection, string $file, ?calla } if (rtrim($sql) !== '') { - $pdo->exec($sql); + $connection->execute($sql); $count++; if ($onProgress) { $onProgress($count, isset($stat['size']) ? 100 : null); @@ -303,22 +202,23 @@ public static function loadFromFile(Connection $connection, string $file, ?calla } - /** @deprecated use Nette\Bridges\DatabaseTracy\ConnectionPanel::initialize() */ + #[\Deprecated('use Nette\Bridges\DatabaseTracy\ConnectionPanel::initialize()')] public static function createDebugPanel( - Connection $connection, + Explorer $connection, bool $explain, string $name, Tracy\Bar $bar, Tracy\BlueScreen $blueScreen, ): ?ConnectionPanel { + trigger_error(__METHOD__ . '() is deprecated, use Nette\Bridges\DatabaseTracy\ConnectionPanel::initialize()', E_USER_DEPRECATED); return ConnectionPanel::initialize($connection, true, $name, $explain, $bar, $blueScreen); } - /** @deprecated use Nette\Bridges\DatabaseTracy\ConnectionPanel::initialize() */ + #[\Deprecated('use Nette\Bridges\DatabaseTracy\ConnectionPanel::initialize()')] public static function initializeTracy( - Connection $connection, + Explorer $connection, bool $addBarPanel = false, string $name = '', bool $explain = true, @@ -326,6 +226,7 @@ public static function initializeTracy( ?Tracy\BlueScreen $blueScreen = null, ): ?ConnectionPanel { + trigger_error(__METHOD__ . '() is deprecated, use Nette\Bridges\DatabaseTracy\ConnectionPanel::initialize()', E_USER_DEPRECATED); return ConnectionPanel::initialize($connection, $addBarPanel, $name, $explain, $bar, $blueScreen); } @@ -378,43 +279,16 @@ public static function toPairs(array $rows, string|int|\Closure|null $key, strin } - /** - * Returns a human-readable string listing duplicate column names in the result set. - */ - public static function findDuplicates(\PDOStatement $statement): string - { - $cols = []; - for ($i = 0; $i < $statement->columnCount(); $i++) { - $meta = $statement->getColumnMeta($i); - if ($meta === false) { - continue; - } - - $cols[$meta['name']][] = $meta['table'] ?? ''; - } - - $duplicates = []; - foreach ($cols as $name => $tables) { - if (count($tables) > 1) { - $tables = array_filter(array_unique($tables)); - $duplicates[] = "'$name'" . ($tables ? ' (from ' . implode(', ', $tables) . ')' : ''); - } - } - - return implode(', ', $duplicates); - } - - /** * Parses a SQL column type string into its components. - * @return array{type: ?string, length: ?int, scale: ?int, parameters: ?string} + * @return array{type: ?string, size: ?int, scale: ?int, parameters: ?string} */ public static function parseColumnType(string $type): array { preg_match('/^([^(]+)(?:\((?:(\d+)(?:,(\d+))?|([^)]+))\))?/', $type, $m, PREG_UNMATCHED_AS_NULL); return [ 'type' => $m[1] ?? null, - 'length' => isset($m[2]) ? (int) $m[2] : null, + 'size' => isset($m[2]) ? (int) $m[2] : null, 'scale' => isset($m[3]) ? (int) $m[3] : null, 'parameters' => $m[4] ?? null, ]; diff --git a/src/Database/IRow.php b/src/Database/IRow.php deleted file mode 100644 index df1cbbbbc..000000000 --- a/src/Database/IRow.php +++ /dev/null @@ -1,14 +0,0 @@ - - */ - function getTables(): array; - - /** - * Returns all columns in a table. - * @return list}> - */ - function getColumns(string $table): array; - - /** - * Returns table primary key. - * @return string|list|null - */ - function getPrimaryKey(string $table): string|array|null; - - /** - * Returns autoincrement primary key name. - */ - function getPrimaryAutoincrementKey(string $table): ?string; - - /** - * Returns table primary key sequence. - */ - function getPrimaryKeySequence(string $table): ?string; - - /** - * Returns tables referencing the given table via foreign key, or null if unknown. - * @return array>|null referencing table name => list of referencing columns - */ - function getHasManyReference(string $table): ?array; - - /** - * Returns foreign key columns in the given table mapped to their referenced tables, or null if unknown. - * @return ?array local column name => referenced table name - */ - function getBelongsToReference(string $table): ?array; - - /** - * Rebuilds database structure cache. - */ - function rebuild(): void; - - /** - * Checks whether the structure has been rebuilt from the database during this request. - */ - function isRebuilt(): bool; -} diff --git a/src/Database/Reflection.php b/src/Database/Reflection.php index 036f0e9be..82a233c9e 100644 --- a/src/Database/Reflection.php +++ b/src/Database/Reflection.php @@ -21,7 +21,7 @@ final class Reflection public function __construct( - private readonly Driver $driver, + private readonly Drivers\Engine $engine, ) { unset($this->tables); } @@ -73,16 +73,16 @@ private function getFullName(string $name): string /** @internal */ - public function getDriver(): Driver + public function getDatabaseEngine(): Drivers\Engine { - return $this->driver; + return $this->engine; } private function initTables(): void { $res = []; - foreach ($this->driver->getTables() as $row) { + foreach ($this->engine->getTables() as $row) { $res[$row['fullName'] ?? $row['name']] = new Table($this, $row['name'], $row['view'], $row['fullName'] ?? null, $row['comment'] ?? null); } $this->tables = $res; diff --git a/src/Database/Reflection/Column.php b/src/Database/Reflection/Column.php index 16a6dba13..6db439da6 100644 --- a/src/Database/Reflection/Column.php +++ b/src/Database/Reflection/Column.php @@ -19,6 +19,7 @@ public function __construct( public readonly ?Table $table = null, public readonly string $nativeType = '', public readonly ?int $size = null, + public readonly ?int $scale = null, public readonly bool $nullable = false, public readonly mixed $default = null, public readonly bool $autoIncrement = false, diff --git a/src/Database/Reflection/Table.php b/src/Database/Reflection/Table.php index 8dc3cc651..6e96ac46d 100644 --- a/src/Database/Reflection/Table.php +++ b/src/Database/Reflection/Table.php @@ -52,8 +52,9 @@ public function getColumn(string $name): Column private function initColumns(): void { $res = []; - foreach ($this->reflection->getDriver()->getColumns($this->name) as $row) { - $res[$row['name']] = new Column($row['name'], $this, $row['nativetype'], $row['size'], $row['nullable'], $row['default'], $row['autoincrement'], $row['primary'], $row['comment'] ?? null, $row['vendor']); + foreach ($this->reflection->getDatabaseEngine()->getColumns($this->name) as $row) { + $row['table'] = $this; + $res[$row['name']] = new Column(...$row); } $this->columns = $res; } @@ -68,7 +69,7 @@ private function initIndexes(): void $row['primary'], $row['name'], ), - $this->reflection->getDriver()->getIndexes($this->name), + $this->reflection->getDatabaseEngine()->getIndexes($this->name), ); } @@ -86,13 +87,13 @@ private function initPrimaryKey(): void private function initForeignKeys(): void { $tmp = []; - foreach ($this->reflection->getDriver()->getForeignKeys($this->name) as $row) { + foreach ($this->reflection->getDatabaseEngine()->getForeignKeys($this->name) as $row) { $id = $row['name']; $foreignTable = $this->reflection->getTable($row['table']); $tmp[$id][0] = $foreignTable; - $tmp[$id][1][] = $this->getColumn($row['local']); - $tmp[$id][2][] = $foreignTable->getColumn($row['foreign']); - $tmp[$id][3] = $id; + $tmp[$id][1] = array_map(fn($name) => $this->getColumn($name), $row['local']); + $tmp[$id][2] = array_map(fn($name) => $foreignTable->getColumn($name), $row['foreign']); + $tmp[$id][3] = is_string($id) ? $id : null; } $this->foreignKeys = array_map(fn($row) => new ForeignKey(...$row), array_values($tmp)); } diff --git a/src/Database/Result.php b/src/Database/Result.php new file mode 100644 index 000000000..234eea0fa --- /dev/null +++ b/src/Database/Result.php @@ -0,0 +1,254 @@ + + */ +class Result implements \IteratorAggregate +{ + private bool $fetched = false; + + /** @var list */ + private array $rows; + + /** @var list */ + private array $rowsArray; + + /** @var array */ + private array $meta; + + + public function __construct( + private readonly Explorer $explorer, + private readonly SqlLiteral $query, + private readonly ?Drivers\Result $result, + private readonly float $time, + ) { + } + + + #[\Deprecated] + public function getConnection(): Explorer + { + trigger_error(__METHOD__ . '() is deprecated.', E_USER_DEPRECATED); + return $this->explorer; + } + + + public function getQuery(): SqlLiteral + { + return $this->query; + } + + + /** @deprecated use getQuery()->getSql() */ + public function getQueryString(): string + { + return $this->query->getSql(); + } + + + #[\Deprecated('use getQuery()->getParameters()')] + public function getParameters(): array + { + return $this->query->getParameters(); + } + + + public function getColumnCount(): ?int + { + return $this->result?->getColumnCount(); + } + + + public function getRowCount(): ?int + { + return $this->result?->getRowCount(); + } + + + public function getTime(): float + { + return $this->time; + } + + + /********************* misc tools ****************d*g**/ + + + /** + * Displays result set as HTML table. + */ + public function dump(): void + { + Helpers::dumpResult($this); + } + + + /********************* interface IteratorAggregate ****************d*g**/ + + + /** @return \Generator */ + public function getIterator(): \Generator + { + if ($this->fetched) { + throw new Nette\InvalidStateException(self::class . ' implements only one way iterator.'); + } + + $counter = 0; + while (($row = $this->fetch()) !== null) { + yield $counter++ => $row; + } + } + + + /********************* fetch ****************d*g**/ + + + /** + * Returns the next row as an associative array, or null if there are no more rows. + * When $path is given, fetches all rows and restructures them using Arrays::associate(). + * @return ?array + */ + public function fetchAssoc(?string $path = null): ?array + { + if ($path !== null) { + return Arrays::associate($this->fetchAll(), $path); + } + + $data = $this->result?->fetch(); + if ($data === null) { + $this->fetched = true; + return null; + + } elseif (!$this->fetched && count($data) !== $this->result->getColumnCount()) { + $duplicates = array_filter(array_count_values(array_column($this->result->getColumnsInfo(), 'name')), fn($val) => $val > 1); + trigger_error("Found duplicate columns in database result set: '" . implode("', '", array_keys($duplicates)) . "'."); + } + + $this->fetched = true; + return $this->normalizeRow($data); + } + + + /** + * Returns the next row as a Row object or null if there are no more rows. + */ + public function fetch(): ?Row + { + $data = $this->fetchAssoc(); + return $data === null ? null : Arrays::toObject($data, new Row); + } + + + /** + * Returns the first field of the next row or null if there are no more rows. + */ + public function fetchField(): mixed + { + $row = $this->fetchAssoc(); + return $row ? reset($row) : null; + } + + + /** + * Returns the next row as indexed array or null if there are no more rows. + * @return ?list + */ + public function fetchList(): ?array + { + $row = $this->fetchAssoc(); + return $row ? array_values($row) : null; + } + + + /** + * Alias for fetchList(). + * @return ?list + */ + public function fetchFields(): ?array + { + return $this->fetchList(); + } + + + /** + * Returns all rows as associative array, where first argument specifies key column and second value column. + * For duplicate keys, the last value is used. When using null as key, array is indexed from zero. + * Alternatively accepts callback returning value or key-value pairs. + * @param string|int|(\Closure(Row): array{0: mixed, 1?: mixed})|null $keyOrCallback + * @return array + */ + public function fetchPairs(string|int|\Closure|null $keyOrCallback = null, string|int|null $value = null): array + { + return Helpers::toPairs($this->fetchAll(), $keyOrCallback, $value); + } + + + /** + * Returns all remaining rows as array of Row objects. + * @return list + */ + public function fetchAll(): array + { + return $this->rows ??= iterator_to_array($this); + } + + + /** + * Returns all remaining rows as array of associative arrays. + * Useful because the rows can be typed using @var. + * @return list + */ + public function fetchAllAssoc(): array + { + if(!isset($this->rowsArray)){ + $this->rowsArray = []; + while(true){ + $data = $this->fetchAssoc(); + if($data === null) break; + $this->rowsArray[] = $data; + } + } + return $this->rowsArray; + } + + + private function normalizeRow(array $row): array + { + $engine = $this->explorer->getDatabaseEngine(); + $converter = $this->explorer->getTypeConverter(); + $this->meta ??= $this->getColumnsMeta(); + foreach ($row as $key => $value) { + $row[$key] = isset($value, $this->meta[$key]) + ? $engine->convertToPhp($value, $this->meta[$key], $converter) + : $value; + } + return $row; + } + + + private function getColumnsMeta(): array + { + $res = []; + foreach ($this->result->getColumnsInfo() as $meta) { + $res[$meta['name']] = $meta; + } + return $res; + } +} + + +class_exists(ResultSet::class); diff --git a/src/Database/ResultSet.php b/src/Database/ResultSet.php deleted file mode 100644 index 6051bbeb0..000000000 --- a/src/Database/ResultSet.php +++ /dev/null @@ -1,286 +0,0 @@ - - */ -class ResultSet implements \Iterator, IRowContainer -{ - private ?\PDOStatement $pdoStatement = null; - private Row|false|null $lastRow = null; - private int $lastRowKey = -1; - - /** @var list */ - private array $rows; - private float $time; - - /** @var array column name => type */ - private array $types; - - - public function __construct( - private readonly Connection $connection, - private readonly string $queryString, - /** @var mixed[] */ - private readonly array $params, - /** @var ?\Closure(array, self): array */ - private readonly ?\Closure $normalizer = null, - ) { - $time = microtime(true); - $types = ['boolean' => PDO::PARAM_BOOL, 'integer' => PDO::PARAM_INT, 'resource' => PDO::PARAM_LOB, 'NULL' => PDO::PARAM_NULL]; - - try { - if (str_starts_with($queryString, '::')) { - $connection->getPdo()->{substr($queryString, 2)}(); - } else { - $this->pdoStatement = $connection->getPdo()->prepare($queryString); - foreach ($params as $key => $value) { - $type = gettype($value); - $this->pdoStatement->bindValue(is_int($key) ? $key + 1 : $key, $value, $types[$type] ?? PDO::PARAM_STR); - } - - $this->pdoStatement->setFetchMode(PDO::FETCH_ASSOC); - $this->pdoStatement->execute(); - } - } catch (\PDOException $e) { - $e = $connection->getDriver()->convertException($e); - $e->queryString = $queryString; - $e->params = $params; - throw $e; - } - - $this->time = microtime(as_float: true) - $time; - } - - - /** @deprecated */ - public function getConnection(): Connection - { - return $this->connection; - } - - - /** @internal */ - public function getPdoStatement(): ?\PDOStatement - { - return $this->pdoStatement; - } - - - public function getQueryString(): string - { - return $this->queryString; - } - - - /** @return mixed[] */ - public function getParameters(): array - { - return $this->params; - } - - - public function getColumnCount(): ?int - { - return $this->pdoStatement ? $this->pdoStatement->columnCount() : null; - } - - - public function getRowCount(): ?int - { - return $this->pdoStatement ? $this->pdoStatement->rowCount() : null; - } - - - /** @return array */ - public function getColumnTypes(): array - { - if ($this->pdoStatement === null) { - return []; - } - - $this->types ??= $this->connection->getDriver()->getColumnTypes($this->pdoStatement); - return $this->types; - } - - - public function getTime(): float - { - return $this->time; - } - - - /** - * @internal - * @param array $row - * @return array - */ - public function normalizeRow(array $row): array - { - return $this->normalizer - ? ($this->normalizer)($row, $this) - : $row; - } - - - /********************* misc tools ****************d*g**/ - - - /** - * Displays result set as HTML table. - */ - public function dump(): void - { - Helpers::dumpResult($this); - } - - - /********************* interface Iterator ****************d*g**/ - - - public function rewind(): void - { - if ($this->lastRow === false) { - throw new Nette\InvalidStateException(self::class . ' implements only one way iterator.'); - } - } - - - public function current(): Row|false|null - { - return $this->lastRow; - } - - - public function key(): int - { - return $this->lastRowKey; - } - - - public function next(): void - { - $this->lastRow = false; - } - - - public function valid(): bool - { - if ($this->lastRow) { - return true; - } - - return $this->fetch() !== null; - } - - - /********************* fetch ****************d*g**/ - - - /** - * Returns the next row as an associative array, or null if there are no more rows. - * When $path is given, fetches all rows and restructures them using Arrays::associate(). - * @return ?array - */ - public function fetchAssoc(?string $path = null): ?array - { - if ($path !== null) { - return Arrays::associate($this->fetchAll(), $path); - } - - $data = $this->pdoStatement ? $this->pdoStatement->fetch() : null; - if (!$data) { - $this->pdoStatement?->closeCursor(); - return null; - - } elseif ($this->lastRow === null && count($data) !== $this->pdoStatement->columnCount()) { - $duplicates = Helpers::findDuplicates($this->pdoStatement); - trigger_error("Found duplicate columns in database result set: $duplicates."); - } - - return $this->normalizeRow($data); - } - - - /** - * Returns the next row as a Row object or null if there are no more rows. - */ - public function fetch(): ?Row - { - $data = $this->fetchAssoc(); - if ($data === null) { - return null; - } - - $this->lastRowKey++; - return $this->lastRow = Arrays::toObject($data, new Row); - } - - - /** - * Returns the first field of the next row or null if there are no more rows. - */ - public function fetchField(): mixed - { - $row = $this->fetchAssoc(); - return $row ? reset($row) : null; - } - - - /** - * Returns the next row as indexed array or null if there are no more rows. - * @return ?list - */ - public function fetchList(): ?array - { - $row = $this->fetchAssoc(); - return $row ? array_values($row) : null; - } - - - /** - * Alias for fetchList(). - * @return ?list - */ - public function fetchFields(): ?array - { - return $this->fetchList(); - } - - - /** - * Returns all rows as associative array, where first argument specifies key column and second value column. - * For duplicate keys, the last value is used. When using null as key, array is indexed from zero. - * Alternatively accepts callback returning value or key-value pairs. - * @param string|int|(\Closure(Row): array{0: mixed, 1?: mixed})|null $keyOrCallback - * @return array - */ - public function fetchPairs(string|int|\Closure|null $keyOrCallback = null, string|int|null $value = null): array - { - return Helpers::toPairs($this->fetchAll(), $keyOrCallback, $value); - } - - - /** - * Returns all remaining rows as array of Row objects. - * @return list - */ - public function fetchAll(): array - { - $this->rows ??= iterator_to_array($this, preserve_keys: false); - return $this->rows; - } -} diff --git a/src/Database/Row.php b/src/Database/Row.php index 3143a19fd..90edc5e17 100644 --- a/src/Database/Row.php +++ b/src/Database/Row.php @@ -15,7 +15,7 @@ * Represents a single database table row. * @extends Nette\Utils\ArrayHash */ -class Row extends Nette\Utils\ArrayHash implements IRow +class Row extends Nette\Utils\ArrayHash { public function __get(mixed $key): mixed { @@ -62,3 +62,6 @@ public function offsetExists($key): bool return parent::offsetExists($key); } } + + +class_alias(Row::class, IRow::class); diff --git a/src/Database/SqlPreprocessor.php b/src/Database/SqlPreprocessor.php index 8a3e72348..e5702990a 100644 --- a/src/Database/SqlPreprocessor.php +++ b/src/Database/SqlPreprocessor.php @@ -46,8 +46,8 @@ class SqlPreprocessor 'EXPLAIN' => 1, ]; - private readonly Connection $connection; - private readonly Driver $driver; + private readonly Drivers\Connection $connection; + private readonly Drivers\Engine $engine; /** @var list */ private array $params; @@ -61,10 +61,10 @@ class SqlPreprocessor private ?string $arrayMode; - public function __construct(Connection $connection) + public function __construct(Explorer $explorer) { - $this->connection = $connection; - $this->driver = $connection->getDriver(); + $this->connection = $explorer->getConnection(); + $this->engine = $explorer->getDatabaseEngine(); } @@ -197,8 +197,8 @@ private function formatValue(mixed $value): string $value === null => 'NULL', $value instanceof SqlLiteral => $this->formatLiteral($value), $value instanceof Table\ActiveRow => $this->formatValue($value->getPrimary()), - $value instanceof \DateTimeInterface => $this->driver->formatDateTime($value), - $value instanceof \DateInterval => $this->driver->formatDateInterval($value), + $value instanceof \DateTimeInterface => $this->engine->formatDateTime($value), + $value instanceof \DateInterval => $this->engine->formatDateInterval($value), $value instanceof \BackedEnum => $this->formatValue($value->value), $value instanceof \Stringable => $this->formatValue((string) $value), default => throw new Nette\InvalidArgumentException('Unexpected type of parameter: ' . get_debug_type($value)) @@ -251,7 +251,7 @@ private function formatMultiInsert(array $groups): string throw new Nette\InvalidArgumentException('Automaticaly detected multi-insert, but values aren\'t array. If you need try to change ?mode.'); } - $cols = array_keys(is_array($groups[0]) ? $groups[0] : iterator_to_array($groups[0])); + $cols = array_keys(iterator_to_array($groups[0])); $vals = []; foreach ($groups as $group) { $rowVals = []; @@ -262,7 +262,7 @@ private function formatMultiInsert(array $groups): string $vals[] = implode(', ', $rowVals); } - $useSelect = $this->driver->isSupported(Driver::SupportMultiInsertAsSelect); + $useSelect = $this->engine->isSupported(Drivers\Engine::SupportMultiInsertAsSelect); return '(' . implode(', ', array_map($this->delimit(...), $cols)) . ($useSelect ? ') SELECT ' : ') VALUES (') . implode($useSelect ? ' UNION ALL SELECT ' : '), (', $vals) @@ -364,6 +364,6 @@ private function formatLiteral(SqlLiteral $value): string */ private function delimit(string $name): string { - return implode('.', array_map($this->driver->delimite(...), explode('.', $name))); + return implode('.', array_map($this->engine->delimit(...), explode('.', $name))); } } diff --git a/src/Database/Structure.php b/src/Database/Structure.php index f989935a1..8f78009f2 100644 --- a/src/Database/Structure.php +++ b/src/Database/Structure.php @@ -13,21 +13,19 @@ /** * Provides database structure metadata with caching. + * @internal */ -class Structure implements IStructure +class Structure { - protected readonly Nette\Caching\Cache $cache; - /** @var array{tables: list, columns: array>>, primary: array|null>, aliases: array, hasMany: array>>, belongsTo: array>} */ protected array $structure; protected bool $isRebuilt = false; public function __construct( - protected readonly Connection $connection, - Nette\Caching\Storage $cacheStorage, + protected readonly Drivers\Engine $engine, + protected readonly Nette\Caching\Cache $cache, ) { - $this->cache = new Nette\Caching\Cache($cacheStorage, 'Nette.Database.Structure.' . hash('xxh128', $connection->getDsn())); } @@ -69,11 +67,11 @@ public function getPrimaryAutoincrementKey(string $table): ?string return null; } - // Search for autoincrement key from multi primary key + // Search for autoIncrement key from multi primary key if (is_array($primaryKey)) { $keys = array_flip($primaryKey); foreach ($this->getColumns($table) as $column) { - if (isset($keys[$column['name']]) && $column['autoincrement']) { + if (isset($keys[$column['name']]) && $column['autoIncrement']) { return $column['name']; } } @@ -81,10 +79,10 @@ public function getPrimaryAutoincrementKey(string $table): ?string return null; } - // Search for autoincrement key from simple primary key + // Search for auto-increment key from simple primary key foreach ($this->getColumns($table) as $column) { if ($column['name'] === $primaryKey) { - return $column['autoincrement'] ? $column['name'] : null; + return $column['autoIncrement'] ? $column['name'] : null; } } @@ -100,18 +98,18 @@ public function getPrimaryKeySequence(string $table): ?string $this->needStructure(); $table = $this->resolveFQTableName($table); - if (!$this->connection->getDriver()->isSupported(Driver::SupportSequence)) { + if (!$this->engine->isSupported(Drivers\Engine::SupportSequence)) { return null; } - $autoincrementPrimaryKeyName = $this->getPrimaryAutoincrementKey($table); - if (!$autoincrementPrimaryKeyName) { + $autoIncrementPrimaryKeyName = $this->getPrimaryAutoincrementKey($table); + if (!$autoIncrementPrimaryKeyName) { return null; } // Search for sequence from simple primary key foreach ($this->structure['columns'][$table] as $columnMeta) { - if ($columnMeta['name'] === $autoincrementPrimaryKeyName) { + if ($columnMeta['name'] === $autoIncrementPrimaryKeyName) { return $columnMeta['vendor']['sequence'] ?? null; } } @@ -176,10 +174,8 @@ protected function needStructure(): void */ protected function loadStructure(): array { - $driver = $this->connection->getDriver(); - $structure = ['tables' => [], 'columns' => [], 'primary' => [], 'aliases' => [], 'hasMany' => [], 'belongsTo' => []]; - $structure['tables'] = $driver->getTables(); + $structure['tables'] = $this->engine->getTables(); foreach ($structure['tables'] as $tablePair) { if (isset($tablePair['fullName'])) { @@ -189,7 +185,7 @@ protected function loadStructure(): array $table = $tablePair['name']; } - $structure['columns'][strtolower($table)] = $columns = $driver->getColumns($table); + $structure['columns'][strtolower($table)] = $columns = $this->engine->getColumns($table); if (!$tablePair['view']) { $structure['primary'][strtolower($table)] = $this->analyzePrimaryKey($columns); @@ -237,20 +233,13 @@ protected function analyzeForeignKeys(array &$structure, string $table): void { $lowerTable = strtolower($table); - $foreignKeys = $this->connection->getDriver()->getForeignKeys($table); - - /** @var array $fksColumnsCounts */ - $fksColumnsCounts = []; - foreach ($foreignKeys as $foreignKey) { - $tmp = &$fksColumnsCounts[$foreignKey['name']]; - $tmp++; - } + $foreignKeys = $this->engine->getForeignKeys($table); - usort($foreignKeys, fn($a, $b): int => $fksColumnsCounts[$b['name']] <=> $fksColumnsCounts[$a['name']]); + usort($foreignKeys, fn($a, $b): int => count($b['local']) <=> count($a['local'])); foreach ($foreignKeys as $row) { - $structure['belongsTo'][$lowerTable][$row['local']] = $row['table']; - $structure['hasMany'][strtolower($row['table'])][$table][] = $row['local']; + $structure['belongsTo'][$lowerTable][$row['local'][0]] = $row['table']; + $structure['hasMany'][strtolower($row['table'])][$table][] = $row['local'][0]; } if (isset($structure['belongsTo'][$lowerTable])) { diff --git a/src/Database/Table/ActiveRow.php b/src/Database/Table/ActiveRow.php index 53d8e706d..c38dfb32b 100644 --- a/src/Database/Table/ActiveRow.php +++ b/src/Database/Table/ActiveRow.php @@ -16,7 +16,7 @@ * ActiveRow is based on the great library NotORM http://www.notorm.com written by Jakub Vrana. * @implements \IteratorAggregate */ -class ActiveRow implements \IteratorAggregate, IRow +class ActiveRow implements \IteratorAggregate, \ArrayAccess { private bool $dataRefreshed = false; @@ -150,9 +150,7 @@ public function related(string $key, ?string $throughColumn = null): GroupedSele */ public function update(iterable $data): bool { - if ($data instanceof \Traversable) { - $data = iterator_to_array($data); - } + $data = iterator_to_array($data); $primary = $this->getPrimary(); if (!is_array($primary)) { @@ -248,7 +246,7 @@ public function __set(string $column, mixed $value): void * @return ActiveRow|mixed * @throws Nette\MemberAccessException if the column does not exist and no relationship is found */ - public function &__get(string $key): mixed + public function __get(string $key): mixed { if ($this->accessColumn($key)) { return $this->data[$key]; diff --git a/src/Database/Table/GroupedSelection.php b/src/Database/Table/GroupedSelection.php index b7ac2a170..300a2545f 100644 --- a/src/Database/Table/GroupedSelection.php +++ b/src/Database/Table/GroupedSelection.php @@ -7,8 +7,6 @@ namespace Nette\Database\Table; -use Nette; -use Nette\Database\Conventions; use Nette\Database\Explorer; use function array_keys, count, iterator_to_array, preg_match, reset; @@ -34,15 +32,13 @@ class GroupedSelection extends Selection */ public function __construct( Explorer $explorer, - Conventions $conventions, string $tableName, /** grouping column name */ protected readonly string $column, /** referenced table */ protected readonly Selection $refTable, - ?Nette\Caching\Storage $cacheStorage = null, ) { - parent::__construct($explorer, $conventions, $tableName, $cacheStorage); + parent::__construct($explorer, $tableName); } @@ -110,7 +106,7 @@ public function aggregation(string $function, ?string $groupFunction = null): mi if (!isset($aggregations[$key])) { $aggregation = []; - $selection = $this->createSelectionInstance(); + $selection = $this->explorer->table($this->name); $selection->getSqlBuilder()->importConditions($this->getSqlBuilder()); if ($groupFunction && $selection->getSqlBuilder()->importGroupConditions($this->getSqlBuilder())) { diff --git a/src/Database/Table/IRow.php b/src/Database/Table/IRow.php deleted file mode 100644 index 96464f9b9..000000000 --- a/src/Database/Table/IRow.php +++ /dev/null @@ -1,16 +0,0 @@ - + * @implements \IteratorAggregate * @implements \ArrayAccess */ -class Selection implements \Iterator, IRowContainer, \ArrayAccess, \Countable +class Selection implements \IteratorAggregate, \ArrayAccess, \Countable { protected readonly Explorer $explorer; - - /** back compatibility */ - protected Explorer $context; - protected readonly Conventions $conventions; protected readonly ?Nette\Caching\Cache $cache; protected SqlBuilder $sqlBuilder; @@ -54,7 +49,7 @@ class Selection implements \Iterator, IRowContainer, \ArrayAccess, \Countable protected ?string $generalCacheKey = null; protected ?string $specificCacheKey = null; - /** @var array> of [conditions => [group value => row]]; used by GroupedSelection */ + /** @var array> of [conditions => [group value => row]]; used by GroupedSelection */ protected array $aggregation = []; /** @var array|false|null column => selected */ @@ -66,27 +61,18 @@ class Selection implements \Iterator, IRowContainer, \ArrayAccess, \Countable /** @var ?self should instance observe accessed columns caching */ protected ?self $observeCache = null; - /** @var list of primary key values */ - protected array $keys = []; - /** * Creates filtered table representation. */ public function __construct( Explorer $explorer, - Conventions $conventions, string $tableName, - ?Nette\Caching\Storage $cacheStorage = null, ) { - $this->explorer = $this->context = $explorer; - $this->conventions = $conventions; + $this->explorer = $explorer; $this->name = $tableName; - - $this->cache = $cacheStorage - ? new Nette\Caching\Cache($cacheStorage, 'Nette.Database.' . hash('xxh128', $explorer->getConnection()->getDsn())) - : null; - $this->primary = $conventions->getPrimary($tableName); + $this->cache = $explorer->getCache(); + $this->primary = $explorer->getConventions()->getPrimary($tableName); $this->sqlBuilder = new SqlBuilder($tableName, $explorer); $this->refCache = &$this->getRefTable($refPath)->globalRefCache[$refPath]; } @@ -472,7 +458,7 @@ public function alias(string $tableChain, string $alias): static */ public function aggregation(string $function, ?string $groupFunction = null): mixed { - $selection = $this->createSelectionInstance(); + $selection = $this->explorer->table($this->name); $selection->getSqlBuilder()->importConditions($this->getSqlBuilder()); if ($groupFunction && $selection->getSqlBuilder()->importGroupConditions($this->getSqlBuilder())) { $selection->select("$function AS aggregate"); @@ -555,14 +541,13 @@ protected function execute(): void } } + $key = 0; $this->rows = []; $usedPrimary = true; - $pdoStatement = $result->getPdoStatement() ?? throw new Nette\ShouldNotHappenException; - foreach ($pdoStatement as $key => $row) { - $row = $this->createRow($result->normalizeRow($row)); - $primary = $row->getSignature(throw: false); - $usedPrimary = $usedPrimary && $primary !== ''; - $this->rows[$usedPrimary ? $primary : $key] = $row; + while ($row = @$result->fetchAssoc()) { // @ may contain duplicate columns + $row = $this->explorer->createActiveRow($this, $row); + $usedPrimary = $usedPrimary && ($primary = $row->getSignature(throw: false)) !== ''; + $this->rows[$usedPrimary ? $primary : $key++] = $row; } $this->data = $this->rows; @@ -575,39 +560,15 @@ protected function execute(): void } - /** - * @deprecated - * @param array $row - * @return T - */ - protected function createRow(array $row): ActiveRow - { - /** @var T */ - return $this->explorer->createActiveRow($row, $this); - } - - - /** - * @deprecated - * @return ($table is null ? Selection : Selection) - */ + /** @deprecated */ public function createSelectionInstance(?string $table = null): self { - return $this->explorer->table($table ?: $this->name); + // DELETE + return $this->explorer->table($table ?? $this->name); } - /** - * @deprecated - * @return GroupedSelection - */ - protected function createGroupedSelectionInstance(string $table, string $column): GroupedSelection - { - return $this->explorer->createGroupedSelection($this, $table, $column); - } - - - protected function query(string $query): Nette\Database\ResultSet + protected function query(string $query): Nette\Database\Result { return $this->explorer->query($query, ...$this->sqlBuilder->getParameters()); } @@ -824,16 +785,13 @@ public function insert(iterable $data): ActiveRow|array|int $return = $this->explorer->query($this->sqlBuilder->buildInsertQuery() . ' ' . $data->getSql(), ...$data->getSqlBuilder()->getParameters()); } else { - if ($data instanceof \Traversable) { - $data = iterator_to_array($data); - } - + $data = iterator_to_array($data); $return = $this->explorer->query($this->sqlBuilder->buildInsertQuery() . ' ?values', $data); } $this->loadRefCache(); - if ($data instanceof self || $this->primary === null) { + if ($data instanceof self || $this->primary === null || array_is_list($data)) { unset($this->refCache['referencing'][$this->getGeneralCacheKey()][$this->getSpecificCacheKey()]); return $return->getRowCount() ?? throw new Nette\InvalidStateException('Cannot determine the number of affected rows.'); @@ -848,7 +806,7 @@ public function insert(iterable $data): ActiveRow|array|int // First check sequence if (!empty($primarySequenceName) && $primaryAutoincrementKey) { - $primaryKey[$primaryAutoincrementKey] = $this->explorer->getInsertId($this->explorer->getConnection()->getDriver()->delimite($primarySequenceName)); + $primaryKey[$primaryAutoincrementKey] = $this->explorer->getInsertId($this->explorer->getDatabaseEngine()->delimit($primarySequenceName)); // Autoincrement primary without sequence } elseif ($primaryAutoincrementKey) { @@ -873,8 +831,7 @@ public function insert(iterable $data): ActiveRow|array|int ?? throw new Nette\InvalidStateException('Cannot determine the number of affected rows.'); } - /** @phpstan-var T $row */ - $row = $this->createSelectionInstance() + $row = $this->explorer->table($this->name) ->select('*') ->wherePrimary($primaryKey) ->fetch() @@ -901,10 +858,7 @@ public function insert(iterable $data): ActiveRow|array|int */ public function update(iterable $data): int { - if ($data instanceof \Traversable) { - $data = iterator_to_array($data); - } - + $data = iterator_to_array($data); if (!$data) { return 0; } @@ -942,7 +896,7 @@ public function getReferencedTable(ActiveRow $row, ?string $table, ?string $colu return false; } - $belongsTo = $this->conventions->getBelongsToReference($this->name, $table); + $belongsTo = $this->explorer->getConventions()->getBelongsToReference($this->name, $table); if (!$belongsTo) { return false; } @@ -972,7 +926,7 @@ public function getReferencedTable(ActiveRow $row, ?string $table, ?string $colu } if ($cacheKeys) { - $selection = $this->createSelectionInstance($table); + $selection = $this->explorer->table($table); $selection->where($selection->getPrimary(), array_keys($cacheKeys)); } else { $selection = []; @@ -996,7 +950,7 @@ public function getReferencingTable( if (str_contains($table, '.')) { [$table, $column] = explode('.', $table); } elseif (!$column) { - $hasMany = $this->conventions->getHasManyReference($this->name, $table); + $hasMany = $this->explorer->getConventions()->getHasManyReference($this->name, $table); if (!$hasMany) { return null; } @@ -1007,7 +961,7 @@ public function getReferencingTable( /** @var ?GroupedSelection $prototype */ $prototype = &$this->refCache['referencingPrototype'][$this->getSpecificCacheKey()]["$table.$column"]; if (!$prototype) { - $prototype = $this->createGroupedSelectionInstance($table, $column); + $prototype = $this->explorer->createGroupedSelectionInstance($this, $table, $column); $prototype->where("$table.$column", array_keys((array) $this->rows)); $prototype->getSpecificCacheKey(); } @@ -1021,45 +975,18 @@ public function getReferencingTable( } - /********************* interface Iterator ****************d*g**/ + /********************* interface IteratorAggregate ****************d*g**/ - public function rewind(): void + /** @return \Generator */ + public function getIterator(): \Generator { $this->execute(); - $this->keys = array_keys($this->data ?? []); - reset($this->keys); - } - - - /** @return T|false */ - public function current(): ActiveRow|false - { - $key = current($this->keys); - return $key !== false && isset($this->data[$key]) - ? $this->data[$key] - : false; - } - - - public function key(): string|int - { - $key = current($this->keys); - return $key === false ? -1 : $key; - } - - - public function next(): void - { - do { - next($this->keys); - } while (($key = current($this->keys)) !== false && !isset($this->data[$key])); - } - - - public function valid(): bool - { - return current($this->keys) !== false; + foreach ($this->data as $key => $value) { + if (isset($this->data[$key])) { // may be unset by offsetUnset + yield $key => $value; + } + } } diff --git a/src/Database/Table/SqlBuilder.php b/src/Database/Table/SqlBuilder.php index 6b33520be..f943d86cc 100644 --- a/src/Database/Table/SqlBuilder.php +++ b/src/Database/Table/SqlBuilder.php @@ -9,11 +9,12 @@ use Nette; use Nette\Database\Conventions; -use Nette\Database\Driver; +use Nette\Database\Drivers\Engine; use Nette\Database\Explorer; -use Nette\Database\IStructure; use Nette\Database\SqlLiteral; -use function array_flip, array_keys, array_map, array_merge, array_pop, array_shift, array_unshift, array_values, count, end, explode, hash, implode, is_array, iterator_to_array, json_encode, key, preg_match, preg_match_all, preg_replace, preg_replace_callback, rtrim, str_contains, str_repeat, strlen, strtoupper, substr, substr_count, substr_replace, trim; +use Nette\Database\Structure; +use function array_flip, array_keys, array_map, array_merge, array_pop, array_shift, array_unshift, array_values, count, end, explode, hash, implode, is_array, is_string, json_encode, key, preg_match, preg_match_all, preg_replace, preg_replace_callback, rtrim, str_contains, str_repeat, strlen, strtoupper, substr, substr_count, substr_replace, trim; +use const PREG_OFFSET_CAPTURE, PREG_SET_ORDER; /** @@ -60,8 +61,8 @@ class SqlBuilder /** @var array alias => chain */ protected array $aliases = []; protected string $currentAlias = ''; - private readonly Driver $driver; - private readonly IStructure $structure; + private readonly Engine $engine; + private readonly Structure $structure; /** @var array table fullName => exists */ private array $cacheTableList = []; @@ -73,11 +74,11 @@ class SqlBuilder public function __construct(string $tableName, Explorer $explorer) { $this->tableName = $tableName; - $this->driver = $explorer->getConnection()->getDriver(); + $this->engine = $explorer->getDatabaseEngine(); $this->conventions = $explorer->getConventions(); $this->structure = $explorer->getStructure(); $tableNameParts = explode('.', $tableName); - $this->delimitedTable = implode('.', array_map($this->driver->delimite(...), $tableNameParts)); + $this->delimitedTable = implode('.', array_map($this->engine->delimit(...), $tableNameParts)); $this->checkUniqueTableName(end($tableNameParts), $tableName); } @@ -96,14 +97,14 @@ public function buildInsertQuery(): string public function buildUpdateQuery(): string { - $query = "UPDATE {$this->delimitedTable} SET ?set" . $this->tryDelimite($this->buildConditions()); + $query = "UPDATE {$this->delimitedTable} SET ?set" . $this->tryDelimit($this->buildConditions()); if ($this->order !== []) { $query .= ' ORDER BY ' . implode(', ', $this->order); } if ($this->limit !== null || $this->offset) { - $this->driver->applyLimit($query, $this->limit, $this->offset); + $query = $this->engine->applyLimit($query, $this->limit, $this->offset); } return $query; @@ -112,9 +113,9 @@ public function buildUpdateQuery(): string public function buildDeleteQuery(): string { - $query = "DELETE FROM {$this->delimitedTable}" . $this->tryDelimite($this->buildConditions()); + $query = "DELETE FROM {$this->delimitedTable}" . $this->tryDelimit($this->buildConditions()); if ($this->limit !== null || $this->offset) { - $this->driver->applyLimit($query, $this->limit, $this->offset); + $query = $this->engine->applyLimit($query, $this->limit, $this->offset); } return $query; @@ -138,7 +139,7 @@ public function getSelectQueryHash(?array $columns = null): string $parts[] = $this->select; } elseif ($columns) { $parts[] = [$this->delimitedTable, $columns]; - } elseif ($this->group && !$this->driver->isSupported(Driver::SupportSelectUngroupedColumns)) { + } elseif ($this->group && !$this->engine->isSupported(Engine::SupportSelectUngroupedColumns)) { $parts[] = [$this->group]; } else { $parts[] = "{$this->delimitedTable}.*"; @@ -190,7 +191,7 @@ public function buildSelectQuery(?array $columns = null): string $querySelect = $this->buildSelect($cols); - } elseif ($this->group && !$this->driver->isSupported(Driver::SupportSelectUngroupedColumns)) { + } elseif ($this->group && !$this->engine->isSupported(Engine::SupportSelectUngroupedColumns)) { $querySelect = $this->buildSelect([$this->group]); $this->parseJoins($joins, $querySelect); @@ -202,9 +203,9 @@ public function buildSelectQuery(?array $columns = null): string $queryJoins = $this->buildQueryJoins($joins, $finalJoinConditions); $query = "{$querySelect} FROM {$this->delimitedTable}{$queryJoins}{$queryCondition}{$queryEnd}"; - $this->driver->applyLimit($query, $this->limit, $this->offset); + $query = $this->engine->applyLimit($query, $this->limit, $this->offset); - return $this->tryDelimite($query); + return $this->tryDelimit($query); } @@ -730,7 +731,7 @@ public function parseJoinsCb(array &$joins, array $match): string $parentAlias = preg_replace('#^(.*\.)?(.*)$#', '$2', $this->tableName); // join schema keyMatch and table keyMatch to schema.table keyMatch - if ($this->driver->isSupported(Driver::SupportSchema) && count($keyMatches) > 1) { + if ($this->engine->isSupported(Engine::SupportSchema) && count($keyMatches) > 1) { $tables = $this->getCachedTableList(); if ( !isset($tables[$keyMatches[0]['key']]) @@ -896,13 +897,13 @@ protected function buildQueryEnd(): string /** * Delimits lowercase identifiers in a SQL fragment while leaving uppercase keywords untouched. */ - protected function tryDelimite(string $s): string + protected function tryDelimit(string $s): string { return preg_replace_callback( '#(?<=[^\w`"\[?:]|^)[a-z_][a-z0-9_]*(?=[^\w`"(\]]|$)#Di', fn(array $m): string => strtoupper($m[0]) === $m[0] ? $m[0] - : $this->driver->delimite($m[0]), + : $this->engine->delimit($m[0]), $s, ); } @@ -922,7 +923,7 @@ protected function addConditionComposition( array &$conditionsParameters, ): bool { - if ($this->driver->isSupported(Driver::SupportMultiColumnAsOrCondition)) { + if ($this->engine->isSupported(Engine::SupportMultiColumnAsOrCondition)) { $conditionFragment = '(' . implode(' = ? AND ', $columns) . ' = ?) OR '; $condition = substr(str_repeat($conditionFragment, count($parameters)), 0, -4); return $this->addCondition($condition, [Nette\Utils\Arrays::flatten($parameters)], $conditions, $conditionsParameters); diff --git a/src/Database/TypeConverter.php b/src/Database/TypeConverter.php new file mode 100644 index 000000000..78d5edc2c --- /dev/null +++ b/src/Database/TypeConverter.php @@ -0,0 +1,119 @@ + self::Text, // PostgreSQL arrays + '(TINY|SMALL|SHORT|MEDIUM|BIG|LONG)(INT)?|INT(EGER|\d+| IDENTITY| UNSIGNED)?|(SMALL|BIG|)SERIAL\d*|COUNTER|YEAR|BYTE|LONGLONG|UNSIGNED BIG INT' => self::Integer, + '(NEW)?DEC(IMAL)?(\(.*)?|NUMERIC|(SMALL)?MONEY|CURRENCY|NUMBER' => self::Decimal, + 'REAL|DOUBLE( PRECISION)?|FLOAT\d*' => self::Float, + 'BOOL(EAN)?' => self::Boolean, + 'TIME' => self::Time, + 'DATE' => self::Date, + '(SMALL)?DATETIME(OFFSET)?\d*|TIME(STAMP.*)?' => self::DateTime, + 'BYTEA|(TINY|MEDIUM|LONG|)BLOB|(LONG )?(VAR)?BINARY|IMAGE' => self::Binary, + ]; + + public bool $convertBoolean = true; + public bool $convertDateTime = true; + public bool $convertDecimal = true; + public bool $newDateTime = true; + + + /** + * Heuristic column type detection. + */ + private function detectType(string $nativeType): int + { + static $cache; + if (!isset($cache[$nativeType])) { + $cache[$nativeType] = self::Text; + foreach (self::Patterns as $s => $val) { + if (preg_match("#^($s)$#i", $nativeType)) { + return $cache[$nativeType] = $val; + } + } + } + + return $cache[$nativeType]; + } + + + public function convertToPhp(mixed $value, array $meta): mixed + { + return match ($this->detectType($meta['nativeType'] ?? '')) { + self::Integer => $this->toInt($value), + self::Float => $this->toFloat($value), + self::Decimal => $this->convertDecimal + ? ($meta['scale'] === 0 ? $this->toInt($value) : $this->toFloat($value)) + : $value, + self::Boolean => $this->convertBoolean ? $this->toBool($value) : $value, + self::DateTime, self::Date => $this->convertDateTime ? $this->toDateTime($value) : $value, + self::Time => $this->convertDateTime ? $this->toTime($value) : $value, + self::Interval => $this->convertDateTime ? self::toInterval($value) : $value, + default => $value, + }; + } + + + public function toInt(int|float|string $value): int|float|string + { + return is_float($tmp = $value * 1) ? $value : $tmp; + } + + + public function toFloat(float|string $value): float + { + return (float) $value; + } + + + public function toBool(bool|int|string $value): bool + { + return (bool) $value; + } + + + public function toDateTime(string $value): \DateTimeInterface + { + return $this->newDateTime ? new DateTime($value) : new \Nette\Utils\DateTime($value); + } + + + public function toTime(string $value): \DateTimeInterface + { + return $this->toDateTime($value)->setDate(1, 1, 1); + } + + + public function toInterval(string $value): \DateInterval + { + preg_match('#^(-?)(\d+)\D(\d+)\D(\d+)(\.\d+)?$#D', $value, $m); + $interval = new \DateInterval("PT$m[2]H$m[3]M$m[4]S"); + $interval->f = isset($m[5]) ? (float) $m[5] : 0.0; + $interval->invert = (int) (bool) $m[1]; + return $interval; + } +} diff --git a/src/compatibility-intf.php b/src/compatibility-intf.php deleted file mode 100644 index 6c28b0dea..000000000 --- a/src/compatibility-intf.php +++ /dev/null @@ -1,26 +0,0 @@ -initialize(); - $connection = $container->getService('database.default'); - Assert::type(Nette\Database\Connection::class, $connection); - Assert::same('sqlite::memory:', $connection->getDsn()); - - $explorer = $container->getService('database.default.explorer'); + $explorer = $container->getService('database.default'); Assert::type(Nette\Database\Explorer::class, $explorer); - Assert::same($connection, $explorer->getConnection()); - Assert::same($container->getService('database.default.context'), $explorer); - - Assert::type(Nette\Database\Structure::class, $explorer->getStructure()); - Assert::type(Nette\Database\Conventions\DiscoveredConventions::class, $explorer->getConventions()); + Assert::type(Nette\Caching\Cache::class, $explorer->getCache()); // aliases - Assert::same($connection, $container->getService('nette.database.default')); + Assert::same($explorer, $container->getService('database.default.explorer')); + Assert::same($explorer, $container->getService('nette.database.default')); Assert::same($explorer, $container->getService('nette.database.default.context')); }); diff --git a/tests/Database.DI/DatabaseExtension.multiple.phpt b/tests/Database.DI/DatabaseExtension.multiple.phpt index 1ff250c7a..6b93637d3 100644 --- a/tests/Database.DI/DatabaseExtension.multiple.phpt +++ b/tests/Database.DI/DatabaseExtension.multiple.phpt @@ -21,16 +21,12 @@ test('', function () { user: name password: secret debugger: no - options: - lazy: yes second: dsn: "sqlite::memory:" user: name password: secret debugger: no - options: - lazy: yes services: cache: Nette\Caching\Storages\DevNullStorage @@ -43,23 +39,13 @@ test('', function () { $container = new Container1; $container->initialize(); - $connection = $container->getService('database.first'); - Assert::type(Nette\Database\Connection::class, $connection); - Assert::same($connection, $container->getByType(Nette\Database\Connection::class)); - Assert::same('sqlite::memory:', $connection->getDsn()); - - $explorer = $container->getService('database.first.explorer'); + $explorer = $container->getService('database.first'); Assert::type(Nette\Database\Explorer::class, $explorer); Assert::same($explorer, $container->getByType(Nette\Database\Explorer::class)); - Assert::same($connection, $explorer->getConnection()); - Assert::same($container->getService('database.first.context'), $explorer); - - Assert::type(Nette\Database\Structure::class, $explorer->getStructure()); - Assert::same($explorer->getStructure(), $container->getByType(Nette\Database\IStructure::class)); - Assert::type(Nette\Database\Conventions\DiscoveredConventions::class, $explorer->getConventions()); - Assert::same($explorer->getConventions(), $container->getByType(Nette\Database\Conventions::class)); + Assert::type(Nette\Caching\Cache::class, $explorer->getCache()); // aliases - Assert::same($connection, $container->getService('nette.database.first')); + Assert::same($explorer, $container->getService('database.first.explorer')); + Assert::same($explorer, $container->getService('nette.database.first')); Assert::same($explorer, $container->getService('nette.database.first.context')); }); diff --git a/tests/Database.Tracy/ConnectionPanel.phpt b/tests/Database.Tracy/ConnectionPanel.phpt index ef979c55d..2d90090f3 100644 --- a/tests/Database.Tracy/ConnectionPanel.phpt +++ b/tests/Database.Tracy/ConnectionPanel.phpt @@ -13,7 +13,7 @@ require __DIR__ . '/../bootstrap.php'; test('Tracy Bar', function () { $connection = new Connection('sqlite::memory:'); - $panel = ConnectionPanel::initialize($connection, addBarPanel: true, name: 'foo'); + $panel = ConnectionPanel::initialize($connection, name: 'foo'); $connection->beginTransaction(); $connection->query('SELECT 1'); @@ -45,7 +45,7 @@ test('Bluescreen Panel', function () { test('deprecated initialization', function () { $connection = new Connection('sqlite::memory:'); - $panel = Nette\Database\Helpers::initializeTracy($connection, addBarPanel: true, name: 'foo'); + $panel = @Nette\Database\Helpers::initializeTracy($connection, addBarPanel: true, name: 'foo'); // deprecated $connection->beginTransaction(); $connection->query('SELECT 1'); diff --git a/tests/Database.Tracy/panel.html b/tests/Database.Tracy/panel.html index 81cac1c74..d64fad933 100644 --- a/tests/Database.Tracy/panel.html +++ b/tests/Database.Tracy/panel.html @@ -1,5 +1,5 @@ %A% -

Queries: 4, time: %a% ms, foo

+

Queries: 4, time: %a% ms, foo

@@ -7,14 +7,13 @@

Queries: 4, time: %a% ms, foo

@@ -42,11 +41,10 @@

Queries: 4, time: %a% ms, foo

@@ -65,7 +63,7 @@

Queries: 4, time: %a% ms, foo

- %a%SqliteDriver.php:%d% + %a%SQLiteEngine.php:%d%
%A% -
::beginTransaction
+
BEGIN TRANSACTION
- - %a%Connection.php:%d% + %a%Database.php:%d% - + %A% @@ -32,7 +31,7 @@

Queries: 4, time: %a% ms, foo

%a%Connection.php:%d%%a%Database.php:%d% Nette\Utils\Arrays::invoke()
%A%
- %a%Connection.php:%d% + %a%Database.php:%d% %A%
%A% -
::commit
- +
COMMIT
- %a%Connection.php:%d% + %a%Database.php:%d% %A%
%A%