CreoleSQLExecTask.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593
  1. <?php
  2. /*
  3. * $Id: CreoleSQLExecTask.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/creole/CreoleTask.php';
  22. include_once 'phing/system/io/StringReader.php';
  23. /**
  24. * Executes a series of SQL statements on a database using Creole.
  25. *
  26. * <p>Statements can
  27. * either be read in from a text file using the <i>src</i> attribute or from
  28. * between the enclosing SQL tags.</p>
  29. *
  30. * <p>Multiple statements can be provided, separated by semicolons (or the
  31. * defined <i>delimiter</i>). Individual lines within the statements can be
  32. * commented using either --, // or REM at the start of the line.</p>
  33. *
  34. * <p>The <i>autocommit</i> attribute specifies whether auto-commit should be
  35. * turned on or off whilst executing the statements. If auto-commit is turned
  36. * on each statement will be executed and committed. If it is turned off the
  37. * statements will all be executed as one transaction.</p>
  38. *
  39. * <p>The <i>onerror</i> attribute specifies how to proceed when an error occurs
  40. * during the execution of one of the statements.
  41. * The possible values are: <b>continue</b> execution, only show the error;
  42. * <b>stop</b> execution and commit transaction;
  43. * and <b>abort</b> execution and transaction and fail task.</p>
  44. *
  45. * @author Hans Lellelid <hans@xmpl.org> (Phing)
  46. * @author Jeff Martin <jeff@custommonkey.org> (Ant)
  47. * @author Michael McCallum <gholam@xtra.co.nz> (Ant)
  48. * @author Tim Stephenson <tim.stephenson@sybase.com> (Ant)
  49. * @package phing.tasks.ext.creole
  50. * @version $Revision: 905 $
  51. */
  52. class CreoleSQLExecTask extends CreoleTask {
  53. private $goodSql = 0;
  54. private $totalSql = 0;
  55. const DELIM_ROW = "row";
  56. const DELIM_NORMAL = "normal";
  57. /**
  58. * Database connection
  59. */
  60. private $conn = null;
  61. /**
  62. * files to load
  63. */
  64. private $filesets = array();
  65. /**
  66. * all filterchains objects assigned to this task
  67. */
  68. private $filterChains = array();
  69. /**
  70. * SQL statement
  71. */
  72. private $statement = null;
  73. /**
  74. * SQL input file
  75. */
  76. private $srcFile = null;
  77. /**
  78. * SQL input command
  79. */
  80. private $sqlCommand = "";
  81. /**
  82. * SQL transactions to perform
  83. */
  84. private $transactions = array();
  85. /**
  86. * SQL Statement delimiter
  87. */
  88. private $delimiter = ";";
  89. /**
  90. * The delimiter type indicating whether the delimiter will
  91. * only be recognized on a line by itself
  92. */
  93. private $delimiterType = "normal"; // can't use constant just defined
  94. /**
  95. * Print SQL results.
  96. */
  97. private $print = false;
  98. /**
  99. * Print header columns.
  100. */
  101. private $showheaders = true;
  102. /**
  103. * Results Output file.
  104. */
  105. private $output = null;
  106. /**
  107. * Action to perform if an error is found
  108. **/
  109. private $onError = "abort";
  110. /**
  111. * Encoding to use when reading SQL statements from a file
  112. */
  113. private $encoding = null;
  114. /**
  115. * Append to an existing file or overwrite it?
  116. */
  117. private $append = false;
  118. /**
  119. * Set the name of the SQL file to be run.
  120. * Required unless statements are enclosed in the build file
  121. */
  122. public function setSrc(PhingFile $srcFile) {
  123. $this->srcFile = $srcFile;
  124. }
  125. /**
  126. * Set an inline SQL command to execute.
  127. * NB: Properties are not expanded in this text.
  128. */
  129. public function addText($sql) {
  130. $this->sqlCommand .= $sql;
  131. }
  132. /**
  133. * Adds a set of files (nested fileset attribute).
  134. */
  135. public function addFileset(FileSet $set) {
  136. $this->filesets[] = $set;
  137. }
  138. /**
  139. * Creates a filterchain
  140. *
  141. * @access public
  142. * @return object The created filterchain object
  143. */
  144. function createFilterChain() {
  145. $num = array_push($this->filterChains, new FilterChain($this->project));
  146. return $this->filterChains[$num-1];
  147. }
  148. /**
  149. * Add a SQL transaction to execute
  150. */
  151. public function createTransaction() {
  152. $t = new SQLExecTransaction($this);
  153. $this->transactions[] = $t;
  154. return $t;
  155. }
  156. /**
  157. * Set the file encoding to use on the SQL files read in
  158. *
  159. * @param encoding the encoding to use on the files
  160. */
  161. public function setEncoding($encoding) {
  162. $this->encoding = $encoding;
  163. }
  164. /**
  165. * Set the statement delimiter.
  166. *
  167. * <p>For example, set this to "go" and delimitertype to "ROW" for
  168. * Sybase ASE or MS SQL Server.</p>
  169. *
  170. * @param delimiter
  171. */
  172. public function setDelimiter($delimiter)
  173. {
  174. $this->delimiter = $delimiter;
  175. }
  176. /**
  177. * Set the Delimiter type for this sql task. The delimiter type takes two
  178. * values - normal and row. Normal means that any occurence of the delimiter
  179. * terminate the SQL command whereas with row, only a line containing just
  180. * the delimiter is recognized as the end of the command.
  181. *
  182. * @param string $delimiterType
  183. */
  184. public function setDelimiterType($delimiterType)
  185. {
  186. $this->delimiterType = $delimiterType;
  187. }
  188. /**
  189. * Set the print flag.
  190. *
  191. * @param boolean $print
  192. */
  193. public function setPrint($print)
  194. {
  195. $this->print = (boolean) $print;
  196. }
  197. /**
  198. * Print headers for result sets from the
  199. * statements; optional, default true.
  200. * @param boolean $showheaders
  201. */
  202. public function setShowheaders($showheaders) {
  203. $this->showheaders = (boolean) $showheaders;
  204. }
  205. /**
  206. * Set the output file;
  207. * optional, defaults to the console.
  208. * @param PhingFile $output
  209. */
  210. public function setOutput(PhingFile $output) {
  211. $this->output = $output;
  212. }
  213. /**
  214. * whether output should be appended to or overwrite
  215. * an existing file. Defaults to false.
  216. * @param $append
  217. */
  218. public function setAppend($append) {
  219. $this->append = (boolean) $append;
  220. }
  221. /**
  222. * Action to perform when statement fails: continue, stop, or abort
  223. * optional; default &quot;abort&quot;
  224. */
  225. public function setOnerror($action) {
  226. $this->onError = $action;
  227. }
  228. /**
  229. * Load the sql file and then execute it
  230. * @throws BuildException
  231. */
  232. public function main() {
  233. $savedTransaction = array();
  234. for($i=0,$size=count($this->transactions); $i < $size; $i++) {
  235. $savedTransaction[] = clone $this->transactions[$i];
  236. }
  237. $savedSqlCommand = $this->sqlCommand;
  238. $this->sqlCommand = trim($this->sqlCommand);
  239. try {
  240. if ($this->srcFile === null && $this->sqlCommand === ""
  241. && empty($this->filesets)) {
  242. if (count($this->transactions) === 0) {
  243. throw new BuildException("Source file or fileset, "
  244. . "transactions or sql statement "
  245. . "must be set!", $this->location);
  246. }
  247. }
  248. if ($this->srcFile !== null && !$this->srcFile->exists()) {
  249. throw new BuildException("Source file does not exist!", $this->location);
  250. }
  251. // deal with the filesets
  252. for ($i = 0,$size=count($this->filesets); $i < $size; $i++) {
  253. $fs = $this->filesets[$i];
  254. $ds = $fs->getDirectoryScanner($this->project);
  255. $srcDir = $fs->getDir($this->project);
  256. $srcFiles = $ds->getIncludedFiles();
  257. // Make a transaction for each file
  258. for ($j=0, $size=count($srcFiles); $j < $size; $j++) {
  259. $t = $this->createTransaction();
  260. $t->setSrc(new PhingFile($srcDir, $srcFiles[$j]));
  261. }
  262. }
  263. // Make a transaction group for the outer command
  264. $t = $this->createTransaction();
  265. if ($this->srcFile) $t->setSrc($this->srcFile);
  266. $t->addText($this->sqlCommand);
  267. $this->conn = $this->getConnection();
  268. try {
  269. $this->statement = $this->conn->createStatement();
  270. $out = null;
  271. try {
  272. if ($this->output !== null) {
  273. $this->log("Opening output file " . $this->output, Project::MSG_VERBOSE);
  274. $out = new BufferedWriter(new FileWriter($this->output->getAbsolutePath(), $this->append));
  275. }
  276. // Process all transactions
  277. for ($i=0,$size=count($this->transactions); $i < $size; $i++) {
  278. $this->transactions[$i]->runTransaction($out);
  279. if (!$this->isAutocommit()) {
  280. $this->log("Commiting transaction", Project::MSG_VERBOSE);
  281. $this->conn->commit();
  282. }
  283. }
  284. if ($out) $out->close();
  285. } catch (Exception $e) {
  286. if ($out) $out->close();
  287. throw $e;
  288. }
  289. } catch (IOException $e) {
  290. if (!$this->isAutocommit() && $this->conn !== null && $this->onError == "abort") {
  291. try {
  292. $this->conn->rollback();
  293. } catch (SQLException $ex) {}
  294. }
  295. throw new BuildException($e->getMessage(), $this->location);
  296. } catch (SQLException $e){
  297. if (!$this->isAutocommit() && $this->conn !== null && $this->onError == "abort") {
  298. try {
  299. $this->conn->rollback();
  300. } catch (SQLException $ex) {}
  301. }
  302. throw new BuildException($e->getMessage(), $this->location);
  303. }
  304. $this->log($this->goodSql . " of " . $this->totalSql .
  305. " SQL statements executed successfully");
  306. } catch (Exception $e) {
  307. $this->transactions = $savedTransaction;
  308. $this->sqlCommand = $savedSqlCommand;
  309. throw $e;
  310. }
  311. // finally {
  312. $this->transactions = $savedTransaction;
  313. $this->sqlCommand = $savedSqlCommand;
  314. }
  315. /**
  316. * read in lines and execute them
  317. * @throws SQLException, IOException
  318. */
  319. public function runStatements(Reader $reader, $out = null) {
  320. $sql = "";
  321. $line = "";
  322. $buffer = '';
  323. if ((is_array($this->filterChains)) && (!empty($this->filterChains))) {
  324. $in = FileUtils::getChainedReader(new BufferedReader($reader), $this->filterChains, $this->getProject());
  325. while(-1 !== ($read = $in->read())) { // -1 indicates EOF
  326. $buffer .= $read;
  327. }
  328. $lines = explode("\n", $buffer);
  329. } else {
  330. $in = new BufferedReader($reader);
  331. while (($line = $in->readLine()) !== null) {
  332. $lines[] = $line;
  333. }
  334. }
  335. try {
  336. foreach ($lines as $line) {
  337. $line = trim($line);
  338. $line = ProjectConfigurator::replaceProperties($this->project, $line,
  339. $this->project->getProperties());
  340. if (StringHelper::startsWith("//", $line) ||
  341. StringHelper::startsWith("--", $line) ||
  342. StringHelper::startsWith("#", $line)) {
  343. continue;
  344. }
  345. if (strlen($line) > 4
  346. && strtoupper(substr($line,0, 4)) == "REM ") {
  347. continue;
  348. }
  349. $sql .= " " . $line;
  350. $sql = trim($sql);
  351. // SQL defines "--" as a comment to EOL
  352. // and in Oracle it may contain a hint
  353. // so we cannot just remove it, instead we must end it
  354. if (strpos($line, "--") !== false) {
  355. $sql .= "\n";
  356. }
  357. if ($this->delimiterType == self::DELIM_NORMAL
  358. && StringHelper::endsWith($this->delimiter, $sql)
  359. || $this->delimiterType == self::DELIM_ROW
  360. && $line == $this->delimiter) {
  361. $this->log("SQL: " . $sql, Project::MSG_VERBOSE);
  362. $this->execSQL(StringHelper::substring($sql, 0, strlen($sql) - strlen($this->delimiter)), $out);
  363. $sql = "";
  364. }
  365. }
  366. // Catch any statements not followed by ;
  367. if ($sql !== "") {
  368. $this->execSQL($sql, $out);
  369. }
  370. } catch (SQLException $e) {
  371. throw new BuildException("Error running statements", $e);
  372. }
  373. }
  374. /**
  375. * Exec the sql statement.
  376. * @throws SQLException
  377. */
  378. protected function execSQL($sql, $out = null) {
  379. // Check and ignore empty statements
  380. if (trim($sql) == "") {
  381. return;
  382. }
  383. try {
  384. $this->totalSql++;
  385. if (!$this->statement->execute($sql)) {
  386. $this->log($this->statement->getUpdateCount() . " rows affected", Project::MSG_VERBOSE);
  387. } else {
  388. if ($this->print) {
  389. $this->printResults($out);
  390. }
  391. }
  392. $this->goodSql++;
  393. } catch (SQLException $e) {
  394. $this->log("Failed to execute: " . $sql, Project::MSG_ERR);
  395. if ($this->onError != "continue") {
  396. throw new BuildException("Failed to execute SQL", $e);
  397. }
  398. $this->log($e->getMessage(), Project::MSG_ERR);
  399. }
  400. }
  401. /**
  402. * print any results in the statement.
  403. * @throw SQLException
  404. */
  405. protected function printResults($out = null) {
  406. $rs = null;
  407. do {
  408. $rs = $this->statement->getResultSet();
  409. if ($rs !== null) {
  410. $this->log("Processing new result set.", Project::MSG_VERBOSE);
  411. $line = "";
  412. $colsprinted = false;
  413. while ($rs->next()) {
  414. $fields = $rs->getRow();
  415. if (!$colsprinted && $this->showheaders) {
  416. $first = true;
  417. foreach($fields as $fieldName => $ignore) {
  418. if ($first) $first = false; else $line .= ",";
  419. $line .= $fieldName;
  420. }
  421. if ($out !== null) {
  422. $out->write($line);
  423. $out->newLine();
  424. } else {
  425. print($line.PHP_EOL);
  426. }
  427. $line = "";
  428. $colsprinted = true;
  429. } // if show headers
  430. $first = true;
  431. foreach($fields as $columnValue) {
  432. if ($columnValue != null) {
  433. $columnValue = trim($columnValue);
  434. }
  435. if ($first) {
  436. $first = false;
  437. } else {
  438. $line .= ",";
  439. }
  440. $line .= $columnValue;
  441. }
  442. if ($out !== null) {
  443. $out->write($line);
  444. $out->newLine();
  445. } else {
  446. print($line . PHP_EOL);
  447. }
  448. $line = "";
  449. } // while rs->next()
  450. }
  451. } while ($this->statement->getMoreResults());
  452. print(PHP_EOL);
  453. if ($out !== null) $out->newLine();
  454. }
  455. }
  456. /**
  457. * "Inner" class that contains the definition of a new transaction element.
  458. * Transactions allow several files or blocks of statements
  459. * to be executed using the same JDBC connection and commit
  460. * operation in between.
  461. *
  462. * @package phing.tasks.ext.creole
  463. */
  464. class SQLExecTransaction {
  465. private $tSrcFile = null;
  466. private $tSqlCommand = "";
  467. private $parent;
  468. function __construct($parent)
  469. {
  470. // Parent is required so that we can log things ...
  471. $this->parent = $parent;
  472. }
  473. public function setSrc(PhingFile $src)
  474. {
  475. $this->tSrcFile = $src;
  476. }
  477. public function addText($sql)
  478. {
  479. $this->tSqlCommand .= $sql;
  480. }
  481. /**
  482. * @throws IOException, SQLException
  483. */
  484. public function runTransaction($out = null)
  485. {
  486. if (!empty($this->tSqlCommand)) {
  487. $this->parent->log("Executing commands", Project::MSG_INFO);
  488. $this->parent->runStatements(new StringReader($this->tSqlCommand), $out);
  489. }
  490. if ($this->tSrcFile !== null) {
  491. $this->parent->log("Executing file: " . $this->tSrcFile->getAbsolutePath(),
  492. Project::MSG_INFO);
  493. $reader = new FileReader($this->tSrcFile);
  494. $this->parent->runStatements($reader, $out);
  495. $reader->close();
  496. }
  497. }
  498. }