Subversion-Projekte lars-tiefland.php_share

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 1
<?php
2
/*
3
 *  $Id: CreoleSQLExecTask.php 83 2006-07-07 18:17:00Z mrook $
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
 
22
require_once 'phing/tasks/ext/pdo/PDOTask.php';
23
include_once 'phing/system/io/StringReader.php';
24
include_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
53
 * @version   $Revision: 1.21 $
54
 */
55
class 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
     * Formatter elements.
86
     * @var array PDOSQLExecFormatterElement[]
87
     */
88
    private $formatters = array();
89
 
90
    /**
91
     * SQL statement
92
     * @var PDOStatement
93
     */
94
    private $statement;
95
 
96
    /**
97
     * SQL input file
98
     * @var PhingFile
99
     */
100
    private $srcFile;
101
 
102
    /**
103
     * SQL input command
104
     * @var string
105
     */
106
    private $sqlCommand = "";
107
 
108
    /**
109
     * SQL transactions to perform
110
     */
111
    private $transactions = array();
112
 
113
    /**
114
     * SQL Statement delimiter (for parsing files)
115
     * @var string
116
     */
117
    private $delimiter = ";";
118
 
119
    /**
120
     * The delimiter type indicating whether the delimiter will
121
     * only be recognized on a line by itself
122
     */
123
    private $delimiterType = "normal"; // can't use constant just defined
124
 
125
    /**
126
     * Action to perform if an error is found
127
     **/
128
    private $onError = "abort";
129
 
130
    /**
131
     * Encoding to use when reading SQL statements from a file
132
     */
133
    private $encoding = null;
134
 
135
    /**
136
     * Fetch mode for PDO select queries.
137
     * @var int
138
     */
139
    private $fetchMode;
140
 
141
    /**
142
     * Set the name of the SQL file to be run.
143
     * Required unless statements are enclosed in the build file
144
     */
145
    public function setSrc(PhingFile $srcFile) {
146
    	$this->srcFile = $srcFile;
147
    }
148
 
149
    /**
150
     * Set an inline SQL command to execute.
151
     * NB: Properties are not expanded in this text.
152
     */
153
    public function addText($sql) {
154
    	$this->sqlCommand .= $sql;
155
    }
156
 
157
    /**
158
     * Adds a set of files (nested fileset attribute).
159
     */
160
    public function addFileset(FileSet $set) {
161
    	$this->filesets[] = $set;
162
    }
163
 
164
    /**
165
     * Creates a new PDOSQLExecFormatterElement for <formatter> element.
166
     * @return PDOSQLExecFormatterElement
167
     */
168
    public function createFormatter()
169
    {
170
    	$fe = new PDOSQLExecFormatterElement($this);
171
    	$this->formatters[] = $fe;
172
    	return $fe;
173
    }
174
 
175
    /**
176
     * Add a SQL transaction to execute
177
     */
178
    public function createTransaction() {
179
    	$t = new PDOSQLExecTransaction($this);
180
    	$this->transactions[] = $t;
181
    	return $t;
182
    }
183
 
184
    /**
185
     * Set the file encoding to use on the SQL files read in
186
     *
187
     * @param encoding the encoding to use on the files
188
     */
189
    public function setEncoding($encoding) {
190
    	$this->encoding = $encoding;
191
    }
192
 
193
    /**
194
     * Set the statement delimiter.
195
     *
196
     * <p>For example, set this to "go" and delimitertype to "ROW" for
197
     * Sybase ASE or MS SQL Server.</p>
198
     *
199
     * @param delimiter
200
     */
201
    public function setDelimiter($delimiter)
202
    {
203
    	$this->delimiter = $delimiter;
204
    }
205
 
206
    /**
207
     * Set the Delimiter type for this sql task. The delimiter type takes two
208
     * values - normal and row. Normal means that any occurence of the delimiter
209
     * terminate the SQL command whereas with row, only a line containing just
210
     * the delimiter is recognized as the end of the command.
211
     *
212
     * @param string $delimiterType
213
     */
214
    public function setDelimiterType($delimiterType)
215
    {
216
    	$this->delimiterType = $delimiterType;
217
    }
218
 
219
    /**
220
     * Action to perform when statement fails: continue, stop, or abort
221
     * optional; default &quot;abort&quot;
222
     */
223
    public function setOnerror($action) {
224
    	$this->onError = $action;
225
    }
226
 
227
    /**
228
     * Sets the fetch mode to use for the PDO resultset.
229
     * @param mixed $mode The PDO fetchmode integer or constant name.
230
     */
