Subversion-Projekte lars-tiefland.php_share

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 1
<?php
2
 
3
/*
4
 *  $Id: PropelSQLExec.php 1262 2009-10-26 20:54:39Z francois $
5
 *
6
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
7
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
8
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
9
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
10
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
11
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
12
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
13
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
14
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
15
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
16
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
17
 *
18
 * This software consists of voluntary contributions made by many individuals
19
 * and is licensed under the LGPL. For more information please see
20
 * <http://propel.phpdb.org>.
21
 */
22
 
23
require_once 'phing/Task.php';
24
 
25
/**
26
 * Executes all SQL files referenced in the sqldbmap file against their mapped databases.
27
 *
28
 * This task uses an SQL -> Database map in the form of a properties
29
 * file to insert each SQL file listed into its designated database.
30
 *
31
 * @author     Hans Lellelid <hans@xmpl.org>
32
 * @author     Dominik del Bondio
33
 * @author     Jeff Martin <jeff@custommonkey.org> (Torque)
34
 * @author     Michael McCallum <gholam@xtra.co.nz> (Torque)
35
 * @author     Tim Stephenson <tim.stephenson@sybase.com> (Torque)
36
 * @author     Jason van Zyl <jvanzyl@apache.org> (Torque)
37
 * @author     Martin Poeschl <mpoeschl@marmot.at> (Torque)
38
 * @version    $Revision: 1262 $
39
 * @package    propel.phing
40
 */
