diff --git a/WebFiori/Database/Database.php b/WebFiori/Database/Database.php
index 47618514..5b6d4c29 100644
--- a/WebFiori/Database/Database.php
+++ b/WebFiori/Database/Database.php
@@ -1,936 +1,946 @@
-setConnectionInfo($connectionInfo);
- }
- $this->queries = [];
- $this->tablesArr = [];
- $this->lastErr = [
- 'code' => 0,
- 'message' => ''
- ];
- }
- /**
- * Adds a database query to the set of queries at which they were executed.
- *
- * This method is called internally by the library to add the query. The
- * developer does not have to call this method manually.
- *
- * @param string $query SQL query as string.
- *
- * @param string $type The type of the query such as 'select', 'update' or
- * 'delete'.
- *
- *
- */
- public function addQuery(string $query, string $type) {
- $this->queries[] = [
- 'type' => $type,
- 'query' => $query
- ];
- }
- /**
- * Adds a table to the instance.
- *
- * @param Table $table the table that will be added.
- *
- * @param bool $updateOwnerDb If the owner database of the table is already
- * set and this parameter is set to true, the owner database will be
- * updated to the database specified in the instance. This parameter
- * is used to maintain foreign key relationships between tables which
- * belongs to different databases.
- *
- * @return bool If the table is added, the method will return true. False
- * otherwise.
- *
- *
- */
- public function addTable(Table $table, bool $updateOwnerDb = true) : bool {
- $trimmedName = $table->getNormalName();
-
- if (!$this->hasTable($trimmedName)) {
- if ($table->getOwner() === null || ($table->getOwner() !== null && $updateOwnerDb)) {
- $table->setOwner($this);
- }
- $this->tablesArr[$trimmedName] = $table;
-
- return true;
- }
-
- return false;
- }
-
- /**
- * Build a 'where' expression.
- *
- * This method can be used to append an 'and' condition to an already existing
- * 'where' condition.
- *
- * @param AbstractQuery|string $col A string that represents the name of the
- * column that will be evaluated. This also can be an object of type
- * 'AbstractQuery' in case the developer would like to build a sub-where
- * condition.
- *
- *
- * @param mixed $val The value (or values) at which the column will be evaluated
- * against. Can be ignored if first parameter is of
- * type 'AbstractQuery'.
- *
- * @param string $cond A string that represents the condition at which column
- * value will be evaluated against. Can be ignored if first parameter is of
- * type 'AbstractQuery'.
- *
- * @return AbstractQuery The method will return an instance of the class
- * 'AbstractQuery' which can be used to build SQL queries.
- *
- * @throws DatabaseException
- */
- public function andWhere($col, $val, string $cond = '=') : AbstractQuery {
- return $this->where($col, $val, $cond);
- }
- /**
- * Rest all attributes of the class to original values.
- *
- *
- */
- /**
- * Clear all queries and reset the query generator state.
- *
- * This method clears the internal query queue and resets the query generator
- * to its initial state, preparing for new query operations.
- */
- public function clear() {
- $this->queries = [];
- $this->getQueryGenerator()->reset();
- $this->resetBinding();
- }
-
- /**
- * Clear all collected performance metrics.
- *
- * Removes all stored performance data from memory or database storage.
- */
- public function clearPerformanceMetrics(): void {
- if ($this->performanceMonitor !== null) {
- $this->performanceMonitor->clearMetrics();
- }
- }
- /**
- * Creates a blueprint of a table that can be used to build table structure.
- *
- * @param string $name The name of the table as it appears in the database.
- *
- * @return Table the method will return an instance of the class 'Table'
- * which will be based on the type of DBMS at which the instance is
- * connected to. If connected to MySQL, an instance of 'MySQLTable' is
- * returned. If connected to MSSQL, an instance of MSSQLTable is returned
- * and so on.
- */
- public function createBlueprint(string $name) : Table {
- $connection = $this->getConnection();
-
- if ($connection === null) {
- $dbType = 'mysql';
- } else {
- $dbType = $connection->getConnectionInfo()->getDatabaseType();
- }
-
- if ($dbType == 'mssql') {
- $blueprint = new MSSQLTable($name);
- } else {
- $blueprint = new MySQLTable($name);
- }
- $this->addTable($blueprint);
-
- return $blueprint;
- }
- /**
- * Constructs a query which can be used to create selected database table.
- *
- * @return AbstractQuery The method will return an instance of the class
- * 'AbstractQuery' which can be used to build SQL queries.
- *
- *
- */
- public function createTable() : AbstractQuery {
- return $this->getQueryGenerator()->createTable();
- }
- /**
- * Create SQL query which can be used to create all database tables.
- *
- * @return AbstractQuery The method will return an instance of the class
- * 'AbstractQuery' which can be used to build SQL queries.
- *
- * .1
- */
- public function createTables() : AbstractQuery {
- $generatedQuery = '';
-
- foreach ($this->getTables() as $tableObj) {
- if ($tableObj->getColsCount() != 0) {
- $generatedQuery .= $tableObj->toSQL()."\n";
- }
- }
- $this->getQueryGenerator()->setQuery($generatedQuery, true);
-
- return $this->getQueryGenerator();
- }
- /**
- * Constructs a query which can be used to remove a record from the
- * selected table.
- *
- * @return AbstractQuery The method will return an instance of the class
- * 'AbstractQuery' which can be used to build SQL queries.
- *
- *
- */
- public function delete() : AbstractQuery {
- $this->clear();
-
- return $this->getQueryGenerator()->delete();
- }
-
- /**
- * Disable query performance monitoring.
- *
- * Stops collecting performance data for query executions.
- * Existing collected data is preserved.
- */
- public function disablePerformanceMonitoring(): void {
- $this->performanceEnabled = false;
- }
- /**
- * Constructs a query which will drop a database table when executed.
- *
- * @return AbstractQuery The method will return an instance of the class
- * 'AbstractQuery' which can be used to build SQL queries.
- *
- *
- */
- public function drop() : AbstractQuery {
- $this->clear();
-
- return $this->getQueryGenerator()->drop();
- }
-
- /**
- * Enable query performance monitoring.
- *
- * Initializes the performance monitoring system with default configuration.
- * Performance data will be collected for all subsequent query executions.
- */
- public function enablePerformanceMonitoring(): void {
- $this->performanceEnabled = true;
-
- if ($this->performanceMonitor === null) {
- $this->performanceMonitor = new QueryPerformanceMonitor([
- PerformanceOption::ENABLED => true
- ], $this);
- }
- }
- /**
- * Execute SQL query.
- *
- * @throws DatabaseException The method will throw an exception if one
- * of 3 cases happens:
- *
- * - No connection was established with any database.
- * - An error has occurred while executing the query.
- *
- *
- * @return ResultSet|null If the last executed query was a select, show or
- * describe query, the method will return an object of type 'ResultSet' that
- * holds fetched records. Other than that, the method will return null.
- *
- *
- */
- public function execute() {
- $conn = $this->getConnection();
- $lastQuery = $this->getLastQuery();
-
- // Start performance monitoring
- $startTime = $this->performanceEnabled ? microtime(true) : null;
-
- if (!$conn->runQuery($this->getQueryGenerator())) {
- throw new DatabaseException($conn->getLastErrCode().' - '.$conn->getLastErrMessage(), $conn->getLastErrCode());
- }
- $this->queries[] = $lastQuery;
- $lastQueryType = $this->getQueryGenerator()->getLastQueryType();
- $this->clear();
- $resultSet = null;
-
- if (in_array($lastQueryType, ['select', 'show', 'describe'])) {
- $resultSet = $this->getLastResultSet();
- }
- $this->getQueryGenerator()->setQuery(null);
-
- // Record performance metrics
- if ($this->performanceEnabled && $this->performanceMonitor && $startTime !== null) {
- $executionTime = (microtime(true) - $startTime) * 1000; // Convert to milliseconds
- $this->performanceMonitor->recordQuery($lastQuery, $executionTime, $resultSet);
- }
-
- return $resultSet;
- }
- /**
- * Returns the connection at which the instance will use to run SQL queries.
- *
- * This method will try to connect to the database if no connection is active.
- * If the connection was not established, the method will throw an exception.
- * If the connection is already active, the method will return it.
- *
- * @return Connection The connection at which the instance will use to run SQL queries.
- *
- *
- *
- */
- public function getConnection() : ?Connection {
- $connInfo = $this->getConnectionInfo();
-
- if ($this->connection === null && $connInfo !== null) {
- $driver = $connInfo->getDatabaseType();
-
- if ($driver == 'mysql') {
- $conn = new MySQLConnection($connInfo);
- $this->setConnection($conn);
- } else if ($driver == 'mssql') {
- $conn = new MSSQLConnection($connInfo);
- $this->setConnection($conn);
- }
- }
-
- return $this->connection;
- }
- /**
- * Returns an object that holds connection information.
- *
- * @return ConnectionInfo|null An object that holds connection information.
- *
- *
- */
- public function getConnectionInfo() : ?ConnectionInfo {
- return $this->connectionInfo;
- }
-
- /**
- * Returns an indexed array that contains all executed SQL queries.
- *
- * @return array An indexed array that contains all executed SQL queries.
- *
- */
- public function getExecutedQueries() : array {
- return $this->getConnection()->getExecutedQueries();
- }
- /**
- * Returns the last database error info.
- *
- * @return array The method will return an associative array with two indices.
- * The first one is 'message' which contains error message and the second one
- * is 'code' which contains error code.
- *
- *
- */
- public function getLastError() : array {
- if ($this->connection !== null) {
- $this->lastErr = [
- 'message' => $this->connection->getLastErrMessage(),
- 'code' => $this->connection->getLastErrCode()
- ];
- }
-
- return $this->lastErr;
- }
- /**
- * Returns the last generated SQL query.
- *
- * @return string Last generated SQL query as string.
- *
- *
- */
- public function getLastQuery() : string {
- return trim($this->getQueryGenerator()->getQuery());
- }
-
- /**
- * Returns the last result set which was generated from executing a query such
- * as a 'select' query.
- *
- * @return ResultSet|null The last result set. If no result set is available,
- * the method will return null.
- */
- public function getLastResultSet() {
- return $this->getConnection()->getLastResultSet();
- }
- /**
- * Returns the name of the database.
- *
- * @return string The name of the database.
- *
- *
- */
- public function getName() : string {
- return $this->getConnectionInfo()->getDBName();
- }
-
- /**
- * Get all collected performance metrics.
- *
- * @return array Array of QueryMetric instances or metric arrays
- */
- public function getPerformanceMetrics(): array {
- if ($this->performanceMonitor === null) {
- return [];
- }
-
- return $this->performanceMonitor->getMetrics();
- }
-
- /**
- * Get the performance monitor instance.
- *
- * @return QueryPerformanceMonitor|null The performance monitor instance or null if not initialized.
- */
- public function getPerformanceMonitor(): ?QueryPerformanceMonitor {
- return $this->performanceMonitor;
- }
-
- /**
- * Get performance statistics summary.
- *
- * @return array Statistics including total queries, average execution time,
- * min/max times, and slow query count
- */
- public function getPerformanceStatistics(): array {
- if ($this->performanceMonitor === null) {
- return [
- 'total_queries' => 0,
- 'avg_execution_time' => 0,
- 'min_execution_time' => 0,
- 'max_execution_time' => 0,
- 'slow_queries_count' => 0
- ];
- }
-
- return $this->performanceMonitor->getStatistics();
- }
- /**
- * Returns an indexed array that contains all generated SQL queries.
- *
- * @return array An indexed array that contains all generated SQL queries.
- *
- *
- */
- public function getQueries() : array {
- return $this->queries;
- }
- /**
- * Returns the query builder which is used to build SQL queries.
- *
- * @return AbstractQuery
- *
- *
- */
- public function getQueryGenerator() : AbstractQuery {
- if (!$this->isConnected()) {
- if ($this->getConnectionInfo() === null) {
- throw new DatabaseException("Connection information not set.");
- } else {
- $lastErr = $this->getLastError();
- throw new DatabaseException("Not connected to database. Error Code: ".$lastErr['code'].'. Message: "'.$lastErr['message']);
- }
- }
-
- return $this->queryGenerator;
- }
-
- /**
- * Get slow queries based on configured or custom threshold.
- *
- * @param int|null $thresholdMs Custom threshold in milliseconds. If null,
- * uses configured slow query threshold.
- * @return array Array of slow query metrics
- */
- public function getSlowQueries(?int $thresholdMs = null): array {
- if ($this->performanceMonitor === null) {
- return [];
- }
-
- return $this->performanceMonitor->getSlowQueries($thresholdMs);
- }
- /**
- * Returns a table structure as an object given its name.
- *
- * @param string $tblName The name of the table.
- *
- * @return Table|null If a table which has the given name is existed, it will
- * be returned as an object. Other than that, null is returned.
- *
- *
- */
- public function getTable(string $tblName) {
- $trimmed = trim($tblName);
-
- if (!isset($this->tablesArr[$trimmed])) {
- return null;
- }
- $engine = 'mysql';
- $info = $this->getConnectionInfo();
-
- if ($info !== null) {
- $engine = $info->getDatabaseType();
- }
- $table = $this->tablesArr[$trimmed];
-
- return TableFactory::map($engine, $table);
- }
- /**
- * Returns an array that contains all added tables.
- *
- * @return array The method will return an associative array. The indices
- * of the array are tables names and the values are objects of type 'Table'.
- *
- * .1
- */
- public function getTables() : array {
- return $this->tablesArr;
- }
- /**
- * Checks if a table exist in the database or not.
- *
- * @param string $tableName The name of the table.
- *
- * @return bool If the table exist, the method will return true.
- * False if it does not exist.
- *
- *
- */
- public function hasTable(string $tableName) : bool {
- return isset($this->tablesArr[$tableName]);
- }
- /**
- * Constructs a query which can be used to insert a record in the selected
- * table.
- *
- * @param array $colsAndVals An associative array that holds the columns and
- * values. The indices of the array should be column keys and the values
- * of the indices are the new values.
- *
- * @return AbstractQuery The method will return an instance of the class
- * 'AbstractQuery' which can be used to build SQL queries.
- *
- *
- */
- public function insert(array $colsAndVals) : AbstractQuery {
- $this->clear();
-
- return $this->getQueryGenerator()->insert($colsAndVals);
- }
- /**
- * Check if database connection is established and active.
- *
- * @return bool True if connected to database, false otherwise.
- */
- public function isConnected() : bool {
- if ($this->getConnectionInfo() === null) {
- return false;
- }
- try {
- if ($this->getConnection() === null) {
- return false;
- }
- } catch (DatabaseException $ex) {
- $this->lastErr = [
- 'code' => $ex->getCode(),
- 'message' => $ex->getMessage()
- ];
-
- return false;
- }
-
- return true;
- }
- /**
- * Sets the number of records that will be fetched by the query.
- *
- * @param int $limit A number which is greater than 0.
- *
- * @return AbstractQuery The method will return an instance of the class
- * 'AbstractQuery' which can be used to build SQL queries.
- *
- *
- */
- public function limit(int $limit) : AbstractQuery {
- return $this->getQueryGenerator()->limit($limit);
- }
- /**
- * Sets the offset.
- *
- * The offset is basically the number of records that will be skipped from the
- * start.
- *
- * @param int $offset Number of records to skip.
- *
- * @return AbstractQuery The method will return an instance of the class
- * 'AbstractQuery' which can be used to build SQL queries.
- *
- *
- */
- public function offset(int $offset) : AbstractQuery {
- return $this->getQueryGenerator()->offset($offset);
- }
-
- /**
- * Build a 'where' expression.
- *
- * This method can be used to append an 'or' condition to an already existing
- * 'where' condition.
- *
- * @param AbstractQuery|string $col A string that represents the name of the
- * column that will be evaluated. This also can be an object of type
- * 'AbstractQuery' in case the developer would like to build a sub-where
- * condition.
- *
- * @param mixed $val The value (or values) at which the column will be evaluated
- * against. Can be ignored if first parameter is of
- * type 'AbstractQuery'.
- *
- * @param string $cond A string that represents the condition at which column
- * value will be evaluated against. Can be ignored if first parameter is of
- * type 'AbstractQuery'.
- *
- * @return AbstractQuery The method will return an instance of the class
- * 'AbstractQuery' which can be used to build SQL queries.
- *
- * @throws DatabaseException
- */
- public function orWhere(string $col, mixed $val = null, string $cond = '=') : AbstractQuery {
- return $this->where($col, $val, $cond, 'or');
- }
- /**
- * Constructs a query which can be used to fetch a set of records as a page.
- *
- * @param int $num Page number. It should be a number greater than or equals
- * to 1.
- *
- * @param int $itemsCount Number of records per page. Must be a number greater than or equals to 1.
- *
- * @return AbstractQuery The method will return an instance of the class
- * 'AbstractQuery' which can be used to build SQL queries.
- *
- *
- */
- public function page(int $num, int $itemsCount) : AbstractQuery {
- return $this->getQueryGenerator()->page($num, $itemsCount);
- }
- /**
- * Reset the bindings which was set by building and executing a query.
- *
- * @return Database The method will return the instance at which the method
- * is called on.
- */
- public function resetBinding() : Database {
- $this->getQueryGenerator()->resetBinding();
-
- return $this;
- }
- /**
- * Constructs a query that can be used to get records from a table.
- *
- * @param array $cols An array that holds the keys of the columns that will
- * be selected.
- *
- * @return AbstractQuery The method will return an instance of the class
- * 'AbstractQuery' which can be used to build SQL queries.
- *
- *
- */
- public function select(array $cols = ['*']) : AbstractQuery {
- $this->clear();
-
- return $this->getQueryGenerator()->select($cols);
- }
- /**
- * Sets the connection that will be used by the schema.
- *
- * @param Connection $con An active connection.
- *
- *
- */
- public function setConnection(Connection $con) {
- $this->connection = $con;
- }
- /**
- * Sets database connection information.
- *
- * @param ConnectionInfo $info An object that holds connection information.
- *
- * @throws DatabaseException The method will throw an exception if database
- * driver is not supported.
- *
- *
- */
- public function setConnectionInfo(ConnectionInfo $info) {
- $driver = $info->getDatabaseType();
-
- if ($driver == 'mysql') {
- $this->queryGenerator = new MySQLQuery();
- $this->queryGenerator->setSchema($this);
- } else if ($driver == 'mssql') {
- $this->queryGenerator = new MSSQLQuery();
- $this->queryGenerator->setSchema($this);
- } else {
- throw new DatabaseException('Driver not supported: "'.$driver.'".');
- }
- $this->connectionInfo = $info;
- }
-
- /**
- * Configure performance monitoring settings.
- *
- * @param array $config Configuration array using PerformanceOption constants
- *
- * @throws InvalidArgumentException If configuration values are invalid
- */
- public function setPerformanceConfig(array $config): void {
- if ($this->performanceMonitor === null) {
- $this->performanceMonitor = new QueryPerformanceMonitor($config, $this);
- } else {
- $this->performanceMonitor->updateConfig($config);
- }
-
- $this->performanceEnabled = $config[PerformanceOption::ENABLED] ?? $this->performanceEnabled;
- }
-
- /**
- * Sets the database query to a raw SQL query.
- *
- * @param string $query A string that represents the query.
- *
- * @return Database The method will return the same instance at which the
- * method is called on.
- *
- * @throws DatabaseException
- */
- public function setQuery(string $query) : Database {
- $t = $this->getQueryGenerator()->getTable();
-
- if ($t !== null) {
- $t->getSelect()->clear();
- }
- $this->getQueryGenerator()->setQuery($query);
-
- return $this;
- }
- /**
- * Select one of the tables which exist on the schema and use it to build
- * SQL queries.
- *
- * @param string $tblName The name of the table.
- *
- * @return AbstractQuery The method will return an instance of the class
- * 'AbstractQuery' which can be used to build SQL queries.
- *
- *
- */
- public function table(string $tblName) : AbstractQuery {
- return $this->getQueryGenerator()->table($tblName);
- }
- /**
- * Start SQL transaction.
- *
- * This will disable auto-commit.
- *
- * @param callable $transaction A function that holds the logic of the transaction.
- * The function must return true or null for success. If false is
- * returned, it means the transaction failed and will be rolled back.
- *
- * @param array $transactionArgs An optional array of parameters to be passed
- * to the transaction.
- *
- * @return bool If the transaction completed without errors, the method will
- * return true. False otherwise.
- *
- * @throws DatabaseException The method will throw an exception if it was
- * rolled back due to an error.
- */
- public function transaction(callable $transaction, array $transactionArgs = []) : bool {
- $conn = $this->getConnection();
- $name = 'transaction_'.rand();
-
- try {
- $args = array_merge([$this], $transactionArgs);
- $conn->beginTransaction($name);
- $result = call_user_func_array($transaction, $args);
-
- if ($result === null || $result === true) {
- $conn->commit($name);
-
- return true;
- } else {
- $conn->rollBack($name);
-
- return false;
- }
- } catch (Exception $ex) {
- $conn->rollBack($name);
- $query = $ex instanceof DatabaseException ? $ex->getSQLQuery() : '';
- throw new DatabaseException($ex->getMessage(), $ex->getCode(), $query, $ex);
- }
- }
- /**
- * Constructs a query which will truncate a database table when executed.
- *
- * @return AbstractQuery The method will return an instance of the class
- * 'AbstractQuery' which can be used to build SQL queries.
- *
- *
- */
- public function truncate() : AbstractQuery {
- $this->clear();
-
- return $this->getQueryGenerator()->truncate();
- }
- /**
- * Constructs a query which can be used to update a record in the selected
- * table.
- *
- * @param array $newColsVals An associative array that holds the columns and
- * values. The indices of the array should be column keys and the values
- * of the indices are the new values.
- *
- * @return AbstractQuery The method will return an instance of the class
- * 'AbstractQuery' which can be used to build SQL queries.
- *
- *
- */
- public function update(array $newColsVals) : AbstractQuery {
- $this->clear();
-
- return $this->getQueryGenerator()->update($newColsVals);
- }
-
- /**
- * Build a where condition.
- *
- *
- * @param AbstractQuery|string $col A string that represents the name of the
- * column that will be evaluated. This also can be an object of type
- * 'AbstractQuery' in case the developer would like to build a sub-where
- * condition.
- *
- * @param mixed $val The value (or values) at which the column will be evaluated
- * against. Can be ignored if first parameter is of
- * type 'AbstractQuery'.
- *
- * @param string $cond A string that represents the condition at which column
- * value will be evaluated against. Can be ignored if first parameter is of
- * type 'AbstractQuery'.
- *
- * @param string $joinCond An optional string which can be used to join
- * multiple where conditions. If not provided, 'and' will be used by default.
- *
- * @return AbstractQuery The method will return an instance of the class
- * 'AbstractQuery' which can be used to build SQL queries.
- *
- * @throws DatabaseException
- */
- public function where($col, mixed $val = null, string $cond = '=', string $joinCond = 'and') : AbstractQuery {
- return $this->getQueryGenerator()->where($col, $val, $cond, $joinCond);
- }
-}
+setConnectionInfo($connectionInfo);
+ }
+ $this->queries = [];
+ $this->tablesArr = [];
+ $this->lastErr = [
+ 'code' => 0,
+ 'message' => ''
+ ];
+ }
+ /**
+ * Adds a database query to the set of queries at which they were executed.
+ *
+ * This method is called internally by the library to add the query. The
+ * developer does not have to call this method manually.
+ *
+ * @param string $query SQL query as string.
+ *
+ * @param string $type The type of the query such as 'select', 'update' or
+ * 'delete'.
+ *
+ *
+ */
+ public function addQuery(string $query, string $type) {
+ $this->queries[] = [
+ 'type' => $type,
+ 'query' => $query
+ ];
+ }
+ /**
+ * Adds a table to the instance.
+ *
+ * @param Table $table the table that will be added.
+ *
+ * @param bool $updateOwnerDb If the owner database of the table is already
+ * set and this parameter is set to true, the owner database will be
+ * updated to the database specified in the instance. This parameter
+ * is used to maintain foreign key relationships between tables which
+ * belongs to different databases.
+ *
+ * @return bool If the table is added, the method will return true. False
+ * otherwise.
+ *
+ *
+ */
+ public function addTable(Table $table, bool $updateOwnerDb = true) : bool {
+ $trimmedName = $table->getNormalName();
+
+ if (!$this->hasTable($trimmedName)) {
+ if ($table->getOwner() === null || ($table->getOwner() !== null && $updateOwnerDb)) {
+ $table->setOwner($this);
+ }
+ $this->tablesArr[$trimmedName] = $table;
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Build a 'where' expression.
+ *
+ * This method can be used to append an 'and' condition to an already existing
+ * 'where' condition.
+ *
+ * @param AbstractQuery|string $col A string that represents the name of the
+ * column that will be evaluated. This also can be an object of type
+ * 'AbstractQuery' in case the developer would like to build a sub-where
+ * condition.
+ *
+ *
+ * @param mixed $val The value (or values) at which the column will be evaluated
+ * against. Can be ignored if first parameter is of
+ * type 'AbstractQuery'.
+ *
+ * @param string $cond A string that represents the condition at which column
+ * value will be evaluated against. Can be ignored if first parameter is of
+ * type 'AbstractQuery'.
+ *
+ * @return AbstractQuery The method will return an instance of the class
+ * 'AbstractQuery' which can be used to build SQL queries.
+ *
+ * @throws DatabaseException
+ */
+ public function andWhere($col, $val, string $cond = '=') : AbstractQuery {
+ return $this->where($col, $val, $cond);
+ }
+ /**
+ * Rest all attributes of the class to original values.
+ *
+ *
+ */
+ /**
+ * Clear all queries and reset the query generator state.
+ *
+ * This method clears the internal query queue and resets the query generator
+ * to its initial state, preparing for new query operations.
+ */
+ public function clear() {
+ $this->queries = [];
+ $this->getQueryGenerator()->reset();
+ $this->resetBinding();
+ }
+
+ /**
+ * Clear all collected performance metrics.
+ *
+ * Removes all stored performance data from memory or database storage.
+ */
+ public function clearPerformanceMetrics(): void {
+ if ($this->performanceMonitor !== null) {
+ $this->performanceMonitor->clearMetrics();
+ }
+ }
+ /**
+ * Creates a blueprint of a table that can be used to build table structure.
+ *
+ * @param string $name The name of the table as it appears in the database.
+ *
+ * @return Table the method will return an instance of the class 'Table'
+ * which will be based on the type of DBMS at which the instance is
+ * connected to. If connected to MySQL, an instance of 'MySQLTable' is
+ * returned. If connected to MSSQL, an instance of MSSQLTable is returned
+ * and so on.
+ */
+ public function createBlueprint(string $name) : Table {
+ $connection = $this->getConnection();
+
+ if ($connection === null) {
+ $dbType = 'mysql';
+ } else {
+ $dbType = $connection->getConnectionInfo()->getDatabaseType();
+ }
+
+ if ($dbType == 'mssql') {
+ $blueprint = new MSSQLTable($name);
+ } else {
+ $blueprint = new MySQLTable($name);
+ }
+ $this->addTable($blueprint);
+
+ return $blueprint;
+ }
+ /**
+ * Constructs a query which can be used to create selected database table.
+ *
+ * @return AbstractQuery The method will return an instance of the class
+ * 'AbstractQuery' which can be used to build SQL queries.
+ *
+ *
+ */
+ public function createTable() : AbstractQuery {
+ return $this->getQueryGenerator()->createTable();
+ }
+ /**
+ * Create SQL query which can be used to create all database tables.
+ *
+ * @return AbstractQuery The method will return an instance of the class
+ * 'AbstractQuery' which can be used to build SQL queries.
+ *
+ * .1
+ */
+ public function createTables() : AbstractQuery {
+ $generatedQuery = '';
+
+ foreach ($this->getTables() as $tableObj) {
+ if ($tableObj->getColsCount() != 0) {
+ $generatedQuery .= $tableObj->toSQL()."\n";
+ }
+ }
+ $this->getQueryGenerator()->setQuery($generatedQuery, true);
+
+ return $this->getQueryGenerator();
+ }
+ /**
+ * Constructs a query which can be used to remove a record from the
+ * selected table.
+ *
+ * @return AbstractQuery The method will return an instance of the class
+ * 'AbstractQuery' which can be used to build SQL queries.
+ *
+ *
+ */
+ public function delete() : AbstractQuery {
+ $this->clear();
+
+ return $this->getQueryGenerator()->delete();
+ }
+
+ /**
+ * Disable query performance monitoring.
+ *
+ * Stops collecting performance data for query executions.
+ * Existing collected data is preserved.
+ */
+ public function disablePerformanceMonitoring(): void {
+ $this->performanceEnabled = false;
+ }
+ /**
+ * Constructs a query which will drop a database table when executed.
+ *
+ * @return AbstractQuery The method will return an instance of the class
+ * 'AbstractQuery' which can be used to build SQL queries.
+ *
+ *
+ */
+ public function drop() : AbstractQuery {
+ $this->clear();
+
+ return $this->getQueryGenerator()->drop();
+ }
+
+ /**
+ * Enable query performance monitoring.
+ *
+ * Initializes the performance monitoring system with default configuration.
+ * Performance data will be collected for all subsequent query executions.
+ */
+ public function enablePerformanceMonitoring(): void {
+ $this->performanceEnabled = true;
+
+ if ($this->performanceMonitor === null) {
+ $this->performanceMonitor = new QueryPerformanceMonitor([
+ PerformanceOption::ENABLED => true
+ ], $this);
+ }
+ }
+ /**
+ * Execute SQL query.
+ *
+ * @throws DatabaseException The method will throw an exception if one
+ * of 3 cases happens:
+ *
+ * - No connection was established with any database.
+ * - An error has occurred while executing the query.
+ *
+ *
+ * @return ResultSet|null If the last executed query was a select, show or
+ * describe query, the method will return an object of type 'ResultSet' that
+ * holds fetched records. Other than that, the method will return null.
+ *
+ *
+ */
+ public function execute() {
+ $conn = $this->getConnection();
+ $lastQuery = $this->getLastQuery();
+
+ // Start performance monitoring
+ $startTime = $this->performanceEnabled ? microtime(true) : null;
+
+ if (!$conn->runQuery($this->getQueryGenerator())) {
+ throw new DatabaseException($conn->getLastErrCode().' - '.$conn->getLastErrMessage(), $conn->getLastErrCode());
+ }
+ $this->queries[] = $lastQuery;
+ $this->clear();
+ $resultSet = $this->getLastResultSet();
+
+ $this->getQueryGenerator()->setQuery(null);
+
+ // Record performance metrics
+ if ($this->performanceEnabled && $this->performanceMonitor && $startTime !== null) {
+ $executionTime = (microtime(true) - $startTime) * 1000; // Convert to milliseconds
+ $this->performanceMonitor->recordQuery($lastQuery, $executionTime, $resultSet);
+ }
+
+ return $resultSet;
+ }
+ /**
+ * Returns the connection at which the instance will use to run SQL queries.
+ *
+ * This method will try to connect to the database if no connection is active.
+ * If the connection was not established, the method will throw an exception.
+ * If the connection is already active, the method will return it.
+ *
+ * @return Connection The connection at which the instance will use to run SQL queries.
+ *
+ *
+ *
+ */
+ public function getConnection() : ?Connection {
+ $connInfo = $this->getConnectionInfo();
+
+ if ($this->connection === null && $connInfo !== null) {
+ $driver = $connInfo->getDatabaseType();
+
+ if ($driver == 'mysql') {
+ $conn = new MySQLConnection($connInfo);
+ $this->setConnection($conn);
+ } else if ($driver == 'mssql') {
+ $conn = new MSSQLConnection($connInfo);
+ $this->setConnection($conn);
+ }
+ }
+
+ return $this->connection;
+ }
+ /**
+ * Returns an object that holds connection information.
+ *
+ * @return ConnectionInfo|null An object that holds connection information.
+ *
+ *
+ */
+ public function getConnectionInfo() : ?ConnectionInfo {
+ return $this->connectionInfo;
+ }
+
+ /**
+ * Returns an indexed array that contains all executed SQL queries.
+ *
+ * @return array An indexed array that contains all executed SQL queries.
+ *
+ */
+ public function getExecutedQueries() : array {
+ return $this->getConnection()->getExecutedQueries();
+ }
+ /**
+ * Returns the last database error info.
+ *
+ * @return array The method will return an associative array with two indices.
+ * The first one is 'message' which contains error message and the second one
+ * is 'code' which contains error code.
+ *
+ *
+ */
+ public function getLastError() : array {
+ if ($this->connection !== null) {
+ $this->lastErr = [
+ 'message' => $this->connection->getLastErrMessage(),
+ 'code' => $this->connection->getLastErrCode()
+ ];
+ }
+
+ return $this->lastErr;
+ }
+ /**
+ * Returns the last generated SQL query.
+ *
+ * @return string Last generated SQL query as string.
+ *
+ *
+ */
+ public function getLastQuery() : string {
+ return trim($this->getQueryGenerator()->getQuery());
+ }
+
+ /**
+ * Returns the last result set which was generated from executing a query such
+ * as a 'select' query.
+ *
+ * @return ResultSet|null The last result set. If no result set is available,
+ * the method will return null.
+ */
+ public function getLastResultSet() {
+ return $this->getConnection()->getLastResultSet();
+ }
+ /**
+ * Returns the name of the database.
+ *
+ * @return string The name of the database.
+ *
+ *
+ */
+ public function getName() : string {
+ return $this->getConnectionInfo()->getDBName();
+ }
+
+ /**
+ * Get all collected performance metrics.
+ *
+ * @return array Array of QueryMetric instances or metric arrays
+ */
+ public function getPerformanceMetrics(): array {
+ if ($this->performanceMonitor === null) {
+ return [];
+ }
+
+ return $this->performanceMonitor->getMetrics();
+ }
+
+ /**
+ * Get the performance monitor instance.
+ *
+ * @return QueryPerformanceMonitor|null The performance monitor instance or null if not initialized.
+ */
+ public function getPerformanceMonitor(): ?QueryPerformanceMonitor {
+ return $this->performanceMonitor;
+ }
+
+ /**
+ * Get performance statistics summary.
+ *
+ * @return array Statistics including total queries, average execution time,
+ * min/max times, and slow query count
+ */
+ public function getPerformanceStatistics(): array {
+ if ($this->performanceMonitor === null) {
+ return [
+ 'total_queries' => 0,
+ 'avg_execution_time' => 0,
+ 'min_execution_time' => 0,
+ 'max_execution_time' => 0,
+ 'slow_queries_count' => 0
+ ];
+ }
+
+ return $this->performanceMonitor->getStatistics();
+ }
+ /**
+ * Returns an indexed array that contains all generated SQL queries.
+ *
+ * @return array An indexed array that contains all generated SQL queries.
+ *
+ *
+ */
+ public function getQueries() : array {
+ return $this->queries;
+ }
+ /**
+ * Returns the query builder which is used to build SQL queries.
+ *
+ * @return AbstractQuery
+ *
+ *
+ */
+ public function getQueryGenerator() : AbstractQuery {
+ if (!$this->isConnected()) {
+ if ($this->getConnectionInfo() === null) {
+ throw new DatabaseException("Connection information not set.");
+ } else {
+ $lastErr = $this->getLastError();
+ throw new DatabaseException("Not connected to database. Error Code: ".$lastErr['code'].'. Message: "'.$lastErr['message']);
+ }
+ }
+
+ return $this->queryGenerator;
+ }
+
+ /**
+ * Get slow queries based on configured or custom threshold.
+ *
+ * @param int|null $thresholdMs Custom threshold in milliseconds. If null,
+ * uses configured slow query threshold.
+ * @return array Array of slow query metrics
+ */
+ public function getSlowQueries(?int $thresholdMs = null): array {
+ if ($this->performanceMonitor === null) {
+ return [];
+ }
+
+ return $this->performanceMonitor->getSlowQueries($thresholdMs);
+ }
+ /**
+ * Returns a table structure as an object given its name.
+ *
+ * @param string $tblName The name of the table.
+ *
+ * @return Table|null If a table which has the given name is existed, it will
+ * be returned as an object. Other than that, null is returned.
+ *
+ *
+ */
+ public function getTable(string $tblName) {
+ $trimmed = trim($tblName);
+
+ if (!isset($this->tablesArr[$trimmed])) {
+ return null;
+ }
+ $engine = 'mysql';
+ $info = $this->getConnectionInfo();
+
+ if ($info !== null) {
+ $engine = $info->getDatabaseType();
+ }
+ $table = $this->tablesArr[$trimmed];
+
+ return TableFactory::map($engine, $table);
+ }
+ /**
+ * Returns an array that contains all added tables.
+ *
+ * @return array The method will return an associative array. The indices
+ * of the array are tables names and the values are objects of type 'Table'.
+ *
+ * .1
+ */
+ public function getTables() : array {
+ return $this->tablesArr;
+ }
+ /**
+ * Checks if a table exist in the database or not.
+ *
+ * @param string $tableName The name of the table.
+ *
+ * @return bool If the table exist, the method will return true.
+ * False if it does not exist.
+ *
+ *
+ */
+ public function hasTable(string $tableName) : bool {
+ return isset($this->tablesArr[$tableName]);
+ }
+ /**
+ * Constructs a query which can be used to insert a record in the selected
+ * table.
+ *
+ * @param array $colsAndVals An associative array that holds the columns and
+ * values. The indices of the array should be column keys and the values
+ * of the indices are the new values.
+ *
+ * @return AbstractQuery The method will return an instance of the class
+ * 'AbstractQuery' which can be used to build SQL queries.
+ *
+ *
+ */
+ public function insert(array $colsAndVals) : AbstractQuery {
+ $this->clear();
+
+ return $this->getQueryGenerator()->insert($colsAndVals);
+ }
+ /**
+ * Check if database connection is established and active.
+ *
+ * @return bool True if connected to database, false otherwise.
+ */
+ public function isConnected() : bool {
+ if ($this->getConnectionInfo() === null) {
+ return false;
+ }
+ try {
+ if ($this->getConnection() === null) {
+ return false;
+ }
+ } catch (DatabaseException $ex) {
+ $this->lastErr = [
+ 'code' => $ex->getCode(),
+ 'message' => $ex->getMessage()
+ ];
+
+ return false;
+ }
+
+ return true;
+ }
+ /**
+ * Sets the number of records that will be fetched by the query.
+ *
+ * @param int $limit A number which is greater than 0.
+ *
+ * @return AbstractQuery The method will return an instance of the class
+ * 'AbstractQuery' which can be used to build SQL queries.
+ *
+ *
+ */
+ public function limit(int $limit) : AbstractQuery {
+ return $this->getQueryGenerator()->limit($limit);
+ }
+ /**
+ * Sets the offset.
+ *
+ * The offset is basically the number of records that will be skipped from the
+ * start.
+ *
+ * @param int $offset Number of records to skip.
+ *
+ * @return AbstractQuery The method will return an instance of the class
+ * 'AbstractQuery' which can be used to build SQL queries.
+ *
+ *
+ */
+ public function offset(int $offset) : AbstractQuery {
+ return $this->getQueryGenerator()->offset($offset);
+ }
+
+ /**
+ * Build a 'where' expression.
+ *
+ * This method can be used to append an 'or' condition to an already existing
+ * 'where' condition.
+ *
+ * @param AbstractQuery|string $col A string that represents the name of the
+ * column that will be evaluated. This also can be an object of type
+ * 'AbstractQuery' in case the developer would like to build a sub-where
+ * condition.
+ *
+ * @param mixed $val The value (or values) at which the column will be evaluated
+ * against. Can be ignored if first parameter is of
+ * type 'AbstractQuery'.
+ *
+ * @param string $cond A string that represents the condition at which column
+ * value will be evaluated against. Can be ignored if first parameter is of
+ * type 'AbstractQuery'.
+ *
+ * @return AbstractQuery The method will return an instance of the class
+ * 'AbstractQuery' which can be used to build SQL queries.
+ *
+ * @throws DatabaseException
+ */
+ public function orWhere(string $col, mixed $val = null, string $cond = '=') : AbstractQuery {
+ return $this->where($col, $val, $cond, 'or');
+ }
+ /**
+ * Constructs a query which can be used to fetch a set of records as a page.
+ *
+ * @param int $num Page number. It should be a number greater than or equals
+ * to 1.
+ *
+ * @param int $itemsCount Number of records per page. Must be a number greater than or equals to 1.
+ *
+ * @return AbstractQuery The method will return an instance of the class
+ * 'AbstractQuery' which can be used to build SQL queries.
+ *
+ *
+ */
+ public function page(int $num, int $itemsCount) : AbstractQuery {
+ return $this->getQueryGenerator()->page($num, $itemsCount);
+ }
+ /**
+ * Reset the bindings which was set by building and executing a query.
+ *
+ * @return Database The method will return the instance at which the method
+ * is called on.
+ */
+ public function resetBinding() : Database {
+ $this->getQueryGenerator()->resetBinding();
+
+ return $this;
+ }
+ /**
+ * Constructs a query that can be used to get records from a table.
+ *
+ * @param array $cols An array that holds the keys of the columns that will
+ * be selected.
+ *
+ * @return AbstractQuery The method will return an instance of the class
+ * 'AbstractQuery' which can be used to build SQL queries.
+ *
+ *
+ */
+ public function select(array $cols = ['*']) : AbstractQuery {
+ $this->clear();
+
+ return $this->getQueryGenerator()->select($cols);
+ }
+ /**
+ * Sets the connection that will be used by the schema.
+ *
+ * @param Connection $con An active connection.
+ *
+ *
+ */
+ public function setConnection(Connection $con) {
+ $this->connection = $con;
+ }
+ /**
+ * Sets database connection information.
+ *
+ * @param ConnectionInfo $info An object that holds connection information.
+ *
+ * @throws DatabaseException The method will throw an exception if database
+ * driver is not supported.
+ *
+ *
+ */
+ public function setConnectionInfo(ConnectionInfo $info) {
+ $driver = $info->getDatabaseType();
+
+ if ($driver == 'mysql') {
+ $this->queryGenerator = new MySQLQuery();
+ $this->queryGenerator->setSchema($this);
+ } else if ($driver == 'mssql') {
+ $this->queryGenerator = new MSSQLQuery();
+ $this->queryGenerator->setSchema($this);
+ } else {
+ throw new DatabaseException('Driver not supported: "'.$driver.'".');
+ }
+ $this->connectionInfo = $info;
+ }
+
+ /**
+ * Configure performance monitoring settings.
+ *
+ * @param array $config Configuration array using PerformanceOption constants
+ *
+ * @throws InvalidArgumentException If configuration values are invalid
+ */
+ public function setPerformanceConfig(array $config): void {
+ if ($this->performanceMonitor === null) {
+ $this->performanceMonitor = new QueryPerformanceMonitor($config, $this);
+ } else {
+ $this->performanceMonitor->updateConfig($config);
+ }
+
+ $this->performanceEnabled = $config[PerformanceOption::ENABLED] ?? $this->performanceEnabled;
+ }
+
+ /**
+ * Sets the database query to a raw SQL query.
+ *
+ * @param string $query A string that represents the query.
+ *
+ * @return Database The method will return the same instance at which the
+ * method is called on.
+ *
+ * @throws DatabaseException
+ */
+ public function setQuery(string $query) : Database {
+
+ return $this->raw($query);
+ }
+ /**
+ * Sets the database query to a raw SQL query.
+ *
+ * @param string $query A string that represents the query.
+ *
+ * @return Database The method will return the same instance at which the
+ * method is called on.
+ *
+ * @throws DatabaseException
+ */
+ public function raw(string $query) : Database {
+ $t = $this->getQueryGenerator()->getTable();
+
+ if ($t !== null) {
+ $t->getSelect()->clear();
+ }
+ $this->getQueryGenerator()->setQuery($query);
+
+ return $this;
+ }
+ /**
+ * Select one of the tables which exist on the schema and use it to build
+ * SQL queries.
+ *
+ * @param string $tblName The name of the table.
+ *
+ * @return AbstractQuery The method will return an instance of the class
+ * 'AbstractQuery' which can be used to build SQL queries.
+ *
+ *
+ */
+ public function table(string $tblName) : AbstractQuery {
+ return $this->getQueryGenerator()->table($tblName);
+ }
+ /**
+ * Start SQL transaction.
+ *
+ * This will disable auto-commit.
+ *
+ * @param callable $transaction A function that holds the logic of the transaction.
+ * The function must return true or null for success. If false is
+ * returned, it means the transaction failed and will be rolled back.
+ *
+ * @param array $transactionArgs An optional array of parameters to be passed
+ * to the transaction.
+ *
+ * @return bool If the transaction completed without errors, the method will
+ * return true. False otherwise.
+ *
+ * @throws DatabaseException The method will throw an exception if it was
+ * rolled back due to an error.
+ */
+ public function transaction(callable $transaction, array $transactionArgs = []) : bool {
+ $conn = $this->getConnection();
+ $name = 'transaction_'.rand();
+
+ try {
+ $args = array_merge([$this], $transactionArgs);
+ $conn->beginTransaction($name);
+ $result = call_user_func_array($transaction, $args);
+
+ if ($result === null || $result === true) {
+ $conn->commit($name);
+
+ return true;
+ } else {
+ $conn->rollBack($name);
+
+ return false;
+ }
+ } catch (Exception $ex) {
+ $conn->rollBack($name);
+ $query = $ex instanceof DatabaseException ? $ex->getSQLQuery() : '';
+ throw new DatabaseException($ex->getMessage(), $ex->getCode(), $query, $ex);
+ }
+ }
+ /**
+ * Constructs a query which will truncate a database table when executed.
+ *
+ * @return AbstractQuery The method will return an instance of the class
+ * 'AbstractQuery' which can be used to build SQL queries.
+ *
+ *
+ */
+ public function truncate() : AbstractQuery {
+ $this->clear();
+
+ return $this->getQueryGenerator()->truncate();
+ }
+ /**
+ * Constructs a query which can be used to update a record in the selected
+ * table.
+ *
+ * @param array $newColsVals An associative array that holds the columns and
+ * values. The indices of the array should be column keys and the values
+ * of the indices are the new values.
+ *
+ * @return AbstractQuery The method will return an instance of the class
+ * 'AbstractQuery' which can be used to build SQL queries.
+ *
+ *
+ */
+ public function update(array $newColsVals) : AbstractQuery {
+ $this->clear();
+
+ return $this->getQueryGenerator()->update($newColsVals);
+ }
+
+ /**
+ * Build a where condition.
+ *
+ *
+ * @param AbstractQuery|string $col A string that represents the name of the
+ * column that will be evaluated. This also can be an object of type
+ * 'AbstractQuery' in case the developer would like to build a sub-where
+ * condition.
+ *
+ * @param mixed $val The value (or values) at which the column will be evaluated
+ * against. Can be ignored if first parameter is of
+ * type 'AbstractQuery'.
+ *
+ * @param string $cond A string that represents the condition at which column
+ * value will be evaluated against. Can be ignored if first parameter is of
+ * type 'AbstractQuery'.
+ *
+ * @param string $joinCond An optional string which can be used to join
+ * multiple where conditions. If not provided, 'and' will be used by default.
+ *
+ * @return AbstractQuery The method will return an instance of the class
+ * 'AbstractQuery' which can be used to build SQL queries.
+ *
+ * @throws DatabaseException
+ */
+ public function where($col, mixed $val = null, string $cond = '=', string $joinCond = 'and') : AbstractQuery {
+ return $this->getQueryGenerator()->where($col, $val, $cond, $joinCond);
+ }
+}
diff --git a/WebFiori/Database/MsSql/MSSQLConnection.php b/WebFiori/Database/MsSql/MSSQLConnection.php
index 64ea93a3..1bc91dfc 100644
--- a/WebFiori/Database/MsSql/MSSQLConnection.php
+++ b/WebFiori/Database/MsSql/MSSQLConnection.php
@@ -244,6 +244,17 @@ private function runOtherQuery() {
$this->setSqlErr();
return false;
+ } else {
+
+
+ if (sqlsrv_has_rows($r)) {
+ $data = [];
+ while ($row = sqlsrv_fetch_array($r, SQLSRV_FETCH_ASSOC)) {
+ $data[] = $row;
+ }
+ $this->setResultSet(new ResultSet($data));
+ }
+
}
return true;
@@ -258,7 +269,7 @@ private function runSelectQuery() {
if (is_resource($r)) {
$data = [];
- while ($row = sqlsrv_fetch_array($r,SQLSRV_FETCH_ASSOC)) {
+ while ($row = sqlsrv_fetch_array($r, SQLSRV_FETCH_ASSOC)) {
$data[] = $row;
}
$this->setResultSet(new ResultSet($data));
diff --git a/WebFiori/Database/MySql/MySQLConnection.php b/WebFiori/Database/MySql/MySQLConnection.php
index 9b39fc3e..104a5b8f 100644
--- a/WebFiori/Database/MySql/MySQLConnection.php
+++ b/WebFiori/Database/MySql/MySQLConnection.php
@@ -274,12 +274,13 @@ private function runInsertQuery() {
}
private function runOtherQuery() {
$query = $this->getLastQuery()->getQuery();
- $retVal = false;
$sql = $this->getLastQuery()->getQuery();
$params = $this->getLastQuery()->getBindings()['bind'];
$values = array_merge($this->getLastQuery()->getBindings()['values']);
+ $r = false;
+
if (count($values) != 0 && !empty($params)) {
// Count the number of ? placeholders in the SQL
$paramCount = substr_count($sql, '?');
@@ -288,7 +289,8 @@ private function runOtherQuery() {
if ($paramCount == count($values) && strlen($params) == count($values)) {
$sqlStatement = mysqli_prepare($this->link, $sql);
$sqlStatement->bind_param($params, ...$values);
- $r = $sqlStatement->execute();
+ $sqlStatement->execute();
+ $r = $sqlStatement->get_result();
if ($sqlStatement) {
mysqli_stmt_close($sqlStatement);
@@ -298,37 +300,39 @@ private function runOtherQuery() {
$r = mysqli_query($this->link, $sql);
}
} else {
- if (!$this->getLastQuery()->isMultiQuery()) {
- $r = mysqli_query($this->link, $query);
- } else {
- $r = mysqli_multi_query($this->link, $query);
-
- // Clean up multi-query results to prevent "Commands out of sync"
- if ($r) {
- while (mysqli_next_result($this->link)) {
- // Consume all result sets
- }
- }
- }
+ $r = mysqli_query($this->link, $query);
}
-
-
if (!$r) {
$this->setErrMessage($this->link->error);
$this->setErrCode($this->link->errno);
+ return false;
} else {
$this->setErrMessage('NO ERRORS');
$this->setErrCode(0);
$this->getLastQuery()->setIsBlobInsertOrUpdate(false);
- $retVal = true;
- }
-
- if ($r === true || gettype($r) == 'object') {
- $retVal = true;
+
+ // Handle result sets
+
+ if (gettype($r) == 'object' && method_exists($r, 'fetch_assoc')) {
+ $data = [];
+ while ($row = $r->fetch_assoc()) {
+ $data[] = $row;
+ }
+ $this->setResultSet(new ResultSet($data));
+ mysqli_free_result($r);
+ }
+
+ // Clean up any additional result sets (for stored procedures)
+ while (mysqli_more_results($this->link)) {
+ mysqli_next_result($this->link);
+ if ($result = mysqli_store_result($this->link)) {
+ mysqli_free_result($result);
+ }
+ }
}
- return $retVal;
+ return true;
}
private function runSelectQuery() {
$sql = $this->getLastQuery()->getQuery();
diff --git a/tests/WebFiori/Tests/Database/MsSql/MSSQLQueryBuilderTest.php b/tests/WebFiori/Tests/Database/MsSql/MSSQLQueryBuilderTest.php
index aff410c7..edf18069 100644
--- a/tests/WebFiori/Tests/Database/MsSql/MSSQLQueryBuilderTest.php
+++ b/tests/WebFiori/Tests/Database/MsSql/MSSQLQueryBuilderTest.php
@@ -1324,4 +1324,28 @@ public function testAfreggate07() {
->groupBy('function');
$this->assertEquals('select [reports_list].[function], count(*) as count from [reports_list] group by [reports_list].[function] order by [reports_list].[count]', $schema->getLastQuery());
}
+ /**
+ * @test
+ */
+ public function testStoredProcedureExecution() {
+ $schema = new MSSQLTestSchema();
+
+ // Clean up first in case procedure exists
+ $schema->raw("IF OBJECT_ID('GetUserCount', 'P') IS NOT NULL DROP PROCEDURE GetUserCount")->execute();
+
+ // Create stored procedure that returns results
+ $schema->raw("CREATE PROCEDURE GetUserCount AS BEGIN SELECT COUNT(*) as user_count FROM users END")->execute();
+
+ // Execute stored procedure - this should call runOtherQuery and return results
+ $result = $schema->raw("EXEC GetUserCount")->execute();
+ $this->assertInstanceOf('WebFiori\\Database\\ResultSet', $result);
+
+ $this->assertEquals(1, $result->getRowsCount());
+ $row = $result->getRows()[0];
+ $this->assertArrayHasKey('user_count', $row);
+ $this->assertIsInt($row['user_count']);
+
+ // Clean up
+ $schema->raw("DROP PROCEDURE GetUserCount")->execute();
+ }
}
diff --git a/tests/WebFiori/Tests/Database/MySql/MySQLQueryBuilderTest.php b/tests/WebFiori/Tests/Database/MySql/MySQLQueryBuilderTest.php
index c07f2c2c..4f45343c 100644
--- a/tests/WebFiori/Tests/Database/MySql/MySQLQueryBuilderTest.php
+++ b/tests/WebFiori/Tests/Database/MySql/MySQLQueryBuilderTest.php
@@ -2179,4 +2179,28 @@ public function testAggregate06() {
$q->table('users')->selectCount('id');
$this->assertEquals('select count(`users`.`id`) as `count` from `users`', $q->getQuery());
}
+ /**
+ * @test
+ */
+ public function testStoredProcedureExecution() {
+ $schema = new MySQLTestSchema();
+
+ // Clean up first in case procedure exists
+ $schema->raw("DROP PROCEDURE IF EXISTS GetUserCount")->execute();
+
+ // Create stored procedure that returns results
+ $schema->raw("CREATE PROCEDURE GetUserCount() BEGIN SELECT COUNT(*) as user_count FROM users; END")->execute();
+
+ // Execute stored procedure - this should call runOtherQuery and return results
+ $result = $schema->raw("CALL GetUserCount()")->execute();
+ $this->assertInstanceOf('WebFiori\\Database\\ResultSet', $result);
+
+ $this->assertEquals(1, $result->getRowsCount());
+ $row = $result->getRows()[0];
+ $this->assertArrayHasKey('user_count', $row);
+ $this->assertIsNumeric($row['user_count']);
+
+ // Clean up
+ $schema->raw("DROP PROCEDURE GetUserCount")->execute();
+ }
}