source: classes/phing/tasks/ext/pdo/PDOSQLExecTask.php @ ed3ff78

Last change on this file since ed3ff78 was ed3ff78, checked in by Michiel Rook <mrook@…>, 3 years ago

Documentation fixes

  • Property mode set to 100644
File size: 20.2 KB
Line 
1<?php
2/*
3 *  $Id$
4 *
5 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
6 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
7 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
8 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
9 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
10 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
11 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
12 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
13 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
14 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
15 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
16 *
17 * This software consists of voluntary contributions made by many individuals
18 * and is licensed under the LGPL. For more information please see
19 * <http://phing.info>.
20 */
21
22require_once 'phing/tasks/ext/pdo/PDOTask.php';
23include_once 'phing/system/io/StringReader.php';
24include_once 'phing/tasks/ext/pdo/PDOSQLExecFormatterElement.php';
25
26/**
27 * Executes a series of SQL statements on a database using PDO.
28 *
29 * <p>Statements can
30 * either be read in from a text file using the <i>src</i> attribute or from
31 * between the enclosing SQL tags.</p>
32 *
33 * <p>Multiple statements can be provided, separated by semicolons (or the
34 * defined <i>delimiter</i>). Individual lines within the statements can be
35 * commented using either --, // or REM at the start of the line.</p>
36 *
37 * <p>The <i>autocommit</i> attribute specifies whether auto-commit should be
38 * turned on or off whilst executing the statements. If auto-commit is turned
39 * on each statement will be executed and committed. If it is turned off the
40 * statements will all be executed as one transaction.</p>
41 *
42 * <p>The <i>onerror</i> attribute specifies how to proceed when an error occurs
43 * during the execution of one of the statements.
44 * The possible values are: <b>continue</b> execution, only show the error;
45 * <b>stop</b> execution and commit transaction;
46 * and <b>abort</b> execution and transaction and fail task.</p>
47 *
48 * @author    Hans Lellelid <hans@xmpl.org> (Phing)
49 * @author    Jeff Martin <jeff@custommonkey.org> (Ant)
50 * @author    Michael McCallum <gholam@xtra.co.nz> (Ant)
51 * @author    Tim Stephenson <tim.stephenson@sybase.com> (Ant)
52 * @package   phing.tasks.ext.pdo
53 * @version   $Revision$
54 */
55class PDOSQLExecTask extends PDOTask {
56
57    /**
58     * Count of how many statements were executed successfully.
59     * @var int
60     */
61    private $goodSql = 0;
62
63    /**
64     * Count of total number of SQL statements.
65     * @var int
66     */
67    private $totalSql = 0;
68
69    const DELIM_ROW = "row";
70    const DELIM_NORMAL = "normal";
71
72    /**
73     * Database connection
74     * @var PDO
75     */
76    private $conn = null;
77
78    /**
79     * Files to load
80     * @var array FileSet[]
81     */
82    private $filesets = array();
83
84    /**
85     * Files to load
86     * @var array FileList[]
87     */
88    private $filelists = array();
89   
90    /**
91     * Formatter elements.
92     * @var array PDOSQLExecFormatterElement[]
93     */
94    private $formatters = array();
95
96    /**
97     * SQL statement
98     * @var PDOStatement
99     */
100    private $statement;
101
102    /**
103     * SQL input file
104     * @var PhingFile
105     */
106    private $srcFile;
107
108    /**
109     * SQL input command
110     * @var string
111     */
112    private $sqlCommand = "";
113
114    /**
115     * SQL transactions to perform
116     */
117    private $transactions = array();
118
119    /**
120     * SQL Statement delimiter (for parsing files)
121     * @var string
122     */
123    private $delimiter = ";";
124
125    /**
126     * The delimiter type indicating whether the delimiter will
127     * only be recognized on a line by itself
128     */
129    private $delimiterType = "normal"; // can't use constant just defined
130
131    /**
132     * Action to perform if an error is found
133     **/
134    private $onError = "abort";
135
136    /**
137     * Encoding to use when reading SQL statements from a file
138     */
139    private $encoding = null;
140
141    /**
142     * Fetch mode for PDO select queries.
143     * @var int
144     */
145    private $fetchMode;
146
147    /**
148     * Set the name of the SQL file to be run.
149     * Required unless statements are enclosed in the build file
150     */
151    public function setSrc(PhingFile $srcFile) {
152        $this->srcFile = $srcFile;
153    }
154
155    /**
156     * Set an inline SQL command to execute.
157     * NB: Properties are not expanded in this text.
158     */
159    public function addText($sql) {
160        $this->sqlCommand .= $sql;
161    }
162
163    /**
164     * Adds a set of files (nested fileset attribute).
165     */
166    public function addFileset(FileSet $set) {
167        $this->filesets[] = $set;
168    }
169
170    /**
171     * Adds a set of files (nested filelist attribute).
172     */
173    public function addFilelist(FileList $list) {
174        $this->filelists[] = $list;
175    }
176   
177    /**
178     * Creates a new PDOSQLExecFormatterElement for <formatter> element.
179     * @return PDOSQLExecFormatterElement
180     */
181    public function createFormatter()
182    {
183        $fe = new PDOSQLExecFormatterElement($this);
184        $this->formatters[] = $fe;
185        return $fe;
186    }
187
188    /**
189     * Add a SQL transaction to execute
190     */
191    public function createTransaction() {
192        $t = new PDOSQLExecTransaction($this);
193        $this->transactions[] = $t;
194        return $t;
195    }
196
197    /**
198     * Set the file encoding to use on the SQL files read in
199     *
200     * @param encoding the encoding to use on the files
201     */
202    public function setEncoding($encoding) {
203        $this->encoding = $encoding;
204    }
205
206    /**
207     * Set the statement delimiter.
208     *
209     * <p>For example, set this to "go" and delimitertype to "ROW" for
210     * Sybase ASE or MS SQL Server.</p>
211     *
212     * @param delimiter
213     */
214    public function setDelimiter($delimiter)
215    {
216        $this->delimiter = $delimiter;
217    }
218
219    /**
220     * Set the Delimiter type for this sql task. The delimiter type takes two
221     * values - normal and row. Normal means that any occurence of the delimiter
222     * terminate the SQL command whereas with row, only a line containing just
223     * the delimiter is recognized as the end of the command.
224     *
225     * @param string $delimiterType
226     */
227    public function setDelimiterType($delimiterType)
228    {
229        $this->delimiterType = $delimiterType;
230    }
231
232    /**
233     * Action to perform when statement fails: continue, stop, or abort
234     * optional; default &quot;abort&quot;
235     */
236    public function setOnerror($action) {
237        $this->onError = $action;
238    }
239
240    /**
241     * Sets the fetch mode to use for the PDO resultset.
242     * @param mixed $mode The PDO fetchmode integer or constant name.
243     */
244    public function setFetchmode($mode) {
245        if (is_numeric($mode)) {
246            $this->fetchMode = (int) $mode;
247        } else {
248            if (defined($mode)) {
249                $this->fetchMode = constant($mode);
250            } else {
251                throw new BuildException("Invalid PDO fetch mode specified: " . $mode, $this->getLocation());
252            }
253        }
254    }
255
256    /**
257     * Gets a default output writer for this task.
258     * @return Writer
259     */
260    private function getDefaultOutput()
261    {
262        return new LogWriter($this);
263    }
264
265    /**
266     * Load the sql file and then execute it
267     * @throws BuildException
268     */
269    public function main()  {
270
271        // Set a default fetchmode if none was specified
272        // (We're doing that here to prevent errors loading the class is PDO is not available.)
273        if ($this->fetchMode === null) {
274            $this->fetchMode = PDO::FETCH_BOTH;
275        }
276
277        // Initialize the formatters here.  This ensures that any parameters passed to the formatter
278        // element get passed along to the actual formatter object
279        foreach($this->formatters as $fe) {
280            $fe->prepare();
281        }
282
283        $savedTransaction = array();
284        for($i=0,$size=count($this->transactions); $i < $size; $i++) {
285            $savedTransaction[] = clone $this->transactions[$i];
286        }
287
288        $savedSqlCommand = $this->sqlCommand;
289
290        $this->sqlCommand = trim($this->sqlCommand);
291
292        try {
293            if ($this->srcFile === null && $this->sqlCommand === ""
294                && empty($this->filesets) && empty($this->filelists) 
295                && count($this->transactions) === 0) {
296                    throw new BuildException("Source file or fileset/filelist, "
297                    . "transactions or sql statement "
298                    . "must be set!", $this->location);
299            }
300
301            if ($this->srcFile !== null && !$this->srcFile->exists()) {
302                throw new BuildException("Source file does not exist!", $this->location);
303            }
304
305            // deal with the filesets
306            foreach($this->filesets as $fs) {
307                $ds = $fs->getDirectoryScanner($this->project);
308                $srcDir = $fs->getDir($this->project);
309                $srcFiles = $ds->getIncludedFiles();
310                // Make a transaction for each file
311                foreach($srcFiles as $srcFile) {
312                    $t = $this->createTransaction();
313                    $t->setSrc(new PhingFile($srcDir, $srcFile));
314                }
315            }
316           
317            // process filelists
318            foreach($this->filelists as $fl) {
319                $srcDir  = $fl->getDir($this->project);
320                $srcFiles = $fl->getFiles($this->project);               
321                // Make a transaction for each file
322                foreach($srcFiles as $srcFile) {
323                    $t = $this->createTransaction();
324                    $t->setSrc(new PhingFile($srcDir, $srcFile));
325                }
326            }
327
328            // Make a transaction group for the outer command
329            $t = $this->createTransaction();
330            if ($this->srcFile) $t->setSrc($this->srcFile);
331            $t->addText($this->sqlCommand);
332            $this->conn = $this->getConnection();
333
334            try {
335
336                $this->statement = null;
337
338                // Initialize the formatters.
339                $this->initFormatters();
340
341                try {
342
343                    // Process all transactions
344                    for ($i=0,$size=count($this->transactions); $i < $size; $i++) {
345                        if (!$this->isAutocommit()) {
346                            $this->log("Beginning transaction", Project::MSG_VERBOSE);
347                            $this->conn->beginTransaction();
348                        }
349                        $this->transactions[$i]->runTransaction();
350                        if (!$this->isAutocommit()) {
351                            $this->log("Commiting transaction", Project::MSG_VERBOSE);
352                            $this->conn->commit();
353                        }
354                    }
355                } catch (Exception $e) {
356                    $this->closeConnection();
357                    throw $e;
358                }
359            } catch (IOException $e) {
360                if (!$this->isAutocommit() && $this->conn !== null && $this->onError == "abort") {
361                    try {
362                        $this->conn->rollback();
363                    } catch (PDOException $ex) {}
364                }
365                $this->closeConnection();
366                throw new BuildException($e->getMessage(), $this->location);
367            } catch (PDOException $e){
368                if (!$this->isAutocommit() && $this->conn !== null && $this->onError == "abort") {
369                    try {
370                        $this->conn->rollback();
371                    } catch (PDOException $ex) {}
372                }
373                $this->closeConnection();
374                throw new BuildException($e->getMessage(), $this->location);
375            }
376               
377            // Close the formatters.
378            $this->closeFormatters();
379
380            $this->log($this->goodSql . " of " . $this->totalSql .
381                " SQL statements executed successfully");
382
383        } catch (Exception $e) {
384            $this->transactions = $savedTransaction;
385            $this->sqlCommand = $savedSqlCommand;
386            $this->closeConnection();
387            throw $e;
388        }
389        // finally {
390        $this->transactions = $savedTransaction;
391        $this->sqlCommand = $savedSqlCommand;
392        $this->closeConnection();
393    }
394
395
396    /**
397     * read in lines and execute them
398     * @throws PDOException, IOException
399     */
400    public function runStatements(Reader $reader) {
401        $sql = "";
402        $line = "";
403        $sqlBacklog = "";
404        $hasQuery = false;
405
406        $in = new BufferedReader($reader);
407
408        try {
409            while (($line = $in->readLine()) !== null) {
410                $line = trim($line);
411                $line = ProjectConfigurator::replaceProperties($this->project, $line,
412                        $this->project->getProperties());
413
414                if (($line != $this->delimiter) && (
415                    StringHelper::startsWith("//", $line) ||
416                    StringHelper::startsWith("--", $line) ||
417                    StringHelper::startsWith("#", $line))) {
418                    continue;
419                }
420
421                if (strlen($line) > 4
422                        && strtoupper(substr($line,0, 4)) == "REM ") {
423                    continue;
424                }
425
426                // MySQL supports defining new delimiters
427                if (preg_match('/DELIMITER [\'"]?([^\'" $]+)[\'"]?/i', $line, $matches)) {
428                    $this->setDelimiter($matches[1]);
429                    continue;
430                }
431
432                if ($sqlBacklog !== "") {
433                    $sql = $sqlBacklog;
434                    $sqlBacklog = "";
435                }
436
437                $sql .= " " . $line . "\n";
438
439                // SQL defines "--" as a comment to EOL
440                // and in Oracle it may contain a hint
441                // so we cannot just remove it, instead we must end it
442                if (strpos($line, "--") !== false) {
443                    $sql .= "\n";
444                }
445
446                // DELIM_ROW doesn't need this (as far as i can tell)
447                if ($this->delimiterType == self::DELIM_NORMAL) {
448
449                    $reg = "#((?:\"(?:\\\\.|[^\"])*\"?)+|'(?:\\\\.|[^'])*'?|" . preg_quote($this->delimiter) . ")#";
450
451                    $sqlParts = preg_split($reg, $sql, 0, PREG_SPLIT_DELIM_CAPTURE);
452                    $sqlBacklog = "";
453                    foreach ($sqlParts as $sqlPart) {
454                        // we always want to append, even if it's a delim (which will be stripped off later)
455                        $sqlBacklog .= $sqlPart;
456
457                        // we found a single (not enclosed by ' or ") delimiter, so we can use all stuff before the delim as the actual query
458                        if ($sqlPart === $this->delimiter) {
459                            $sql = $sqlBacklog;
460                            $sqlBacklog = "";
461                            $hasQuery = true;
462                        }
463                    }
464                }
465
466                if ($hasQuery || ($this->delimiterType == self::DELIM_ROW && $line == $this->delimiter)) {
467                    // this assumes there is always a delimter on the end of the SQL statement.
468                    $sql = StringHelper::substring($sql, 0, strlen($sql) - 1 - strlen($this->delimiter));
469                    $this->log("SQL: " . $sql, Project::MSG_VERBOSE);
470                    $this->execSQL($sql);
471                    $sql = "";
472                    $hasQuery = false;
473                }
474            }
475
476            // Catch any statements not followed by ;
477            if ($sql !== "") {
478                $this->execSQL($sql);
479            }
480        } catch (PDOException $e) {
481            throw $e;
482        }
483    }
484
485    /**
486     * Whether the passed-in SQL statement is a SELECT statement.
487     * This does a pretty simple match, checking to see if statement starts with
488     * 'select' (but not 'select into').
489     *
490     * @param string $sql
491     * @return boolean Whether specified SQL looks like a SELECT query.
492     */
493    protected function isSelectSql($sql)
494    {
495        $sql = trim($sql);
496        return (stripos($sql, 'select') === 0 && stripos($sql, 'select into ') !== 0);
497    }
498
499    /**
500     * Exec the sql statement.
501     * @throws PDOException
502     */
503    protected function execSQL($sql) {
504
505        // Check and ignore empty statements
506        if (trim($sql) == "") {
507            return;
508        }
509
510        try {
511            $this->totalSql++;
512
513            $this->statement = $this->conn->prepare($sql);
514            $this->statement->execute();
515            $this->log($this->statement->rowCount() . " rows affected", Project::MSG_VERBOSE);
516
517            // only call processResults() for statements that return actual data (such as 'select')
518            if ($this->statement->columnCount() > 0)
519            {
520                $this->processResults();
521            }
522
523            $this->statement->closeCursor();
524            $this->statement = null;
525
526            $this->goodSql++;
527
528        } catch (PDOException $e) {
529            $this->log("Failed to execute: " . $sql, Project::MSG_ERR);
530            if ($this->onError != "continue") {
531                throw new BuildException("Failed to execute SQL", $e);
532            }
533            $this->log($e->getMessage(), Project::MSG_ERR);
534        }
535    }
536
537    /**
538     * Returns configured PDOResultFormatter objects (which were created from PDOSQLExecFormatterElement objects).
539     * @return array PDOResultFormatter[]
540     */
541    protected function getConfiguredFormatters()
542    {
543        $formatters = array();
544        foreach ($this->formatters as $fe) {
545            $formatters[] = $fe->getFormatter();
546        }
547        return $formatters;
548    }
549
550    /**
551     * Initialize the formatters.
552     */
553    protected function initFormatters() {
554        $formatters = $this->getConfiguredFormatters();
555        foreach ($formatters as $formatter) {
556            $formatter->initialize();
557        }
558
559    }
560
561    /**
562     * Run cleanup and close formatters.
563     */
564    protected function closeFormatters() {
565        $formatters = $this->getConfiguredFormatters();
566        foreach ($formatters as $formatter) {
567            $formatter->close();
568        }
569    }
570
571    /**
572     * Passes results from query to any formatters.
573     * @throws PDOException
574     */
575    protected function processResults() {
576
577        try {
578
579            $this->log("Processing new result set.", Project::MSG_VERBOSE);
580
581            $formatters = $this->getConfiguredFormatters();
582
583            while ($row = $this->statement->fetch($this->fetchMode)) {
584                foreach ($formatters as $formatter) {
585                    $formatter->processRow($row);
586                }
587            }
588
589        } catch (Exception $x) {
590            $this->log("Error processing reults: " . $x->getMessage(), Project::MSG_ERR);
591            foreach ($formatters as $formatter) {
592                $formatter->close();
593            }
594            throw $x;
595        }
596
597    }
598
599    /**
600     * Closes current connection
601     */
602    protected function closeConnection()
603    {
604        if ($this->conn) {
605            unset($this->conn);
606        }
607    }
608}
609
610/**
611 * "Inner" class that contains the definition of a new transaction element.
612 * Transactions allow several files or blocks of statements
613 * to be executed using the same JDBC connection and commit
614 * operation in between.
615 *
616 * @package   phing.tasks.ext.pdo
617 */
618class PDOSQLExecTransaction {
619
620    private $tSrcFile = null;
621    private $tSqlCommand = "";
622    private $parent;
623
624    function __construct($parent)
625    {
626        // Parent is required so that we can log things ...
627        $this->parent = $parent;
628    }
629
630    public function setSrc(PhingFile $src)
631    {
632        $this->tSrcFile = $src;
633    }
634
635    public function addText($sql)
636    {
637        $this->tSqlCommand .= $sql;
638    }
639
640    /**
641     * @throws IOException, PDOException
642     */
643    public function runTransaction()
644    {
645        if (!empty($this->tSqlCommand)) {
646            $this->parent->log("Executing commands", Project::MSG_INFO);
647            $this->parent->runStatements(new StringReader($this->tSqlCommand));
648        }
649
650        if ($this->tSrcFile !== null) {
651            $this->parent->log("Executing file: " . $this->tSrcFile->getAbsolutePath(),
652            Project::MSG_INFO);
653            $reader = new FileReader($this->tSrcFile);
654            $this->parent->runStatements($reader);
655            $reader->close();
656        }
657    }
658}
659
660
Note: See TracBrowser for help on using the repository browser.