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: 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', 'store' => 'json',
'tab_name' => 'Updates', 'tab_name' => 'Updates',
'db' => [ 'db' => [
'type' => 'TEXT', 'type' => 'MEDIUMTEXT',
'default' => 'EMPTY', 'default' => 'EMPTY',
'null_switch' => 'NOT NULL', 'null_switch' => 'NOT NULL',
'unique_key' => false, 'unique_key' => false,
@ -10526,7 +10526,7 @@ class Table extends BaseTable implements Tableinterface
'store' => 'json', 'store' => 'json',
'tab_name' => 'Updates', 'tab_name' => 'Updates',
'db' => [ 'db' => [
'type' => 'TEXT', 'type' => 'MEDIUMTEXT',
'default' => 'EMPTY', 'default' => 'EMPTY',
'null_switch' => 'NOT NULL', 'null_switch' => 'NOT NULL',
'unique_key' => false, 'unique_key' => false,
@ -10734,7 +10734,7 @@ class Table extends BaseTable implements Tableinterface
'store' => 'json', 'store' => 'json',
'tab_name' => 'Updates', 'tab_name' => 'Updates',
'db' => [ 'db' => [
'type' => 'TEXT', 'type' => 'MEDIUMTEXT',
'default' => 'EMPTY', 'default' => 'EMPTY',
'null_switch' => 'NOT NULL', 'null_switch' => 'NOT NULL',
'unique_key' => false, 'unique_key' => false,

View File

@ -48,7 +48,7 @@ abstract class BaseTable implements Tableinterface
'tab_name' => NULL, 'tab_name' => NULL,
'db' => [ 'db' => [
'type' => 'INT(11)', 'type' => 'INT(11)',
'default' => '', 'default' => 'EMPTY',
'auto_increment' => true, 'auto_increment' => true,
'primary_key' => true, 'primary_key' => true,
'null_switch' => 'NOT NULL' 'null_switch' => 'NOT NULL'
@ -227,7 +227,7 @@ abstract class BaseTable implements Tableinterface
'tab_name' => NULL, 'tab_name' => NULL,
'db' => [ 'db' => [
'type' => 'TEXT', 'type' => 'TEXT',
'default' => '', 'default' => 'EMPTY',
'null_switch' => 'NULL' 'null_switch' => 'NULL'
] ]
] ]

View File

@ -24,7 +24,7 @@
'tab_name' => NULL, 'tab_name' => NULL,
'db' => [ 'db' => [
'type' => 'INT(11)', 'type' => 'INT(11)',
'default' => '', 'default' => 'EMPTY',
'auto_increment' => true, 'auto_increment' => true,
'primary_key' => true, 'primary_key' => true,
'null_switch' => 'NOT NULL' 'null_switch' => 'NOT NULL'
@ -203,7 +203,7 @@
'tab_name' => NULL, 'tab_name' => NULL,
'db' => [ 'db' => [
'type' => 'TEXT', 'type' => 'TEXT',
'default' => '', 'default' => 'EMPTY',
'null_switch' => 'NULL' 'null_switch' => 'NULL'
] ]
] ]

View File

@ -19,23 +19,28 @@ abstract Schema #Orange {
- array $keys - array $keys
- array $columns - array $columns
- array $success - array $success
# $currentVersion
+ __construct(Table $table) + __construct(Table $table)
+ update() : array + update() : array
+ createTable(string $table) : void
+ updateSchema(string $table) : void
# {abstract} getCode() : string # {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 # addMissingColumns(string $table, array $columns) : void
# checkColumnsDataType(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 # 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 # updateColumnDataType(string $updateString, string $table, ...) : bool
# getTableKeys() : string # getTableKeys() : string
# setKeys(array $column) : void
# setUniqueKey(array $column) : void # setUniqueKey(array $column) : void
# setKey(array $column) : void # setKey(array $column) : void
# getTable(string $table) : string # getDefaultValue(string $type, ?string $defaultValue, ...) : string
- tableExists(string $table) : bool
- getExistingColumns(string $table) : array
- getColumnDefinition(string $table, string $field) : ?string
- setKeys(array $column) : void
} }
note right of Schema::__construct note right of Schema::__construct
@ -51,25 +56,39 @@ note left of Schema::update
return: array return: array
end note end note
note right of Schema::createTable note right of Schema::getCode
Create a table with all necessary fields. Get the targeted component code
since: 3.2.1 since: 3.2.1
return: void return: string
end note 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. Update the schema of an existing table.
since: 3.2.1 since: 3.2.1
return: void return: void
end note end note
note right of Schema::getCode note left of Schema::createTable
Get the targeted component code Create a table with all necessary fields.
since: 3.2.1 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 end note
note left of Schema::addMissingColumns note left of Schema::addMissingColumns
@ -86,6 +105,21 @@ note right of Schema::checkColumnsDataType
return: void return: void
end note 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 note left of Schema::updateColumnsDataType
Update the data type of the given fields. Update the data type of the given fields.
@ -93,7 +127,38 @@ note left of Schema::updateColumnsDataType
return: void return: void
end note 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. Update the data type of the given field.
since: 3.2.1 since: 3.2.1
@ -105,13 +170,20 @@ note right of Schema::updateColumnDataType
string $field string $field
end note end note
note left of Schema::getTableKeys note right of Schema::getTableKeys
Key all needed keys for this table Key all needed keys for this table
since: 3.2.1 since: 3.2.1
return: string return: string
end note 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 note right of Schema::setUniqueKey
Function to set the unique key Function to set the unique key
@ -126,40 +198,19 @@ note left of Schema::setKey
return: void return: void
end note end note
note right of Schema::getTable note right of Schema::getDefaultValue
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.
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 since: 3.2.1
return: string return: string
end note
arguments:
note left of Schema::tableExists string $type
Check if a table exists in the database. ?string $defaultValue
bool $pure = false
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
end note end note
@enduml @enduml

View File

@ -12,7 +12,8 @@
namespace VDM\Joomla\Abstraction; 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\Tableinterface as Table;
use VDM\Joomla\Interfaces\SchemaInterface; use VDM\Joomla\Interfaces\SchemaInterface;
@ -87,6 +88,14 @@ abstract class Schema implements SchemaInterface
*/ */
private array $success; private array $success;
/**
* Current Joomla Version We are IN
*
* @var int
* @since 3.2.1
**/
protected $currentVersion;
/** /**
* Constructor. * Constructor.
* *
@ -108,8 +117,11 @@ abstract class Schema implements SchemaInterface
// set the component table // set the component table
$this->prefix = $this->db->getPrefix() . $this->getCode(); $this->prefix = $this->db->getPrefix() . $this->getCode();
// set the current version
$this->currentVersion = Version::MAJOR_VERSION;
} catch (\Exception $e) { } 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) { } 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) if (count($this->success) == 1)
@ -156,6 +168,63 @@ abstract class Schema implements SchemaInterface
return $this->success; 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. * Create a table with all necessary fields.
* *
@ -189,56 +258,27 @@ abstract class Schema implements SchemaInterface
$this->db->setQuery($createTableSql); $this->db->setQuery($createTableSql);
$this->db->execute(); $this->db->execute();
} catch (\Exception $e) { } 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."; $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 * @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 { $this->columns = $this->db->getTableColumns($this->getTable($table), false);
$existingColumns = $this->getExistingColumns($table);
$expectedColumns = $this->table->fields($table, true);
$missingColumns = array_diff($expectedColumns, $existingColumns); return array_keys($this->columns);
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.";
}
} }
/**
* Get the targeted component code
*
* @return string
* @since 3.2.1
*/
abstract protected function getCode(): string;
/** /**
* Add missing columns to a table. * Add missing columns to a table.
* *
@ -270,7 +310,7 @@ abstract class Schema implements SchemaInterface
} catch (\Exception $e) { } catch (\Exception $e) {
$column_s = (count($columns) == 1) ? 'column' : 'columns'; $column_s = (count($columns) == 1) ? 'column' : 'columns';
$columns = implode(', ', $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; $current = $this->columns[$column] ?? null;
if ($current === null || ($expected = $this->table->get($table, $column, 'db')) === 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; continue;
} }
// check if the data type and size match // check if the data type and size match
if (strcasecmp($current->Type, $expected['type']) != 0) if ($this->isDataTypeChangeSignificant($current->Type, $expected['type']))
{ {
$requireUpdate[$column] = [ $requireUpdate[$column] = [
'column' => $column, 'column' => $column,
'current' => $current->Type, 'current' => $current->Type,
'expected' => $expected['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. * Update the data type of the given fields.
* *
@ -336,13 +461,120 @@ abstract class Schema implements SchemaInterface
if ($this->updateColumnDataType($alterQuery, $table, $column)) if ($this->updateColumnDataType($alterQuery, $table, $column))
{ {
$current = (string) $types['current'] ?? 'error'; $current = $types['current'] ?? 'error';
$expected = (string) $types['expected'] ?? 'error'; $expected = $types['expected'] ?? 'error';
$this->success[] = "Success: updated ($column) column datatype $current to $expected in $table table."; $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. * Update the data type of the given field.
* *
@ -360,7 +592,7 @@ abstract class Schema implements SchemaInterface
$this->db->setQuery($updateString); $this->db->setQuery($updateString);
return $this->db->execute(); return $this->db->execute();
} catch (\Exception $e) { } 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); 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 * 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 * @param string $type The type of the database field (e.g., 'DATETIME').
* @since 3.2.1 * @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.
protected function getTable(string $table): string
{
return $this->prefix . '_' . $table;
}
/**
* Check if a table exists in the database.
* *
* @param string $table The name of the table to check. * @return string The SQL fragment to set the default value for a field.
*
* @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.
* @since 3.2.1 * @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 { if ($defaultValue === null || strtoupper($defaultValue) === 'EMPTY')
// Retrieve the database schema details for the specified table and field {
if (($db = $this->table->get($table, $field, 'db')) === null) return '';
{
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);
} }
}
/** // Set default for DATETIME fields in Joomla versions above 3
* Function to set the view keys if (strtoupper($type) === 'DATETIME' && $this->currentVersion != 3)
* {
* @param string $column The field column database array values return $pure ? "CURRENT_TIMESTAMP" : " DEFAULT CURRENT_TIMESTAMP";
* }
* @return void
* @since 3.2.1 // Apply and quote the default value
*/ return $pure ? $defaultValue : " DEFAULT " . $this->db->quote($defaultValue);
private function setKeys(array $column): void
{
$this->setUniqueKey($column);
$this->setKey($column);
} }
} }

View File

@ -61,6 +61,14 @@
*/ */
private array $success; private array $success;
/**
* Current Joomla Version We are IN
*
* @var int
* @since 3.2.1
**/
protected $currentVersion;
/** /**
* Constructor. * Constructor.
* *
@ -82,8 +90,11 @@
// set the component table // set the component table
$this->prefix = $this->db->getPrefix() . $this->getCode(); $this->prefix = $this->db->getPrefix() . $this->getCode();
// set the current version
$this->currentVersion = Version::MAJOR_VERSION;
} catch (\Exception $e) { } 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) { } 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) if (count($this->success) == 1)
@ -130,6 +141,63 @@
return $this->success; 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. * Create a table with all necessary fields.
* *
@ -163,56 +231,27 @@
$this->db->setQuery($createTableSql); $this->db->setQuery($createTableSql);
$this->db->execute(); $this->db->execute();
} catch (\Exception $e) { } 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."; $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 * @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 { $this->columns = $this->db->getTableColumns($this->getTable($table), false);
$existingColumns = $this->getExistingColumns($table);
$expectedColumns = $this->table->fields($table, true);
$missingColumns = array_diff($expectedColumns, $existingColumns); return array_keys($this->columns);
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.";
}
} }
/**
* Get the targeted component code
*
* @return string
* @since 3.2.1
*/
abstract protected function getCode(): string;
/** /**
* Add missing columns to a table. * Add missing columns to a table.
* *
@ -244,7 +283,7 @@
} catch (\Exception $e) { } catch (\Exception $e) {
$column_s = (count($columns) == 1) ? 'column' : 'columns'; $column_s = (count($columns) == 1) ? 'column' : 'columns';
$columns = implode(', ', $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; $current = $this->columns[$column] ?? null;
if ($current === null || ($expected = $this->table->get($table, $column, 'db')) === 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; continue;
} }
// check if the data type and size match // check if the data type and size match
if (strcasecmp($current->Type, $expected['type']) != 0) if ($this->isDataTypeChangeSignificant($current->Type, $expected['type']))
{ {
$requireUpdate[$column] = [ $requireUpdate[$column] = [
'column' => $column, 'column' => $column,
'current' => $current->Type, 'current' => $current->Type,
'expected' => $expected['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. * Update the data type of the given fields.
* *
@ -310,13 +434,120 @@
if ($this->updateColumnDataType($alterQuery, $table, $column)) if ($this->updateColumnDataType($alterQuery, $table, $column))
{ {
$current = (string) $types['current'] ?? 'error'; $current = $types['current'] ?? 'error';
$expected = (string) $types['expected'] ?? 'error'; $expected = $types['expected'] ?? 'error';
$this->success[] = "Success: updated ($column) column datatype $current to $expected in $table table."; $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. * Update the data type of the given field.
* *
@ -334,7 +565,7 @@
$this->db->setQuery($updateString); $this->db->setQuery($updateString);
return $this->db->execute(); return $this->db->execute();
} catch (\Exception $e) { } 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); 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 * 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 * @param string $type The type of the database field (e.g., 'DATETIME').
* @since 3.2.1 * @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.
protected function getTable(string $table): string
{
return $this->prefix . '_' . $table;
}
/**
* Check if a table exists in the database.
* *
* @param string $table The name of the table to check. * @return string The SQL fragment to set the default value for a field.
*
* @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.
* @since 3.2.1 * @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 { if ($defaultValue === null || strtoupper($defaultValue) === 'EMPTY')
// Retrieve the database schema details for the specified table and field {
if (($db = $this->table->get($table, $field, 'db')) === null) return '';
{
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);
} }
}
/** // Set default for DATETIME fields in Joomla versions above 3
* Function to set the view keys if (strtoupper($type) === 'DATETIME' && $this->currentVersion != 3)
* {
* @param string $column The field column database array values return $pure ? "CURRENT_TIMESTAMP" : " DEFAULT CURRENT_TIMESTAMP";
* }
* @return void
* @since 3.2.1 // Apply and quote the default value
*/ return $pure ? $defaultValue : " DEFAULT " . $this->db->quote($defaultValue);
private function setKeys(array $column): void
{
$this->setUniqueKey($column);
$this->setKey($column);
} }

View File

@ -20,6 +20,6 @@
"namespace": "[[[NamespacePrefix]]]\\Joomla\\Abstraction.Schema", "namespace": "[[[NamespacePrefix]]]\\Joomla\\Abstraction.Schema",
"description": "Schema Checking\r\n\r\n@since 3.2.1", "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", "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": "" "composer": ""
} }