diff --git a/src/Ampersand/Plugs/MysqlDB/MysqlDB.php b/src/Ampersand/Plugs/MysqlDB/MysqlDB.php index 6ef2ff752..62a201b7f 100644 --- a/src/Ampersand/Plugs/MysqlDB/MysqlDB.php +++ b/src/Ampersand/Plugs/MysqlDB/MysqlDB.php @@ -45,32 +45,32 @@ class MysqlDB implements ConceptPlugInterface, RelationPlugInterface, IfcPlugInt * Logger */ private LoggerInterface $logger; - + /** * A connection to the mysql database */ protected \mysqli $dbLink; - + /** * Host/server of mysql database */ protected string $dbHost; - + /** * Username for mysql database */ protected string $dbUser; - + /** * Password for mysql database */ protected string $dbPass; - + /** * Database name */ protected string $dbName; - + /** * Specifies if database transaction is active */ @@ -100,7 +100,7 @@ class MysqlDB implements ConceptPlugInterface, RelationPlugInterface, IfcPlugInt * Attribute is reset to 0 on start of (new) transaction */ protected int $queryCount = 0; - + /** * Constructor * @@ -118,18 +118,18 @@ public function __construct(string $dbHost, string $dbUser, string $dbPass, stri $this->debugMode = $debugMode; $this->productionMode = $productionMode; - + try { // Enable mysqli errors to be thrown as Exceptions mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); - + // Connect to MYSQL database $this->dbLink = mysqli_init(); - + // Flag MYSQLI_CLIENT_FOUND_ROWS -> https://www.codepuppet.com/2014/02/16/mysql-affected-rows-vs-rows-matched/ $this->dbLink->real_connect($this->dbHost, $this->dbUser, $this->dbPass, '', 0, '', MYSQLI_CLIENT_FOUND_ROWS); $this->dbLink->set_charset("utf8mb4"); - + // Set sql_mode to ANSI $this->dbLink->query("SET SESSION sql_mode = 'ANSI,TRADITIONAL'"); } catch (Exception $e) { @@ -156,7 +156,7 @@ protected function selectDB(): void } } } - + /** * Function to create new database. Drops database (and loose all data) if already exists */ @@ -165,7 +165,7 @@ protected function createDB(): void // Drop database $this->logger->info("Drop database if exists: '{$this->dbName}'"); $this->doQuery("DROP DATABASE IF EXISTS {$this->dbName}"); - + // Create new database $this->logger->info("Create new database: '{$this->dbName}'"); $this->doQuery("CREATE DATABASE {$this->dbName} DEFAULT CHARACTER SET UTF8MB4 COLLATE UTF8MB4_NOPAD_BIN"); @@ -203,7 +203,7 @@ public function getInstalledModelHash(): string return current($result)['checksum']; } - + /** * Return escaped mysql representation of Atom (identifier) according to Ampersand technical types (TTypes) * @@ -214,7 +214,7 @@ protected function getDBRepresentation(Atom $atom): mixed if (is_null($atom->getId())) { throw new InvalidOptionException("Atom identifier MUST NOT be NULL"); } - + switch ($atom->concept->type) { case TType::ALPHANUMERIC: case TType::BIGALPHANUMERIC: @@ -251,7 +251,7 @@ protected function getDBRepresentation(Atom $atom): mixed throw new MetaModelException("Unknown/unsupported ttype '{$atom->concept->type->value}' for concept '[{$atom->concept}]'"); } } - + /** * Execute query on database. Function replaces reserved words by their corresponding value (e.g. _SESSION) * @@ -270,7 +270,7 @@ public function execute(string $query): bool|array } elseif ($result === true) { return true; } - + $arr = []; while ($row = mysqli_fetch_assoc($result)) { $arr[] = $row; @@ -288,7 +288,7 @@ public function prepare(string $query): \mysqli_stmt return $statement; } - + /** * Execute query on database. * Set multiQuery if query is a single command or multiple commands concatenated by a semicolon @@ -298,7 +298,7 @@ protected function doQuery(string $query, bool $multiQuery = false): mixed $this->lastQuery = $query; try { $this->queryCount++; // multi queries are counted as 1 - + if ($multiQuery) { // MYSQLI_REPORT_STRICT mode doesn't throw Exceptions for multi_query execution, // therefore we throw exceptions @@ -342,7 +342,7 @@ protected function doQuery(string $query, bool $multiQuery = false): mixed } } } - + /** * Escape identifier for use in database queries * @@ -358,12 +358,12 @@ public function escape(string $escapestr): ?string } } -/************************************************************************************************** - * - * Implementation of StorageInterface methods (incl. database transaction handling) - * - *************************************************************************************************/ - + /************************************************************************************************** + * + * Implementation of StorageInterface methods (incl. database transaction handling) + * + *************************************************************************************************/ + /** * Returns name of storage implementation */ @@ -371,7 +371,7 @@ public function getLabel(): string { return "MySQL database {$this->dbHost} - {$this->dbName}"; } - + /** * Function to start/open a database transaction to track of all changes and be able to rollback */ @@ -384,7 +384,7 @@ public function startTransaction(Transaction $transaction): void $this->dbTransactionActive = true; // set flag dbTransactionActive } } - + /** * Function to commit the open database transaction */ @@ -395,7 +395,7 @@ public function commitTransaction(Transaction $transaction): void $this->dbTransactionActive = false; $this->logger->info("{$this->queryCount} queries executed in this transaction"); } - + /** * Function to rollback changes made in the open database transaction */ @@ -406,32 +406,32 @@ public function rollbackTransaction(Transaction $transaction): void $this->dbTransactionActive = false; $this->logger->info("{$this->queryCount} queries executed in this transaction"); } - -/************************************************************************************************** - * - * Implementation of ConceptPlugInterface methods - * - *************************************************************************************************/ + + /************************************************************************************************** + * + * Implementation of ConceptPlugInterface methods + * + *************************************************************************************************/ /** - * Check if atom exists in database - */ + * Check if atom exists in database + */ public function atomExists(Atom $atom): bool { $tableInfo = $atom->concept->getConceptTableInfo(); $firstCol = current($tableInfo->getCols()); $atomId = $this->getDBRepresentation($atom); - + $query = "SELECT \"{$firstCol->getName()}\" FROM \"{$tableInfo->getName()}\" WHERE \"{$firstCol->getName()}\" = '{$atomId}'"; $result = $this->execute($query); - + if (empty($result)) { return false; } else { return true; } } - + /** * Get all atoms for given concept * @@ -440,7 +440,7 @@ public function atomExists(Atom $atom): bool public function getAllAtoms(Concept $concept): array { $tableInfo = $concept->getConceptTableInfo(); - + // Query all atoms in table if (isset($tableInfo->allAtomsQuery)) { $query = $tableInfo->allAtomsQuery; @@ -448,7 +448,7 @@ public function getAllAtoms(Concept $concept): array $firstCol = current($tableInfo->getCols()); // We can query an arbitrary concept col for checking the existence of an atom $query = "SELECT DISTINCT \"{$firstCol->getName()}\" as \"atomId\" FROM \"{$tableInfo->getName()}\" WHERE \"{$firstCol->getName()}\" IS NOT NULL"; } - + $arr = []; foreach ((array)$this->execute($query) as $row) { $tgtAtom = new Atom($row['atomId'], $concept); @@ -457,79 +457,79 @@ public function getAllAtoms(Concept $concept): array } return $arr; } - + /** * Add atom to database */ public function addAtom(Atom $atom): void { $atomId = $this->getDBRepresentation($atom); - + // Get table properties $conceptTableInfo = $atom->concept->getConceptTableInfo(); $conceptTable = $conceptTableInfo->getName(); $conceptCols = $conceptTableInfo->getCols(); // Concept are registered in multiple cols in case of specializations. We insert the new atom in every column. - + // Create query string: "", "", etc $allConceptCols = '"' . implode('", "', $conceptTableInfo->getColNames()) . '"'; - - + + // Create query string: '', 'getName()}\" = '{$atomId}'"; } $duplicateStatement = substr($str, 1); - + $this->execute("INSERT INTO \"$conceptTable\" ($allConceptCols) VALUES ($allValues)" - ." ON DUPLICATE KEY UPDATE $duplicateStatement"); - + . " ON DUPLICATE KEY UPDATE $duplicateStatement"); + // Check if query resulted in an affected row $this->checkForAffectedRows(); } - + /** * Removing an atom as member from a concept set */ public function removeAtom(Atom $atom): void { $atomId = $this->getDBRepresentation($atom); - + // Get table and col for WHERE clause $conceptTable = $atom->concept->getConceptTableInfo(); $conceptCol = $atom->concept->getConceptTableInfo()->getFirstCol(); - + // Get cols for UPDATE clause $colNames = []; $colNames[] = $conceptCol->getName(); // also update the concept col itself foreach ($atom->concept->getSpecializations() as $specConcept) { $colNames[] = $specConcept->getConceptTableInfo()->getFirstCol()->getName(); } - + // Create query string: "" = '', "" = '', etc $queryString = "\"" . implode("\" = NULL, \"", $colNames) . "\" = NULL"; - + $this->execute("UPDATE \"{$conceptTable->getName()}\" SET $queryString WHERE \"{$conceptCol->getName()}\" = '{$atomId}'"); - + // Check if query resulted in an affected row $this->checkForAffectedRows(); } - + /** * Delete atom from concept table in the database */ public function deleteAtom(Atom $atom): void { $atomId = $this->getDBRepresentation($atom); - + // Delete atom from concept table $conceptTable = $atom->concept->getConceptTableInfo(); $query = "DELETE FROM \"{$conceptTable->getName()}\" WHERE \"{$conceptTable->getFirstCol()->getName()}\" = '{$atomId}' LIMIT 1"; $this->execute($query); - + // Check if query resulted in an affected row $this->checkForAffectedRows(); } @@ -538,44 +538,44 @@ public function executeCustomSQLQuery(string $query): bool|array { return $this->execute($query); } - -/************************************************************************************************** - * - * Implementation of RelationPlugInterface methods - * - *************************************************************************************************/ - + + /************************************************************************************************** + * + * Implementation of RelationPlugInterface methods + * + *************************************************************************************************/ + /** - * Check if link exists in database - */ + * Check if link exists in database + */ public function linkExists(Link $link): bool { $relTable = $link->relation()->getMysqlTable(); $srcAtomId = $this->getDBRepresentation($link->src()); $tgtAtomId = $this->getDBRepresentation($link->tgt()); - + $result = $this->execute("SELECT * FROM \"{$relTable->getName()}\" WHERE \"{$relTable->srcCol()->getName()}\" = '{$srcAtomId}' AND \"{$relTable->tgtCol()->getName()}\" = '{$tgtAtomId}'"); - + if (empty($result)) { return false; } else { return true; } } - + /** - * Get all links given a relation - * - * If src and/or tgt atom is specified only links are returned with these atoms - * @return \Ampersand\Core\Link[] - */ + * Get all links given a relation + * + * If src and/or tgt atom is specified only links are returned with these atoms + * @return \Ampersand\Core\Link[] + */ public function getAllLinks(Relation $relation, ?Atom $srcAtom = null, ?Atom $tgtAtom = null): array { $relTable = $relation->getMysqlTable(); - + // Query all atoms in table $query = "SELECT \"{$relTable->srcCol()->getName()}\" as \"src\", \"{$relTable->tgtCol()->getName()}\" as \"tgt\" FROM \"{$relTable->getName()}\""; - + // Construct WHERE-clause if applicable if (isset($srcAtom)) { $query .= " WHERE \"{$relTable->srcCol()->getName()}\" = '{$this->getDBRepresentation($srcAtom)}'"; @@ -588,15 +588,15 @@ public function getAllLinks(Relation $relation, ?Atom $srcAtom = null, ?Atom $tg } else { $query .= " AND \"{$relTable->tgtCol()->getName()}\" IS NOT NULL"; } - + $links = []; foreach ((array)$this->execute($query) as $row) { $links[] = new Link($relation, new Atom($row['src'], $relation->srcConcept), new Atom($row['tgt'], $relation->tgtConcept)); } - + return $links; } - + /** * Add link (srcAtom,tgtAtom) into database table for relation r */ @@ -605,12 +605,12 @@ public function addLink(Link $link): void $relation = $link->relation(); $srcAtomId = $this->getDBRepresentation($link->src()); $tgtAtomId = $this->getDBRepresentation($link->tgt()); - + $relTable = $relation->getMysqlTable(); $table = $relTable->getName(); $srcCol = $relTable->srcCol()->getName(); $tgtCol = $relTable->tgtCol()->getName(); - + switch ($relTable->inTableOf()) { case TableType::Binary: // Relation is administrated in n-n table $this->execute("REPLACE INTO \"{$table}\" (\"{$srcCol}\", \"{$tgtCol}\") VALUES ('{$srcAtomId}', '{$tgtAtomId}')"); @@ -624,11 +624,11 @@ public function addLink(Link $link): void default: throw new FatalException("Unsupported TableType '{$relTable->inTableOf()->value}' to addLink for for relation '{$relation}'"); } - + // Check if query resulted in an affected row $this->checkForAffectedRows(); } - + /** * Delete link (srcAtom,tgtAtom) into database table for relation r */ @@ -637,12 +637,12 @@ public function deleteLink(Link $link): void $relation = $link->relation(); $srcAtomId = $this->getDBRepresentation($link->src()); $tgtAtomId = $this->getDBRepresentation($link->tgt()); - + $relTable = $relation->getMysqlTable(); $table = $relTable->getName(); $srcCol = $relTable->srcCol()->getName(); $tgtCol = $relTable->tgtCol()->getName(); - + switch ($relTable->inTableOf()) { case TableType::Binary: // Relation is administrated in n-n table $this->execute("DELETE FROM \"{$table}\" WHERE \"{$srcCol}\" = '{$srcAtomId}' AND \"{$tgtCol}\" = '{$tgtAtomId}'"); @@ -664,10 +664,10 @@ public function deleteLink(Link $link): void default: throw new FatalException("Unsupported TableType '{$relTable->inTableOf()->value}' to deleteLink for for relation '{$relation}'"); } - + $this->checkForAffectedRows(); // Check if query resulted in an affected row } - + /** * Undocumented function * @@ -679,7 +679,7 @@ public function deleteAllLinks(Relation $relation, Atom $atom, SrcOrTgt $srcOrTg { $relationTable = $relation->getMysqlTable(); $atomId = $this->getDBRepresentation($atom); - + $whereCol = match ($srcOrTgt) { SrcOrTgt::SRC => $relationTable->srcCol(), SrcOrTgt::TGT => $relationTable->tgtCol() @@ -700,7 +700,7 @@ public function deleteAllLinks(Relation $relation, Atom $atom, SrcOrTgt $srcOrTg default: throw new FatalException("Unsupported TableType '{$relationTable->inTableOf()->value}' to deleteAllLinks for for relation '{$relation}'"); } - + $this->execute($query); } @@ -710,8 +710,9 @@ public function deleteAllLinks(Relation $relation, Atom $atom, SrcOrTgt $srcOrTg public function emptyRelation(Relation $relation): void { $relationTable = $relation->getMysqlTable(); + $tableOf = $relationTable->inTableOf()->value; - switch ($relationTable->inTableOf()) { + switch ($tableOf) { case null: // If n-n table, remove all rows $this->execute("DELETE FROM \"{$relationTable->getName()}\""); break; @@ -721,17 +722,20 @@ public function emptyRelation(Relation $relation): void case 'tgt': // If in table of tgt concept, set src col to null $this->execute("UPDATE \"{$relationTable->getName()}\" SET \"{$relationTable->srcCol()->getName()}\" = NULL"); break; + case 'binary': // If n-n table, remove all rows + $this->execute("DELETE FROM \"{$relationTable->getName()}\""); + break; default: - throw new FatalException("Unknown 'tableOf' option for relation '{$relation}'"); + throw new FatalException("Unknown 'tableOf' option '{$tableOf}' for relation '{$relation}'"); } } - -/************************************************************************************************** - * - * Implementation of PlugInterface methods - * - *************************************************************************************************/ - + + /************************************************************************************************** + * + * Implementation of PlugInterface methods + * + *************************************************************************************************/ + /** * Execute query for given interface expression and source atom */ @@ -747,7 +751,7 @@ public function executeIfcExpression(InterfaceExprObject $ifc, Atom $srcAtom): m } return $this->execute($query); } - + /** * Execute query for giver view segement and source atom */ @@ -755,22 +759,22 @@ public function executeViewExpression(ViewSegment $view, Atom $srcAtom): array { $srcAtomId = $this->getDBRepresentation($srcAtom); $viewSQL = $view->getQuery(); - + if (strpos($viewSQL, '_SRCATOM') !== false) { $query = str_replace('_SRCATOM', $srcAtomId, $viewSQL); } else { $query = "SELECT DISTINCT \"tgt\" FROM ({$viewSQL}) AS \"results\" WHERE \"src\" = '{$srcAtomId}' AND \"tgt\" IS NOT NULL"; } - + return array_column((array) $this->execute($query), 'tgt'); } - -/************************************************************************************************** - * - * Helper functions - * - *************************************************************************************************/ - + + /************************************************************************************************** + * + * Helper functions + * + *************************************************************************************************/ + /** * Check if insert/update/delete function resulted in updated record(s) * If not, report warning (or throw exception) to indicate that something is going wrong