From ecee5c0e0918ae51fd0784ce2852abc63365ebfe Mon Sep 17 00:00:00 2001 From: Joao M Date: Tue, 11 Jun 2024 04:09:19 +0000 Subject: [PATCH 1/8] Restore Oracle DB --- docker-compose.yml | 13 ++ src/DbOci8Driver.php | 363 +++++++++++++++++++++++++++++++++++++++++ src/Factory.php | 1 + src/Oci8Iterator.php | 107 ++++++++++++ src/PdoOci.php | 5 +- testsdb/PdoOciTest.php | 56 +++++++ 6 files changed, 542 insertions(+), 3 deletions(-) create mode 100644 src/DbOci8Driver.php create mode 100644 src/Oci8Iterator.php create mode 100644 testsdb/PdoOciTest.php diff --git a/docker-compose.yml b/docker-compose.yml index 99642c1..498e8ea 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -41,3 +41,16 @@ services: timeout: 20s interval: 10s retries: 10 + + oracle: + container_name: oracle + image: container-registry.oracle.com/database/free:latest + environment: + - ORACLE_PWD=password + ports: + - "1521:1521" +# healthcheck: +# test: [ "CMD", "mysqladmin" ,"ping", "-h", "localhost" ] +# timeout: 20s +# interval: 10s +# retries: 10 diff --git a/src/DbOci8Driver.php b/src/DbOci8Driver.php new file mode 100644 index 0000000..b55589e --- /dev/null +++ b/src/DbOci8Driver.php @@ -0,0 +1,363 @@ +logger = new NullLogger(); + $this->connectionUri = $connectionString; + + $codePage = $this->connectionUri->getQueryPart("codepage"); + $codePage = ($codePage == "") ? 'UTF8' : $codePage; + + $tns = DbOci8Driver::getTnsString($this->connectionUri); + + $this->conn = oci_connect( + $this->connectionUri->getUsername(), + $this->connectionUri->getPassword(), + $tns, + $codePage, + OCI_SYSDBA + ); + + if (!$this->conn) { + $error = oci_error(); + throw new DatabaseException($error['message']); + } + } + + /** + * + * @param Uri $connUri + * @return string + */ + public static function getTnsString(Uri $connUri) + { + $protocol = $connUri->getQueryPart("protocol"); + $protocol = ($protocol == "") ? 'TCP' : $protocol; + + $port = $connUri->getPort(); + $port = ($port == "") ? 1521 : $port; + + $svcName = preg_replace('~^/~', '', $connUri->getPath()); + + $host = $connUri->getHost(); + + return "(DESCRIPTION = " . + " (ADDRESS = (PROTOCOL = $protocol)(HOST = $host)(PORT = $port)) " . + " (CONNECT_DATA = (SERVICE_NAME = $svcName)) " . + ")"; + } + + public function __destruct() + { + $this->conn = null; + } + + /** + * @param $sql + * @param null $array + * @return resource + * @throws DatabaseException + */ + protected function getOci8Cursor($sql, $array = null) + { + list($query, $array) = SqlBind::parseSQL($this->connectionUri, $sql, $array); + + $this->logger->debug("SQL: $query, Params: " . json_encode($array)); + + // Prepare the statement + $query = rtrim($query, ' ;'); + $stid = oci_parse($this->conn, $query); + if (!$stid) { + $error = oci_error($this->conn); + throw new DatabaseException($error['message']); + } + + // Bind the parameters + if (is_array($array)) { + foreach ($array as $key => $value) { + oci_bind_by_name($stid, ":$key", $value); + } + } + + // Perform the logic of the query + $result = oci_execute($stid, $this->transaction); + + // Check if is OK; + if (!$result) { + $error = oci_error($stid); + throw new DatabaseException($error['message']); + } + + return $stid; + } + + /** + * @param $sql + * @param null $params + * @return Oci8Iterator + * @throws DatabaseException + */ + public function getIterator($sql, $params = null) + { + $cur = $this->getOci8Cursor($sql, $params); + return new Oci8Iterator($cur); + } + + /** + * @param $sql + * @param null $array + * @return null + * @throws DatabaseException + */ + public function getScalar($sql, $array = null) + { + $cur = $this->getOci8Cursor($sql, $array); + + $row = oci_fetch_array($cur, OCI_RETURN_NULLS); + if ($row) { + $scalar = $row[0]; + } else { + $scalar = null; + } + + oci_free_cursor($cur); + + return $scalar; + } + + /** + * @param $tablename + * @return array + * @throws DatabaseException + */ + public function getAllFields($tablename) + { + $cur = $this->getOci8Cursor(SqlHelper::createSafeSQL("select * from :table", array(':table' => $tablename))); + + $ncols = oci_num_fields($cur); + + $fields = array(); + for ($i = 1; $i <= $ncols; $i++) { + $fields[] = strtolower(oci_field_name($cur, $i)); + } + + oci_free_statement($cur); + + return $fields; + } + + public function beginTransaction($isolationLevel = null, $allowJoin = false) + { + $this->logger->debug("SQL: Begin Transaction"); + $this->transaction = OCI_NO_AUTO_COMMIT; + } + + /** + * @throws DatabaseException + */ + public function commitTransaction() + { + $this->logger->debug("SQL: Commit Transaction"); + if ($this->transaction == OCI_COMMIT_ON_SUCCESS) { + throw new DatabaseException('No transaction for commit'); + } + + $this->transaction = OCI_COMMIT_ON_SUCCESS; + + $result = oci_commit($this->conn); + if (!$result) { + $error = oci_error($this->conn); + throw new DatabaseException($error['message']); + } + } + + /** + * @throws DatabaseException + */ + public function rollbackTransaction() + { + $this->logger->debug("SQL: Rollback Transaction"); + if ($this->transaction == OCI_COMMIT_ON_SUCCESS) { + throw new DatabaseException('No transaction for rollback'); + } + + $this->transaction = OCI_COMMIT_ON_SUCCESS; + + oci_rollback($this->conn); + } + + /** + * @param $sql + * @param null $array + * @return bool + * @throws DatabaseException + */ + public function execute($sql, $array = null) + { + $cur = $this->getOci8Cursor($sql, $array); + oci_free_cursor($cur); + return true; + } + + /** + * + * @return resource + */ + public function getDbConnection() + { + return $this->conn; + } + + /** + * @param $name + * @throws NotImplementedException + */ + public function getAttribute($name) + { + throw new NotImplementedException('Method not implemented for OCI Driver'); + } + + /** + * @param $name + * @param $value + * @throws NotImplementedException + */ + public function setAttribute($name, $value) + { + throw new NotImplementedException('Method not implemented for OCI Driver'); + } + + /** + * @param $sql + * @param null $array + * @throws NotImplementedException + */ + public function executeAndGetId($sql, $array = null) + { + throw new NotImplementedException('Method not implemented for OCI Driver'); + } + + /** + * @return \ByJG\AnyDataset\Db\DbFunctionsInterface|void + * @throws NotImplementedException + */ + public function getDbHelper() + { + throw new NotImplementedException('Method not implemented for OCI Driver'); + } + + /** + * @return Uri + */ + public function getUri() + { + return $this->connectionUri; + } + + /** + * @throws NotImplementedException + */ + public function isSupportMultRowset() + { + throw new NotImplementedException('Method not implemented for OCI Driver'); + } + + /** + * @param $multipleRowSet + * @throws NotImplementedException + */ + public function setSupportMultRowset($multipleRowSet) + { + throw new NotImplementedException('Method not implemented for OCI Driver'); + } + + public function reconnect($force = false) + { +// throw new NotImplementedException('Method not implemented for OCI Driver'); + } + + public function disconnect() + { + throw new NotImplementedException('Method not implemented for OCI Driver'); + } + + public function isConnected($softCheck = false, $throwError = false) + { + throw new NotImplementedException('Method not implemented for OCI Driver'); + } + + public function enableLogger(LoggerInterface $logger) + { + $this->logger = $logger; + } + + public function log($message, $context = []) + { + $this->logger->debug($message, $context); + } + + public function hasActiveTransaction() + { + // TODO: Implement hasActiveTransaction() method. + } + + public function activeIsolationLevel() + { + // TODO: Implement activeIsolationLevel() method. + } + + public function remainingCommits() + { + // TODO: Implement remainingCommits() method. + } + + public function requiresTransaction() + { + // TODO: Implement requiresTransaction() method. + } +} \ No newline at end of file diff --git a/src/Factory.php b/src/Factory.php index c73c971..8483179 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -59,6 +59,7 @@ public static function getDbInstance($connectionUri) self::registerDbDriver(PdoOdbc::class); self::registerDbDriver(PdoPdo::class); self::registerDbDriver(PdoOci::class); + self::registerDbDriver(DbOci8Driver::class); } $scheme = $connectionUri->getScheme(); diff --git a/src/Oci8Iterator.php b/src/Oci8Iterator.php new file mode 100644 index 0000000..6c076e8 --- /dev/null +++ b/src/Oci8Iterator.php @@ -0,0 +1,107 @@ +cursor = $cursor; + $this->rowBuffer = array(); + } + + /** + * @access public + * @return int + */ + public function count() + { + return -1; + } + + /** + * @access public + * @return bool + * @throws InvalidArgumentException + */ + public function hasNext() + { + if (count($this->rowBuffer) >= Oci8Iterator::RECORD_BUFFER) { + return true; + } + + if (is_null($this->cursor)) { + return (count($this->rowBuffer) > 0); + } + + $rowArray = oci_fetch_assoc($this->cursor); + if (!empty($rowArray)) { + $rowArray = array_change_key_case($rowArray, CASE_LOWER); + $singleRow = new Row($rowArray); + + $this->currentRow++; + + // Enfileira o registo + $this->rowBuffer[] = $singleRow; + // Traz novos até encher o Buffer + if (count($this->rowBuffer) < DbIterator::RECORD_BUFFER) { + $this->hasNext(); + } + return true; + } + + oci_free_statement($this->cursor); + $this->cursor = null; + return (count($this->rowBuffer) > 0); + } + + public function __destruct() + { + if (!is_null($this->cursor)) { + oci_free_statement($this->cursor); + $this->cursor = null; + } + } + + /** + * @return mixed + * @throws IteratorException + * @throws InvalidArgumentException + */ + public function moveNext() + { + if (!$this->hasNext()) { + throw new IteratorException("No more records. Did you used hasNext() before moveNext()?"); + } else { + $row = array_shift($this->rowBuffer); + $this->moveNextRow++; + return $row; + } + } + + public function key() + { + return $this->moveNextRow; + } +} diff --git a/src/PdoOci.php b/src/PdoOci.php index 854b7d8..7b4daf4 100644 --- a/src/PdoOci.php +++ b/src/PdoOci.php @@ -9,7 +9,7 @@ class PdoOci extends PdoLiteral public static function schema() { - return ['oci']; + return ['oracle']; } public function __construct(Uri $connUri) @@ -32,8 +32,7 @@ public static function getTnsString(Uri $connUri) $protocol = $connUri->getQueryPart("protocol"); $protocol = ($protocol == "") ? 'TCP' : $protocol; - $port = $connUri->getPort(); - $port = ($port == "") ? 1521 : $port; + $port = $connUri->getPort() ?? 1521; $svcName = preg_replace('~^/~', '', $connUri->getPath()); diff --git a/testsdb/PdoOciTest.php b/testsdb/PdoOciTest.php new file mode 100644 index 0000000..4a8dfab --- /dev/null +++ b/testsdb/PdoOciTest.php @@ -0,0 +1,56 @@ +escapeQuote = "\'"; + + $host = getenv('MYSQL_TEST_HOST'); + if (empty($host)) { + $host = "127.0.0.1"; + } + $password = getenv('MYSQL_PASSWORD'); + if (empty($password)) { + $password = 'password'; + } + if ($password == '.') { + $password = ""; + } + + return Factory::getDbRelationalInstance("oci8://sys:$password@127.0.0.1/FREEPDB1"); + } + + protected function createDatabase() + { + //create the database + $this->dbDriver->execute("CREATE TABLE Dogs (Id NUMBER GENERATED ALWAYS as IDENTITY(START with 1 INCREMENT by 1), Breed varchar2(50), Name varchar2(50), Age number(10), Weight number(10,2), CONSTRAINT dogs_pk PRIMARY KEY (Id))"); + } + + public function deleteDatabase() + { + $this->dbDriver->execute('drop table Dogs'); + } + + public function testGetDate() { + $data = $this->dbDriver->getScalar("SELECT TO_DATE('2018-07-26', 'YYYY-MM-DD') FROM DUAL "); + $this->assertEquals("26-JUL-18", $data); + + $data = $this->dbDriver->getScalar("SELECT TO_TIMESTAMP('2018-07-26 20:02:03', 'YYYY-MM-DD HH24:MI:SS') FROM DUAL "); + $this->assertEquals("26-JUL-18 08.02.03.000000000 PM", $data); + } + +// public function testDontParseParam_3() { +// $this->expectException(\PDOException::class); +// +// parent::testDontParseParam_3(); +// } + +} From 041a807a7d3960737d6c3f296e610fecaece8c7f Mon Sep 17 00:00:00 2001 From: Joao M Date: Sat, 15 Jun 2024 00:12:34 +0000 Subject: [PATCH 2/8] Restore Oracle DB --- .gitpod.yml | 7 +- src/DbOci8Driver.php | 104 +++++++++++++------ src/DbPdoDriver.php | 51 +--------- src/Helpers/DbBaseFunctions.php | 5 +- src/Helpers/DbOci8Functions.php | 173 ++++++++++++++++++++++++++++++++ src/Traits/DbCacheTrait.php | 69 +++++++++++++ testsdb/PdoOciTest.php | 10 +- 7 files changed, 332 insertions(+), 87 deletions(-) create mode 100644 src/Helpers/DbOci8Functions.php diff --git a/.gitpod.yml b/.gitpod.yml index 635b3d3..55fba05 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -1,10 +1,13 @@ tasks: - name: Run Composer - command: | - composer install + init: | sudo ln -s /etc/php/8.2/mods-available/sqlsrv.ini /etc/php/8.2/cli/conf.d/20-sqlsrv.ini sudo ln -s /etc/php/8.2/mods-available/pdo_sqlsrv.ini /etc/php/8.2/cli/conf.d/30-pdo_sqlsrv.ini sudo ln -s /etc/php/8.2/mods-available/oci8.ini /etc/php/8.2/cli/conf.d/20-oci8.ini + echo oci8.privileged_connect=1 | sudo tee -a /etc/php/8.2/mods-available/oci8.ini + + command: | + composer install docker compose -f docker-compose.yml up -d image: byjg/gitpod-image:full diff --git a/src/DbOci8Driver.php b/src/DbOci8Driver.php index b55589e..bd05048 100644 --- a/src/DbOci8Driver.php +++ b/src/DbOci8Driver.php @@ -4,22 +4,23 @@ use ByJG\AnyDataset\Core\Exception\DatabaseException; use ByJG\AnyDataset\Core\Exception\NotImplementedException; +use ByJG\AnyDataset\Db\Exception\DbDriverNotConnected; use ByJG\AnyDataset\Db\Helpers\SqlBind; use ByJG\AnyDataset\Db\Helpers\SqlHelper; +use ByJG\AnyDataset\Db\Traits\DbCacheTrait; use ByJG\Util\Uri; +use Exception; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; +use Psr\SimpleCache\CacheInterface; class DbOci8Driver implements DbDriverInterface { + use DbCacheTrait; private LoggerInterface $logger; - public function getMaxStmtCache() { } - - public function getCountStmtCache() { } - - public function setMaxStmtCache($maxStmtCache) { } + private DbFunctionsInterface $dbHelper; public static function schema() { @@ -49,24 +50,7 @@ public function __construct(Uri $connectionString) { $this->logger = new NullLogger(); $this->connectionUri = $connectionString; - - $codePage = $this->connectionUri->getQueryPart("codepage"); - $codePage = ($codePage == "") ? 'UTF8' : $codePage; - - $tns = DbOci8Driver::getTnsString($this->connectionUri); - - $this->conn = oci_connect( - $this->connectionUri->getUsername(), - $this->connectionUri->getPassword(), - $tns, - $codePage, - OCI_SYSDBA - ); - - if (!$this->conn) { - $error = oci_error(); - throw new DatabaseException($error['message']); - } + $this->reconnect(); } /** @@ -105,6 +89,9 @@ public function __destruct() */ protected function getOci8Cursor($sql, $array = null) { + if (is_null($this->conn)) { + throw new DbDriverNotConnected('Instance not connected'); + } list($query, $array) = SqlBind::parseSQL($this->connectionUri, $sql, $array); $this->logger->debug("SQL: $query, Params: " . json_encode($array)); @@ -120,7 +107,7 @@ protected function getOci8Cursor($sql, $array = null) // Bind the parameters if (is_array($array)) { foreach ($array as $key => $value) { - oci_bind_by_name($stid, ":$key", $value); + oci_bind_by_name($stid, ":$key", $array[$key], -1); } } @@ -142,10 +129,12 @@ protected function getOci8Cursor($sql, $array = null) * @return Oci8Iterator * @throws DatabaseException */ - public function getIterator($sql, $params = null) + public function getIterator($sql, $params = null, CacheInterface $cache = null, $ttl = 60) { - $cur = $this->getOci8Cursor($sql, $params); - return new Oci8Iterator($cur); + return $this->getIteratorUsingCache($sql, $params, $cache, $ttl, function ($sql, $params) { + $cur = $this->getOci8Cursor($sql, $params); + return new Oci8Iterator($cur); + }); } /** @@ -279,7 +268,7 @@ public function setAttribute($name, $value) */ public function executeAndGetId($sql, $array = null) { - throw new NotImplementedException('Method not implemented for OCI Driver'); + return $this->getDbHelper()->executeAndGetInsertedId($this, $sql, $array); } /** @@ -288,7 +277,10 @@ public function executeAndGetId($sql, $array = null) */ public function getDbHelper() { - throw new NotImplementedException('Method not implemented for OCI Driver'); + if (empty($this->dbHelper)) { + $this->dbHelper = Factory::getDbFunctions($this->getUri()); + } + return $this->dbHelper; } /** @@ -304,7 +296,7 @@ public function getUri() */ public function isSupportMultRowset() { - throw new NotImplementedException('Method not implemented for OCI Driver'); + return false; } /** @@ -318,17 +310,63 @@ public function setSupportMultRowset($multipleRowSet) public function reconnect($force = false) { -// throw new NotImplementedException('Method not implemented for OCI Driver'); + if ($this->isConnected() && !$force) { + return false; + } + + // Release old instance + $this->disconnect(); + + // Connect + $codePage = $this->connectionUri->getQueryPart("codepage"); + $codePage = ($codePage == "") ? 'UTF8' : $codePage; + $tns = DbOci8Driver::getTnsString($this->connectionUri); + + $this->conn = oci_connect( + $this->connectionUri->getUsername(), + $this->connectionUri->getPassword(), + $tns, + $codePage, + OCI_SYSDBA + ); + + if (!$this->conn) { + $error = oci_error(); + throw new DatabaseException($error['message']); + } + + return true; } public function disconnect() { - throw new NotImplementedException('Method not implemented for OCI Driver'); + $this->clearCache(); + $this->conn = null; } public function isConnected($softCheck = false, $throwError = false) { - throw new NotImplementedException('Method not implemented for OCI Driver'); + if (empty($this->conn)) { + if ($throwError) { + throw new DbDriverNotConnected('DbDriver not connected'); + } + return false; + } + + if ($softCheck) { + return true; + } + + try { + oci_parse($this->conn, "SELECT 1 FROM DUAL"); // Do not use $this->getInstance() + } catch (Exception $ex) { + if ($throwError) { + throw new DbDriverNotConnected('DbDriver not connected'); + } + return false; + } + + return true; } public function enableLogger(LoggerInterface $logger) diff --git a/src/DbPdoDriver.php b/src/DbPdoDriver.php index fd9586c..b1272a4 100644 --- a/src/DbPdoDriver.php +++ b/src/DbPdoDriver.php @@ -8,7 +8,6 @@ use ByJG\AnyDataset\Db\Helpers\SqlHelper; use ByJG\AnyDataset\Db\Traits\DbCacheTrait; use ByJG\AnyDataset\Db\Traits\TransactionTrait; -use ByJG\AnyDataset\Lists\ArrayDataset; use ByJG\Util\Uri; use Exception; use PDO; @@ -128,29 +127,11 @@ protected function getDBStatement($sql, $array = null) public function getIterator($sql, $params = null, CacheInterface $cache = null, $ttl = 60) { - if (!empty($cache)) { - // Otherwise try to get from cache - $key = $this->getQueryKey($sql, $params); - - // Get the CACHE - $cachedItem = $cache->get($key); - if (!is_null($cachedItem)) { - return (new ArrayDataset($cachedItem))->getIterator(); - } - } - - - $stmt = $this->getDBStatement($sql, $params); - $stmt->execute(); - $iterator = new DbIterator($stmt); - - if (!empty($cache)) { - $cachedItem = $iterator->toArray(); - $cache->set($key, $cachedItem, $ttl); - return (new ArrayDataset($cachedItem))->getIterator(); - } - - return $iterator; + return $this->getIteratorUsingCache($sql, $params, $cache, $ttl, function ($sql, $params) { + $stmt = $this->getDBStatement($sql, $params); + $stmt->execute(); + return new DbIterator($stmt); + }); } public function getScalar($sql, $array = null) @@ -297,26 +278,4 @@ public function log($message, $context = []) { $this->logger->debug($message, $context); } - - protected function array_map_assoc($callback, $array) - { - $r = array(); - foreach ($array as $key=>$value) { - $r[$key] = $callback($key, $value); - } - return $r; - } - - protected function getQueryKey($sql, $array) - { - $key1 = md5($sql); - $key2 = ""; - - // Check which parameter exists in the SQL - if (is_array($array)) { - $key2 = md5(":" . implode(',', $this->array_map_assoc(function($k,$v){return "$k:$v";},$array))); - } - - return "qry:" . $key1 . $key2; - } } diff --git a/src/Helpers/DbBaseFunctions.php b/src/Helpers/DbBaseFunctions.php index c9b9492..e1546ef 100644 --- a/src/Helpers/DbBaseFunctions.php +++ b/src/Helpers/DbBaseFunctions.php @@ -199,10 +199,7 @@ protected function getTableMetadataFromSql(DbDriverInterface $dbdataset, $sql) return $this->parseColumnMetadata($metadata); } - protected function parseColumnMetadata($metadata) - { - throw new Exception("Not implemented"); - } + abstract protected function parseColumnMetadata($metadata); protected function parseTypeMetadata($type) { diff --git a/src/Helpers/DbOci8Functions.php b/src/Helpers/DbOci8Functions.php new file mode 100644 index 0000000..4f9bcc2 --- /dev/null +++ b/src/Helpers/DbOci8Functions.php @@ -0,0 +1,173 @@ +deliFieldLeft = '"'; + $this->deliFieldRight = '"'; + $this->deliTableLeft = '"'; + $this->deliTableRight = '"'; + } + + public function concat($str1, $str2 = null) + { + return implode(' || ', func_get_args()); + } + + /** + * Given a SQL returns it with the proper LIMIT or equivalent method included + * @param string $sql + * @param int $start + * @param int $qty + * @return string + */ + public function limit($sql, $start, $qty = null) + { + if (is_null($qty)) { + $qty = 50; + } + + if (stripos($sql, ' OFFSET ') === false && stripos($sql, ' FETCH NEXT ') === false) { + $sql = $sql . " OFFSET x ROWS FETCH NEXT y ROWS ONLY"; + } + + return preg_replace( + '~(\s[Oo][Ff][Ff][Ss][Ee][Tt])\s.*?\s([Rr][Oo][Ww][Ss])\s.*?\s([Ff][Ee][Tt][Cc][Hh]\s[Nn][Ee][Xx][Tt])\s.*~', + '$1 ' . $start . ' $2 ' . '$3 ' . $qty . ' ROWS ONLY', + $sql + ); + } + + /** + * Given a SQL returns it with the proper TOP or equivalent method included + * @param string $sql + * @param int $qty + * @return string + */ + public function top($sql, $qty) + { + return $this->limit($sql, 0, $qty); + } + + /** + * Return if the database provider have a top or similar function + * @return bool + */ + public function hasTop() + { + return true; + } + + /** + * Return if the database provider have a limit function + * @return bool + */ + public function hasLimit() + { + return true; + } + + /** + * Format date column in sql string given an input format that understands Y M D + * + * @param string $format + * @param string|null $column + * @return string + * @example $db->getDbFunctions()->SQLDate("d/m/Y H:i", "dtcriacao") + */ + public function sqlDate($format, $column = null) + { + if (is_null($column)) { + $column = 'current_timestamp'; + } + + $pattern = [ + 'Y' => "YYYY", + 'y' => "YY", + 'M' => "Mon", + 'm' => "MM", + 'Q' => "Q", + 'q' => "Q", + 'D' => "DD", + 'd' => "DD", + 'h' => "HH", + 'H' => "HH24", + 'i' => "MI", + 's' => "SS", + 'a' => "AM", + 'A' => "AM", + ]; + + return sprintf( + "TO_CHAR(%s,'%s')", + $column, + implode('', $this->prepareSqlDate($format, $pattern, '')) + ); + } + + /** + * @param DbDriverInterface $dbdataset + * @param string $sql + * @param array $param + * @return int + */ + public function executeAndGetInsertedId(DbDriverInterface $dbdataset, $sql, $param) + { + $dbdataset->execute($sql, $param); + return 4; +// oci_inse +// return $dbdataset->getScalar('select lastval()'); + } + + public function hasForUpdate() + { + return true; + } + + public function getTableMetadata(DbDriverInterface $dbdataset, $tableName) + { + $tableName = strtoupper($tableName); + $sql = "SELECT COLUMN_NAME, DATA_TYPE || '(' || COALESCE(CAST(DATA_LENGTH AS VARCHAR2(10)), CAST(DATA_PRECISION AS VARCHAR2(10)) || ',' || DATA_SCALE) || ')' AS TYPE, NULLABLE FROM ALL_TAB_COLUMNS WHERE TABLE_NAME = '{$tableName}'"; + + return $this->getTableMetadataFromSql($dbdataset, $sql); + } + + protected function parseColumnMetadata($metadata) + { + $return = []; + + foreach ($metadata as $key => $value) { + $return[strtolower($value['column_name'])] = [ + 'name' => $value['column_name'], + 'dbType' => strtolower($value['type']), + 'required' => $value['nullable'] == 'N', + 'default' => isset($value['column_default']) ? $value['column_default'] : "", + ] + $this->parseTypeMetadata(strtolower($value['type'])); + } + + return $return; + } + + public function getIsolationLevelCommand($isolationLevel) + { + switch ($isolationLevel) { + case IsolationLevelEnum::READ_UNCOMMITTED: + return "SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL READ UNCOMMITTED"; + case IsolationLevelEnum::READ_COMMITTED: + return "SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL READ COMMITTED"; + case IsolationLevelEnum::REPEATABLE_READ: + return "SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL REPEATABLE READ"; + case IsolationLevelEnum::SERIALIZABLE: + return "SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL SERIALIZABLE"; + default: + return ""; + } + } +} diff --git a/src/Traits/DbCacheTrait.php b/src/Traits/DbCacheTrait.php index ce042ce..939de50 100644 --- a/src/Traits/DbCacheTrait.php +++ b/src/Traits/DbCacheTrait.php @@ -2,6 +2,10 @@ namespace ByJG\AnyDataset\Db\Traits; +use ByJG\AnyDataset\Core\GenericIterator; +use ByJG\AnyDataset\Lists\ArrayDataset; +use Psr\SimpleCache\CacheInterface; + trait DbCacheTrait { protected $stmtCache = []; @@ -45,4 +49,69 @@ protected function getOrSetSqlCacheStmt($sql) return $this->stmtCache[$sql]; } + + public function getIteratorUsingCache($sql, $params, ?CacheInterface $cache, $ttl, \Closure $closure): GenericIterator + { + $cacheKey = $this->getQueryKey($cache, $sql, $params); + $cachedResult = $this->getCachedResult($cacheKey, $cache); + if (!empty($cachedResult)) { + return $cachedResult; + } + + $iterator = $closure($sql, $params); + + return $this->cacheResult($cacheKey, $iterator, $cache, $ttl); + } + + + protected function getCachedResult($key, ?CacheInterface $cache): ?GenericIterator + { + if (!empty($cache)) { + // Get the CACHE + $cachedItem = $cache->get($key); + if (!is_null($cachedItem)) { + return (new ArrayDataset($cachedItem))->getIterator(); + } + } + + return null; + } + + protected function cacheResult($key, GenericIterator $iterator, ?CacheInterface $cache, $ttl): GenericIterator + { + if (!empty($cache)) { + $cachedItem = $iterator->toArray(); + $cache->set($key, $cachedItem, $ttl); + return (new ArrayDataset($cachedItem))->getIterator(); + } + + return $iterator; + } + + protected function array_map_assoc($callback, $array) + { + $r = array(); + foreach ($array as $key=>$value) { + $r[$key] = $callback($key, $value); + } + return $r; + } + + protected function getQueryKey(?CacheInterface $cache, $sql, $array) + { + if (empty($cache)) { + return null; + } + + $key1 = md5($sql); + $key2 = ""; + + // Check which parameter exists in the SQL + if (is_array($array)) { + $key2 = md5(":" . implode(',', $this->array_map_assoc(function($k,$v){return "$k:$v";},$array))); + } + + return "qry:" . $key1 . $key2; + } + } \ No newline at end of file diff --git a/testsdb/PdoOciTest.php b/testsdb/PdoOciTest.php index 4a8dfab..f1904ab 100644 --- a/testsdb/PdoOciTest.php +++ b/testsdb/PdoOciTest.php @@ -11,7 +11,7 @@ class PdoOciTest extends BasePdo protected function createInstance() { - $this->escapeQuote = "\'"; + $this->escapeQuote = "''"; $host = getenv('MYSQL_TEST_HOST'); if (empty($host)) { @@ -31,7 +31,13 @@ protected function createInstance() protected function createDatabase() { //create the database - $this->dbDriver->execute("CREATE TABLE Dogs (Id NUMBER GENERATED ALWAYS as IDENTITY(START with 1 INCREMENT by 1), Breed varchar2(50), Name varchar2(50), Age number(10), Weight number(10,2), CONSTRAINT dogs_pk PRIMARY KEY (Id))"); + $this->dbDriver->execute("CREATE TABLE Dogs ( + Id NUMBER GENERATED ALWAYS as IDENTITY(START with 1 INCREMENT by 1), + Breed varchar2(50), + Name varchar2(50), + Age number(10), + Weight number(10,2), + CONSTRAINT dogs_pk PRIMARY KEY (Id))"); } public function deleteDatabase() From 75b1a219cfff7ed4bb8ed19d71edc17609bdc550 Mon Sep 17 00:00:00 2001 From: Joao M Date: Tue, 18 Jun 2024 23:02:49 +0000 Subject: [PATCH 3/8] Restore Oracle DB --- composer.json | 3 +- docker-compose.yml | 5 +- src/DbOci8Driver.php | 117 +++++++++++++++----------------- src/DbPdoDriver.php | 18 +++++ src/Helpers/DbOci8Functions.php | 7 +- src/Traits/TransactionTrait.php | 9 +-- testsdb/PdoOciTest.php | 24 ++++++- testsdb/assets/01-users.sql | 4 ++ 8 files changed, 109 insertions(+), 78 deletions(-) create mode 100644 testsdb/assets/01-users.sql diff --git a/composer.json b/composer.json index fc3a00f..70a1afe 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,8 @@ }, "require-dev": { "phpunit/phpunit": "5.7.*|7.4.*|^9.5", - "byjg/cache-engine": "4.9.*" + "byjg/cache-engine": "4.9.*", + "ext-oci8": "*" }, "suggest": { "ext-curl": "*", diff --git a/docker-compose.yml b/docker-compose.yml index 498e8ea..a2ed622 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -44,11 +44,14 @@ services: oracle: container_name: oracle - image: container-registry.oracle.com/database/free:latest + image: container-registry.oracle.com/database/express:21.3.0-xe environment: - ORACLE_PWD=password ports: - "1521:1521" + volumes: + - ./testsdb/assets:/opt/oracle/scripts/startup + # healthcheck: # test: [ "CMD", "mysqladmin" ,"ping", "-h", "localhost" ] # timeout: 20s diff --git a/src/DbOci8Driver.php b/src/DbOci8Driver.php index bd05048..0ca8b3a 100644 --- a/src/DbOci8Driver.php +++ b/src/DbOci8Driver.php @@ -8,6 +8,7 @@ use ByJG\AnyDataset\Db\Helpers\SqlBind; use ByJG\AnyDataset\Db\Helpers\SqlHelper; use ByJG\AnyDataset\Db\Traits\DbCacheTrait; +use ByJG\AnyDataset\Db\Traits\TransactionTrait; use ByJG\Util\Uri; use Exception; use Psr\Log\LoggerInterface; @@ -16,6 +17,7 @@ class DbOci8Driver implements DbDriverInterface { + use TransactionTrait; use DbCacheTrait; private LoggerInterface $logger; @@ -36,12 +38,12 @@ public static function schema() /** Used for OCI8 connections * */ protected $conn; - protected $transaction = OCI_COMMIT_ON_SUCCESS; + protected $ociAutoCommit; /** * Ex. * - * oci8://username:password@host:1521/servicename?protocol=TCP&codepage=WE8MSWIN1252 + * oci8://username:password@host:1521/servicename?protocol=TCP&codepage=WE8MSWIN1252&conntype=persistent|new|default * * @param Uri $connectionString * @throws DatabaseException @@ -50,6 +52,7 @@ public function __construct(Uri $connectionString) { $this->logger = new NullLogger(); $this->connectionUri = $connectionString; + $this->ociAutoCommit = OCI_COMMIT_ON_SUCCESS; $this->reconnect(); } @@ -72,7 +75,9 @@ public static function getTnsString(Uri $connUri) return "(DESCRIPTION = " . " (ADDRESS = (PROTOCOL = $protocol)(HOST = $host)(PORT = $port)) " . - " (CONNECT_DATA = (SERVICE_NAME = $svcName)) " . + " (CONNECT_DATA = ". + " (SERVICE_NAME = $svcName) " . + " ) " . ")"; } @@ -112,7 +117,7 @@ protected function getOci8Cursor($sql, $array = null) } // Perform the logic of the query - $result = oci_execute($stid, $this->transaction); + $result = oci_execute($stid, $this->ociAutoCommit); // Check if is OK; if (!$result) { @@ -180,46 +185,40 @@ public function getAllFields($tablename) return $fields; } - public function beginTransaction($isolationLevel = null, $allowJoin = false) + protected function transactionHandler($action, $isolLevelCommand = "") { - $this->logger->debug("SQL: Begin Transaction"); - $this->transaction = OCI_NO_AUTO_COMMIT; - } - - /** - * @throws DatabaseException - */ - public function commitTransaction() - { - $this->logger->debug("SQL: Commit Transaction"); - if ($this->transaction == OCI_COMMIT_ON_SUCCESS) { - throw new DatabaseException('No transaction for commit'); - } - - $this->transaction = OCI_COMMIT_ON_SUCCESS; - - $result = oci_commit($this->conn); - if (!$result) { - $error = oci_error($this->conn); - throw new DatabaseException($error['message']); + switch ($action) { + case 'begin': + $this->ociAutoCommit = OCI_NO_AUTO_COMMIT; + $this->execute($isolLevelCommand); + break; + + case 'commit': + if ($this->ociAutoCommit == OCI_COMMIT_ON_SUCCESS) { + throw new DatabaseException('No transaction for commit'); + } + + $this->ociAutoCommit = OCI_COMMIT_ON_SUCCESS; + + $result = oci_commit($this->conn); + if (!$result) { + $error = oci_error($this->conn); + throw new DatabaseException($error['message']); + } + break; + + case 'rollback': + if ($this->ociAutoCommit == OCI_COMMIT_ON_SUCCESS) { + throw new DatabaseException('No transaction for rollback'); + } + + $this->ociAutoCommit = OCI_COMMIT_ON_SUCCESS; + + oci_rollback($this->conn); + break; } } - - /** - * @throws DatabaseException - */ - public function rollbackTransaction() - { - $this->logger->debug("SQL: Rollback Transaction"); - if ($this->transaction == OCI_COMMIT_ON_SUCCESS) { - throw new DatabaseException('No transaction for rollback'); - } - - $this->transaction = OCI_COMMIT_ON_SUCCESS; - - oci_rollback($this->conn); - } - + /** * @param $sql * @param null $array @@ -321,13 +320,25 @@ public function reconnect($force = false) $codePage = $this->connectionUri->getQueryPart("codepage"); $codePage = ($codePage == "") ? 'UTF8' : $codePage; $tns = DbOci8Driver::getTnsString($this->connectionUri); + $connType = $this->connectionUri->getQueryPart("conntype"); + switch ($connType) { + case "persistent": + $connectMethod = "oci_pconnect"; + break; + case "new": + $connectMethod = "oci_new_connect"; + break; + default: + $connectMethod = "oci_connect"; + break; + } - $this->conn = oci_connect( + $this->conn = $connectMethod( $this->connectionUri->getUsername(), $this->connectionUri->getPassword(), $tns, $codePage, - OCI_SYSDBA + $this->connectionUri->getQueryPart('session_mode') ?? OCI_DEFAULT ); if (!$this->conn) { @@ -378,24 +389,4 @@ public function log($message, $context = []) { $this->logger->debug($message, $context); } - - public function hasActiveTransaction() - { - // TODO: Implement hasActiveTransaction() method. - } - - public function activeIsolationLevel() - { - // TODO: Implement activeIsolationLevel() method. - } - - public function remainingCommits() - { - // TODO: Implement remainingCommits() method. - } - - public function requiresTransaction() - { - // TODO: Implement requiresTransaction() method. - } } \ No newline at end of file diff --git a/src/DbPdoDriver.php b/src/DbPdoDriver.php index b1272a4..187efec 100644 --- a/src/DbPdoDriver.php +++ b/src/DbPdoDriver.php @@ -278,4 +278,22 @@ public function log($message, $context = []) { $this->logger->debug($message, $context); } + + protected function transactionHandler($action, $isolLevelCommand = "") + { + switch ($action) { + case 'begin': + if (!empty($isolLevelCommand)) { + $this->getInstance()->exec($isolLevelCommand); + } + $this->getInstance()->beginTransaction(); + break; + case 'commit': + $this->getInstance()->commit(); + break; + case 'rollback': + $this->getInstance()->rollBack(); + break; + } + } } diff --git a/src/Helpers/DbOci8Functions.php b/src/Helpers/DbOci8Functions.php index 4f9bcc2..6d6cc34 100644 --- a/src/Helpers/DbOci8Functions.php +++ b/src/Helpers/DbOci8Functions.php @@ -159,13 +159,12 @@ public function getIsolationLevelCommand($isolationLevel) { switch ($isolationLevel) { case IsolationLevelEnum::READ_UNCOMMITTED: - return "SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL READ UNCOMMITTED"; + return "SET TRANSACTION READ WRITE"; case IsolationLevelEnum::READ_COMMITTED: - return "SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL READ COMMITTED"; case IsolationLevelEnum::REPEATABLE_READ: - return "SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL REPEATABLE READ"; + return "SET TRANSACTION ISOLATION LEVEL READ COMMITTED"; case IsolationLevelEnum::SERIALIZABLE: - return "SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL SERIALIZABLE"; + return "SET TRANSACTION ISOLATION LEVEL SERIALIZABLE"; default: return ""; } diff --git a/src/Traits/TransactionTrait.php b/src/Traits/TransactionTrait.php index b9e4670..fab1b30 100644 --- a/src/Traits/TransactionTrait.php +++ b/src/Traits/TransactionTrait.php @@ -30,10 +30,7 @@ public function beginTransaction($isolationLevel = null, $allowJoin = false) $this->logger->debug("SQL: Begin transaction"); $isolLevelCommand = $this->getDbHelper()->getIsolationLevelCommand($isolationLevel); - if (!empty($isolLevelCommand)) { - $this->getInstance()->exec($isolLevelCommand); - } - $this->getInstance()->beginTransaction(); + $this->transactionHandler('begin', $isolLevelCommand); $this->transactionCount = 1; $this->isolationLevel = $isolationLevel; } @@ -48,7 +45,7 @@ public function commitTransaction() if ($this->transactionCount > 0) { return; } - $this->getInstance()->commit(); + $this->transactionHandler('commit'); $this->isolationLevel = null; } @@ -58,7 +55,7 @@ public function rollbackTransaction() if (!$this->hasActiveTransaction()) { throw new TransactionNotStartedException("There is no active transaction"); } - $this->getInstance()->rollBack(); + $this->transactionHandler('rollback'); $this->transactionCount = 0; $this->isolationLevel = null; } diff --git a/testsdb/PdoOciTest.php b/testsdb/PdoOciTest.php index f1904ab..d5f23d1 100644 --- a/testsdb/PdoOciTest.php +++ b/testsdb/PdoOciTest.php @@ -9,23 +9,35 @@ class PdoOciTest extends BasePdo { + protected $connType = "default"; + + public function setUp(): void + { + $this->connType = "default"; + parent::setUp(); + } + protected function createInstance() { $this->escapeQuote = "''"; - $host = getenv('MYSQL_TEST_HOST'); + $host = getenv('ORACLE_TEST_HOST'); if (empty($host)) { $host = "127.0.0.1"; } - $password = getenv('MYSQL_PASSWORD'); + $password = getenv('ORACLE_PASSWORD'); if (empty($password)) { $password = 'password'; } if ($password == '.') { $password = ""; } + $database = getenv('ORACLE_DATABASE'); + if (empty($database)) { + $database = 'XE'; + } - return Factory::getDbRelationalInstance("oci8://sys:$password@127.0.0.1/FREEPDB1"); + return Factory::getDbRelationalInstance("oci8://C##TEST:$password@$host/$database?session_mode=" . OCI_DEFAULT . "&conntype=" . $this->connType); } protected function createDatabase() @@ -53,6 +65,12 @@ public function testGetDate() { $this->assertEquals("26-JUL-18 08.02.03.000000000 PM", $data); } + public function testTwoDifferentTransactions() + { + $this->connType = "new"; + parent::testTwoDifferentTransactions(); + } + // public function testDontParseParam_3() { // $this->expectException(\PDOException::class); // diff --git a/testsdb/assets/01-users.sql b/testsdb/assets/01-users.sql new file mode 100644 index 0000000..8b766cf --- /dev/null +++ b/testsdb/assets/01-users.sql @@ -0,0 +1,4 @@ + +CREATE USER c##test IDENTIFIED BY password; + +GRANT ALL PRIVILEGES TO C##TEST; From 5f74258ee6c5d88dbd6e63fbe52d737714fa38fb Mon Sep 17 00:00:00 2001 From: Joao M Date: Wed, 19 Jun 2024 00:00:41 +0000 Subject: [PATCH 4/8] Restore Oracle DB --- .github/workflows/phpunit.yml | 2 ++ src/Helpers/DbBaseFunctions.php | 2 +- src/Helpers/DbOci8Functions.php | 15 +++++++++++++-- testsdb/BasePdo.php | 9 +++++++-- testsdb/PdoOciTest.php | 30 ++++++++++++++++++++++++++++-- 5 files changed, 51 insertions(+), 7 deletions(-) diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 8096c64..8f8cd08 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -34,6 +34,7 @@ jobs: --health-interval=10s --health-timeout=20s --health-retries=10 + postgres: image: postgres env: @@ -44,6 +45,7 @@ jobs: --health-interval 10s --health-timeout 5s --health-retries 5 + sqlserver: image: mcr.microsoft.com/mssql/server env: diff --git a/src/Helpers/DbBaseFunctions.php b/src/Helpers/DbBaseFunctions.php index e1546ef..e083786 100644 --- a/src/Helpers/DbBaseFunctions.php +++ b/src/Helpers/DbBaseFunctions.php @@ -204,7 +204,7 @@ abstract protected function parseColumnMetadata($metadata); protected function parseTypeMetadata($type) { $matches = []; - if (!preg_match('/(?[a-z\s]+)(\((?\d+)(,(?\d+))?\))?/i', $type, $matches)) { + if (!preg_match('/(?[a-z0-9\s]+)(\((?\d+)(,(?\d+))?\))?/i', $type, $matches)) { return [ 'phpType' => 'string', 'length' => null, 'precision' => null ]; } diff --git a/src/Helpers/DbOci8Functions.php b/src/Helpers/DbOci8Functions.php index 6d6cc34..e5ad581 100644 --- a/src/Helpers/DbOci8Functions.php +++ b/src/Helpers/DbOci8Functions.php @@ -134,7 +134,18 @@ public function hasForUpdate() public function getTableMetadata(DbDriverInterface $dbdataset, $tableName) { $tableName = strtoupper($tableName); - $sql = "SELECT COLUMN_NAME, DATA_TYPE || '(' || COALESCE(CAST(DATA_LENGTH AS VARCHAR2(10)), CAST(DATA_PRECISION AS VARCHAR2(10)) || ',' || DATA_SCALE) || ')' AS TYPE, NULLABLE FROM ALL_TAB_COLUMNS WHERE TABLE_NAME = '{$tableName}'"; + $sql = "SELECT + COLUMN_NAME, + DATA_TYPE || + CASE + WHEN COALESCE(DATA_PRECISION, CHAR_LENGTH, 0) <> 0 + THEN '(' || COALESCE(DATA_PRECISION, CHAR_LENGTH) || ( + CASE WHEN COALESCE(DATA_SCALE, 0) <> 0 THEN ',' || DATA_SCALE END + ) || ')' + END AS TYPE, + DATA_DEFAULT AS COLUMN_DEFAULT, + NULLABLE + FROM ALL_TAB_COLUMNS WHERE TABLE_NAME = '{$tableName}'"; return $this->getTableMetadataFromSql($dbdataset, $sql); } @@ -148,7 +159,7 @@ protected function parseColumnMetadata($metadata) 'name' => $value['column_name'], 'dbType' => strtolower($value['type']), 'required' => $value['nullable'] == 'N', - 'default' => isset($value['column_default']) ? $value['column_default'] : "", + 'default' => isset($value['column_default']) ? $value['column_default'] : null, ] + $this->parseTypeMetadata(strtolower($value['type'])); } diff --git a/testsdb/BasePdo.php b/testsdb/BasePdo.php index f82edc3..43e73c6 100644 --- a/testsdb/BasePdo.php +++ b/testsdb/BasePdo.php @@ -384,7 +384,12 @@ public function testGetMetadata() unset($metadata[$key]['dbType']); } - $this->assertEquals([ + $this->assertEquals($this->getExpectedMetadata(), $metadata); + } + + protected function getExpectedMetadata() + { + return [ 'id' => [ 'name' => 'Id', 'required' => true, @@ -425,7 +430,7 @@ public function testGetMetadata() 'length' => $this->floatSize, 'precision' => 2, ], - ], $metadata); + ]; } public function testDisconnect() diff --git a/testsdb/PdoOciTest.php b/testsdb/PdoOciTest.php index d5f23d1..f7283b5 100644 --- a/testsdb/PdoOciTest.php +++ b/testsdb/PdoOciTest.php @@ -44,10 +44,10 @@ protected function createDatabase() { //create the database $this->dbDriver->execute("CREATE TABLE Dogs ( - Id NUMBER GENERATED ALWAYS as IDENTITY(START with 1 INCREMENT by 1), + Id INT GENERATED ALWAYS as IDENTITY(START with 1 INCREMENT by 1), Breed varchar2(50), Name varchar2(50), - Age number(10), + Age INT, Weight number(10,2), CONSTRAINT dogs_pk PRIMARY KEY (Id))"); } @@ -65,6 +65,32 @@ public function testGetDate() { $this->assertEquals("26-JUL-18 08.02.03.000000000 PM", $data); } + public function testGetMetadata() + { + $metadata = $this->dbDriver->getDbHelper()->getTableMetadata($this->dbDriver, 'Dogs'); + + foreach ($metadata as $key => $field) { + unset($metadata[$key]['dbType']); + } + $this->assertStringContainsString('.nextval', $metadata['id']['default']); + $metadata['id']['default'] = null; + + $this->assertEquals($this->getExpectedMetadata(), $metadata); + } + + protected function getExpectedMetadata() + { + $expected = parent::getExpectedMetadata(); + + foreach ($expected as $key => $value) { + $expected[$key]["name"] = strtoupper($expected[$key]["name"]); + } + $expected['id']["phpType"] = "float"; + $expected['age']["phpType"] = "float"; + + return $expected; + } + public function testTwoDifferentTransactions() { $this->connType = "new"; From 5eb42b2cd4db2c90b101d7a44299ed641a4f1fba Mon Sep 17 00:00:00 2001 From: Joao M Date: Wed, 19 Jun 2024 00:20:45 +0000 Subject: [PATCH 5/8] Update Documentation --- README.md | 25 ++++++++++++++----------- docs/freetds.md | 6 ------ docs/{mysql-ssl.md => mysql.md} | 6 +++++- docs/oracle.md | 27 +++++++++++++++++++++++++++ docs/sqlserver.md | 14 ++++++++++++++ 5 files changed, 60 insertions(+), 18 deletions(-) delete mode 100644 docs/freetds.md rename docs/{mysql-ssl.md => mysql.md} (84%) create mode 100644 docs/oracle.md create mode 100644 docs/sqlserver.md diff --git a/README.md b/README.md index c81f3bf..932bc2c 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,6 @@ See below the current implemented drivers: | Postgres | psql://username:password@hostname:port/database | getDbRelationalInstance() | | Sql Server (DbLib) | dblib://username:password@hostname:port/database | getDbRelationalInstance() | | Sql Server (Sqlsrv) | sqlsrv://username:password@hostname:port/database | getDbRelationalInstance() | -| Oracle (OCI) | oci://username:password@hostname:port/database | getDbRelationalInstance() | | Oracle (OCI8) | oci8://username:password@hostname:port/database | getDbRelationalInstance() | | Generic PDO | pdo://username:password@pdo_driver?PDO_PARAMETERS | getDbRelationalInstance() | @@ -42,19 +41,23 @@ $conn = \ByJG\AnyDataset\Db\Factory::getDbRelationalInstance("mysql://root:passw ## Examples -- [Basic Query and Update](basic-query.md) -- [Cache results](cache.md) -- [Database Transaction](transaction.md) -- [Load Balance and Connection Pooling](load-balance.md) -- [Database Helper](helper.md) +- [Basic Query and Update](docs/basic-query.md) +- [Cache results](docs/cache.md) +- [Database Transaction](docs/transaction.md) +- [Load Balance and Connection Pooling](docs/load-balance.md) +- [Database Helper](docs/helper.md) ## Advanced Topics -- [Passing Parameters to PDODriver](parameters.md) -- [MySQL SSL Connection](mysql-ssl.md) -- [FreeTDS/Dblib Date Issue](freetds.md) -- [Generic PDO Driver](generic-pdo-driver.md) -- [Running Tests](tests.md) +- [Passing Parameters to PDODriver](docs/parameters.md) +- [Generic PDO Driver](docs/generic-pdo-driver.md) +- [Running Tests](docs/tests.md) + +## Database Specifics + +- [MySQL](docs/mysql.md) +- [Oracle](docs/oracle.md) +- [SQLServer](docs/sqlserver.md) ## Install diff --git a/docs/freetds.md b/docs/freetds.md deleted file mode 100644 index 599ab65..0000000 --- a/docs/freetds.md +++ /dev/null @@ -1,6 +0,0 @@ -# FreeDTS / Dblib Date format Issues - -Date has the format `"Jul 27 2016 22:00:00.860"`. The solution is: - -Follow the solution: -[https://stackoverflow.com/questions/38615458/freetds-dateformat-issues](https://stackoverflow.com/questions/38615458/freetds-dateformat-issues) diff --git a/docs/mysql-ssl.md b/docs/mysql.md similarity index 84% rename from docs/mysql-ssl.md rename to docs/mysql.md index c1373ef..09699d5 100644 --- a/docs/mysql-ssl.md +++ b/docs/mysql.md @@ -1,4 +1,8 @@ -# Connecting To MySQL via SSL +# Driver: MySQL + +The connection string can have special attributes to connect using SSL. + +## Connecting To MySQL via SSL Read [here](https://gist.github.com/byjg/860065a828150caf29c20209ecbd5692) about create SSL mysql diff --git a/docs/oracle.md b/docs/oracle.md new file mode 100644 index 0000000..a2e5e1b --- /dev/null +++ b/docs/oracle.md @@ -0,0 +1,27 @@ +# Driver: Oracle + +The Oracle Driver don't use the PHP PDO Driver. Instead, uses the OCI library. + +The Oracle Connection String has the following format: + + +```text + oci8://user:pass@server:port/serviceName?parameters +``` + +The `parameters` can be: + +* `codepage`=UTF8 +* `conntype`=default|persistent|new +* `session_mode`=OCI_DEFAULT|OCI_SYSDBA|OCI_SYSOPER + +## conntype + +* If conntype = default will call the `oci_connect()` command; +* If conntype = new will call the `oci_new_connect()` command; +* If conntype = persistent will call the `oci_pconnect()` command; + +## session_mode + +The `OCI_DEFAULT`, `OCI_SYSDBA` AND `OCI_SYSOPER` are the PHP Constants +and they are `0`, `2` and `4` respectively; \ No newline at end of file diff --git a/docs/sqlserver.md b/docs/sqlserver.md new file mode 100644 index 0000000..f38ddaf --- /dev/null +++ b/docs/sqlserver.md @@ -0,0 +1,14 @@ +# Driver: Microsoft SQL Server + +The SQLServer can be connected using both FreeDTS / Dblib (Sybase) or SQLSVR driver. + +They have specifics, but both are able to connect to SQLServer. + +There are some specifics as you can see below. + +## The Date format Issues + +Date has the format `"Jul 27 2016 22:00:00.860"`. The solution is: + +Follow the solution: +[https://stackoverflow.com/questions/38615458/freetds-dateformat-issues](https://stackoverflow.com/questions/38615458/freetds-dateformat-issues) From 632f3c973e040357c37302f08b51c90a9822876c Mon Sep 17 00:00:00 2001 From: Joao M Date: Wed, 19 Jun 2024 00:23:25 +0000 Subject: [PATCH 6/8] Update Composer --- composer.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 70a1afe..fc3a00f 100644 --- a/composer.json +++ b/composer.json @@ -19,8 +19,7 @@ }, "require-dev": { "phpunit/phpunit": "5.7.*|7.4.*|^9.5", - "byjg/cache-engine": "4.9.*", - "ext-oci8": "*" + "byjg/cache-engine": "4.9.*" }, "suggest": { "ext-curl": "*", From cb733c6fc4f27890c76b6164d4737b001b6c31df Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Wed, 19 Jun 2024 19:58:31 -0500 Subject: [PATCH 7/8] Add Mutex to cache --- src/Traits/DbCacheTrait.php | 47 ++++++++++++++++++++++++++++++++----- tests/PdoSqliteTest.php | 43 ++++++++++++++++++++++++++------- 2 files changed, 76 insertions(+), 14 deletions(-) diff --git a/src/Traits/DbCacheTrait.php b/src/Traits/DbCacheTrait.php index 939de50..7742e26 100644 --- a/src/Traits/DbCacheTrait.php +++ b/src/Traits/DbCacheTrait.php @@ -53,16 +53,51 @@ protected function getOrSetSqlCacheStmt($sql) public function getIteratorUsingCache($sql, $params, ?CacheInterface $cache, $ttl, \Closure $closure): GenericIterator { $cacheKey = $this->getQueryKey($cache, $sql, $params); - $cachedResult = $this->getCachedResult($cacheKey, $cache); - if (!empty($cachedResult)) { - return $cachedResult; - } - $iterator = $closure($sql, $params); + do { + $lock = $this->mutexIsLocked($cache, $cacheKey); + if ($lock !== false) { + usleep(1000); + continue; + } + + $cachedResult = $this->getCachedResult($cacheKey, $cache); + if (!empty($cachedResult)) { + return $cachedResult; + } + $this->mutexLock($cache, $cacheKey); + try { + $iterator = $closure($sql, $params); + return $this->cacheResult($cacheKey, $iterator, $cache, $ttl); + } finally { + $this->mutexRelease($cache, $cacheKey); + } + } while (true); + } + + protected function mutexIsLocked(?CacheInterface $cache, ?string $cacheKey) + { + if (empty($cache)) { + return false; + } + return $cache->get($cacheKey . ".lock", false); + } - return $this->cacheResult($cacheKey, $iterator, $cache, $ttl); + protected function mutexLock(?CacheInterface $cache, ?string $cacheKey) + { + if (empty($cache)) { + return; + } + $cache->set($cacheKey . ".lock", time(), \DateInterval::createFromDateString('5 min'));; } + protected function mutexRelease(?CacheInterface $cache, ?string $cacheKey) + { + if (empty($cache)) { + return; + } + $cache->delete($cacheKey . ".lock"); + } protected function getCachedResult($key, ?CacheInterface $cache): ?GenericIterator { diff --git a/tests/PdoSqliteTest.php b/tests/PdoSqliteTest.php index c23b7f7..fee8304 100644 --- a/tests/PdoSqliteTest.php +++ b/tests/PdoSqliteTest.php @@ -295,7 +295,7 @@ public function testCachedResults() ); } - public function testCachedResultsNotFound() + public function testCachedResults1() { $cache = new ArrayCacheEngine(); @@ -305,18 +305,31 @@ public function testCachedResultsNotFound() [], $iterator->toArray() ); - $iterator = $this->dbDriver->getIterator('select * from info where id = :id', ['id' => 1], $cache, 60); + + // Add a new record to DB + $id = $this->dbDriver->execute("insert into info (iduser, number, property) values (2, 20, 40)"); + $this->assertEquals(4, $id); + $iterator = $this->dbDriver->getIterator('select * from info where id = :id', ['id' => 4]); $this->assertEquals( [ - [ "__id" => 0, "__key" => 0, 'id'=> 1, 'iduser' => 1, 'number' => 10.45, 'property' => 'xxx'], + ["id" => 4, "iduser" => 2, "number" => 20, "property" => '40'], ], $iterator->toArray() ); - // Remove it from DB (Still in cache) - $this->dbDriver->execute("insert into users (name, createdate) values ('John Doe 2', '2018-01-02')"); + // Get from cache, should return the same values as before the insert + $iterator = $this->dbDriver->getIterator('select * from info where id = :id', ['id' => 4], $cache, 60); + $this->assertEquals( + [], + $iterator->toArray() + ); + } - // Try get from cache + public function testCachedResults2() + { + $cache = new ArrayCacheEngine(); + + // Try get from cache (still return the same values) $iterator = $this->dbDriver->getIterator('select * from info where id = :id', ['id' => 1], $cache, 60); $this->assertEquals( [ @@ -324,9 +337,23 @@ public function testCachedResultsNotFound() ], $iterator->toArray() ); - $iterator = $this->dbDriver->getIterator('select * from info where id = :id', ['id' => 4], $cache, 60); + + // Update a record to DB + $id = $this->dbDriver->execute("update info set number = 1500 where id = :id", ["id" => 1]); + $iterator = $this->dbDriver->getIterator('select * from info where id = :id', ['id' => 1]); $this->assertEquals( - [], + [ + ["id" => 1, "iduser" => 1, "number" => 1500, "property" => 'xxx'], + ], + $iterator->toArray() + ); + + // Get from cache, should return the same values as before the update + $iterator = $this->dbDriver->getIterator('select * from info where id = :id', ['id' => 1], $cache, 60); + $this->assertEquals( + [ + [ "__id" => 0, "__key" => 0, 'id'=> 1, 'iduser' => 1, 'number' => 10.45, 'property' => 'xxx'], + ], $iterator->toArray() ); } From 5e31e22ee9b546b8b077548d30443dc77f8edc40 Mon Sep 17 00:00:00 2001 From: Joao M Date: Thu, 27 Jun 2024 20:26:49 +0000 Subject: [PATCH 8/8] Fix Oracle executeAndGetId() --- src/Helpers/DbOci8Functions.php | 40 ++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/src/Helpers/DbOci8Functions.php b/src/Helpers/DbOci8Functions.php index e5ad581..1258aa7 100644 --- a/src/Helpers/DbOci8Functions.php +++ b/src/Helpers/DbOci8Functions.php @@ -120,10 +120,44 @@ public function sqlDate($format, $column = null) */ public function executeAndGetInsertedId(DbDriverInterface $dbdataset, $sql, $param) { + preg_match('/INSERT INTO ([a-zA-Z0-9_]+)/i', $sql, $matches); + $tableName = $matches[1] ?? null; + + if (!empty($tableName)) { + $tableName = strtoupper($tableName); + + // Get the primary key of the table + $primaryKeyResult = $dbdataset->getScalar("SELECT cols.column_name + FROM all_constraints cons, all_cons_columns cols + WHERE cols.table_name = '{$tableName}' + AND cons.constraint_type = 'P' + AND cons.constraint_name = cols.constraint_name + AND cons.owner = cols.owner + AND ROWNUM = 1"); + + // Get the default value of the primary key + $defaultValueResult = $dbdataset->getScalar("SELECT DATA_DEFAULT + FROM USER_TAB_COLUMNS + WHERE TABLE_NAME = '{$tableName}' + AND COLUMN_NAME = '{$primaryKeyResult}'"); + } + $dbdataset->execute($sql, $param); - return 4; -// oci_inse -// return $dbdataset->getScalar('select lastval()'); + + if (!empty($tableName) && !empty($defaultValueResult)) { + + // Check if the default value is a sequence's nextval + if (strpos($defaultValueResult, '.nextval') !== false) { + // Extract the sequence name + $sequenceName = str_replace('.nextval', '', $defaultValueResult); + + // Return the CURRVAL of the sequence + return $dbdataset->getScalar("SELECT {$sequenceName}.currval FROM DUAL"); + } + } + + return null; + } public function hasForUpdate()