diff --git a/4dev/tests/DB/CoreLibsDBSqLiteTest.php b/4dev/tests/DB/CoreLibsDBSqLiteTest.php new file mode 100644 index 00000000..14eff9fe --- /dev/null +++ b/4dev/tests/DB/CoreLibsDBSqLiteTest.php @@ -0,0 +1,46 @@ +markTestSkipped( + 'The SqLite extension is not available.' + ); + } + } + + /** + * Undocumented function + * + * @testdox DB\SqLite Class tests + * + * @return void + */ + public function testSqLite() + { + $this->markTestIncomplete( + 'DB\SqLite Tests have not yet been implemented' + ); + } +} + +// __END__ diff --git a/www/admin/class_test.db.sqlite.php b/www/admin/class_test.db.sqlite.php new file mode 100644 index 00000000..f481344d --- /dev/null +++ b/www/admin/class_test.db.sqlite.php @@ -0,0 +1,157 @@ + BASE . LOG, + 'log_file_id' => $LOG_FILE_ID, + 'log_per_date' => true, +]); +// db connection and attach logger +$db = new CoreLibs\DB\SqLite($log, "sqlite:" . $sql_file); +$db->log->debug('START', '=============================>'); + +$PAGE_NAME = 'TEST CLASS: DB: SqLite'; +print ""; +print "" . $PAGE_NAME . ""; +print ""; +print '
Class Test Master
'; + +print "
"; + +echo "Create Tables on demand
"; + +$query = <<dbExec($query); +// ********************** +$query = <<dbExec($query); + +print "
"; + +$table = 'test'; +echo "Table info for: " . $table . "
"; + +if (($table_info = $db->dbShowTableMetaData($table)) === false) { + print "Read problem for: $table
"; +} else { + print "TABLE INFO:
" . print_r($table_info, true) . "

"; +} + +print "
"; + +echo "Insert into 'test'
"; + +$query = <<dbExecParams($query, [ + 'test', rand(1, 100), true, + date('Y-m-d H:i:s'), date_format(date_create("now"), 'Y-m-d H:i:s.u'), date('Y-m-d'), + // julianday pass through + date('Y-m-d H:i:s'), + // use "U" if no unixepoch in query + date('U'), date('Y-m-d H:i:s'), + 1.5, 10.5, 'Anything' +]); + +print "
"; + +echo "Insert into 'test_no_pk'
"; + +$query = <<dbExecParams($query, ['test no pk', rand(100, 200)]); + +print "
"; + +$query = <<dbReturnArray($query))) { + print "ROW: PK(test_id): " . $row["test_id"] + . ", Text: " . $row["c_text"] . ", Int: " . $row["c_integer"] + . ", Int Default: " . $row["c_integer_default"] + . ", Date Default: " . $row["c_datetime_default"] + . "
"; +} + +echo "
"; + +$query = <<dbReturnArray($query))) { + print "ROW[CURSOR]: PK(rowid): " . $row["rowid"] + . ", Text: " . $row["c_text"] . ", Int: " . $row["c_integer"] + . "
"; +} + +print ""; + +// __END__ diff --git a/www/admin/class_test.php b/www/admin/class_test.php index 315effd3..7276f51d 100644 --- a/www/admin/class_test.php +++ b/www/admin/class_test.php @@ -73,6 +73,7 @@ $test_files = [ 'class_test.db.query-placeholder.php' => 'Class Test: DB query placeholder convert', 'class_test.db.dbReturn.php' => 'Class Test: DB dbReturn', 'class_test.db.single.php' => 'Class Test: DB single query tests', + 'class_test.db.sqlite.php' => 'Class Test: DB: SqLite', 'class_test.convert.colors.php' => 'Class Test: CONVERT COLORS', 'class_test.check.colors.php' => 'Class Test: CHECK COLORS', 'class_test.mime.php' => 'Class Test: MIME', diff --git a/www/lib/CoreLibs/DB/Interface/DatabaseInterface.php b/www/lib/CoreLibs/DB/Interface/DatabaseInterface.php new file mode 100644 index 00000000..6960237e --- /dev/null +++ b/www/lib/CoreLibs/DB/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/SqLite.php b/www/lib/CoreLibs/DB/SqLite.php new file mode 100644 index 00000000..89e348f9 --- /dev/null +++ b/www/lib/CoreLibs/DB/SqLite.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->dbh instanceof \PDO) { + $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__