231
    public function setFetchmode($mode) {
232
    	if (is_numeric($mode)) {
233
    		$this->fetchMode = (int) $mode;
234
    	} else {
235
    		if (defined($mode)) {
236
    			$this->fetchMode = constant($mode);
237
    		} else {
238
    			throw new BuildException("Invalid PDO fetch mode specified: " . $mode, $this->getLocation());
239
    		}
240
    	}
241
    }
242
 
243
    /**
244
     * Gets a default output writer for this task.
245
     * @return Writer
246
     */
247
    private function getDefaultOutput()
248
    {
249
    	return new LogWriter($this);
250
    }
251
 
252
    /**
253
     * Load the sql file and then execute it
254
     * @throws BuildException
255
     */
256
    public function main()  {
257
 
258
    	// Set a default fetchmode if none was specified
259
    	// (We're doing that here to prevent errors loading the class is PDO is not available.)
260
    	if ($this->fetchMode === null) {
261
    		$this->fetchMode = PDO::FETCH_BOTH;
262
    	}
263
 
264
    	// Initialize the formatters here.  This ensures that any parameters passed to the formatter
265
    	// element get passed along to the actual formatter object
266
    	foreach($this->formatters as $fe) {
267
    		$fe->prepare();
268
    	}
269
 
270
    	$savedTransaction = array();
271
    	for($i=0,$size=count($this->transactions); $i < $size; $i++) {
272
    		$savedTransaction[] = clone $this->transactions[$i];
273
    	}
274
 
275
    	$savedSqlCommand = $this->sqlCommand;
276
 
277
    	$this->sqlCommand = trim($this->sqlCommand);
278
 
279
    	try {
280
    		if ($this->srcFile === null && $this->sqlCommand === ""
281
    		&& empty($this->filesets)) {
282
    			if (count($this->transactions) === 0) {
283
    				throw new BuildException("Source file or fileset, "
284
    				. "transactions or sql statement "
285
    				. "must be set!", $this->location);
286
    			}
287
    		}
288
 
289
    		if ($this->srcFile !== null && !$this->srcFile->exists()) {
290
    			throw new BuildException("Source file does not exist!", $this->location);
291
    		}
292
 
293
    		// deal with the filesets
294
    		foreach($this->filesets as $fs) {
295
    			$ds = $fs->getDirectoryScanner($this->project);
296
    			$srcDir = $fs->getDir($this->project);
297
    			$srcFiles = $ds->getIncludedFiles();
298
    			// Make a transaction for each file
299
    			foreach($srcFiles as $srcFile) {
300
    				$t = $this->createTransaction();
301
    				$t->setSrc(new PhingFile($srcDir, $srcFile));
302
    			}
303
    		}
304
 
305
    		// Make a transaction group for the outer command
306
    		$t = $this->createTransaction();
307
    		if ($this->srcFile) $t->setSrc($this->srcFile);
308
    		$t->addText($this->sqlCommand);
309
    		$this->conn = $this->getConnection();
310
 
311
    		try {
312
 
313
    			$this->statement = null;
314
 
315
    			// Initialize the formatters.
316
    			$this->initFormatters();
317
 
318
    			try {
319
 
320
    				// Process all transactions
321
    				for ($i=0,$size=count($this->transactions); $i < $size; $i++) {
322
    					if (!$this->isAutocommit()) {
323
    						$this->log("Beginning transaction", Project::MSG_VERBOSE);
324
    						$this->conn->beginTransaction();
325
    					}
326
    					$this->transactions[$i]->runTransaction();
327
    					if (!$this->isAutocommit()) {
328
    						$this->log("Commiting transaction", Project::MSG_VERBOSE);
329
    						$this->conn->commit();
330
    					}
331
    				}
332
    			} catch (Exception $e) {
333
    				throw $e;
334
    			}
335
    		} catch (IOException $e) {
336
    			if (!$this->isAutocommit() && $this->conn !== null && $this->onError == "abort") {
337
    				try {
338
    					$this->conn->rollback();
339
    				} catch (PDOException $ex) {}
340
    			}
341
    			throw new BuildException($e->getMessage(), $this->location);
342
    		} catch (PDOException $e){
343
    			if (!$this->isAutocommit() && $this->conn !== null && $this->onError == "abort") {
344
    				try {
345
    					$this->conn->rollback();
346
    				} catch (PDOException $ex) {}
347
    			}
348
    			throw new BuildException($e->getMessage(), $this->location);
349
    		}
350
 
351
    		// Close the formatters.
352
    		$this->closeFormatters();
353
 
354
    		$this->log($this->goodSql . " of " . $this->totalSql .
355
                " SQL statements executed successfully");
356
 
357
    	} catch (Exception $e) {
358
    		$this->transactions = $savedTransaction;
359
    		$this->sqlCommand = $savedSqlCommand;
360
    		throw $e;
361
    	}
362
    	// finally {
363
    	$this->transactions = $savedTransaction;
364
    	$this->sqlCommand = $savedSqlCommand;
365
 
366
    }
