update 2024-04-24 21:22:23

This commit is contained in:
Robot 2024-04-24 21:22:27 +02:00
parent 321d89954a
commit c300122c3d
Signed by untrusted user: Robot
GPG Key ID: 14DECD44E7E1BB95
7 changed files with 728 additions and 312 deletions

View File

@ -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,

View File

@ -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'
]
]

View File

@ -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'
]
]

View File

@ -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

View File

@ -13,6 +13,7 @@ namespace VDM\Joomla\Abstraction;
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);
}
}

View File

@ -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);
}

View File

@ -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 <https:\/\/dev.vdm.io>\r\n * @git Joomla Component Builder <https:\/\/git.vdm.dev\/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": ""
}