You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

688 lines
16 KiB

7 years ago
  1. <?php
  2. /**
  3. * CodeIgniter
  4. *
  5. * An open source application development framework for PHP
  6. *
  7. * This content is released under the MIT License (MIT)
  8. *
  9. * Copyright (c) 2014 - 2017, British Columbia Institute of Technology
  10. *
  11. * Permission is hereby granted, free of charge, to any person obtaining a copy
  12. * of this software and associated documentation files (the "Software"), to deal
  13. * in the Software without restriction, including without limitation the rights
  14. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  15. * copies of the Software, and to permit persons to whom the Software is
  16. * furnished to do so, subject to the following conditions:
  17. *
  18. * The above copyright notice and this permission notice shall be included in
  19. * all copies or substantial portions of the Software.
  20. *
  21. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  22. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  23. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  24. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  25. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  26. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  27. * THE SOFTWARE.
  28. *
  29. * @package CodeIgniter
  30. * @author EllisLab Dev Team
  31. * @copyright Copyright (c) 2008 - 2014, EllisLab, Inc. (https://ellislab.com/)
  32. * @copyright Copyright (c) 2014 - 2017, British Columbia Institute of Technology (http://bcit.ca/)
  33. * @license http://opensource.org/licenses/MIT MIT License
  34. * @link https://codeigniter.com
  35. * @since Version 1.4.1
  36. * @filesource
  37. */
  38. defined('BASEPATH') OR exit('No direct script access allowed');
  39. /**
  40. * oci8 Database Adapter Class
  41. *
  42. * Note: _DB is an extender class that the app controller
  43. * creates dynamically based on whether the query builder
  44. * class is being used or not.
  45. *
  46. * @package CodeIgniter
  47. * @subpackage Drivers
  48. * @category Database
  49. * @author EllisLab Dev Team
  50. * @link https://codeigniter.com/user_guide/database/
  51. */
  52. /**
  53. * oci8 Database Adapter Class
  54. *
  55. * This is a modification of the DB_driver class to
  56. * permit access to oracle databases
  57. *
  58. * @author Kelly McArdle
  59. */
  60. class CI_DB_oci8_driver extends CI_DB {
  61. /**
  62. * Database driver
  63. *
  64. * @var string
  65. */
  66. public $dbdriver = 'oci8';
  67. /**
  68. * Statement ID
  69. *
  70. * @var resource
  71. */
  72. public $stmt_id;
  73. /**
  74. * Cursor ID
  75. *
  76. * @var resource
  77. */
  78. public $curs_id;
  79. /**
  80. * Commit mode flag
  81. *
  82. * @var int
  83. */
  84. public $commit_mode = OCI_COMMIT_ON_SUCCESS;
  85. /**
  86. * Limit used flag
  87. *
  88. * If we use LIMIT, we'll add a field that will
  89. * throw off num_fields later.
  90. *
  91. * @var bool
  92. */
  93. public $limit_used;
  94. // --------------------------------------------------------------------
  95. /**
  96. * Reset $stmt_id flag
  97. *
  98. * Used by stored_procedure() to prevent _execute() from
  99. * re-setting the statement ID.
  100. */
  101. protected $_reset_stmt_id = TRUE;
  102. /**
  103. * List of reserved identifiers
  104. *
  105. * Identifiers that must NOT be escaped.
  106. *
  107. * @var string[]
  108. */
  109. protected $_reserved_identifiers = array('*', 'rownum');
  110. /**
  111. * ORDER BY random keyword
  112. *
  113. * @var array
  114. */
  115. protected $_random_keyword = array('ASC', 'ASC'); // not currently supported
  116. /**
  117. * COUNT string
  118. *
  119. * @used-by CI_DB_driver::count_all()
  120. * @used-by CI_DB_query_builder::count_all_results()
  121. *
  122. * @var string
  123. */
  124. protected $_count_string = 'SELECT COUNT(1) AS ';
  125. // --------------------------------------------------------------------
  126. /**
  127. * Class constructor
  128. *
  129. * @param array $params
  130. * @return void
  131. */
  132. public function __construct($params)
  133. {
  134. parent::__construct($params);
  135. $valid_dsns = array(
  136. 'tns' => '/^\(DESCRIPTION=(\(.+\)){2,}\)$/', // TNS
  137. // Easy Connect string (Oracle 10g+)
  138. 'ec' => '/^(\/\/)?[a-z0-9.:_-]+(:[1-9][0-9]{0,4})?(\/[a-z0-9$_]+)?(:[^\/])?(\/[a-z0-9$_]+)?$/i',
  139. 'in' => '/^[a-z0-9$_]+$/i' // Instance name (defined in tnsnames.ora)
  140. );
  141. /* Space characters don't have any effect when actually
  142. * connecting, but can be a hassle while validating the DSN.
  143. */
  144. $this->dsn = str_replace(array("\n", "\r", "\t", ' '), '', $this->dsn);
  145. if ($this->dsn !== '')
  146. {
  147. foreach ($valid_dsns as $regexp)
  148. {
  149. if (preg_match($regexp, $this->dsn))
  150. {
  151. return;
  152. }
  153. }
  154. }
  155. // Legacy support for TNS in the hostname configuration field
  156. $this->hostname = str_replace(array("\n", "\r", "\t", ' '), '', $this->hostname);
  157. if (preg_match($valid_dsns['tns'], $this->hostname))
  158. {
  159. $this->dsn = $this->hostname;
  160. return;
  161. }
  162. elseif ($this->hostname !== '' && strpos($this->hostname, '/') === FALSE && strpos($this->hostname, ':') === FALSE
  163. && (( ! empty($this->port) && ctype_digit($this->port)) OR $this->database !== ''))
  164. {
  165. /* If the hostname field isn't empty, doesn't contain
  166. * ':' and/or '/' and if port and/or database aren't
  167. * empty, then the hostname field is most likely indeed
  168. * just a hostname. Therefore we'll try and build an
  169. * Easy Connect string from these 3 settings, assuming
  170. * that the database field is a service name.
  171. */
  172. $this->dsn = $this->hostname
  173. .(( ! empty($this->port) && ctype_digit($this->port)) ? ':'.$this->port : '')
  174. .($this->database !== '' ? '/'.ltrim($this->database, '/') : '');
  175. if (preg_match($valid_dsns['ec'], $this->dsn))
  176. {
  177. return;
  178. }
  179. }
  180. /* At this point, we can only try and validate the hostname and
  181. * database fields separately as DSNs.
  182. */
  183. if (preg_match($valid_dsns['ec'], $this->hostname) OR preg_match($valid_dsns['in'], $this->hostname))
  184. {
  185. $this->dsn = $this->hostname;
  186. return;
  187. }
  188. $this->database = str_replace(array("\n", "\r", "\t", ' '), '', $this->database);
  189. foreach ($valid_dsns as $regexp)
  190. {
  191. if (preg_match($regexp, $this->database))
  192. {
  193. return;
  194. }
  195. }
  196. /* Well - OK, an empty string should work as well.
  197. * PHP will try to use environment variables to
  198. * determine which Oracle instance to connect to.
  199. */
  200. $this->dsn = '';
  201. }
  202. // --------------------------------------------------------------------
  203. /**
  204. * Non-persistent database connection
  205. *
  206. * @param bool $persistent
  207. * @return resource
  208. */
  209. public function db_connect($persistent = FALSE)
  210. {
  211. $func = ($persistent === TRUE) ? 'oci_pconnect' : 'oci_connect';
  212. return empty($this->char_set)
  213. ? $func($this->username, $this->password, $this->dsn)
  214. : $func($this->username, $this->password, $this->dsn, $this->char_set);
  215. }
  216. // --------------------------------------------------------------------
  217. /**
  218. * Database version number
  219. *
  220. * @return string
  221. */
  222. public function version()
  223. {
  224. if (isset($this->data_cache['version']))
  225. {
  226. return $this->data_cache['version'];
  227. }
  228. if ( ! $this->conn_id OR ($version_string = oci_server_version($this->conn_id)) === FALSE)
  229. {
  230. return FALSE;
  231. }
  232. elseif (preg_match('#Release\s(\d+(?:\.\d+)+)#', $version_string, $match))
  233. {
  234. return $this->data_cache['version'] = $match[1];
  235. }
  236. return FALSE;
  237. }
  238. // --------------------------------------------------------------------
  239. /**
  240. * Execute the query
  241. *
  242. * @param string $sql an SQL query
  243. * @return resource
  244. */
  245. protected function _execute($sql)
  246. {
  247. /* Oracle must parse the query before it is run. All of the actions with
  248. * the query are based on the statement id returned by oci_parse().
  249. */
  250. if ($this->_reset_stmt_id === TRUE)
  251. {
  252. $this->stmt_id = oci_parse($this->conn_id, $sql);
  253. }
  254. oci_set_prefetch($this->stmt_id, 1000);
  255. return oci_execute($this->stmt_id, $this->commit_mode);
  256. }
  257. // --------------------------------------------------------------------
  258. /**
  259. * Get cursor. Returns a cursor from the database
  260. *
  261. * @return resource
  262. */
  263. public function get_cursor()
  264. {
  265. return $this->curs_id = oci_new_cursor($this->conn_id);
  266. }
  267. // --------------------------------------------------------------------
  268. /**
  269. * Stored Procedure. Executes a stored procedure
  270. *
  271. * @param string package name in which the stored procedure is in
  272. * @param string stored procedure name to execute
  273. * @param array parameters
  274. * @return mixed
  275. *
  276. * params array keys
  277. *
  278. * KEY OPTIONAL NOTES
  279. * name no the name of the parameter should be in :<param_name> format
  280. * value no the value of the parameter. If this is an OUT or IN OUT parameter,
  281. * this should be a reference to a variable
  282. * type yes the type of the parameter
  283. * length yes the max size of the parameter
  284. */
  285. public function stored_procedure($package, $procedure, array $params)
  286. {
  287. if ($package === '' OR $procedure === '')
  288. {
  289. log_message('error', 'Invalid query: '.$package.'.'.$procedure);
  290. return ($this->db_debug) ? $this->display_error('db_invalid_query') : FALSE;
  291. }
  292. // Build the query string
  293. $sql = 'BEGIN '.$package.'.'.$procedure.'(';
  294. $have_cursor = FALSE;
  295. foreach ($params as $param)
  296. {
  297. $sql .= $param['name'].',';
  298. if (isset($param['type']) && $param['type'] === OCI_B_CURSOR)
  299. {
  300. $have_cursor = TRUE;
  301. }
  302. }
  303. $sql = trim($sql, ',').'); END;';
  304. $this->_reset_stmt_id = FALSE;
  305. $this->stmt_id = oci_parse($this->conn_id, $sql);
  306. $this->_bind_params($params);
  307. $result = $this->query($sql, FALSE, $have_cursor);
  308. $this->_reset_stmt_id = TRUE;
  309. return $result;
  310. }
  311. // --------------------------------------------------------------------
  312. /**
  313. * Bind parameters
  314. *
  315. * @param array $params
  316. * @return void
  317. */
  318. protected function _bind_params($params)
  319. {
  320. if ( ! is_array($params) OR ! is_resource($this->stmt_id))
  321. {
  322. return;
  323. }
  324. foreach ($params as $param)
  325. {
  326. foreach (array('name', 'value', 'type', 'length') as $val)
  327. {
  328. if ( ! isset($param[$val]))
  329. {
  330. $param[$val] = '';
  331. }
  332. }
  333. oci_bind_by_name($this->stmt_id, $param['name'], $param['value'], $param['length'], $param['type']);
  334. }
  335. }
  336. // --------------------------------------------------------------------
  337. /**
  338. * Begin Transaction
  339. *
  340. * @return bool
  341. */
  342. protected function _trans_begin()
  343. {
  344. $this->commit_mode = OCI_NO_AUTO_COMMIT;
  345. return TRUE;
  346. }
  347. // --------------------------------------------------------------------
  348. /**
  349. * Commit Transaction
  350. *
  351. * @return bool
  352. */
  353. protected function _trans_commit()
  354. {
  355. $this->commit_mode = OCI_COMMIT_ON_SUCCESS;
  356. return oci_commit($this->conn_id);
  357. }
  358. // --------------------------------------------------------------------
  359. /**
  360. * Rollback Transaction
  361. *
  362. * @return bool
  363. */
  364. protected function _trans_rollback()
  365. {
  366. $this->commit_mode = OCI_COMMIT_ON_SUCCESS;
  367. return oci_rollback($this->conn_id);
  368. }
  369. // --------------------------------------------------------------------
  370. /**
  371. * Affected Rows
  372. *
  373. * @return int
  374. */
  375. public function affected_rows()
  376. {
  377. return oci_num_rows($this->stmt_id);
  378. }
  379. // --------------------------------------------------------------------
  380. /**
  381. * Insert ID
  382. *
  383. * @return int
  384. */
  385. public function insert_id()
  386. {
  387. // not supported in oracle
  388. return $this->display_error('db_unsupported_function');
  389. }
  390. // --------------------------------------------------------------------
  391. /**
  392. * Show table query
  393. *
  394. * Generates a platform-specific query string so that the table names can be fetched
  395. *
  396. * @param bool $prefix_limit
  397. * @return string
  398. */
  399. protected function _list_tables($prefix_limit = FALSE)
  400. {
  401. $sql = 'SELECT "TABLE_NAME" FROM "ALL_TABLES"';
  402. if ($prefix_limit !== FALSE && $this->dbprefix !== '')
  403. {
  404. return $sql.' WHERE "TABLE_NAME" LIKE \''.$this->escape_like_str($this->dbprefix)."%' "
  405. .sprintf($this->_like_escape_str, $this->_like_escape_chr);
  406. }
  407. return $sql;
  408. }
  409. // --------------------------------------------------------------------
  410. /**
  411. * Show column query
  412. *
  413. * Generates a platform-specific query string so that the column names can be fetched
  414. *
  415. * @param string $table
  416. * @return string
  417. */
  418. protected function _list_columns($table = '')
  419. {
  420. if (strpos($table, '.') !== FALSE)
  421. {
  422. sscanf($table, '%[^.].%s', $owner, $table);
  423. }
  424. else
  425. {
  426. $owner = $this->username;
  427. }
  428. return 'SELECT COLUMN_NAME FROM ALL_TAB_COLUMNS
  429. WHERE UPPER(OWNER) = '.$this->escape(strtoupper($owner)).'
  430. AND UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table));
  431. }
  432. // --------------------------------------------------------------------
  433. /**
  434. * Returns an object with field data
  435. *
  436. * @param string $table
  437. * @return array
  438. */
  439. public function field_data($table)
  440. {
  441. if (strpos($table, '.') !== FALSE)
  442. {
  443. sscanf($table, '%[^.].%s', $owner, $table);
  444. }
  445. else
  446. {
  447. $owner = $this->username;
  448. }
  449. $sql = 'SELECT COLUMN_NAME, DATA_TYPE, CHAR_LENGTH, DATA_PRECISION, DATA_LENGTH, DATA_DEFAULT, NULLABLE
  450. FROM ALL_TAB_COLUMNS
  451. WHERE UPPER(OWNER) = '.$this->escape(strtoupper($owner)).'
  452. AND UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table));
  453. if (($query = $this->query($sql)) === FALSE)
  454. {
  455. return FALSE;
  456. }
  457. $query = $query->result_object();
  458. $retval = array();
  459. for ($i = 0, $c = count($query); $i < $c; $i++)
  460. {
  461. $retval[$i] = new stdClass();
  462. $retval[$i]->name = $query[$i]->COLUMN_NAME;
  463. $retval[$i]->type = $query[$i]->DATA_TYPE;
  464. $length = ($query[$i]->CHAR_LENGTH > 0)
  465. ? $query[$i]->CHAR_LENGTH : $query[$i]->DATA_PRECISION;
  466. if ($length === NULL)
  467. {
  468. $length = $query[$i]->DATA_LENGTH;
  469. }
  470. $retval[$i]->max_length = $length;
  471. $default = $query[$i]->DATA_DEFAULT;
  472. if ($default === NULL && $query[$i]->NULLABLE === 'N')
  473. {
  474. $default = '';
  475. }
  476. $retval[$i]->default = $default;
  477. }
  478. return $retval;
  479. }
  480. // --------------------------------------------------------------------
  481. /**
  482. * Error
  483. *
  484. * Returns an array containing code and message of the last
  485. * database error that has occurred.
  486. *
  487. * @return array
  488. */
  489. public function error()
  490. {
  491. // oci_error() returns an array that already contains
  492. // 'code' and 'message' keys, but it can return false
  493. // if there was no error ....
  494. if (is_resource($this->curs_id))
  495. {
  496. $error = oci_error($this->curs_id);
  497. }
  498. elseif (is_resource($this->stmt_id))
  499. {
  500. $error = oci_error($this->stmt_id);
  501. }
  502. elseif (is_resource($this->conn_id))
  503. {
  504. $error = oci_error($this->conn_id);
  505. }
  506. else
  507. {
  508. $error = oci_error();
  509. }
  510. return is_array($error)
  511. ? $error
  512. : array('code' => '', 'message' => '');
  513. }
  514. // --------------------------------------------------------------------
  515. /**
  516. * Insert batch statement
  517. *
  518. * Generates a platform-specific insert string from the supplied data
  519. *
  520. * @param string $table Table name
  521. * @param array $keys INSERT keys
  522. * @param array $values INSERT values
  523. * @return string
  524. */
  525. protected function _insert_batch($table, $keys, $values)
  526. {
  527. $keys = implode(', ', $keys);
  528. $sql = "INSERT ALL\n";
  529. for ($i = 0, $c = count($values); $i < $c; $i++)
  530. {
  531. $sql .= ' INTO '.$table.' ('.$keys.') VALUES '.$values[$i]."\n";
  532. }
  533. return $sql.'SELECT * FROM dual';
  534. }
  535. // --------------------------------------------------------------------
  536. /**
  537. * Truncate statement
  538. *
  539. * Generates a platform-specific truncate string from the supplied data
  540. *
  541. * If the database does not support the TRUNCATE statement,
  542. * then this method maps to 'DELETE FROM table'
  543. *
  544. * @param string $table
  545. * @return string
  546. */
  547. protected function _truncate($table)
  548. {
  549. return 'TRUNCATE TABLE '.$table;
  550. }
  551. // --------------------------------------------------------------------
  552. /**
  553. * Delete statement
  554. *
  555. * Generates a platform-specific delete string from the supplied data
  556. *
  557. * @param string $table
  558. * @return string
  559. */
  560. protected function _delete($table)
  561. {
  562. if ($this->qb_limit)
  563. {
  564. $this->where('rownum <= ',$this->qb_limit, FALSE);
  565. $this->qb_limit = FALSE;
  566. }
  567. return parent::_delete($table);
  568. }
  569. // --------------------------------------------------------------------
  570. /**
  571. * LIMIT
  572. *
  573. * Generates a platform-specific LIMIT clause
  574. *
  575. * @param string $sql SQL Query
  576. * @return string
  577. */
  578. protected function _limit($sql)
  579. {
  580. if (version_compare($this->version(), '12.1', '>='))
  581. {
  582. // OFFSET-FETCH can be used only with the ORDER BY clause
  583. empty($this->qb_orderby) && $sql .= ' ORDER BY 1';
  584. return $sql.' OFFSET '.(int) $this->qb_offset.' ROWS FETCH NEXT '.$this->qb_limit.' ROWS ONLY';
  585. }
  586. $this->limit_used = TRUE;
  587. return 'SELECT * FROM (SELECT inner_query.*, rownum rnum FROM ('.$sql.') inner_query WHERE rownum < '.($this->qb_offset + $this->qb_limit + 1).')'
  588. .($this->qb_offset ? ' WHERE rnum >= '.($this->qb_offset + 1) : '');
  589. }
  590. // --------------------------------------------------------------------
  591. /**
  592. * Close DB Connection
  593. *
  594. * @return void
  595. */
  596. protected function _close()
  597. {
  598. oci_close($this->conn_id);
  599. }
  600. }