367
 
368
 
369
    /**
370
     * read in lines and execute them
371
     * @throws PDOException, IOException
372
     */
373
    public function runStatements(Reader $reader) {
374
    	$sql = "";
375
		$line = "";
376
		$sqlBacklog = "";
377
		$hasQuery = false;
378
 
379
		$in = new BufferedReader($reader);
380
 
381
		try {
382
			while (($line = $in->readLine()) !== null) {
383
				$line = trim($line);
384
				$line = ProjectConfigurator::replaceProperties($this->project, $line,
385
						$this->project->getProperties());
386
 
387
				if (StringHelper::startsWith("//", $line) ||
388
					StringHelper::startsWith("--", $line) ||
389
					StringHelper::startsWith("#", $line)) {
390
					continue;
391
				}
392
 
393
				if (strlen($line) > 4
394
						&& strtoupper(substr($line,0, 4)) == "REM ") {
395
					continue;
396
				}
397
 
398
				if ($sqlBacklog !== "") {
399
					$sql = $sqlBacklog;
400
					$sqlBacklog = "";
401
				}
402
 
403
				$sql .= " " . $line . "\n";
404
 
405
				// SQL defines "--" as a comment to EOL
406
				// and in Oracle it may contain a hint
407
				// so we cannot just remove it, instead we must end it
408
				if (strpos($line, "--") !== false) {
409
					$sql .= "\n";
410
				}
411
 
412
				// DELIM_ROW doesn't need this (as far as i can tell)
413
				if ($this->delimiterType == self::DELIM_NORMAL) {
414
 
415
					$reg = "#((?:\"(?:\\\\.|[^\"])*\"?)+|'(?:\\\\.|[^'])*'?|" . preg_quote($this->delimiter) . ")#";
416
 
417
					$sqlParts = preg_split($reg, $sql, 0, PREG_SPLIT_DELIM_CAPTURE);
418
					$sqlBacklog = "";
419
					foreach ($sqlParts as $sqlPart) {
420
						// we always want to append, even if it's a delim (which will be stripped off later)
421
						$sqlBacklog .= $sqlPart;
422
 
423
						// we found a single (not enclosed by ' or ") delimiter, so we can use all stuff before the delim as the actual query
424
						if ($sqlPart === $this->delimiter) {
425
							$sql = $sqlBacklog;
426
							$sqlBacklog = "";
427
							$hasQuery = true;
428
						}
429
					}
430
				}
431
 
432
				if ($hasQuery || ($this->delimiterType == self::DELIM_ROW && $line == $this->delimiter)) {
433
					// this assumes there is always a delimter on the end of the SQL statement.
434
					$sql = StringHelper::substring($sql, 0, strlen($sql) - 1 - strlen($this->delimiter));
435
					$this->log("SQL: " . $sql, Project::MSG_VERBOSE);
436
					$this->execSQL($sql);
437
					$sql = "";
438
					$hasQuery = false;
439
				}
440
			}
441
 
442
			// Catch any statements not followed by ;
443
			if ($sql !== "") {
444
				$this->execSQL($sql);
445
			}
446
		} catch (PDOException $e) {
447
			throw $e;
448
		}
449
    }
450
 
451
    /**
452
     * Whether the passed-in SQL statement is a SELECT statement.
453
     * This does a pretty simple match, checking to see if statement starts with
454
     * 'select' (but not 'select into').
455
     *
456
     * @param string $sql
457
     * @return boolean Whether specified SQL looks like a SELECT query.
458
     */
459
    protected function isSelectSql($sql)
460
    {
461
    	$sql = trim($sql);
462
    	return (stripos($sql, 'select') === 0 && stripos($sql, 'select into ') !== 0);
463
    }
464
 
465
    /**
466
     * Exec the sql statement.
467
     * @throws PDOException
468
     */