41
class PropelSQLExec extends Task {
42
 
43
	private $goodSql = 0;
44
	private $totalSql = 0;
45
 
46
	const DELIM_ROW = "row";
47
	const DELIM_NORMAL = "normal";
48
 
49
	/**
50
	 * The delimiter type indicating whether the delimiter will
51
	 * only be recognized on a line by itself
52
	 */
53
	private $delimiterType = "normal"; // can't use constant just defined
54
 
55
	//private static $delimiterTypes = array(DELIM_NORMAL, DELIM_ROW);
56
	//private static $errorActions = array("continue", "stop", "abort");
57
 
58
	/** PDO Database connection */
59
	private $conn = null;
60
 
61
	/** Autocommit flag. Default value is false */
62
	private $autocommit = false;
63
 
64
	/** DB url. */
65
	private $url = null;
66
 
67
	/** User name. */
68
	private $userId = null;
69
 
70
	/** Password */
71
	private $password = null;
72
 
73
	/** SQL input command */
74
	private $sqlCommand = "";
75
 
76
	/** SQL transactions to perform */
77
	private $transactions = array();
78
 
79
	/** SQL Statement delimiter */
80
	private $delimiter = ";";
81
 
82
	/** Print SQL results. */
83
	private $print = false;
84
 
85
	/** Print header columns. */
86
	private $showheaders = true;
87
 
88
	/** Results Output file. */
89
	private $output = null;
90
 
91
	/** RDBMS Product needed for this SQL. */
92
	private $rdbms = null;
93
 
94
	/** RDBMS Version needed for this SQL. */
95
	private $version = null;
96
 
97
	/** Action to perform if an error is found */
98
	private $onError = "abort";
99
 
100
	/** Encoding to use when reading SQL statements from a file */
101
	private $encoding = null;
102
 
103
	/** Src directory for the files listed in the sqldbmap. */
104
	private $srcDir;
105
 
106
	/** Properties file that maps an individual SQL file to a database. */
107
	private $sqldbmap;
108
 
109
	/**
110
	 * Set the sqldbmap properties file.
111
	 *
112
	 * @param      sqldbmap filename for the sqldbmap
113
	 */
114
	public function setSqlDbMap($sqldbmap)
115
	{
116
		$this->sqldbmap = $this->project->resolveFile($sqldbmap);
117
	}
118
 
119
	/**
120
	 * Get the sqldbmap properties file.
121
	 *
122
	 * @return     filename for the sqldbmap
123
	 */
124
	public function getSqlDbMap()
125
	{
126
		return $this->sqldbmap;
127
	}
128
 
129
	/**
130
	 * Set the src directory for the sql files listed in the sqldbmap file.
131
	 *
132
	 * @param      PhingFile $srcDir sql source directory
133
	 */
134
	public function setSrcDir(PhingFile $srcDir)
135
	{
136
		$this->srcDir = $srcDir;
137
	}
138
 
139
	/**
140
	 * Get the src directory for the sql files listed in the sqldbmap file.
141
	 *
142
	 * @return     PhingFile SQL Source directory
143
	 */
144
	public function getSrcDir()
145
	{
146
		return $this->srcDir;
147
	}
148
 
149
	/**
150
	 * Set the sql command to execute
151
	 *
152
	 * @param      sql sql command to execute
153
	 */
154
	public function addText($sql)
155
	{
156
		$this->sqlCommand .= $sql;
157
	}
158
 
159
	/**
160
	 * Set the DB connection url.
161
	 *
162
	 * @param      string $url connection url
163
	 */
164
	public function setUrl($url)
165
	{
166
		$this->url = $url;
167
	}
168
 
169
	/**
170
	 * Set the user name for the DB connection.
171
	 *
172
	 * @param      string $userId database user
173
	 * @deprecated Specify userid in the DSN URL.
174
	 */
175
	public function setUserid($userId)
176
	{
177
		$this->userId = $userId;
178
	}
179
 
180
	/**
181
	 * Set the password for the DB connection.
182
	 *
183
	 * @param      string $password database password
184
	 * @deprecated Specify password in the DSN URL.
185
	 */
186
	public function setPassword($password)
187
	{
188
		$this->password = $password;
189
	}
190
 
191
	/**
192
	 * Set the autocommit flag for the DB connection.
193
	 *
194
	 * @param      boolean $autocommit the autocommit flag
195
	 */
196
	public function setAutoCommit($autocommit)
197
	{
198
		$this->autocommit = (boolean) $autocommit;
199
	}
200
 
201
	/**
202
	 * Set the statement delimiter.
203
	 *
204
	 * <p>For example, set this to "go" and delimitertype to "ROW" for
205
	 * Sybase ASE or MS SQL Server.</p>
206
	 *
207
	 * @param      string $delimiter
208
	 */
209
	public function setDelimiter($delimiter)
210
	{
211
		$this->delimiter = $delimiter;
212
	}
213
 
214
	/**
215
	 * Set the Delimiter type for this sql task. The delimiter type takes two
216
	 * values - normal and row. Normal means that any occurence of the delimiter
217
	 * terminate the SQL command whereas with row, only a line containing just
218
	 * the delimiter is recognized as the end of the command.
219
	 *
220
	 * @param      string $delimiterType
221
	 */
222
	public function setDelimiterType($delimiterType)
223
	{
224
		$this->delimiterType = $delimiterType;
225
	}
226
 
227
	/**
228
	 * Set the print flag.
229
	 *
230
	 * @param      boolean $print
231
	 */
232
	public function setPrint($print)
233
	{
234
		$this->print = (boolean) $print;
235
	}
236
 
237
	/**
238
	 * Set the showheaders flag.
239
	 *
240
	 * @param      boolean $showheaders
241
	 */
242
	public function setShowheaders($showheaders)
243
	{
244
		$this->showheaders = (boolean) $showheaders;
245
	}
246
 
247
	/**
248
	 * Set the output file.
249
	 *
250
	 * @param      PhingFile $output
251
	 */
252
	public function setOutput(PhingFile $output)
253
	{
254
		$this->output = $output;
255
	}
256
 
257
	/**
258
	 * Set the action to perform onerror
259
	 *
260
	 * @param      string $action
261
	 */
262
	public function setOnerror($action)
263
	{
264
		$this->onError = $action;
265
	}
266
 
267
	/**
268
	 * Load the sql file and then execute it
269
	 *
270
	 * @throws     BuildException
271
	 */
272
	public function main()
273
	{
274
		$this->sqlCommand = trim($this->sqlCommand);
275
 
276
		if ($this->sqldbmap === null || $this->getSqlDbMap()->exists() === false) {
277
			throw new BuildException("You haven't provided an sqldbmap, or "
278
					. "the one you specified doesn't exist: " . $this->sqldbmap->getPath());
279
		}
280
 
281
		if ($this->url === null) {
282
			throw new BuildException("DSN url attribute must be set!");
283
		}
284
 
285
		$map = new Properties();
286
 
287
		try {
288
			$map->load($this->getSqlDbMap());
289
		} catch (IOException $ioe) {
290
			throw new BuildException("Cannot open and process the sqldbmap!");
291
		}
292
 
293
		$databases = array();
294
 
295
		foreach ($map->keys() as $sqlfile) {
296
 
297
			$database = $map->getProperty($sqlfile);
298
 
299
			// Q: already there?
300
			if (!isset($databases[$database])) {
301
			// A: No.
302
				$databases[$database] = array();
303
			}
304
 
305
			// We want to make sure that the base schemas
306
			// are inserted first.
307
			if (strpos($sqlfile, "schema.sql") !== false) {
308
				// add to the beginning of the array
309
				array_unshift($databases[$database], $sqlfile);
310
			} else {
311
				array_push($databases[$database], $sqlfile);
312
			}
313
		}
314
 
315
		foreach ($databases as $db => $files) {
316
			$transactions = array();
317
 
318
			foreach ($files as $fileName) {
319
 
320
				$file = new PhingFile($this->srcDir, $fileName);
321
 
322
				if ($file->exists()) {
323
					$this->log("Executing statements in file: " . $file->__toString());
324
					$transaction = new PropelSQLExecTransaction($this);
325
					$transaction->setSrc($file);
326
					$transactions[] = $transaction;
327
				} else {
328
					$this->log("File '" . $file->__toString()
329
							. "' in sqldbmap does not exist, so skipping it.");
330
				}
331
			}
332
			$this->insertDatabaseSqlFiles($this->url, $db, $transactions);
333
		}
334
	}
335
 
336
	/**
337
	 * Take the base url, the target database and insert a set of SQL
338
	 * files into the target database.
339
	 *
340
	 * @param      string $url
341
	 * @param      string $database
342
	 * @param      array $transactions
343
	 */
344
	private function insertDatabaseSqlFiles($url, $database, $transactions)
345
	{
346
		$url = str_replace("@DB@", $database, $url);
347
		$this->log("Our new url -> " . $url);
348
 
349
		try {
350
 
351
			$buf = "Database settings:" . PHP_EOL
352
			. " URL: " . $url . PHP_EOL
353
			. ($this->userId ? " user: " . $this->userId . PHP_EOL : "")
354
			. ($this->password ? " password: " . $this->password . PHP_EOL : "");
355
 
356
			$this->log($buf, Project::MSG_VERBOSE);
357
 
358
			// Set user + password to null if they are empty strings
359
			if (!$this->userId) { $this->userId = null; }
360
 
361
			if (!$this->password) { $this->password = null; }
362
 
363
			$this->conn = new PDO($url, $this->userId, $this->password);
364
			$this->conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
365
 
366
			// $this->conn->setAutoCommit($this->autocommit);
367
			// $this->statement = $this->conn->createStatement();
368
 
369
			$out = null;
370
 
371
			try {
372
				if ($this->output !== null) {
373
					$this->log("Opening PrintStream to output file " . $this->output->__toString(), Project::MSG_VERBOSE);
374
					$out = new FileWriter($this->output);
375
				}
376
 
377
				// Process all transactions
378
				for ($i=0,$size=count($transactions); $i < $size; $i++) {
379
					$transactions[$i]->runTransaction($out);
380
					if (!$this->autocommit) {
381
						$this->log("Commiting transaction", Project::MSG_VERBOSE);
382
						$this->conn->commit();
383
					}
384
				}
385
			} catch (Exception $e) {
386
				if ($out) $out->close();
387
			}
388
 
389
		} catch (IOException $e) {
390
 
391
			if (!$this->autocommit && $this->conn !== null && $this->onError == "abort") {
392
				try {
393
					$this->conn->rollBack();
394
				} catch (PDOException $ex) {
395
					// do nothing.
396
					System::println("Rollback failed.");
397
				}
398
			}
399
			if ($this->statement) $this->statement = null; // close
400
			throw new BuildException($e);
401
		} catch (PDOException $e) {
402
			if (!$this->autocommit && $this->conn !== null && $this->onError == "abort") {
403
				try {
404
					$this->conn->rollBack();
405
				} catch (PDOException $ex) {
406
					// do nothing.
407
					System::println("Rollback failed");
408
				}
409
			}
410
			if ($this->statement) $this->statement = null; // close
411
			throw new BuildException($e);
412
		}
413
 
414
		   $this->statement = null; // close
415
 
416
		$this->log($this->goodSql . " of " . $this->totalSql
417
				. " SQL statements executed successfully");
418
	}
419
 
420
	/**
421
	 * Read the statements from the .sql file and execute them.
422
	 * Lines starting with '//', '--' or 'REM ' are ignored.
423
	 *
424
	 * Developer note:  must be public in order to be called from
425
	 * sudo-"inner" class PropelSQLExecTransaction.
426
	 *
427
	 * @param      Reader $reader
428
	 * @param      $out Optional output stream.
429
	 * @throws     PDOException
430
	 * @throws     IOException
431
	 */
432
	public function runStatements(Reader $reader, $out = null)
433
	{
434
		$sql = "";
435
		$line = "";
436
		$sqlBacklog = "";
437
		$hasQuery = false;
438
 
439
		$in = new BufferedReader($reader);
440
 
441
		$parser['pointer'] = 0;
442
		$parser['isInString'] = false;
443
		$parser['stringQuotes'] = "";
444
		$parser['backslashCount'] = 0;
445
		$parser['parsedString'] = "";
446
 
447
		$sqlParts = array();
448
 
449
		while (($line = $in->readLine()) !== null) {
450
 
451
			$line = trim($line);
452
			$line = ProjectConfigurator::replaceProperties($this->project, $line,
453
			$this->project->getProperties());
454
 
455
			if (StringHelper::startsWith("//", $line)
456
				|| StringHelper::startsWith("--", $line)
457
		 		|| StringHelper::startsWith("#", $line)) {
458
				continue;
459
			}
460
 
461
			if (strlen($line) > 4 && strtoupper(substr($line,0, 4)) == "REM ") {
462
				continue;
463
			}
464
 
465
			if ($sqlBacklog !== "") {
466
				$sql = $sqlBacklog;
467
				$sqlBacklog = "";
468
			}
469
 
470
			$sql .= " " . $line . PHP_EOL;
471
 
472
			// SQL defines "--" as a comment to EOL
473
			// and in Oracle it may contain a hint
474
			// so we cannot just remove it, instead we must end it
475
			if (strpos($line, "--") !== false) {
476
				$sql .= PHP_EOL;
477
			}
478
 
479
			// DELIM_ROW doesn't need this (as far as i can tell)
480
			if ($this->delimiterType == self::DELIM_NORMAL) {
481
 
482
				// old regex, being replaced due to segfaults:
483
				// See: http://propel.phpdb.org/trac/ticket/294
484
				//$reg = "#((?:\"(?:\\\\.|[^\"])*\"?)+|'(?:\\\\.|[^'])*'?|" . preg_quote($this->delimiter) . ")#";
485
				//$sqlParts = preg_split($reg, $sql, 0, PREG_SPLIT_DELIM_CAPTURE);
486
 
487
				$i = $parser['pointer'];
488
				$c = strlen($sql);
489
				while ($i < $c) {
490
 
491
					$char = $sql[$i];
492
 
493
					switch($char) {
494
						case "\\":
495
							$parser['backslashCount']++;
496
							$this->log("c$i: found ".$parser['backslashCount']." backslash(es)", Project::MSG_VERBOSE);
497
							break;
498
						case "'":
499
						case "\"":
500
							if ($parser['isInString'] && $parser['stringQuotes'] == $char) {
501
								if (($parser['backslashCount'] & 1) == 0) {
502
									#$this->log("$i: out of string", Project::MSG_VERBOSE);
503
									$parser['isInString'] = false;
504
								} else {
505
									$this->log("c$i: rejected quoted delimiter", Project::MSG_VERBOSE);
506
								}
507
 
508
							} elseif (!$parser['isInString']) {
509
								$parser['stringQuotes']	= $char;
510
								$parser['isInString'] = true;
511
								#$this->log("$i: into string with $parser['stringQuotes']", Project::MSG_VERBOSE);
512
							}
513
							break;
514
					}
515
 
516
					if ($char == $this->delimiter && !$parser['isInString']) {
517
						$this->log("c$i: valid end of command found!", Project::MSG_VERBOSE);
518
						$sqlParts[] = $parser['parsedString'];
519
						$sqlParts[] = $this->delimiter;
520
						break;
521
					}
522
					$parser['parsedString'] .= $char;
523
					if ($char !== "\\") {
524
						if ($parser['backslashCount']) $this->log("$i: backslash reset", Project::MSG_VERBOSE);
525
						$parser['backslashCount'] = 0;
526
					}
527
					$i++;
528
					$parser['pointer']++;
529
				}
530
 
531
				$sqlBacklog = "";
532
				foreach ($sqlParts as $sqlPart) {
533
					// we always want to append, even if it's a delim (which will be stripped off later)
534
					$sqlBacklog .= $sqlPart;
535
 
536
					// we found a single (not enclosed by ' or ") delimiter, so we can use all stuff before the delim as the actual query
537
					if ($sqlPart === $this->delimiter) {
538
						$sql = $sqlBacklog;
539
						$sqlBacklog = "";
540
						$hasQuery = true;
541
					}
542
				}
543
			}
544
 
545
			if ($hasQuery || ($this->delimiterType == self::DELIM_ROW && $line == $this->delimiter)) {
546
				// this assumes there is always a delimter on the end of the SQL statement.
547
				$sql = StringHelper::substring($sql, 0, strlen($sql) - 1 - strlen($this->delimiter));
548
				$this->log("SQL: " . $sql, Project::MSG_VERBOSE);
549
				$this->execSQL($sql, $out);
550
				$sql = "";
551
				$hasQuery = false;
552
 
553
				$parser['pointer'] = 0;
554
				$parser['isInString'] = false;
555
				$parser['stringQuotes'] = "";
556
				$parser['backslashCount'] = 0;
557
				$parser['parsedString'] = "";
558
				$sqlParts = array();
559
			}
560
		}
561
 
562
		// Catch any statements not followed by ;
563
		if ($sql !== "") {
564
			$this->execSQL($sql, $out);
565
		}
566
	}
567
 
568
	/**
569
	 * Exec the sql statement.
570
	 *
571
	 * @param      sql
572
	 * @param      out
573
	 * @throws     PDOException
574
	 */
575
	protected function execSQL($sql, $out = null)
576
	{
577
		// Check and ignore empty statements
578
		if (trim($sql) == "") {
579
			return;
580
		}
581
 
582
		try {
583
			$this->totalSql++;
584
 
585
			if (!$this->autocommit) $this->conn->beginTransaction();
586
 
587
			$stmt = $this->conn->prepare($sql);
588
			$stmt->execute();
589
			$this->log($stmt->rowCount() . " rows affected", Project::MSG_VERBOSE);
590
 
591
			if (!$this->autocommit) $this->conn->commit();
592
 
593
			$this->goodSql++;
594
		} catch (PDOException $e) {
595
			$this->log("Failed to execute: " . $sql, Project::MSG_ERR);
596
			if ($this->onError != "continue") {
597
				throw $e;
598
			}
599
			$this->log($e->getMessage(), Project::MSG_ERR);
600
		}
601
	}
602
 
603
	/**
604
	 * print any results in the statement.
605
	 *
606
	 * @param      out
607
	 * @throws     PDOException
608
	 */
609
	protected function printResults($out = null)
610
	{
611
		$rs = null;
612
 
613
		do {
614
			$rs = $this->statement->getResultSet();
615
 
616
			if ($rs !== null) {
617
 
618
				$this->log("Processing new result set.", Project::MSG_VERBOSE);
619
 
620
				$line = "";
621
 
622
				$colsprinted = false;
623
 
624
				while ($rs->next()) {
625
 
626
					if (!$colsprinted && $this->showheaders) {
627
						$first = true;
628
						foreach ($this->fields as $fieldName => $ignore) {
629
							if ($first) $first = false; else $line .= ",";
630
							$line .= $fieldName;
631
						}
632
					} // if show headers
633
 
634
					$first = true;
635
					foreach ($rs->fields as $columnValue) {
636
 
637
						if ($columnValue != null) {
638
							$columnValue = trim($columnValue);
639
						}
640
 
641
						if ($first) {
642
							$first = false;
643
						} else {
644
							$line .= ",";
645
						}
646
						$line .= $columnValue;
647
					}
648
 
649
					if ($out !== null) {
650
						$out->write($line);
651
						$out->newLine();
652
					}
653
 
654
					System::println($line);
655
					$line = "";
656
				} // while rs->next()
657
			}
658
		} while ($this->statement->getMoreResults());
659
		System::println();
660
		if ($out !== null) $out->newLine();
661
	}
662
 
663
}
664
 
