diff --git a/spec/Builder/Binding.spec.php b/spec/Builder/Binding.spec.php new file mode 100644 index 0000000..4ccc2c7 --- /dev/null +++ b/spec/Builder/Binding.spec.php @@ -0,0 +1,162 @@ +builder = new BaseBuilder(new MockConnection([])); + }); + + describe("BindingCollection simple", function() { + it(": BindingCollection ajout simple", function() { + $collection = new BindingCollection(); + $collection->add('value1'); + $collection->add(123); + $collection->add(true); + $collection->add(null); + + expect($collection->count())->toBe(4); + expect($collection->getOrdered())->toBe(['value1', 123, true, null]); + }); + + it(": BindingCollection ajout nommé", function() { + $collection = new BindingCollection(); + $collection->addNamed(':name', 'John'); + $collection->addNamed(':age', 30); + + expect($collection->get('where', ':name'))->toBe('John'); + expect($collection->get('where', ':age'))->toBe(30); + }); + + it(": BindingCollection types", function() { + $collection = new BindingCollection(); + $collection->add('string'); + $collection->add(123); + $collection->add(true); + $collection->add(null); + + $types = $collection->getTypesOrdered(); + expect($types[0])->toBe(PDO::PARAM_STR); + expect($types[1])->toBe(PDO::PARAM_INT); + expect($types[2])->toBe(PDO::PARAM_BOOL); + expect($types[3])->toBe(PDO::PARAM_NULL); + }); + + it(": BindingCollection merge", function() { + $col1 = new BindingCollection(); + $col1->add('a')->add('b'); + + $col2 = new BindingCollection(); + $col2->add('c')->add('d'); + + $col1->merge($col2); + + expect($col1->count())->toBe(4); + expect($col1->getOrdered())->toBe(['a', 'b', 'c', 'd']); + }); + + it(": BindingCollection clear", function() { + $collection = new BindingCollection(); + $collection->add('test'); + expect($collection->isEmpty())->toBe(false); + + $collection->clear(); + expect($collection->isEmpty())->toBe(true); + }); + + it(": Les bindings sont correctement transmis dans la requête", function() { + $builder = $this->builder->testMode() + ->from('users') + ->where('id', 5) + ->where('name', 'John') + ->whereIn('status', [1, 2, 3]); + + expect($builder->bindings->count())->toBe(5); + expect($builder->bindings->getOrdered())->toBe([5, 'John', 1, 2, 3]); + }); + + it(": Les bindings sont réinitialisés après exécution", function() { + $builder = $this->builder->from('users')->where('id', 5); + + expect($builder->bindings->isEmpty())->toBe(false); + + try { + $sql = $builder->get(); + } catch(Exception) { + // l'execution ne passera pas car on a pas de bd. + // on veut juste se rassuer que les bindings sont reset + expect($builder->bindings->isEmpty())->toBe(true); + } + }); + + it(": Les bindings sont préservés dans les sous-requêtes", function() { + $builder = $this->builder->testMode() + ->from('users') + ->whereIn('id', function($q) { + $q->from('profiles') + ->select('user_id') + ->where('active', 1) + ->where('points >', 100); + }); + + expect($builder->bindings->count())->toBe(2); + expect($builder->bindings->getOrdered())->toBe([1, 100]); + }); + }); + + describe("BindingCollection avec types", function() { + it(": getOrdered avec types spécifiques", function() { + $bindings = new BindingCollection(); + $bindings->add('value1', 'values'); + $bindings->add(5, 'where'); + $bindings->add('join_cond', 'join'); + + expect($bindings->getOrdered(['values', 'where']))->toBe(['value1', 5]); + expect($bindings->getOrdered(['where', 'values']))->toBe([5, 'value1']); + expect($bindings->getOrdered())->toHaveLength(3); + }); + + it(": getOrdered ignore les types vides", function() { + $bindings = new BindingCollection(); + $bindings->add('value1', 'values'); + + expect($bindings->getOrdered(['values', 'where', 'having']))->toBe(['value1']); + }); + }); + + describe("BaseBuilder::getBindings", function() { + it(": UPDATE - valeurs avant where", function() { + $builder = $this->builder->table('users') + ->where('id', 5) + ->where('active', 1) + ->set(['name' => 'John']) + ->pending() // pour eviter l'execution + ->update(); + + expect($builder->getBindings())->toBe(['John', 5, 1]); + }); + + it(": INSERT - seulement valeurs", function() { + $builder = $this->builder->table('users') + ->set(['name' => 'John', 'age' => 30]) + ->pending() // pour eviter l'execution + ->insert(); + + expect($builder->getBindings())->toBe(['John', 30]); + }); + + it(": SELECT - where dans l'ordre", function() { + $builder = $this->builder->table('users') + ->where('id', 5) + ->where('name', 'John') + ->orderBy('created_at'); + + expect($builder->getBindings())->toBe([5, 'John']); + }); + }); +}); diff --git a/spec/Builder/Count.spec.php b/spec/Builder/Count.spec.php index ad5d42c..0db0d78 100644 --- a/spec/Builder/Count.spec.php +++ b/spec/Builder/Count.spec.php @@ -18,13 +18,14 @@ it(": Nombre de ligne avec condition", function() { $builder = $this->builder->testMode()->from('jobs j')->where('id >', 3); - expect($builder->count())->toBe('SELECT COUNT(*) AS count_value FROM jobs AS j WHERE id > 3'); + expect($builder->getBindings())->toBe([3]); + expect($builder->count())->toBe('SELECT COUNT(*) AS count_value FROM jobs AS j WHERE id > ?'); }); it(": Nombre de ligne avec regroupement", function() { $builder = $this->builder->testMode()->from('jobs j')->where('id >', 3)->groupBy('id'); - expect($builder->bindings->getValues())->toBe([3]); + expect($builder->getBindings())->toBe([3]); expect($builder->count())->toBe('SELECT COUNT(*) AS count_value FROM (SELECT * FROM jobs AS j WHERE id > ? GROUP BY id) AS count_table'); }); @@ -32,20 +33,21 @@ $this->builder->db()->setPrefix('db_'); $builder = $this->builder->testMode()->select('j.*')->from('jobs j')->where('id >', 3)->groupBy('id'); - expect($builder->bindings->getValues())->toBe([3]); + expect($builder->getBindings())->toBe([3]); expect($builder->count())->toBe('SELECT COUNT(*) AS count_value FROM (SELECT j.* FROM db_jobs AS j WHERE id > ? GROUP BY id) AS count_table'); }); it(": Compter tous les résultats avec GroupBy et Having", function() { $builder = $this->builder->testMode()->from('jobs j')->where('id >', 3)->groupBy('id')->having('1=1'); - expect($builder->bindings->getValues())->toBe([3, 1]); + expect($builder->getBindings())->toBe([3, 1]); expect($builder->count())->toBe('SELECT COUNT(*) AS count_value FROM (SELECT * FROM jobs AS j WHERE id > ? GROUP BY id HAVING 1 = ?) AS count_table'); }); it(": Compter tous les résultats avec Having uniquement", function() { $builder = $this->builder->testMode()->from('jobs j')->where('id >', 3)->having('1=1'); - expect($builder->count())->toBe('SELECT COUNT(*) AS count_value FROM jobs AS j WHERE id > 3 HAVING 1 = 1'); + expect($builder->getBindings())->toBe([3, 1]); + expect($builder->count())->toBe('SELECT COUNT(*) AS count_value FROM jobs AS j WHERE id > ? HAVING 1 = ?'); }); }); diff --git a/spec/Builder/Delete.spec.php b/spec/Builder/Delete.spec.php index 0a9e64c..75f0ea0 100644 --- a/spec/Builder/Delete.spec.php +++ b/spec/Builder/Delete.spec.php @@ -45,7 +45,7 @@ it(": Suppression avec limite", function() { $builder = $this->builder->testMode()->from('jobs')->where('id', 1)->limit(10); - expect($builder->bindings->getValues())->toBe([1]); + expect($builder->getBindings())->toBe([1]); expect($builder->delete())->toBe('DELETE FROM jobs WHERE id = 1 LIMIT 10'); }); }); diff --git a/spec/Builder/Json.spec.php b/spec/Builder/Json.spec.php new file mode 100644 index 0000000..04686ce --- /dev/null +++ b/spec/Builder/Json.spec.php @@ -0,0 +1,66 @@ +builder = new BaseBuilder(new MockConnection([])); + }); + + it(": whereJsonContains", function() { + $builder = $this->builder->testMode() + ->from('users') + ->whereJsonContains('preferences->languages', 'fr'); + + expect($builder->toSql())->toBe( + "SELECT * FROM users WHERE JSON_CONTAINS(preferences->languages, ?)" + ); + expect($builder->getBindings())->toBe(['fr']); + }); + + it(": whereJsonDoesntContain", function() { + $builder = $this->builder->testMode() + ->from('users') + ->whereJsonDoesntContain('preferences->tags', 'premium'); + + expect($builder->toSql())->toBe( + "SELECT * FROM users WHERE NOT JSON_CONTAINS(preferences->tags, ?)" + ); + expect($builder->getBindings())->toBe(['premium']); + }); + + it(": whereJsonContainsKey", function() { + $builder = $this->builder->testMode() + ->from('users') + ->whereJsonContainsKey('settings->notifications'); + + expect($builder->sql())->toBe( + "SELECT * FROM users WHERE JSON_CONTAINS_PATH(settings->notifications, 'one', ?) = 1" + ); + }); + + it(": whereJsonLength", function() { + $builder = $this->builder->testMode() + ->from('users') + ->whereJsonLength('preferences->items', '>', 5); + + expect($builder->toSql())->toBe( + "SELECT * FROM users WHERE JSON_LENGTH(preferences->items) > ?" + ); + expect($builder->getBindings())->toBe([5]); + }); + + it(": orWhereJsonContains", function() { + $builder = $this->builder->testMode() + ->from('users') + ->where('active', 1) + ->orWhereJsonContains('preferences->languages', 'en'); + + expect($builder->toSql())->toBe( + "SELECT * FROM users WHERE active = ? OR JSON_CONTAINS(preferences->languages, ?)" + ); + expect($builder->getBindings())->toBe([1, 'en']); + }); +}); diff --git a/spec/Builder/Union.spec.php b/spec/Builder/Union.spec.php new file mode 100644 index 0000000..48fb17a --- /dev/null +++ b/spec/Builder/Union.spec.php @@ -0,0 +1,99 @@ +builder = new BaseBuilder(new MockConnection([])); + }); + + it(": UNION simple", function() { + $builder = $this->builder->testMode() + ->from('users') + ->select('name, email') + ->where('active', 1) + ->union(function($q) { + $q->from('deleted_users') + ->select('name, email') + ->where('restored', 0); + }); + + expect($builder->toSql())->toBe( + "SELECT name, email FROM users WHERE active = ? " . + "UNION SELECT name, email FROM deleted_users WHERE restored = ?" + ); + expect($builder->getBindings())->toBe([1, 0]); + }); + + it(": UNION ALL", function() { + $builder = $this->builder->testMode() + ->from('orders_2023') + ->select('id, total') + ->unionAll(function($q) { + $q->from('orders_2024') + ->select('id, total'); + }); + + expect($builder->sql())->toBe( + "SELECT id, total FROM orders_2023 " . + "UNION ALL SELECT id, total FROM orders_2024" + ); + }); + + it(": UNION multiples", function() { + $builder = $this->builder->testMode() + ->from('q1') + ->select('data') + ->union(function($q) { $q->from('q2')->select('data'); }) + ->union(function($q) { $q->from('q3')->select('data'); }); + + expect($builder->sql())->toBe( + "SELECT data FROM q1 " . + "UNION SELECT data FROM q2 " . + "UNION SELECT data FROM q3" + ); + }); + + it(": UNION avec ORDER BY et LIMIT", function() { + $builder = $this->builder->testMode() + ->from('products') + ->select('name, price') + ->union(function($q) { + $q->from('archived_products') + ->select('name, price'); + }) + ->orderBy('price', 'DESC') + ->limit(10); + + expect($builder->sql())->toBe( + "SELECT name, price FROM products " . + "UNION SELECT name, price FROM archived_products " . + "ORDER BY price DESC LIMIT 10" + ); + }); + + it(": UNION avec sous-requête complexe", function() { + $subquery = (new BaseBuilder(new MockConnection([]))) + ->from('logs') + ->select('user_id, COUNT(*) as count') + ->groupBy('user_id') + ->having('count >', 5); + + $builder = $this->builder->testMode() + ->from('users') + ->select('id, name') + ->union(function($q) use ($subquery) { + $q->fromSubquery($subquery, 'active_logs') + ->select('user_id as id, count as name'); + }); + + expect($builder->toSql())->toBe( + "SELECT id, name FROM users " . + "UNION SELECT user_id AS id, count AS name FROM " . + "(SELECT user_id, COUNT(*) as count FROM logs GROUP BY user_id HAVING count > ?) AS active_logs" + ); + expect($builder->getBindings())->toBe([5]); + }); +}); diff --git a/spec/Builder/Update.spec.php b/spec/Builder/Update.spec.php new file mode 100644 index 0000000..ad0ed73 --- /dev/null +++ b/spec/Builder/Update.spec.php @@ -0,0 +1,123 @@ +builder = new BaseBuilder(new MockConnection([])); + }); + + it(": Mise à jour simple", function() { + $builder = $this->builder->testMode()->table('users'); + expect($builder->update([ + 'name' => 'John Doe', + 'email' => 'john@example.com' + ]))->toBe("UPDATE users SET name = 'John Doe', email = 'john@example.com'"); + }); + + it(": Mise à jour avec condition", function() { + $builder = $this->builder->testMode()->table('users')->where('id', 5); + expect($builder->update(['name' => 'John Doe'])) + ->toBe("UPDATE users SET name = 'John Doe' WHERE id = 5"); + }); + + it(": Mise à jour avec jointure", function() { + $builder = $this->builder->testMode() + ->table('users u') + ->join('profiles p', 'u.id', '=', 'p.user_id') + ->where('u.id', 5); + + expect($builder->update(['u.name' => 'John Doe', 'p.bio' => 'New bio'])) + ->toBe("UPDATE users AS u INNER JOIN profiles AS p ON u.id = p.user_id SET u.name = 'John Doe', p.bio = 'New bio' WHERE u.id = 5"); + }); + + it(": Mise à jour avec jointure et sans condition", function() { + $builder = $this->builder->testMode() + ->table('users u') + ->join('profiles p', 'u.id', '=', 'p.user_id'); + + expect($builder->update(['u.name' => 'John Doe'])) + ->toBe("UPDATE users AS u INNER JOIN profiles AS p ON u.id = p.user_id SET u.name = 'John Doe'"); + }); + + it(": Mise à jour avec jointures multiples", function() { + $builder = $this->builder->testMode() + ->table('users u') + ->join('profiles p', 'u.id', '=', 'p.user_id') + ->join('roles r', 'u.role_id', '=', 'r.id', 'LEFT') + ->where('u.id', 5); + + expect($builder->update(['u.name' => 'John Doe', 'r.name' => 'Admin'])) + ->toBe("UPDATE users AS u INNER JOIN profiles AS p ON u.id = p.user_id LEFT JOIN roles AS r ON u.role_id = r.id SET u.name = 'John Doe', r.name = 'Admin' WHERE u.id = 5"); + }); + + it(": Mise à jour avec limite", function() { + $builder = $this->builder->testMode()->table('users')->where('active', 1)->limit(10); + expect($builder->update(['status' => 'inactive'])) + ->toBe("UPDATE users SET status = 'inactive' WHERE active = 1 LIMIT 10"); + }); + + it(": Mise à jour avec expression", function() { + $builder = $this->builder->testMode()->table('users'); + expect($builder->update([ + 'views' => $builder::raw('views + 1'), + 'updated_at' => $builder::raw('NOW()') + ]))->toBe("UPDATE users SET views = views + 1, updated_at = NOW()"); + }); + + it(": Increment", function() { + $builder = $this->builder->testMode()->table('users')->where('id', 1); + expect($builder->increment('views', 2)) + ->toBe("UPDATE users SET views = views + 2 WHERE id = 1"); + }); + + it(": Increment avec données supplémentaires", function() { + $builder = $this->builder->testMode()->table('users')->where('id', 1); + expect($builder->increment('views', 1, ['updated_at' => '2024-01-01'])) + ->toBe("UPDATE users SET views = views + 1, updated_at = '2024-01-01' WHERE id = 1"); + }); + + it(": Decrement", function() { + $builder = $this->builder->testMode()->table('users')->where('id', 1); + expect($builder->decrement('score', 5)) + ->toBe("UPDATE users SET score = score - 5 WHERE id = 1"); + }); + + xdescribe('Difficilement testable à ce niveau', function() { + it(": UpdateOrInsert - existant", function() { + $builder = $this->builder->table('users'); + + // Simuler l'existence + allow($builder->clone()->where(['email' => 'john@example.com'])) + ->toReceive('exists') + ->andReturn(true); + + $result = $builder->updateOrInsert( + ['email' => 'john@example.com'], + ['name' => 'John Updated'] + ); + + expect($result)->toBe(true); + }); + + it(": UpdateOrInsert - nouveau", function() { + $builder = $this->builder->table('users'); + + // Simuler la non-existence + allow($builder->clone()->where(['email' => 'new@example.com'])) + ->toReceive('exists') + ->andReturn(false); + + allow($builder)->toReceive('insert')->andReturn(true); + + $result = $builder->updateOrInsert( + ['email' => 'new@example.com'], + ['name' => 'New User'] + ); + + expect($result)->toBe(true); + }); + }); +}); diff --git a/spec/Connection/BaseConnection.spec.php b/spec/Connection/BaseConnection.spec.php new file mode 100644 index 0000000..c1ae00c --- /dev/null +++ b/spec/Connection/BaseConnection.spec.php @@ -0,0 +1,174 @@ +connection = new MockConnection([ + 'driver' => 'mysql', + 'hostname' => 'localhost', + 'database' => 'test', + 'username' => 'root', + 'password' => '', + 'prefix' => 'db_', + ]); + $this->connection->escapeChar = '`'; + $this->connection->initialize(); + }); + + it(": Initialisation", function() { + expect($this->connection->initialize())->toBeNull(); + expect($this->connection->getConnection())->toBeAnInstanceOf(PDO::class); + }); + + it(": Récupération du driver", function() { + expect($this->connection->getDriver())->toBe('mysql'); + }); + + it(": Récupération du nom de la base", function() { + expect($this->connection->getDatabase())->toBe('test'); + }); + + it(": Récupération du préfixe", function() { + expect($this->connection->getPrefix())->toBe('db_'); + }); + + it(": Modification du préfixe", function() { + $this->connection->setPrefix('new_'); + expect($this->connection->getPrefix())->toBe('new_'); + }); + + it(": Création de table avec préfixe", function() { + expect($this->connection->prefixTable('users'))->toBe('`db_users`'); + }); + + it(": Création de table avec alias", function() { + expect($this->connection->makeTableName('users u'))->toBe('`db_users` AS `u`'); + }); + + it(": Échappement des identifiants", function() { + expect($this->connection->escapeIdentifiers('users.id'))->toBe('`users`.`id`'); + // expect($this->connection->escapeIdentifiers('count(*)'))->toBe('count(*)'); + expect($this->connection->escapeIdentifiers(['users.id', 'name']))->toBe(['`users`.`id`', '`name`']); + }); + + it(": Échappement des chaînes", function() { + $escaped = $this->connection->escapeString("O'Reilly"); + expect($escaped)->toBe("'O''Reilly'"); + }); + + it(": Échappement avec LIKE", function() { + $escaped = $this->connection->escapeString("100%", true); + expect($escaped)->toMatch("/'100\\\\%'/"); + }); + + it(": Échappement des valeurs", function() { + expect($this->connection->escape("test"))->toBe("'test'"); + expect($this->connection->escape(123))->toBe(123); + expect($this->connection->escape(true))->toBe('1'); + expect($this->connection->escape(false))->toBe('0'); + expect($this->connection->escape(null))->toBe('NULL'); + }); + + it(": Transaction simple", function() { + $this->connection->query('CREATE TABLE IF NOT EXISTS users(id int auto_increment primary key, name varchar(50));'); + $this->connection->query('CREATE TABLE IF NOT EXISTS profiles(id int auto_increment primary key, user_id int);'); + + $result = $this->connection->transaction(function($db) { + $db->query("INSERT INTO users (name) VALUES ('John')"); + $db->query("INSERT INTO profiles (user_id) VALUES (". $this->connection->insertID() .")"); + return true; + }); + + expect($result)->toBe(true); + + $this->connection->query('DROP TABLE IF EXISTS profiles;'); + $this->connection->query('DROP TABLE IF EXISTS users;'); + }); + + it(": Transaction avec rollback sur exception", function() { + $this->connection->query('CREATE TABLE IF NOT EXISTS users(id int auto_increment primary key, name varchar(50));'); + + $exception = null; + + try { + $this->connection->transaction(function($db) { + $db->query("INSERT INTO users (name) VALUES ('John')"); + throw new Exception("Erreur volontaire"); + }); + } catch (Exception $e) { + $exception = $e; + } + + expect($exception)->toBeAnInstanceOf(Exception::class); + expect($exception->getMessage())->toBe("Erreur volontaire"); + + $this->connection->query('DROP TABLE IF EXISTS users;'); + }); + + it(": Transactions imbriquées", function() { + $this->connection->beginTransaction(); + expect($this->connection->transactionLevel())->toBe(1); + + $this->connection->beginTransaction(); + expect($this->connection->transactionLevel())->toBe(2); + + $this->connection->commit(); + expect($this->connection->transactionLevel())->toBe(1); + + $this->connection->commit(); + expect($this->connection->transactionLevel())->toBe(0); + }); + + it(": beforeExecuting callback", function() { + $this->connection->query('CREATE TABLE IF NOT EXISTS users(id int auto_increment primary key, name varchar(50));'); + + $called = false; + + $this->connection->beforeExecuting(function($query, $bindings, $connection) use (&$called) { + $called = true; + expect($query)->toBe("SELECT * FROM users WHERE id = ?"); + expect($bindings)->toBe([5]); + expect($connection)->toBe($this->connection); + }); + + $this->connection->query("SELECT * FROM users WHERE id = ?", [5]); + + expect($called)->toBe(true); + }); + + it(": prepareBindings avec DateTime", function() { + $date = Date::now(); + $bindings = ['name' => 'John', 'created_at' => $date]; + + $prepared = $this->connection->prepareBindings($bindings); + + expect($prepared['created_at'])->toBe($date->format('Y-m-d H:i:s')); + }); + + it(": prepareBindings avec bool", function() { + $bindings = ['active' => true, 'deleted' => false]; + + $prepared = $this->connection->prepareBindings($bindings); + + expect($prepared['active'])->toBe(1); + expect($prepared['deleted'])->toBe(0); + }); + + it(": QueryException formatage", function() { + try { + $this->connection->query("SELECT * FROM non_existent_table"); + } catch (QueryException $e) { + expect($e->getConnectionName())->toBe($this->connection->getName()); + expect($e->getSql())->toBe("SELECT * FROM non_existent_table"); + expect($e->getBindings())->toBe([]); + expect($e->getConnectionDetails())->toBeAn('array'); + } + }); +}); diff --git a/spec/DatabaseManager.spec.php b/spec/DatabaseManager.spec.php new file mode 100644 index 0000000..495bbcc --- /dev/null +++ b/spec/DatabaseManager.spec.php @@ -0,0 +1,96 @@ +manager = new DatabaseManager(); + + // Mock de la config + allow('config')->toBeCalled()->with('database')->andReturn([ + 'default' => [ + 'driver' => 'mysql', + 'hostname' => 'localhost', + 'database' => 'test', + 'username' => 'root', + 'password' => '', + 'prefix' => '', + ], + 'test' => [ + 'driver' => 'sqlite', + 'database' => ':memory:', + 'prefix' => '', + ], + ]); + }); + + it(": Connexion par défaut", function() { + $connection = $this->manager->connection(); + expect($connection)->toBeAnInstanceOf(MySQL::class); + }); + + it(": Connexion nommée", function() { + $connection = $this->manager->connection('test'); + expect($connection)->toBeAnInstanceOf(SQLite::class); // SQLite utilise Mock + }); + + it(": Connexion partagée", function() { + $conn1 = $this->manager->connection(); + $conn2 = $this->manager->connection(); + + expect($conn1)->toBe($conn2); + }); + + it(": Connexion non partagée", function() { + $conn1 = $this->manager->connect('default', false); + $conn2 = $this->manager->connect('default', false); + + expect($conn1)->not->toBe($conn2); + }); + + it(": ConnectionInfo avec tableau de config", function() { + [$name, $config] = $this->manager->connectionInfo([ + 'driver' => 'mysql', + 'database' => 'custom', + ]); + + expect($name)->toMatch('/^custom-/'); + expect($config['database'])->toBe('custom'); + }); + + it(": Création de builder", function() { + $connection = $this->manager->connection('test'); + $builder = $this->manager->builder($connection); + + expect($builder)->toBeAnInstanceOf(\BlitzPHP\Database\Builder\BaseBuilder::class); + }); + + it(": Changement de connexion par défaut", function() { + $this->manager->setDefaultConnection('test'); + expect($this->manager->getDefaultConnection())->toBe('test'); + + $connection = $this->manager->connection(); + expect($connection)->toBeAnInstanceOf(SQLite::class); + }); + + it(": Fermeture de toutes les connexions", function() { + $this->manager->connection('default'); + $this->manager->connection('test'); + + expect($this->manager->getConnections())->toHaveLength(2); + + $this->manager->closeAll(); + + expect($this->manager->getConnections())->toBe([]); + }); + + it(": Connexion active", function() { + $this->manager->connection('test'); + $active = $this->manager->activeConnection(); + + expect($active)->toBeAnInstanceOf(SQLite::class); + }); +}); diff --git a/spec/Migration/Runner.spec.php b/spec/Migration/Runner.spec.php new file mode 100644 index 0000000..b0c99dd --- /dev/null +++ b/spec/Migration/Runner.spec.php @@ -0,0 +1,67 @@ +dbManager = new DatabaseManager(); + $this->connection = new MockConnection(['prefix' => '', 'driver' => 'sqlite']); + $this->connection->escapeChar = '"'; + + allow($this->dbManager)->toReceive('connect')->andReturn($this->connection); + allow($this->dbManager)->toReceive('activeConnection')->andReturn($this->connection); + + $this->paths = [ + 'App' => [__DIR__ . '/../fixtures/migrations/20240101000000_test_migration.php'] + ]; + + $this->runner = new Runner($this->dbManager, 'default', $this->paths); + }); + + it(": Désactivé si config disabled", function() { + $runner = new Runner($this->dbManager, 'default', $this->paths, ['enabled' => false]); + expect($runner->latest())->toBe(0); + }); + + it(": Récupération des fichiers de migration", function() { + $files = $this->runner->findMigrationFiles(); + + expect($files)->toBeAn('array'); + expect($files)->not->toBeEmpty(); + expect($files[0]->version)->toBe('20240101000000'); + expect($files[0]->migration)->toBe('test_migration'); + expect($files[0]->namespace)->toBe('App'); + }); + + it(": Événements", function() { + $events = [ + 'process.start' => false, + 'process.completed' => false, + 'migration.before' => false, + 'migration.done' => false, + ]; + + $this->runner->on('process.start', function() use (&$events) { + $events['process.start'] = true; + })->on('process.completed', function() use (&$events) { + $events['process.completed'] = true; + })->on('migration.before', function() use (&$events) { + $events['migration.before'] = true; + })->on('migration.done', function() use (&$events) { + $events['migration.done'] = true; + }); + + // Simuler l'absence de migrations en attente + allow($this->runner)->toReceive('getPendingMigrations')->andReturn([]); + + $this->runner->latest(); + + expect($events['process.start'])->toBe(false); + }); +}); diff --git a/spec/Utils.spec.php b/spec/Utils.spec.php new file mode 100644 index 0000000..eddaece --- /dev/null +++ b/spec/Utils.spec.php @@ -0,0 +1,106 @@ +toBe(true); + expect(Utils::isSqlFunction('NOW'))->toBe(true); + expect(Utils::isSqlFunction('RANDOM'))->toBe(false); + expect(Utils::isSqlFunction('NOT EXISTS'))->toBe(true); + }); + + it(": isWritableSql", function() { + expect(Utils::isWritableSql('SELECT * FROM users'))->toBe(false); + expect(Utils::isWritableSql('INSERT INTO users'))->toBe(true); + expect(Utils::isWritableSql('UPDATE users SET'))->toBe(true); + expect(Utils::isWritableSql('DELETE FROM users'))->toBe(true); + expect(Utils::isWritableSql('CREATE TABLE'))->toBe(true); + expect(Utils::isWritableSql('DROP TABLE'))->toBe(true); + }); + + it(": hasOperator", function() { + expect(Utils::hasOperator('id >'))->toBe(true); + expect(Utils::hasOperator('name LIKE'))->toBe(true); + expect(Utils::hasOperator('created_at BETWEEN'))->toBe(true); + expect(Utils::hasOperator('simple_column'))->toBe(false); + }); + + it(": translateOperator", function() { + expect(Utils::translateOperator('%'))->toBe('LIKE'); + expect(Utils::translateOperator('!%'))->toBe('NOT LIKE'); + expect(Utils::translateOperator('@'))->toBe('IN'); + expect(Utils::translateOperator('!@'))->toBe('NOT IN'); + expect(Utils::translateOperator('='))->toBe('='); + }); + + it(": invertOperator", function() { + expect(Utils::invertOperator('='))->toBe('!='); + expect(Utils::invertOperator('!='))->toBe('='); + expect(Utils::invertOperator('<'))->toBe('>='); + expect(Utils::invertOperator('>'))->toBe('<='); + expect(Utils::invertOperator('LIKE'))->toBe('NOT LIKE'); + expect(Utils::invertOperator('NOT LIKE'))->toBe('LIKE'); + expect(Utils::invertOperator('IN'))->toBe('NOT IN'); + }); + + it(": isAlias", function() { + expect(Utils::isAlias('user_alias'))->toBe(true); + expect(Utils::isAlias('AS user_alias'))->toBe(true); + expect(Utils::isAlias('user-alias'))->toBe(false); // tiret pas autorisé + expect(Utils::isAlias('user.alias'))->toBe(false); // point pas autorisé + }); + + it(": extractAlias", function() { + expect(Utils::extractAlias('AS alias'))->toBe('alias'); + expect(Utils::extractAlias('alias'))->toBe('alias'); + expect(Utils::extractAlias(' AS alias '))->toBe('alias'); + }); + + it(": extractOperatorFromColumn", function() { + $result = Utils::extractOperatorFromColumn('id >'); + expect($result)->toBe(['id', '>']); + + $result = Utils::extractOperatorFromColumn('name LIKE', '%'); + expect($result)->toBe(['name', 'LIKE']); + + $result = Utils::extractOperatorFromColumn('age'); + expect($result)->toBe(['age', '=']); + }); + + it(": parseExpression", function() { + $result = Utils::parseExpression('id > 5'); + expect($result)->toBe(['id', '>', 5]); + + $result = Utils::parseExpression('name LIKE "%john%"'); + expect($result)->toBe(['name', 'LIKE', '%john%']); + + $result = Utils::parseExpression('status IN (1,2,3)'); + expect($result[0])->toBe('status'); + expect($result[1])->toBe('IN'); + expect($result[2])->toBe([1, 2, 3]); + + $result = Utils::parseExpression('age BETWEEN 18 AND 30'); + expect($result[0])->toBe('age'); + expect($result[1])->toBe('BETWEEN'); + expect($result[2])->toBe([18, 30]); + + $result = Utils::parseExpression('deleted_at IS NULL'); + expect($result[0])->toBe('deleted_at'); + expect($result[1])->toBe('IS NULL'); + expect($result[2])->toBe(null); + }); + + it(": castValue", function() { + expect(Utils::castValue('123'))->toBe(123); + expect(Utils::castValue('123.45'))->toBe(123.45); + expect(Utils::castValue('true'))->toBe(true); + expect(Utils::castValue('false'))->toBe(false); + expect(Utils::castValue('"string"'))->toBe('string'); + expect(Utils::castValue("'string'"))->toBe('string'); + expect(Utils::castValue('not_quoted'))->toBe('not_quoted'); + }); +}); diff --git a/spec/_support/Mock/MockConnection.php b/spec/_support/Mock/MockConnection.php index e55ed4e..679c345 100644 --- a/spec/_support/Mock/MockConnection.php +++ b/spec/_support/Mock/MockConnection.php @@ -2,7 +2,9 @@ namespace BlitzPHP\Database\Spec\Mock; use BlitzPHP\Contracts\Database\ResultInterface; +use BlitzPHP\Contracts\Event\EventManagerInterface; use BlitzPHP\Database\Connection\SQLite; +use Psr\Log\LoggerInterface; class MockConnection extends SQLite { @@ -18,6 +20,11 @@ class MockConnection extends SQLite */ public $lastQuery; + public function __construct(array $config, ?LoggerInterface $logger = null, ?EventManagerInterface $event = null) + { + return parent::__construct($config + ['driver' => 'mysql'], $logger, $event); + } + public function shouldReturn(string $method, $return): self { $this->returnValues[$method] = $return; @@ -55,7 +62,7 @@ public function getDriver(): string { $this->initialize(); - return 'mysql'; + return $this->config['driver'] ?? 'mysql'; } /** @@ -112,12 +119,4 @@ public function insertID(?string $table = null) { return 1; } - - /** - * {@inheritDoc} - */ - protected function _escapeString(string $str): string - { - return "'" . parent::_escapeString($str) . "'"; - } } diff --git a/src/Builder/BaseBuilder.php b/src/Builder/BaseBuilder.php index 2804f98..07989b9 100644 --- a/src/Builder/BaseBuilder.php +++ b/src/Builder/BaseBuilder.php @@ -954,10 +954,12 @@ public function lockNowait(): self * Incremente un champ numerique par la valeur specifiee. * * @param array $extra + * + * @return int<0, max>|static|string * * @throws DatabaseException */ - public function increment(string $column, float|int $value = 1, array $extra = []): int + public function increment(string $column, float|int $value = 1, array $extra = []) { return $this->incrementEach([$column => $value], $extra); } @@ -968,7 +970,7 @@ public function increment(string $column, float|int $value = 1, array $extra = [ * @param array $columns * @param array $extra * - * @return int<0, max> + * @return int<0, max>|static|string * * @throws InvalidArgumentException */ @@ -991,10 +993,12 @@ public function incrementEach(array $columns, array $extra = []) * Decremente un champ numerique par la valeur specifiee. * * @param array $extra + * + * @return int<0, max>|static|string * * @throws DatabaseException */ - public function decrement(string $column, float|int $value = 1, array $extra = []): int + public function decrement(string $column, float|int $value = 1, array $extra = []) { return $this->decrementEach([$column => $value], $extra); } @@ -1005,7 +1009,7 @@ public function decrement(string $column, float|int $value = 1, array $extra = [ * @param array $columns * @param array $extra * - * @return int<0, max> + * @return int<0, max>|static|string * * @throws InvalidArgumentException */ diff --git a/src/Builder/Concerns/AdvancedMethods.php b/src/Builder/Concerns/AdvancedMethods.php index ee8680b..54fc0b5 100644 --- a/src/Builder/Concerns/AdvancedMethods.php +++ b/src/Builder/Concerns/AdvancedMethods.php @@ -12,6 +12,7 @@ namespace BlitzPHP\Database\Builder\Concerns; use BlitzPHP\Contracts\Database\BuilderInterface; +use BlitzPHP\Database\Builder\BaseBuilder; use BlitzPHP\Utilities\DateTime\Date; use Closure; use DateTimeInterface; @@ -407,15 +408,13 @@ public function whereTodayOrAfter(string $column, string $boolean = 'and'): stat */ public function whereJsonContains(string $column, $value, string $boolean = 'and', bool $not = false): static { - $operator = $not ? 'JSON_NOT_CONTAINS' : 'JSON_CONTAINS'; - $this->wheres[] = [ 'type' => 'json', 'column' => $column, 'value' => $value, 'boolean' => $boolean, 'not' => $not, - 'operator' => $operator + 'operator' => 'JSON_CONTAINS', ]; $this->bindings->add($value); diff --git a/src/Builder/Concerns/CoreMethods.php b/src/Builder/Concerns/CoreMethods.php index c1529ef..e96b53a 100644 --- a/src/Builder/Concerns/CoreMethods.php +++ b/src/Builder/Concerns/CoreMethods.php @@ -16,7 +16,7 @@ use BlitzPHP\Contracts\Database\BuilderInterface; use BlitzPHP\Database\Builder\BaseBuilder; use BlitzPHP\Database\Utils; -use BlitzPHP\Wolke\Collection; +use BlitzPHP\Utilities\Iterable\Collection; use Closure; use DateTimeInterface; use InvalidArgumentException; diff --git a/src/Builder/Concerns/DataMethods.php b/src/Builder/Concerns/DataMethods.php index fae514d..80d3c6a 100644 --- a/src/Builder/Concerns/DataMethods.php +++ b/src/Builder/Concerns/DataMethods.php @@ -75,7 +75,7 @@ public function count(string $column = '*') $builder = $builder->selectRaw('COUNT(' . $column . ') AS count_value'); } - return $this->testMode ? $builder->sql() : (int) ($builder->value('count_value') ?? 0); + return $this->testMode ? $builder->toSql() : (int) ($builder->value('count_value') ?? 0); } /** diff --git a/src/Commands/DatabaseCommand.php b/src/Commands/DatabaseCommand.php index 2ec8d44..7eec34d 100644 --- a/src/Commands/DatabaseCommand.php +++ b/src/Commands/DatabaseCommand.php @@ -70,6 +70,7 @@ public function runner(string $namespace, ?string $group = null): Runner $this->container->get(DatabaseManager::class), $group, $files, + config('migrations'), ); } } diff --git a/src/Config/database.php b/src/Config/database.php index 890f541..b4e764b 100644 --- a/src/Config/database.php +++ b/src/Config/database.php @@ -45,7 +45,7 @@ * * Si défini sur 'auto', alors vaudra true en developpement et false en production */ - 'debug' => 'auto', + 'debug' => !on_prod(), /** @var string */ 'charset' => 'utf8mb4', /** @var string */ diff --git a/src/Connection/BaseConnection.php b/src/Connection/BaseConnection.php index a8a3f34..923d932 100644 --- a/src/Connection/BaseConnection.php +++ b/src/Connection/BaseConnection.php @@ -59,7 +59,8 @@ abstract class BaseConnection implements ConnectionInterface * @var array{ * dsn?: string, * hostname: string, port: int, username?: string, password?: string, - * database?: string, charset?: string, collation?: string, strict_on?: boolean + * database?: string, charset?: string, collation?: string, strict_on?: boolean, + * debug?: bool * } */ protected array $config = []; @@ -466,7 +467,8 @@ public function beginTransaction(bool $testMode = false): bool if ($this->transDepth === 0) { $this->transStatus = !$testMode; - + $this->transDepth = 1; + return $this->pdo->beginTransaction(); } @@ -1032,7 +1034,7 @@ public function numRows(): int */ public function disableForeignKeyChecks() { - return $this->query($this->disableForeignKeyChecks); + return $this->statement($this->disableForeignKeyChecks); } /** @@ -1040,6 +1042,6 @@ public function disableForeignKeyChecks() */ public function enableForeignKeyChecks() { - return $this->query($this->enableForeignKeyChecks); + return $this->statement($this->enableForeignKeyChecks); } } diff --git a/src/Migration/Runner.php b/src/Migration/Runner.php index 0d54a36..194fdfc 100644 --- a/src/Migration/Runner.php +++ b/src/Migration/Runner.php @@ -72,10 +72,8 @@ class Runner * * @param array> $paths Chemins de recherche des migrations */ - public function __construct(protected DatabaseManager $dbManager, protected ?string $group, protected array $paths = []) + public function __construct(protected DatabaseManager $dbManager, protected ?string $group, protected array $paths = [], array $config = []) { - $config = config('migrations'); - $this->enabled = $config['enabled'] ?? false; $this->db = $this->dbManager->connect($group); $this->history = new History($dbManager, $config['table'] ?? 'migrations'); diff --git a/src/Query/Result.php b/src/Query/Result.php index 8e33d05..cc515ec 100644 --- a/src/Query/Result.php +++ b/src/Query/Result.php @@ -58,7 +58,7 @@ public function sql(): string } /** - * Renvoie "true" si la requête correspondante s'est bien passée et "false" au cas contraire + * {@inheritDoc} */ public function successful(): bool { diff --git a/src/Utils.php b/src/Utils.php index 78dbaae..f67b93e 100644 --- a/src/Utils.php +++ b/src/Utils.php @@ -216,7 +216,7 @@ public static function castValue(string $value): mixed return (float) $value; } if (preg_match('/^(true|false)$/i', $value)) { - return (bool) $value; + return $value === 'true'; } if (preg_match('/^["\'](.*)["\']$/', $value, $m)) { return $m[1];