From c300122c3db04791a35e18139c1edefd37d6f261 Mon Sep 17 00:00:00 2001 From: aB0t Date: Wed, 24 Apr 2024 21:22:27 +0200 Subject: [PATCH] update 2024-04-24 21:22:23 --- .../code.php | 6 +- .../code.php | 4 +- .../code.power | 4 +- .../README.md | 145 ++++-- .../code.php | 441 +++++++++++++----- .../code.power | 438 ++++++++++++----- .../settings.json | 2 +- 7 files changed, 728 insertions(+), 312 deletions(-) diff --git a/src/bfd1d6d5-56c1-4fe9-9fee-1c5910e1f5d8/code.php b/src/bfd1d6d5-56c1-4fe9-9fee-1c5910e1f5d8/code.php index 36f8d1e..92805b6 100644 --- a/src/bfd1d6d5-56c1-4fe9-9fee-1c5910e1f5d8/code.php +++ b/src/bfd1d6d5-56c1-4fe9-9fee-1c5910e1f5d8/code.php @@ -9598,7 +9598,7 @@ class Table extends BaseTable implements Tableinterface 'store' => 'json', 'tab_name' => 'Updates', 'db' => [ - 'type' => 'TEXT', + 'type' => 'MEDIUMTEXT', 'default' => 'EMPTY', 'null_switch' => 'NOT NULL', 'unique_key' => false, @@ -10526,7 +10526,7 @@ class Table extends BaseTable implements Tableinterface 'store' => 'json', 'tab_name' => 'Updates', 'db' => [ - 'type' => 'TEXT', + 'type' => 'MEDIUMTEXT', 'default' => 'EMPTY', 'null_switch' => 'NOT NULL', 'unique_key' => false, @@ -10734,7 +10734,7 @@ class Table extends BaseTable implements Tableinterface 'store' => 'json', 'tab_name' => 'Updates', 'db' => [ - 'type' => 'TEXT', + 'type' => 'MEDIUMTEXT', 'default' => 'EMPTY', 'null_switch' => 'NOT NULL', 'unique_key' => false, diff --git a/src/e0f6ddbe-2a35-4537-942c-faff2ebd04f6/code.php b/src/e0f6ddbe-2a35-4537-942c-faff2ebd04f6/code.php index ee9043d..46227ed 100644 --- a/src/e0f6ddbe-2a35-4537-942c-faff2ebd04f6/code.php +++ b/src/e0f6ddbe-2a35-4537-942c-faff2ebd04f6/code.php @@ -48,7 +48,7 @@ abstract class BaseTable implements Tableinterface 'tab_name' => NULL, 'db' => [ 'type' => 'INT(11)', - 'default' => '', + 'default' => 'EMPTY', 'auto_increment' => true, 'primary_key' => true, 'null_switch' => 'NOT NULL' @@ -227,7 +227,7 @@ abstract class BaseTable implements Tableinterface 'tab_name' => NULL, 'db' => [ 'type' => 'TEXT', - 'default' => '', + 'default' => 'EMPTY', 'null_switch' => 'NULL' ] ] diff --git a/src/e0f6ddbe-2a35-4537-942c-faff2ebd04f6/code.power b/src/e0f6ddbe-2a35-4537-942c-faff2ebd04f6/code.power index 8015f46..b114786 100644 --- a/src/e0f6ddbe-2a35-4537-942c-faff2ebd04f6/code.power +++ b/src/e0f6ddbe-2a35-4537-942c-faff2ebd04f6/code.power @@ -24,7 +24,7 @@ 'tab_name' => NULL, 'db' => [ 'type' => 'INT(11)', - 'default' => '', + 'default' => 'EMPTY', 'auto_increment' => true, 'primary_key' => true, 'null_switch' => 'NOT NULL' @@ -203,7 +203,7 @@ 'tab_name' => NULL, 'db' => [ 'type' => 'TEXT', - 'default' => '', + 'default' => 'EMPTY', 'null_switch' => 'NULL' ] ] diff --git a/src/f3c04c28-bce4-422e-be93-7d163e4e342b/README.md b/src/f3c04c28-bce4-422e-be93-7d163e4e342b/README.md index 401fef9..d1fa0c7 100644 --- a/src/f3c04c28-bce4-422e-be93-7d163e4e342b/README.md +++ b/src/f3c04c28-bce4-422e-be93-7d163e4e342b/README.md @@ -19,23 +19,28 @@ abstract Schema #Orange { - array $keys - array $columns - array $success + # $currentVersion + __construct(Table $table) + update() : array - + createTable(string $table) : void - + updateSchema(string $table) : void # {abstract} getCode() : string + # tableExists(string $table) : bool + + updateSchema(string $table) : void + + createTable(string $table) : void + # getExistingColumns(string $table) : array # addMissingColumns(string $table, array $columns) : void # checkColumnsDataType(string $table, array $columns) : void + # getColumnDefinition(string $table, string $field) : ?string + # checkDefault(string $table, string $column) : void # updateColumnsDataType(string $table, array $columns) : void + # getTable(string $table) : string + isDataTypeChangeSignificant(string $currentType, string $expectedType) : bool + # adjustExistingDefaults(string $table, string $column, ...) : bool # updateColumnDataType(string $updateString, string $table, ...) : bool # getTableKeys() : string + # setKeys(array $column) : void # setUniqueKey(array $column) : void # setKey(array $column) : void - # getTable(string $table) : string - - tableExists(string $table) : bool - - getExistingColumns(string $table) : array - - getColumnDefinition(string $table, string $field) : ?string - - setKeys(array $column) : void + # getDefaultValue(string $type, ?string $defaultValue, ...) : string } note right of Schema::__construct @@ -51,25 +56,39 @@ note left of Schema::update return: array end note -note right of Schema::createTable - Create a table with all necessary fields. +note right of Schema::getCode + Get the targeted component code since: 3.2.1 - return: void + return: string end note -note left of Schema::updateSchema +note left of Schema::tableExists + Check if a table exists in the database. + + since: 3.2.1 + return: bool +end note + +note right of Schema::updateSchema Update the schema of an existing table. since: 3.2.1 return: void end note -note right of Schema::getCode - Get the targeted component code +note left of Schema::createTable + Create a table with all necessary fields. since: 3.2.1 - return: string + return: void +end note + +note right of Schema::getExistingColumns + Fetch existing columns from a database table. + + since: 3.2.1 + return: array end note note left of Schema::addMissingColumns @@ -86,6 +105,21 @@ note right of Schema::checkColumnsDataType return: void end note +note left of Schema::getColumnDefinition + Generates a SQL snippet for defining a table column, incorporating column type, +default value, nullability, and auto-increment properties. + + since: 3.2.1 + return: ?string +end note + +note right of Schema::checkDefault + Check and Update the default values if needed, including existing data adjustments + + since: 3.2.1 + return: void +end note + note left of Schema::updateColumnsDataType Update the data type of the given fields. @@ -93,7 +127,38 @@ note left of Schema::updateColumnsDataType return: void end note -note right of Schema::updateColumnDataType +note right of Schema::getTable + Add the component name to get the full table name. + + since: 3.2.1 + return: string +end note + +note left of Schema::isDataTypeChangeSignificant + Determines if the change in data type between two definitions is significant. +This function checks if there's a significant difference between the current +data type and the expected data type that would require updating the database schema. +It ignores size and other modifiers for certain data types where MySQL considers +these attributes irrelevant for storage. + + since: 3.2.1 + return: bool +end note + +note right of Schema::adjustExistingDefaults + Updates existing rows in a column to a new default value + + since: 3.2.1 + return: bool + + arguments: + string $table + string $column + mixed $currentDefault + mixed $newDefault +end note + +note left of Schema::updateColumnDataType Update the data type of the given field. since: 3.2.1 @@ -105,13 +170,20 @@ note right of Schema::updateColumnDataType string $field end note -note left of Schema::getTableKeys +note right of Schema::getTableKeys Key all needed keys for this table since: 3.2.1 return: string end note +note left of Schema::setKeys + Function to set the view keys + + since: 3.2.1 + return: void +end note + note right of Schema::setUniqueKey Function to set the unique key @@ -126,40 +198,19 @@ note left of Schema::setKey return: void end note -note right of Schema::getTable - Add the component name to get the full table name. +note right of Schema::getDefaultValue + Adjusts the default value SQL fragment for a database field based on its type and specific rules. +If the field is of type DATETIME and the Joomla version is not 3, it sets the default to CURRENT_TIMESTAMP +if not explicitly specified otherwise. For all other types, or when a 'EMPTY' default is specified, it handles +defaults by either leaving them unset or applying the provided default, properly quoted for SQL safety. since: 3.2.1 return: string -end note - -note left of Schema::tableExists - Check if a table exists in the database. - - since: 3.2.1 - return: bool -end note - -note right of Schema::getExistingColumns - Fetch existing columns from a database table. - - since: 3.2.1 - return: array -end note - -note left of Schema::getColumnDefinition - Generates a SQL snippet for defining a table column, incorporating column type, -default value, nullability, and auto-increment properties. - - since: 3.2.1 - return: ?string -end note - -note right of Schema::setKeys - Function to set the view keys - - since: 3.2.1 - return: void + + arguments: + string $type + ?string $defaultValue + bool $pure = false end note @enduml diff --git a/src/f3c04c28-bce4-422e-be93-7d163e4e342b/code.php b/src/f3c04c28-bce4-422e-be93-7d163e4e342b/code.php index 764b7d5..53c0591 100644 --- a/src/f3c04c28-bce4-422e-be93-7d163e4e342b/code.php +++ b/src/f3c04c28-bce4-422e-be93-7d163e4e342b/code.php @@ -12,7 +12,8 @@ namespace VDM\Joomla\Abstraction; -use Joomla\CMS\Factory; +use Joomla\CMS\Factory; +use Joomla\CMS\Version; use VDM\Joomla\Interfaces\Tableinterface as Table; use VDM\Joomla\Interfaces\SchemaInterface; @@ -87,6 +88,14 @@ abstract class Schema implements SchemaInterface */ private array $success; + /** + * Current Joomla Version We are IN + * + * @var int + * @since 3.2.1 + **/ + protected $currentVersion; + /** * Constructor. * @@ -108,8 +117,11 @@ abstract class Schema implements SchemaInterface // set the component table $this->prefix = $this->db->getPrefix() . $this->getCode(); + + // set the current version + $this->currentVersion = Version::MAJOR_VERSION; } catch (\Exception $e) { - throw new \Exception("Error: failed to initialize schema class due to a database error.", 0, $e); + throw new \Exception("Error: failed to initialize schema class due to a database error."); } } @@ -141,7 +153,7 @@ abstract class Schema implements SchemaInterface } } } catch (\Exception $e) { - throw new \Exception("Error: updating database schema.", 0, $e); + throw new \Exception("Error: updating database schema. " . $e->getMessage()); } if (count($this->success) == 1) @@ -156,6 +168,63 @@ abstract class Schema implements SchemaInterface return $this->success; } + /** + * Get the targeted component code + * + * @return string + * @since 3.2.1 + */ + abstract protected function getCode(): string; + + /** + * Check if a table exists in the database. + * + * @param string $table The name of the table to check. + * + * @return bool True if table exists, False otherwise. + * @since 3.2.1 + */ + protected function tableExists(string $table): bool + { + return in_array($this->getTable($table), $this->tables); + } + + /** + * Update the schema of an existing table. + * + * @param string $table The table to update. + * + * @return void + * @since 3.2.1 + * @throws \Exception If there is an error while updating the schema. + */ + public function updateSchema(string $table): void + { + try { + $existingColumns = $this->getExistingColumns($table); + $expectedColumns = $this->table->fields($table, true); + + $missingColumns = array_diff($expectedColumns, $existingColumns); + + if (!empty($missingColumns)) + { + $this->addMissingColumns($table, $missingColumns); + } + + $this->checkColumnsDataType($table, $expectedColumns); + + } catch (\Exception $e) { + throw new \Exception("Error: updating schema for $table table. " . $e->getMessage()); + } + + if (!empty($missingColumns)) + { + $column_s = (count($missingColumns) == 1) ? 'column' : 'columns'; + $missingColumns = implode(', ', $missingColumns); + $this->success[] = "Success: added missing ($missingColumns) $column_s to $table table."; + } + } + /** * Create a table with all necessary fields. * @@ -189,56 +258,27 @@ abstract class Schema implements SchemaInterface $this->db->setQuery($createTableSql); $this->db->execute(); } catch (\Exception $e) { - throw new \Exception("Error: failed to create missing $table table.", 0, $e); + throw new \Exception("Error: failed to create missing $table table. " . $e->getMessage()); } $this->success[] = "Success: created missing $table table."; } /** - * Update the schema of an existing table. + * Fetch existing columns from a database table. * - * @param string $table The table to update. + * @param string $table The name of the table. * - * @return void + * @return array An array of column names. * @since 3.2.1 - * @throws \Exception If there is an error while updating the schema. */ - public function updateSchema(string $table): void + protected function getExistingColumns(string $table): array { - try { - $existingColumns = $this->getExistingColumns($table); - $expectedColumns = $this->table->fields($table, true); + $this->columns = $this->db->getTableColumns($this->getTable($table), false); - $missingColumns = array_diff($expectedColumns, $existingColumns); - - if (!empty($missingColumns)) - { - $this->addMissingColumns($table, $missingColumns); - } - - $this->checkColumnsDataType($table, $expectedColumns); - - } catch (\Exception $e) { - throw new \Exception("Error: updating schema for $table table.", 0, $e); - } - - if (!empty($missingColumns)) - { - $column_s = (count($missingColumns) == 1) ? 'column' : 'columns'; - $missingColumns = implode(', ', $missingColumns); - $this->success[] = "Success: added missing ($missingColumns) $column_s to $table table."; - } + return array_keys($this->columns); } - /** - * Get the targeted component code - * - * @return string - * @since 3.2.1 - */ - abstract protected function getCode(): string; - /** * Add missing columns to a table. * @@ -270,7 +310,7 @@ abstract class Schema implements SchemaInterface } catch (\Exception $e) { $column_s = (count($columns) == 1) ? 'column' : 'columns'; $columns = implode(', ', $columns); - throw new \Exception("Error: failed to add ($columns) $column_s to $table table.", 0, $e); + throw new \Exception("Error: failed to add ($columns) $column_s to $table table. " . $e->getMessage()); } } @@ -291,18 +331,20 @@ abstract class Schema implements SchemaInterface $current = $this->columns[$column] ?? null; if ($current === null || ($expected = $this->table->get($table, $column, 'db')) === null) { - // this field is no longer part of the component and can be ignored continue; } // check if the data type and size match - if (strcasecmp($current->Type, $expected['type']) != 0) + if ($this->isDataTypeChangeSignificant($current->Type, $expected['type'])) { $requireUpdate[$column] = [ 'column' => $column, 'current' => $current->Type, 'expected' => $expected['type'] ]; + + // check if update of default values is needed + $this->checkDefault($table, $column); } } @@ -312,6 +354,89 @@ abstract class Schema implements SchemaInterface } } + /** + * Generates a SQL snippet for defining a table column, incorporating column type, + * default value, nullability, and auto-increment properties. + * + * @param string $table The table name to be used. + * @param string $field The field name in the table to generate SQL for. + * + * @return string|null The SQL snippet for the column definition. + * @since 3.2.1 + * @throws \Exception If the schema details cannot be retrieved or the SQL statement cannot be constructed properly. + */ + protected function getColumnDefinition(string $table, string $field): ?string + { + try { + // Retrieve the database schema details for the specified table and field + if (($db = $this->table->get($table, $field, 'db')) === null) + { + return null; + } + + // Prepare the column name + $column_name = $this->db->quoteName($field); + $db['name'] = $field; + + // Prepare the type and default value SQL statement + $type = $db['type'] ?? 'TEXT'; + $db_default = isset($db['default']) ? $db['default'] : null; + $default = $this->getDefaultValue($type, $db_default); + + // Prepare the null switch, and auto increment statement + $null_switch = !empty($db['null_switch']) ? " " . $db['null_switch'] : ''; + $auto_increment = !empty($db['auto_increment']) ? " AUTO_INCREMENT" : ''; + + $this->setKeys($db); + + // Assemble the SQL snippet for the column definition + return "{$column_name} {$type}{$null_switch}{$default}{$auto_increment}"; + } catch (\Exception $e) { + throw new \Exception("Error: failed to generate column definition for ($table.$field). " . $e->getMessage()); + } + } + + /** + * Check and Update the default values if needed, including existing data adjustments + * + * @param string $table The table to update. + * @param string $column The column/field to check. + * + * @return void + * @since 3.2.1 + */ + protected function checkDefault(string $table, string $column): void + { + // Retrieve the expected column configuration + $expected = $this->table->get($table, $column, 'db'); + + // Skip updates if the column is auto_increment + if (isset($expected['auto_increment']) && $expected['auto_increment']) + { + return; + } + + // Retrieve the current column configuration + $current = $this->columns[$column]; + + // Check if default should be empty and current default is null, skip processing + if (strtoupper($expected['default']) === 'EMPTY' && $current->Default === NULL) + { + return; + } + + // Determine the new default value based on the expected settings + $type = $expected['type'] ?? 'TEXT'; + $db_default = isset($expected['default']) ? $expected['default'] : null; + $newDefault = $this->getDefaultValue($type, $db_default, true); + + // First, adjust existing rows to conform to the new default if necessary + if (is_numeric($newDefault) && $this->adjustExistingDefaults($table, $column, $current->Default, $newDefault)) + { + $this->success[] = "Success: updated the ($column) defaults in $table table."; + } + } + /** * Update the data type of the given fields. * @@ -336,13 +461,120 @@ abstract class Schema implements SchemaInterface if ($this->updateColumnDataType($alterQuery, $table, $column)) { - $current = (string) $types['current'] ?? 'error'; - $expected = (string) $types['expected'] ?? 'error'; + $current = $types['current'] ?? 'error'; + $expected = $types['expected'] ?? 'error'; $this->success[] = "Success: updated ($column) column datatype $current to $expected in $table table."; } } } - + + /** + * Add the component name to get the full table name. + * + * @param string $table The table name. + * + * @return void + * @since 3.2.1 + */ + protected function getTable(string $table): string + { + return $this->prefix . '_' . $table; + } + + /** + * Determines if the change in data type between two definitions is significant. + * + * This function checks if there's a significant difference between the current + * data type and the expected data type that would require updating the database schema. + * It ignores size and other modifiers for certain data types where MySQL considers + * these attributes irrelevant for storage. + * + * @param string $currentType The current data type from the database schema. + * @param string $expectedType The expected data type to validate against. + * + * @return bool Returns true if the data type change is significant, otherwise false. + * @since 3.2.1 + */ + function isDataTypeChangeSignificant(string $currentType, string $expectedType): bool + { + // we only do this for Joomla 4+ + if ($this->currentVersion != 3) + { + // Normalize both input types to lowercase for case-insensitive comparison + $currentType = strtolower($currentType); + $expectedType = strtolower($expectedType); + + // Define types where size or other modifiers are irrelevant + $sizeIrrelevantTypes = [ + 'int', 'tinyint', 'smallint', 'mediumint', 'bigint', // Standard integer types + 'int unsigned', 'tinyint unsigned', 'smallint unsigned', 'mediumint unsigned', 'bigint unsigned', // Unsigned integer types + ]; + + // Check if the type involves size-irrelevant types + foreach ($sizeIrrelevantTypes as $type) + { + if (strpos($expectedType, $type) !== false) + { + // Remove any numeric sizes and modifiers for comparison + $pattern = '/\(\d+\)|unsigned|\s*/'; + $cleanCurrentType = preg_replace($pattern, '', $currentType); + $cleanExpectedType = preg_replace($pattern, '', $expectedType); + + // Compare the cleaned types + if ($cleanCurrentType === $cleanExpectedType) + { + return false; // No significant change + } + } + } + } + + // Perform a standard case-insensitive comparison for other types + if (strcasecmp($currentType, $expectedType) == 0) + { + return false; // No significant change + } + + return true; // Significant datatype change detected + } + + /** + * Updates existing rows in a column to a new default value + * + * @param string $table The table to update. + * @param string $column The column to update. + * @param mixed $currentDefault Current default value. + * @param mixed $newDefault The new default value to be set. + * + * @return void + * @since 3.2.1 + * @throws \Exception If there is an error updating column defaults. + */ + protected function adjustExistingDefaults(string $table, string $column, $currentDefault, $newDefault): bool + { + // Determine if adjustment is needed based on new and current defaults + if ($newDefault !== $currentDefault) + { + try { + // Format the new default for SQL use + $sqlDefault = $this->db->quote($newDefault); + + $updateTable = 'UPDATE ' . $this->db->quoteName($this->getTable($table)); + $dbField = $this->db->quoteName($column); + + // Update SQL to set new default on existing rows where the default is currently the old default + $sql = $updateTable . " SET $dbField = $sqlDefault WHERE $dbField IS NULL OR $dbField = ''"; + + // Execute the update + $this->db->setQuery($sql); + return $this->db->execute(); + } catch (\Exception $e) { + throw new \Exception("Error: failed to update ($column) column defaults in $table table. " . $e->getMessage()); + } + } + return false; + } + /** * Update the data type of the given field. * @@ -360,7 +592,7 @@ abstract class Schema implements SchemaInterface $this->db->setQuery($updateString); return $this->db->execute(); } catch (\Exception $e) { - throw new \Exception("Error: failed to update the datatype of ($field) column in $table table.", 0, $e); + throw new \Exception("Error: failed to update the datatype of ($field) column in $table table. " . $e->getMessage()); } } @@ -388,6 +620,20 @@ abstract class Schema implements SchemaInterface return implode(', ', $keys); } + /** + * Function to set the view keys + * + * @param string $column The field column database array values + * + * @return void + * @since 3.2.1 + */ + protected function setKeys(array $column): void + { + $this->setUniqueKey($column); + $this->setKey($column); + } + /** * Function to set the unique key * @@ -423,97 +669,34 @@ abstract class Schema implements SchemaInterface } /** - * Add the component name to get the full table name. + * Adjusts the default value SQL fragment for a database field based on its type and specific rules. * - * @param string $table The table name. + * If the field is of type DATETIME and the Joomla version is not 3, it sets the default to CURRENT_TIMESTAMP + * if not explicitly specified otherwise. For all other types, or when a 'EMPTY' default is specified, it handles + * defaults by either leaving them unset or applying the provided default, properly quoted for SQL safety. * - * @return void - * @since 3.2.1 - */ - protected function getTable(string $table): string - { - return $this->prefix . '_' . $table; - } - - /** - * Check if a table exists in the database. + * @param string $type The type of the database field (e.g., 'DATETIME'). + * @param string|null $defaultValue Optional default value for the field, null if not provided. + * @param bool $pure Optional to add the 'DEFAULT' string or not. * - * @param string $table The name of the table to check. - * - * @return bool True if table exists, False otherwise. - * @since 3.2.1 - */ - private function tableExists(string $table): bool - { - return in_array($this->getTable($table), $this->tables); - } - - /** - * Fetch existing columns from a database table. - * - * @param string $table The name of the table. - * - * @return array An array of column names. - * @since 3.2.1 - */ - private function getExistingColumns(string $table): array - { - $this->columns = $this->db->getTableColumns($this->getTable($table), false); - - return array_keys($this->columns); - } - - /** - * Generates a SQL snippet for defining a table column, incorporating column type, - * default value, nullability, and auto-increment properties. - * - * @param string $table The table name to be used. - * @param string $field The field name in the table to generate SQL for. - * - * @return string|null The SQL snippet for the column definition. + * @return string The SQL fragment to set the default value for a field. * @since 3.2.1 - * @throws \Exception If the schema details cannot be retrieved or the SQL statement cannot be constructed properly. */ - private function getColumnDefinition(string $table, string $field): ?string + protected function getDefaultValue(string $type, ?string $defaultValue, bool $pure = false): string { - try { - // Retrieve the database schema details for the specified table and field - if (($db = $this->table->get($table, $field, 'db')) === null) - { - return null; - } - - // Prepare the column name - $column_name = $this->db->quoteName($field); - $db['name'] = $field; - - // Prepare the default value SQL, null switch, and auto increment statement - $default = !empty($db['default']) ? " DEFAULT " . $this->db->quote($db['default']) : ''; - $null_switch = !empty($db['null_switch']) ? " " . $db['null_switch'] : ''; - $auto_increment = !empty($db['auto_increment']) ? " AUTO_INCREMENT" : ''; - $type = !empty($db['type']) ? $db['type'] : 'TEXT'; - - $this->setKeys($db); - - // Assemble the SQL snippet for the column definition - return "{$column_name} {$type}{$default}{$null_switch}{$auto_increment}"; - } catch (\Exception $e) { - throw new \Exception("Error: failed to generate column definition for $table.$field", 0, $e); + if ($defaultValue === null || strtoupper($defaultValue) === 'EMPTY') + { + return ''; } - } - /** - * Function to set the view keys - * - * @param string $column The field column database array values - * - * @return void - * @since 3.2.1 - */ - private function setKeys(array $column): void - { - $this->setUniqueKey($column); - $this->setKey($column); + // Set default for DATETIME fields in Joomla versions above 3 + if (strtoupper($type) === 'DATETIME' && $this->currentVersion != 3) + { + return $pure ? "CURRENT_TIMESTAMP" : " DEFAULT CURRENT_TIMESTAMP"; + } + + // Apply and quote the default value + return $pure ? $defaultValue : " DEFAULT " . $this->db->quote($defaultValue); } } diff --git a/src/f3c04c28-bce4-422e-be93-7d163e4e342b/code.power b/src/f3c04c28-bce4-422e-be93-7d163e4e342b/code.power index f02ae75..e33b816 100644 --- a/src/f3c04c28-bce4-422e-be93-7d163e4e342b/code.power +++ b/src/f3c04c28-bce4-422e-be93-7d163e4e342b/code.power @@ -61,6 +61,14 @@ */ private array $success; + /** + * Current Joomla Version We are IN + * + * @var int + * @since 3.2.1 + **/ + protected $currentVersion; + /** * Constructor. * @@ -82,8 +90,11 @@ // set the component table $this->prefix = $this->db->getPrefix() . $this->getCode(); + + // set the current version + $this->currentVersion = Version::MAJOR_VERSION; } catch (\Exception $e) { - throw new \Exception("Error: failed to initialize schema class due to a database error.", 0, $e); + throw new \Exception("Error: failed to initialize schema class due to a database error."); } } @@ -115,7 +126,7 @@ } } } catch (\Exception $e) { - throw new \Exception("Error: updating database schema.", 0, $e); + throw new \Exception("Error: updating database schema. " . $e->getMessage()); } if (count($this->success) == 1) @@ -130,6 +141,63 @@ return $this->success; } + /** + * Get the targeted component code + * + * @return string + * @since 3.2.1 + */ + abstract protected function getCode(): string; + + /** + * Check if a table exists in the database. + * + * @param string $table The name of the table to check. + * + * @return bool True if table exists, False otherwise. + * @since 3.2.1 + */ + protected function tableExists(string $table): bool + { + return in_array($this->getTable($table), $this->tables); + } + + /** + * Update the schema of an existing table. + * + * @param string $table The table to update. + * + * @return void + * @since 3.2.1 + * @throws \Exception If there is an error while updating the schema. + */ + public function updateSchema(string $table): void + { + try { + $existingColumns = $this->getExistingColumns($table); + $expectedColumns = $this->table->fields($table, true); + + $missingColumns = array_diff($expectedColumns, $existingColumns); + + if (!empty($missingColumns)) + { + $this->addMissingColumns($table, $missingColumns); + } + + $this->checkColumnsDataType($table, $expectedColumns); + + } catch (\Exception $e) { + throw new \Exception("Error: updating schema for $table table. " . $e->getMessage()); + } + + if (!empty($missingColumns)) + { + $column_s = (count($missingColumns) == 1) ? 'column' : 'columns'; + $missingColumns = implode(', ', $missingColumns); + $this->success[] = "Success: added missing ($missingColumns) $column_s to $table table."; + } + } + /** * Create a table with all necessary fields. * @@ -163,56 +231,27 @@ $this->db->setQuery($createTableSql); $this->db->execute(); } catch (\Exception $e) { - throw new \Exception("Error: failed to create missing $table table.", 0, $e); + throw new \Exception("Error: failed to create missing $table table. " . $e->getMessage()); } $this->success[] = "Success: created missing $table table."; } /** - * Update the schema of an existing table. + * Fetch existing columns from a database table. * - * @param string $table The table to update. + * @param string $table The name of the table. * - * @return void + * @return array An array of column names. * @since 3.2.1 - * @throws \Exception If there is an error while updating the schema. */ - public function updateSchema(string $table): void + protected function getExistingColumns(string $table): array { - try { - $existingColumns = $this->getExistingColumns($table); - $expectedColumns = $this->table->fields($table, true); + $this->columns = $this->db->getTableColumns($this->getTable($table), false); - $missingColumns = array_diff($expectedColumns, $existingColumns); - - if (!empty($missingColumns)) - { - $this->addMissingColumns($table, $missingColumns); - } - - $this->checkColumnsDataType($table, $expectedColumns); - - } catch (\Exception $e) { - throw new \Exception("Error: updating schema for $table table.", 0, $e); - } - - if (!empty($missingColumns)) - { - $column_s = (count($missingColumns) == 1) ? 'column' : 'columns'; - $missingColumns = implode(', ', $missingColumns); - $this->success[] = "Success: added missing ($missingColumns) $column_s to $table table."; - } + return array_keys($this->columns); } - /** - * Get the targeted component code - * - * @return string - * @since 3.2.1 - */ - abstract protected function getCode(): string; - /** * Add missing columns to a table. * @@ -244,7 +283,7 @@ } catch (\Exception $e) { $column_s = (count($columns) == 1) ? 'column' : 'columns'; $columns = implode(', ', $columns); - throw new \Exception("Error: failed to add ($columns) $column_s to $table table.", 0, $e); + throw new \Exception("Error: failed to add ($columns) $column_s to $table table. " . $e->getMessage()); } } @@ -265,18 +304,20 @@ $current = $this->columns[$column] ?? null; if ($current === null || ($expected = $this->table->get($table, $column, 'db')) === null) { - // this field is no longer part of the component and can be ignored continue; } // check if the data type and size match - if (strcasecmp($current->Type, $expected['type']) != 0) + if ($this->isDataTypeChangeSignificant($current->Type, $expected['type'])) { $requireUpdate[$column] = [ 'column' => $column, 'current' => $current->Type, 'expected' => $expected['type'] ]; + + // check if update of default values is needed + $this->checkDefault($table, $column); } } @@ -286,6 +327,89 @@ } } + /** + * Generates a SQL snippet for defining a table column, incorporating column type, + * default value, nullability, and auto-increment properties. + * + * @param string $table The table name to be used. + * @param string $field The field name in the table to generate SQL for. + * + * @return string|null The SQL snippet for the column definition. + * @since 3.2.1 + * @throws \Exception If the schema details cannot be retrieved or the SQL statement cannot be constructed properly. + */ + protected function getColumnDefinition(string $table, string $field): ?string + { + try { + // Retrieve the database schema details for the specified table and field + if (($db = $this->table->get($table, $field, 'db')) === null) + { + return null; + } + + // Prepare the column name + $column_name = $this->db->quoteName($field); + $db['name'] = $field; + + // Prepare the type and default value SQL statement + $type = $db['type'] ?? 'TEXT'; + $db_default = isset($db['default']) ? $db['default'] : null; + $default = $this->getDefaultValue($type, $db_default); + + // Prepare the null switch, and auto increment statement + $null_switch = !empty($db['null_switch']) ? " " . $db['null_switch'] : ''; + $auto_increment = !empty($db['auto_increment']) ? " AUTO_INCREMENT" : ''; + + $this->setKeys($db); + + // Assemble the SQL snippet for the column definition + return "{$column_name} {$type}{$null_switch}{$default}{$auto_increment}"; + } catch (\Exception $e) { + throw new \Exception("Error: failed to generate column definition for ($table.$field). " . $e->getMessage()); + } + } + + /** + * Check and Update the default values if needed, including existing data adjustments + * + * @param string $table The table to update. + * @param string $column The column/field to check. + * + * @return void + * @since 3.2.1 + */ + protected function checkDefault(string $table, string $column): void + { + // Retrieve the expected column configuration + $expected = $this->table->get($table, $column, 'db'); + + // Skip updates if the column is auto_increment + if (isset($expected['auto_increment']) && $expected['auto_increment']) + { + return; + } + + // Retrieve the current column configuration + $current = $this->columns[$column]; + + // Check if default should be empty and current default is null, skip processing + if (strtoupper($expected['default']) === 'EMPTY' && $current->Default === NULL) + { + return; + } + + // Determine the new default value based on the expected settings + $type = $expected['type'] ?? 'TEXT'; + $db_default = isset($expected['default']) ? $expected['default'] : null; + $newDefault = $this->getDefaultValue($type, $db_default, true); + + // First, adjust existing rows to conform to the new default if necessary + if (is_numeric($newDefault) && $this->adjustExistingDefaults($table, $column, $current->Default, $newDefault)) + { + $this->success[] = "Success: updated the ($column) defaults in $table table."; + } + } + /** * Update the data type of the given fields. * @@ -310,13 +434,120 @@ if ($this->updateColumnDataType($alterQuery, $table, $column)) { - $current = (string) $types['current'] ?? 'error'; - $expected = (string) $types['expected'] ?? 'error'; + $current = $types['current'] ?? 'error'; + $expected = $types['expected'] ?? 'error'; $this->success[] = "Success: updated ($column) column datatype $current to $expected in $table table."; } } } - + + /** + * Add the component name to get the full table name. + * + * @param string $table The table name. + * + * @return void + * @since 3.2.1 + */ + protected function getTable(string $table): string + { + return $this->prefix . '_' . $table; + } + + /** + * Determines if the change in data type between two definitions is significant. + * + * This function checks if there's a significant difference between the current + * data type and the expected data type that would require updating the database schema. + * It ignores size and other modifiers for certain data types where MySQL considers + * these attributes irrelevant for storage. + * + * @param string $currentType The current data type from the database schema. + * @param string $expectedType The expected data type to validate against. + * + * @return bool Returns true if the data type change is significant, otherwise false. + * @since 3.2.1 + */ + function isDataTypeChangeSignificant(string $currentType, string $expectedType): bool + { + // we only do this for Joomla 4+ + if ($this->currentVersion != 3) + { + // Normalize both input types to lowercase for case-insensitive comparison + $currentType = strtolower($currentType); + $expectedType = strtolower($expectedType); + + // Define types where size or other modifiers are irrelevant + $sizeIrrelevantTypes = [ + 'int', 'tinyint', 'smallint', 'mediumint', 'bigint', // Standard integer types + 'int unsigned', 'tinyint unsigned', 'smallint unsigned', 'mediumint unsigned', 'bigint unsigned', // Unsigned integer types + ]; + + // Check if the type involves size-irrelevant types + foreach ($sizeIrrelevantTypes as $type) + { + if (strpos($expectedType, $type) !== false) + { + // Remove any numeric sizes and modifiers for comparison + $pattern = '/\(\d+\)|unsigned|\s*/'; + $cleanCurrentType = preg_replace($pattern, '', $currentType); + $cleanExpectedType = preg_replace($pattern, '', $expectedType); + + // Compare the cleaned types + if ($cleanCurrentType === $cleanExpectedType) + { + return false; // No significant change + } + } + } + } + + // Perform a standard case-insensitive comparison for other types + if (strcasecmp($currentType, $expectedType) == 0) + { + return false; // No significant change + } + + return true; // Significant datatype change detected + } + + /** + * Updates existing rows in a column to a new default value + * + * @param string $table The table to update. + * @param string $column The column to update. + * @param mixed $currentDefault Current default value. + * @param mixed $newDefault The new default value to be set. + * + * @return void + * @since 3.2.1 + * @throws \Exception If there is an error updating column defaults. + */ + protected function adjustExistingDefaults(string $table, string $column, $currentDefault, $newDefault): bool + { + // Determine if adjustment is needed based on new and current defaults + if ($newDefault !== $currentDefault) + { + try { + // Format the new default for SQL use + $sqlDefault = $this->db->quote($newDefault); + + $updateTable = 'UPDATE ' . $this->db->quoteName($this->getTable($table)); + $dbField = $this->db->quoteName($column); + + // Update SQL to set new default on existing rows where the default is currently the old default + $sql = $updateTable . " SET $dbField = $sqlDefault WHERE $dbField IS NULL OR $dbField = ''"; + + // Execute the update + $this->db->setQuery($sql); + return $this->db->execute(); + } catch (\Exception $e) { + throw new \Exception("Error: failed to update ($column) column defaults in $table table. " . $e->getMessage()); + } + } + return false; + } + /** * Update the data type of the given field. * @@ -334,7 +565,7 @@ $this->db->setQuery($updateString); return $this->db->execute(); } catch (\Exception $e) { - throw new \Exception("Error: failed to update the datatype of ($field) column in $table table.", 0, $e); + throw new \Exception("Error: failed to update the datatype of ($field) column in $table table. " . $e->getMessage()); } } @@ -362,6 +593,20 @@ return implode(', ', $keys); } + /** + * Function to set the view keys + * + * @param string $column The field column database array values + * + * @return void + * @since 3.2.1 + */ + protected function setKeys(array $column): void + { + $this->setUniqueKey($column); + $this->setKey($column); + } + /** * Function to set the unique key * @@ -397,95 +642,32 @@ } /** - * Add the component name to get the full table name. + * Adjusts the default value SQL fragment for a database field based on its type and specific rules. * - * @param string $table The table name. + * If the field is of type DATETIME and the Joomla version is not 3, it sets the default to CURRENT_TIMESTAMP + * if not explicitly specified otherwise. For all other types, or when a 'EMPTY' default is specified, it handles + * defaults by either leaving them unset or applying the provided default, properly quoted for SQL safety. * - * @return void - * @since 3.2.1 - */ - protected function getTable(string $table): string - { - return $this->prefix . '_' . $table; - } - - /** - * Check if a table exists in the database. + * @param string $type The type of the database field (e.g., 'DATETIME'). + * @param string|null $defaultValue Optional default value for the field, null if not provided. + * @param bool $pure Optional to add the 'DEFAULT' string or not. * - * @param string $table The name of the table to check. - * - * @return bool True if table exists, False otherwise. - * @since 3.2.1 - */ - private function tableExists(string $table): bool - { - return in_array($this->getTable($table), $this->tables); - } - - /** - * Fetch existing columns from a database table. - * - * @param string $table The name of the table. - * - * @return array An array of column names. - * @since 3.2.1 - */ - private function getExistingColumns(string $table): array - { - $this->columns = $this->db->getTableColumns($this->getTable($table), false); - - return array_keys($this->columns); - } - - /** - * Generates a SQL snippet for defining a table column, incorporating column type, - * default value, nullability, and auto-increment properties. - * - * @param string $table The table name to be used. - * @param string $field The field name in the table to generate SQL for. - * - * @return string|null The SQL snippet for the column definition. + * @return string The SQL fragment to set the default value for a field. * @since 3.2.1 - * @throws \Exception If the schema details cannot be retrieved or the SQL statement cannot be constructed properly. */ - private function getColumnDefinition(string $table, string $field): ?string + protected function getDefaultValue(string $type, ?string $defaultValue, bool $pure = false): string { - try { - // Retrieve the database schema details for the specified table and field - if (($db = $this->table->get($table, $field, 'db')) === null) - { - return null; - } - - // Prepare the column name - $column_name = $this->db->quoteName($field); - $db['name'] = $field; - - // Prepare the default value SQL, null switch, and auto increment statement - $default = !empty($db['default']) ? " DEFAULT " . $this->db->quote($db['default']) : ''; - $null_switch = !empty($db['null_switch']) ? " " . $db['null_switch'] : ''; - $auto_increment = !empty($db['auto_increment']) ? " AUTO_INCREMENT" : ''; - $type = !empty($db['type']) ? $db['type'] : 'TEXT'; - - $this->setKeys($db); - - // Assemble the SQL snippet for the column definition - return "{$column_name} {$type}{$default}{$null_switch}{$auto_increment}"; - } catch (\Exception $e) { - throw new \Exception("Error: failed to generate column definition for $table.$field", 0, $e); + if ($defaultValue === null || strtoupper($defaultValue) === 'EMPTY') + { + return ''; } - } - /** - * Function to set the view keys - * - * @param string $column The field column database array values - * - * @return void - * @since 3.2.1 - */ - private function setKeys(array $column): void - { - $this->setUniqueKey($column); - $this->setKey($column); + // Set default for DATETIME fields in Joomla versions above 3 + if (strtoupper($type) === 'DATETIME' && $this->currentVersion != 3) + { + return $pure ? "CURRENT_TIMESTAMP" : " DEFAULT CURRENT_TIMESTAMP"; + } + + // Apply and quote the default value + return $pure ? $defaultValue : " DEFAULT " . $this->db->quote($defaultValue); } \ No newline at end of file diff --git a/src/f3c04c28-bce4-422e-be93-7d163e4e342b/settings.json b/src/f3c04c28-bce4-422e-be93-7d163e4e342b/settings.json index b50363d..23c41aa 100644 --- a/src/f3c04c28-bce4-422e-be93-7d163e4e342b/settings.json +++ b/src/f3c04c28-bce4-422e-be93-7d163e4e342b/settings.json @@ -20,6 +20,6 @@ "namespace": "[[[NamespacePrefix]]]\\Joomla\\Abstraction.Schema", "description": "Schema Checking\r\n\r\n@since 3.2.1", "licensing_template": "\/**\r\n * @package Joomla.Component.Builder\r\n *\r\n * @created 4th September, 2022\r\n * @author Llewellyn van der Merwe \r\n * @git Joomla Component Builder \r\n * @copyright Copyright (C) 2015 Vast Development Method. All rights reserved.\r\n * @license GNU General Public License version 2 or later; see LICENSE.txt\r\n *\/\r\n", - "head": "use Joomla\\CMS\\Factory;", + "head": "use Joomla\\CMS\\Factory;\r\nuse Joomla\\CMS\\Version;", "composer": "" } \ No newline at end of file