Overview
Comment:Ajout documentation dans DB.php et ajout de tests pour la DB
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | dev
Files: files | file ages | folders
SHA1: 8422f038fe1a393140b2c505e10ee3004fbfe93b
User & Date: bohwaz on 2017-03-29 04:48:31
Other Links: branch diff | manifest | tags
Context
2017-03-29
04:49
Mise à jour tests pour nouveau comportement check-in: 3a12d5f2a3 user: bohwaz tags: dev
04:48
Ajout documentation dans DB.php et ajout de tests pour la DB check-in: 8422f038fe user: bohwaz tags: dev
2017-03-17
04:50
Correction de quelques bugs dans la gestion de DB check-in: 171c9591b4 user: bohwaz tags: dev
Changes

Modified src/include/lib/Garradin/DB.php from [7f184d61e2] to [9ea38a4c6c].

162
163
164
165
166
167
168












169
170






171
172
173
174
175
176
177
                    return \SQLITE3_TEXT;
                }
            default:
                throw new \InvalidArgumentException('Argument '.$name.' is of invalid type '.gettype($arg));
        }
    }













    public function query($query, Array $args = [])
    {






        $this->connect();
        $statement = $this->db->prepare($query);
        $nb = $statement->paramCount();

        if (!empty($args))
        {
            if (is_array($args) && count($args) == 1 && is_array(current($args)))







>
>
>
>
>
>
>
>
>
>
>
>


>
>
>
>
>
>







162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
                    return \SQLITE3_TEXT;
                }
            default:
                throw new \InvalidArgumentException('Argument '.$name.' is of invalid type '.gettype($arg));
        }
    }

    /**
     * Performe une requête en utilisant les arguments contenus dans le tableau $args
     * @param  string       $query Requête SQL
     * @param  array|object $args  Arguments à utiliser comme bindings pour la requête
     * @return \SQLite3Statement|boolean Retourne un booléen si c'est une requête 
     * qui exécute une opération d'écriture, ou un statement si c'est une requête de lecture.
     *
     * Note: le fait que cette fonction retourne un booléen est un comportement
     * volontaire pour éviter un bug dans le module SQLite3 de PHP, qui provoque
     * un risque de faire des opérations en double en cas d'exécution de 
     * ->fetchResult() sur un statement d'écriture.
     */
    public function query($query, Array $args = [])
    {
        assert(is_string($query));
        assert(is_array($args) || is_object($args));
        
        // Forcer en tableau
        $args = (array) $args;

        $this->connect();
        $statement = $this->db->prepare($query);
        $nb = $statement->paramCount();

        if (!empty($args))
        {
            if (is_array($args) && count($args) == 1 && is_array(current($args)))
223
224
225
226
227
228
229








230
231
232
233
234
235










236
237
238
239
240
241










242
243
244
245
246
247







248
249




250
251
252
253
254
255
256








257
258









259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275

276
277
278
279
280
281









282
283
284
285
286
287







288
289
290
291
292







293
294
295
296
297
298
299
300
301
302







303
304
305
306
307
308
309
310
311






312
313
314
315
316
317
318
        }
        catch (\Exception $e)
        {
            throw new \RuntimeException($e->getMessage() . "\n" . $query . "\n" . json_encode($args, true));
        }
    }









    public function get($query)
    {
        $args = array_slice(func_get_args(), 1);
        return $this->fetch($this->query($query, $args), self::OBJ);
    }











    public function getAssoc($query)
    {
        $args = array_slice(func_get_args(), 1);
        return $this->fetchAssoc($this->query($query, $args));
    }











    public function getAssocKey($query)
    {
        $args = array_slice(func_get_args(), 1);
        return $this->fetchAssocKey($this->query($query, $args), self::OBJ);
    }








    public function insert($table, Array $fields)
    {




        $fields_names = array_keys($fields);
        $query = sprintf('INSERT INTO %s (%s) VALUES (:%s);', $table, 
            implode(', ', $fields_names), implode(', :', $fields_names));

        return $this->query($query, $fields);
    }









    public function update($table, Array $fields, $where)
    {









        // No fields to update? no need to do a query
        if (empty($fields))
        {
            return false;
        }

        $args = array_slice(func_get_args(), 3);
        $column_updates = [];
        
        foreach ($fields as $key=>$value)
        {
            // Append to arguments
            $args['field_' . $key] = $value;

            $column_updates[] = sprintf('%s = :field_%s', $key, $key);
        }


        $column_updates = implode(', ', $column_updates);
        $query = sprintf('UPDATE %s SET %s WHERE %s;', $table, $column_updates, $where);

        return $this->query($query, $args);
    }










    public function delete($table, $where)
    {
        $query = sprintf('DELETE FROM %s WHERE %s;', $table, $where);
        return $this->query($query, array_slice(func_get_args(), 2));
    }








    public function exec($query)
    {
        return $this->query($query, array_slice(func_get_args(), 1));
    }








    public function first($query)
    {
        $res = $this->query($query, array_slice(func_get_args(), 1));

        $row = $res->fetchArray(SQLITE3_ASSOC);
        $res->finalize();

        return is_array($row) ? (object) $row : false;
    }








    public function firstColumn($query)
    {
        $res = $this->query($query, array_slice(func_get_args(), 1));

        $row = $res->fetchArray(\SQLITE3_NUM);

        return count($row) > 0 ? $row[0] : false;
    }







    public function fetch(\SQLite3Result $result, $mode = null)
    {
        $as_obj = false;

        if ($mode === self::OBJ)
        {
            $as_obj = true;







>
>
>
>
>
>
>
>






>
>
>
>
>
>
>
>
>
>






>
>
>
>
>
>
>
>
>
>






>
>
>
>
>
>
>
|

>
>
>
>







>
>
>
>
>
>
>
>
|

>
>
>
>
>
>
>
>
>






<










>






>
>
>
>
>
>
>
>
>






>
>
>
>
>
>
>





>
>
>
>
>
>
>










>
>
>
>
>
>
>









>
>
>
>
>
>







241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338

339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
        }
        catch (\Exception $e)
        {
            throw new \RuntimeException($e->getMessage() . "\n" . $query . "\n" . json_encode($args, true));
        }
    }

    /**
     * Exécute une requête et retourne le résultat sous forme de tableau
     * @param  string $query Requête SQL
     * @return array Tableau contenant des objets
     *
     * Accepte un ou plusieurs arguments supplémentaires utilisés comme bindings
     * pour la clause WHERE.
     */
    public function get($query)
    {
        $args = array_slice(func_get_args(), 1);
        return $this->fetch($this->query($query, $args), self::OBJ);
    }

    /**
     * Exécute une requête et retourne le résultat sous forme de tableau associatif
     * en utilisant les deux premières colonnes retournées,
     * de la forme [colonne1 => colonne2, colonne1 => colonne2, ...]
     * @param  string $query Requête SQL
     * @return array Tableau associatif
     *
     * Accepte un ou plusieurs arguments supplémentaires utilisés comme bindings
     * pour la clause WHERE.
     */
    public function getAssoc($query)
    {
        $args = array_slice(func_get_args(), 1);
        return $this->fetchAssoc($this->query($query, $args));
    }

    /**
     * Exécute une requête et retourne le résultat sous forme de tableau associatif
     * en utilisant la première colonne comme clé:
     * [colonne1 => (object) [colonne1 => valeur1, colonne2 => valeur2, ...], ...]
     * @param  string $query Requête SQL
     * @return array Tableau associatif contenant des objets
     *
     * Accepte un ou plusieurs arguments supplémentaires utilisés comme bindings
     * pour la clause WHERE.
     */
    public function getAssocKey($query)
    {
        $args = array_slice(func_get_args(), 1);
        return $this->fetchAssocKey($this->query($query, $args), self::OBJ);
    }

    /**
     * Insère une ligne dans la table $table, en remplissant avec les champs donnés
     * dans $fields (tableau associatif ou objet)
     * @param  string $table  Table où insérer
     * @param  string $fields Champs à remplir
     * @return boolean
     */
    public function insert($table, $fields)
    {
        assert(is_array($fields) || is_object($fields));

        $fields = (array) $fields;

        $fields_names = array_keys($fields);
        $query = sprintf('INSERT INTO %s (%s) VALUES (:%s);', $table, 
            implode(', ', $fields_names), implode(', :', $fields_names));

        return $this->query($query, $fields);
    }

    /**
     * Met à jour une ou plusieurs lignes de la table
     * @param  string       $table  Nom de la table
     * @param  array|object $fields Liste des champs à mettre à jour
     * @param  string       $where  Clause WHERE
     * @param  array|object $args   Arguments pour la clause WHERE
     * @return boolean
     */
    public function update($table, $fields, $where, $args = [])
    {
        assert(is_string($table));
        assert(is_string($where) && strlen($where));
        assert(is_array($fields) || is_object($fields));
        assert(is_array($args) || is_object($args));

        // Forcer en tableau
        $fields = (array) $fields;
        $args = (array) $args;

        // No fields to update? no need to do a query
        if (empty($fields))
        {
            return false;
        }


        $column_updates = [];
        
        foreach ($fields as $key=>$value)
        {
            // Append to arguments
            $args['field_' . $key] = $value;

            $column_updates[] = sprintf('%s = :field_%s', $key, $key);
        }

        // Assemblage de la requête
        $column_updates = implode(', ', $column_updates);
        $query = sprintf('UPDATE %s SET %s WHERE %s;', $table, $column_updates, $where);

        return $this->query($query, $args);
    }

    /**
     * Supprime une ou plusieurs lignes d'une table
     * @param  string $table Nom de la table
     * @param  string $where Clause WHERE
     * @return boolean
     *
     * Accepte un ou plusieurs arguments supplémentaires utilisés comme bindings
     * pour la clause WHERE.
     */
    public function delete($table, $where)
    {
        $query = sprintf('DELETE FROM %s WHERE %s;', $table, $where);
        return $this->query($query, array_slice(func_get_args(), 2));
    }

    /**
     * Exécute une requête SQL (alias pour query)
     * @param  string $query Requête SQL
     * @return boolean
     *
     * Accepte un ou plusieurs arguments supplémentaires utilisés comme bindings.
     */
    public function exec($query)
    {
        return $this->query($query, array_slice(func_get_args(), 1));
    }

    /**
     * Exécute une requête et retourne la première ligne
     * @param  string $query Requête SQL
     * @return object
     *
     * Accepte un ou plusieurs arguments supplémentaires utilisés comme bindings.
     */
    public function first($query)
    {
        $res = $this->query($query, array_slice(func_get_args(), 1));

        $row = $res->fetchArray(SQLITE3_ASSOC);
        $res->finalize();

        return is_array($row) ? (object) $row : false;
    }

    /**
     * Exécute une requête et retourne la première colonne de la première ligne
     * @param  string $query Requête SQL
     * @return object
     *
     * Accepte un ou plusieurs arguments supplémentaires utilisés comme bindings.
     */
    public function firstColumn($query)
    {
        $res = $this->query($query, array_slice(func_get_args(), 1));

        $row = $res->fetchArray(\SQLITE3_NUM);

        return count($row) > 0 ? $row[0] : false;
    }

    /**
     * Récupère le résultat d'un statement
     * @param  \SQLite3Result $result Résultat de statement
     * @param  integer        $mode   Mode de récupération (BOTH, OBJ, NUM ou ASSOC)
     * @return array
     */
    public function fetch(\SQLite3Result $result, $mode = null)
    {
        $as_obj = false;

        if ($mode === self::OBJ)
        {
            $as_obj = true;
329
330
331
332
333
334
335








336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351







352
353
354
355
356
357
358

        $result->finalize();
        unset($result, $row);

        return $out;
    }









    protected function fetchAssoc(\SQLite3Result $result)
    {
        $out = [];

        while ($row = $result->fetchArray(\SQLITE3_NUM))
        {
            // FIXME: use generator
            $out[$row[0]] = $row[1];
        }

        $result->finalize();
        unset($result, $row);

        return $out;
    }








    protected function fetchAssocKey(\SQLite3Result $result, $mode = null)
    {
        $as_obj = false;

        if ($mode === self::OBJ)
        {
            $as_obj = true;







>
>
>
>
>
>
>
>
















>
>
>
>
>
>
>







439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483

        $result->finalize();
        unset($result, $row);

        return $out;
    }

    /**
     * Récupère le résultat d'un statement sous forme de tableau associatif
     * avec colonne1 comme clé et colonne2 comme valeur.
     * 
     * @param  \SQLite3Result $result Résultat de statement
     * @param  integer        $mode   Mode de récupération (BOTH, OBJ, NUM ou ASSOC)
     * @return array
     */
    protected function fetchAssoc(\SQLite3Result $result)
    {
        $out = [];

        while ($row = $result->fetchArray(\SQLITE3_NUM))
        {
            // FIXME: use generator
            $out[$row[0]] = $row[1];
        }

        $result->finalize();
        unset($result, $row);

        return $out;
    }

    /**
     * Récupère le résultat d'un statement sous forme de tableau associatif 
     * avec colonne1 comme clé et la ligne comme valeur.
     * @param  \SQLite3Result $result Résultat de statement
     * @param  integer        $mode   Mode de récupération (BOTH, OBJ, NUM ou ASSOC)
     * @return array
     */
    protected function fetchAssocKey(\SQLite3Result $result, $mode = null)
    {
        $as_obj = false;

        if ($mode === self::OBJ)
        {
            $as_obj = true;
370
371
372
373
374
375
376





377
378
379
380
381
382
383
384
385
386
387
388
389
390





391
392
393
394
395
396
397

        $result->finalize();
        unset($result, $row);

        return $out;
    }






    public function countRows(\SQLite3Result $result)
    {
        $i = 0;

        while ($result->fetchArray(\SQLITE3_NUM))
        {
            $i++;
        }

        $result->reset();

        return $i;
    }






    public function prepare($query)
    {
        return $this->db->prepare($query);
    }

    /**
     * @deprecated







>
>
>
>
>














>
>
>
>
>







495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532

        $result->finalize();
        unset($result, $row);

        return $out;
    }

    /**
     * Compte le nombre de lignes dans un résultat
     * @param  \SQLite3Result $result Résultat SQLite3
     * @return integer
     */
    public function countRows(\SQLite3Result $result)
    {
        $i = 0;

        while ($result->fetchArray(\SQLITE3_NUM))
        {
            $i++;
        }

        $result->reset();

        return $i;
    }

    /**
     * Préparer un statement SQLite3
     * @param  string $query Requête SQL
     * @return \SQLite3Statement
     */
    public function prepare($query)
    {
        return $this->db->prepare($query);
    }

    /**
     * @deprecated

Added tests/unit_tests/01_basic/db.php version [88dbf0c61d].





























































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
<?php

namespace Garradin;

use KD2\Test;

const DB_FILE = ':memory:';

require __DIR__ . '/../init.php';

$db = DB::getInstance();

// test exec
Test::assert($db->exec('CREATE TABLE test (a, b);'));

// test insert
Test::assert($db->insert('test', ['a' => 1, 'b' => 2]));
Test::assert($db->insert('test', ['a' => 3, 'b' => 4]));

// test insert object
Test::assert($db->insert('test', (object) ['a' => 9, 'b' => 10]));

// test fetch
Test::equals((object)['a' => 1, 'b' => 2], $db->first('SELECT a, b FROM test;'));
Test::assert(is_object($db->first('SELECT a, b FROM test;')));
Test::equals(1, $db->firstColumn('SELECT a, b FROM test;'));
Test::equals(3, $db->firstColumn('SELECT COUNT(*) FROM test;'));

// test update
Test::assert($db->update('test', ['a' => 5, 'b' => 6], 'a = :a AND b = :b', ['a' => 3, 'b' => 4]));

// test update with mixed type bindings
try {
	$db->update('test', ['a' => 5, 'b' => 6], 'a = ? AND b = ?', [3, 4]);
	$failed = false;
}
catch (\InvalidArgumentException $e) {
	$failed = true;
}

Test::assert($failed === true);

// test if update worked
Test::equals((object)['a' => 5, 'b' => 6], $db->first('SELECT a, b FROM test LIMIT 1, 1;'));

// test delete
Test::assert($db->delete('test', 'a = ? AND b = ?', 5, 6));

// test if delete worked
Test::equals(2, $db->firstColumn('SELECT COUNT(*) FROM test;'));

// test insert again
Test::assert(is_bool($db->insert('test', ['a' => 3, 'b' => 4])));

Test::equals(3, $db->firstColumn('SELECT COUNT(*) FROM test;'));

// Test bindings
Test::equals(1, $db->firstColumn('SELECT a, b FROM test WHERE a = :a;', ['a' => 1]));
Test::equals((object) ['a' => 1, 'b' => 2], $db->first('SELECT a, b FROM test WHERE a = ?;', 1));

// test SELECT
$expected = [(object)['a' => 1, 'b' => 2], (object)['a' => 9, 'b' => 10]];
Test::equals($expected, $db->get('SELECT * FROM test LIMIT 2;'));

$expected = [1 => 2, 9 => 10];
Test::equals($expected, $db->getAssoc('SELECT * FROM test LIMIT 2;'));

$expected = [1 => (object) ['a' => 1, 'b' => 2], 9 => (object) ['a' => 9, 'b' => 10]];
Test::equals($expected, $db->getAssocKey('SELECT * FROM test LIMIT 2;'));

// test transactions
Test::assert($db->begin());

Test::assert($db->insert('test', ['a' => 42, 'b' => 43]));

// test nested transaction
Test::assert(!$db->begin());
Test::assert(!$db->commit());

Test::assert($db->insert('test', ['a' => 44, 'b' => 45]));

// test rollback
Test::assert($db->rollback());

Test::equals(3, $db->firstColumn('SELECT COUNT(*) FROM test;'));

// test successful commit
Test::assert($db->begin());

Test::assert($db->insert('test', ['a' => 42, 'b' => 43]));

Test::assert($db->commit());

Test::equals(4, $db->firstColumn('SELECT COUNT(*) FROM test;'));