469
    protected function execSQL($sql) {
470
 
471
    	// Check and ignore empty statements
472
    	if (trim($sql) == "") {
473
    		return;
474
    	}
475
 
476
    	try {
477
    		$this->totalSql++;
478
 
479
    		$this->statement = $this->conn->prepare($sql);
480
    		$this->statement->execute();
481
    		$this->log($this->statement->rowCount() . " rows affected", Project::MSG_VERBOSE);
482
 
483
    		// only call processResults() for statements that return actual data (such as 'select')
484
    		if ($this->statement->columnCount() > 0)
485
    		{
486
    			$this->processResults();
487
    		}
488
 
489
    		$this->statement->closeCursor();
490
    		$this->statement = null;
491
 
492
    		$this->goodSql++;
493
 
494
    	} catch (PDOException $e) {
495
    		$this->log("Failed to execute: " . $sql, Project::MSG_ERR);
496
    		if ($this->onError != "continue") {
497
    			throw new BuildException("Failed to execute SQL", $e);
498
    		}
499
    		$this->log($e->getMessage(), Project::MSG_ERR);
500
    	}
501
    }
502
 
503
    /**
504
     * Returns configured PDOResultFormatter objects (which were created from PDOSQLExecFormatterElement objects).
505
     * @return array PDOResultFormatter[]
506
     */
507
    protected function getConfiguredFormatters()
508
    {
509
    	$formatters = array();
510
    	foreach ($this->formatters as $fe) {
511
    		$formatters[] = $fe->getFormatter();
512
    	}
513
    	return $formatters;
514
    }
515
 
516
    /**
517
     * Initialize the formatters.
518
     */
519
    protected function initFormatters() {
520
    	$formatters = $this->getConfiguredFormatters();
521
    	foreach ($formatters as $formatter) {
522
    		$formatter->initialize();
523
    	}
524
 
525
    }
526
 
527
    /**
528
     * Run cleanup and close formatters.
529
     */
530
    protected function closeFormatters() {
531
    	$formatters = $this->getConfiguredFormatters();
532
    	foreach ($formatters as $formatter) {
533
    		$formatter->close();
534
    	}
535
    }
536
 
537
    /**
538
     * Passes results from query to any formatters.
539
     * @throw PDOException
540
     */
541
    protected function processResults() {
542
 
543
    	try {
544
 
545
    		$this->log("Processing new result set.", Project::MSG_VERBOSE);
546
 
547
    		$formatters = $this->getConfiguredFormatters();
548
 
549
	    	while ($row = $this->statement->fetch($this->fetchMode)) {
550
	    		foreach ($formatters as $formatter) {
551
	    			$formatter->processRow($row);
552
	    		}
553
	    	}
554
 
555
    	} catch (Exception $x) {
556
    		$this->log("Error processing reults: " . $x->getMessage(), Project::MSG_ERR);
557
    		foreach ($formatters as $formatter) {
558
	    		$formatter->close();
559
	    	}
560
    		throw $x;
561
    	}
562
 
563
    }
564
}
565
 
566
/**
567
 * "Inner" class that contains the definition of a new transaction element.
568
 * Transactions allow several files or blocks of statements
569
 * to be executed using the same JDBC connection and commit
570
 * operation in between.
571
 */
572
class PDOSQLExecTransaction {
573
 
574
	private $tSrcFile = null;
575
	private $tSqlCommand = "";
576
	private $parent;
577
 
578
	function __construct($parent)
579
	{
580
		// Parent is required so that we can log things ...
581
		$this->parent = $parent;
582
	}
583
 
584
	public function setSrc(PhingFile $src)
585
	{
586
		$this->tSrcFile = $src;
587
	}
588
 
589
	public function addText($sql)
590
	{
591
		$this->tSqlCommand .= $sql;
592
	}
593
 
594
    /**
595
     * @throws IOException, PDOException
596
     */
597
    public function runTransaction()
598
    {
599
    	if (!empty($this->tSqlCommand)) {
600
    		$this->parent->log("Executing commands", Project::MSG_INFO);
601
    		$this->parent->runStatements(new StringReader($this->tSqlCommand));
602
    	}
603
 
604
    	if ($this->tSrcFile !== null) {
605
    		$this->parent->log("Executing file: " . $this->tSrcFile->getAbsolutePath(),
606
    		Project::MSG_INFO);
607
    		$reader = new FileReader($this->tSrcFile);
608
    		$this->parent->runStatements($reader);
609
    		$reader->close();
610
    	}
611
    }
612
}
613
 
614