665
/**
666
 * "Inner" class that contains the definition of a new transaction element.
667
 * Transactions allow several files or blocks of statements
668
 * to be executed using the same Creole connection and commit
669
 * operation in between.
670
 * @package    propel.phing
671
 */
672
class PropelSQLExecTransaction {
673
 
674
	private $tSrcFile = null;
675
	private $tSqlCommand = "";
676
	private $parent;
677
 
678
	function __construct($parent)
679
	{
680
		// Parent is required so that we can log things ...
681
		$this->parent = $parent;
682
	}
683
 
684
	public function setSrc(PhingFile $src)
685
	{
686
		$this->tSrcFile = $src;
687
	}
688
 
689
	public function addText($sql)
690
	{
691
		$this->tSqlCommand .= $sql;
692
	}
693
 
694
	/**
695
	 * @throws     IOException, PDOException
696
	 */
697
	public function runTransaction($out = null)
698
	{
699
		if (!empty($this->tSqlCommand)) {
700
			$this->parent->log("Executing commands", Project::MSG_INFO);
701
			$this->parent->runStatements($this->tSqlCommand, $out);
702
		}
703
 
704
		if ($this->tSrcFile !== null) {
705
			$this->parent->log("Executing file: " . $this->tSrcFile->getAbsolutePath(), Project::MSG_INFO);
706
			$reader = new FileReader($this->tSrcFile);
707
			$this->parent->runStatements($reader, $out);
708
			$reader->close();
709
		}
710
	}
711
}