diff --git a/www/lib/CoreLibs/DB/SQL/Interface/DatabaseInterface.php b/www/lib/CoreLibs/DB/SQL/Interface/DatabaseInterface.php new file mode 100644 index 00000000..9c4bbd40 --- /dev/null +++ b/www/lib/CoreLibs/DB/SQL/Interface/DatabaseInterface.php @@ -0,0 +1,90 @@ +>|false + */ + public function dbShowTableMetaData(string $table): array|false; + + /** + * for reading or simple execution, no return data + * + * @param string $query + * @return int|false + */ + public function dbExec(string $query): int|false; + + /** + * Run a simple query and return its statement + * + * @param string $query + * @return \PDOStatement|false + */ + public function dbQuery(string $query): \PDOStatement|false; + + /** + * Execute one query with params + * + * @param string $query + * @param array $params + * @return \PDOStatement|false + */ + public function dbExecParams(string $query, array $params): \PDOStatement|false; + + /** + * Prepare query + * + * @param string $query + * @return \PDOStatement|false + */ + public function dbPrepare(string $query): \PDOStatement|false; + + /** + * execute a cursor + * + * @param \PDOStatement $cursor + * @param array $params + * @return bool + */ + public function dbCursorExecute(\PDOStatement $cursor, array $params): bool; + + /** + * return array with data, when finshed return false + * also returns false on error + * + * TODO: This is currently a one time run + * if the same query needs to be run again, the cursor_ext must be reest + * with dbCacheReset + * + * @param string $query + * @param array $params + * @return array|false + */ + public function dbReturnArray(string $query, array $params = []): array|false; + + /** + * get current db handler + * this is for raw access + * + * @return \PDO + */ + public function getDbh(): \PDO; +} + +// __END__ diff --git a/www/lib/CoreLibs/DB/SQLiteIO.php b/www/lib/CoreLibs/DB/SQLiteIO.php new file mode 100644 index 00000000..6d3713de --- /dev/null +++ b/www/lib/CoreLibs/DB/SQLiteIO.php @@ -0,0 +1,432 @@ + extended cursoers string index with content */ + private array $cursor_ext = []; + + /** + * init database system + * + * @param \CoreLibs\Logging\Logging $log + * @param string $dsn + */ + public function __construct( + \CoreLibs\Logging\Logging $log, + string $dsn + ) { + $this->log = $log; + // open new connection + if ($this->__connectToDB($dsn) === false) { + throw new \ErrorException("Cannot load database: " . $dsn, 1); + } + } + + // ********************************************************************* + // MARK: PRIVATE METHODS + // ********************************************************************* + + /** + * Get a cursor dump with all info + * + * @param \PDOStatement $cursor + * @return string|false + */ + private function __dbGetCursorDump(\PDOStatement $cursor): string|false + { + // get the cursor info + ob_start(); + $cursor->debugDumpParams(); + $cursor_dump = ob_get_contents(); + ob_end_clean(); + return $cursor_dump; + } + + /** + * fetch rows from a cursor (post execute) + * + * @param \PDOStatement $cursor + * @return array|false + */ + private function __dbFetchArray(\PDOStatement $cursor): array|false + { + try { + // on empty array return false + // TODO make that more elegant? + return empty($row = $cursor->fetch(mode:\PDO::FETCH_NAMED)) ? false : $row; + } catch (\PDOException $e) { + $this->log->error( + "Cannot fetch from cursor", + [ + "dsn" => $this->dsn, + "DumpParams" => $this->__dbGetCursorDump($cursor), + "PDOException" => $e + ] + ); + return false; + } + } + + // MARK: open database + + /** + * Open database + * reports errors for wrong DSN or failed connection + * + * @param string $dsn + * @return bool + */ + private function __connectToDB(string $dsn): bool + { + // check if dsn starts with ":" + if (!str_starts_with($dsn, "sqlite:")) { + $this->log->error( + "Invalid dsn string", + [ + "dsn" => $dsn + ] + ); + return false; + } + // TODO: if not ":memory:" check if path to file is writeable by system + // avoid double open + if (!empty($this->dsn) && $dsn == $this->dsn) { + $this->log->info( + "Connection already establisehd with this dsn", + [ + "dsn" => $dsn, + ] + ); + return true; + } + // TODO: check that folder is writeable + // set DSN and open connection + $this->dsn = $dsn; + try { + $this->dbh = new \PDO($this->dsn); + } catch (\PDOException $e) { + $this->log->error( + "Cannot open database", + [ + "dsn" => $this->dsn, + "PDOException" => $e + ] + ); + return false; + } + return true; + } + + // ********************************************************************* + // MARK: PUBLIC METHODS + // ********************************************************************* + + // MARK: db meta data (table info) + + /** + * Table meta data + * Note that if columns have multi + * + * @param string $table + * @return array>|false + */ + public function dbShowTableMetaData(string $table): array|false + { + $table_info = []; + $query = <<dbReturnArray($query, [$table]))) { + $table_info[] = [ + 'cid' => $row['cid'], + 'name' => $row['name'], + 'type' => $row['type'], + 'notnull' => $row['notnull'], + 'dflt_value' => $row['dflt_value'], + 'pk' => $row['pk'], + 'idx_name' => $row['idx_name'], + 'idx_unique' => $row['idx_unique'], + 'idx_origin' => $row['idx_origin'], + 'idx_partial' => $row['idx_partial'], + ]; + } + + if (!$table_info) { + return false; + } + return $table_info; + } + + // MARK: db exec + + /** + * for reading or simple execution, no return data + * + * @param string $query + * @return int|false + */ + public function dbExec(string $query): int|false + { + try { + return $this->dbh->exec($query); + } catch (\PDOException $e) { + $this->log->error( + "Cannot execute query", + [ + "dsn" => $this->dsn, + "query" => $query, + "PDOException" => $e + ] + ); + return false; + } + } + + // MARK: db query + + /** + * Run a simple query and return its statement + * + * @param string $query + * @return \PDOStatement|false + */ + public function dbQuery(string $query): \PDOStatement|false + { + try { + return $this->dbh->query($query, \PDO::FETCH_NAMED); + } catch (\PDOException $e) { + $this->log->error( + "Cannot run query", + [ + "dsn" => $this->dsn, + "query" => $query, + "PDOException" => $e + ] + ); + return false; + } + } + + // MARK: db prepare & execute calls + + /** + * Execute one query with params + * + * @param string $query + * @param array $params + * @return \PDOStatement|false + */ + public function dbExecParams(string $query, array $params): \PDOStatement|false + { + // prepare query + if (($cursor = $this->dbPrepare($query)) === false) { + return false; + } + // execute the query, on failure return false + if ($this->dbCursorExecute($cursor, $params) === false) { + return false; + } + return $cursor; + } + + /** + * Prepare query + * + * @param string $query + * @return \PDOStatement|false + */ + public function dbPrepare(string $query): \PDOStatement|false + { + try { + // store query with cursor so we can reference? + return $this->dbh->prepare($query); + } catch (\PDOException $e) { + $this->log->error( + "Cannot open cursor", + [ + "dsn" => $this->dsn, + "query" => $query, + "PDOException" => $e + ] + ); + return false; + } + } + + /** + * execute a cursor + * + * @param \PDOStatement $cursor + * @param array $params + * @return bool + */ + public function dbCursorExecute(\PDOStatement $cursor, array $params): bool + { + try { + return $cursor->execute($params); + } catch (\PDOException $e) { + // write error log + $this->log->error( + "Cannot execute prepared query", + [ + "dsn" => $this->dsn, + "params" => $params, + "DumpParams" => $this->__dbGetCursorDump($cursor), + "PDOException" => $e + ] + ); + return false; + } + } + + // MARK: db return array + + /** + * Returns hash for query + * Hash is used in all internal storage systems for return data + * + * @param string $query The query to create the hash from + * @param array $params If the query is params type we need params + * data to create a unique call one, optional + * @return string Hash, as set by hash long + */ + public function dbGetQueryHash(string $query, array $params = []): string + { + return Hash::hashLong( + $query . ( + $params !== [] ? + '#' . json_encode($params) : '' + ) + ); + } + + /** + * resets all data stored to this query + * @param string $query The Query whose cache should be cleaned + * @param array $params If the query is params type we need params + * data to create a unique call one, optional + * @return bool False if query not found, true if success + */ + public function dbCacheReset(string $query, array $params = []): bool + { + $query_hash = $this->dbGetQueryHash($query, $params); + // clears cache for this query + if (empty($this->cursor_ext[$query_hash]['query'])) { + $this->log->error('Cannot reset cursor_ext with given query and params', [ + "query" => $query, + "params" => $params, + ]); + return false; + } + unset($this->cursor_ext[$query_hash]); + return true; + } + + /** + * return array with data, when finshed return false + * also returns false on error + * + * TODO: This is currently a one time run + * if the same query needs to be run again, the cursor_ext must be reest + * with dbCacheReset + * + * @param string $query + * @param array $params + * @return array|false + */ + public function dbReturnArray(string $query, array $params = []): array|false + { + $query_hash = $this->dbGetQueryHash($query, $params); + if (!isset($this->cursor_ext[$query_hash])) { + $this->cursor_ext[$query_hash] = [ + // cursor null: unset, if set \PDOStatement + 'cursor' => null, + // the query used in this call + 'query' => $query, + // parameter + 'params' => $params, + // how many rows have been read from db + 'read_rows' => 0, + // when fetch array or cache read returns false + // in loop read that means dbReturn retuns false without error + 'finished' => false, + ]; + if (!empty($params)) { + if (($cursor = $this->dbExecParams($query, $params)) === false) { + return false; + } + } else { + if (($cursor = $this->dbQuery($query)) === false) { + return false; + } + } + $this->cursor_ext[$query_hash]['cursor'] = $cursor; + } + // flag finished if row is false + $row = $this->__dbFetchArray($this->cursor_ext[$query_hash]['cursor']); + if ($row === false) { + $this->cursor_ext[$query_hash]['finished'] = true; + } else { + $this->cursor_ext[$query_hash]['read_rows']++; + } + return $row; + } + + // MARK other interface + + /** + * get current db handler + * this is for raw access + * + * @return \PDO + */ + public function getDbh(): \PDO + { + return $this->dbh; + } +} + +// __END__