PDOSQLExecTask.php 20 KB

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