Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 73 additions & 32 deletions src/Database/PicoDatabasePersistence.php
Original file line number Diff line number Diff line change
Expand Up @@ -1124,6 +1124,26 @@ private function addGeneratedValue($info, $firstCall)
return $this;
}

/**
* Determines whether a value should be generated for the given property
* during the first invocation of the generation process.
*
* A value will be generated only if:
* - This is the first call of the generation lifecycle
* - The specified property does not yet have a value (null or empty string)
* - No generated value has been assigned previously
*
* @param bool $firstCall Indicates whether this is the first call in the generation lifecycle.
* @param string $prop The property name to be evaluated.
* @return bool Returns true if a new value should be generated; false otherwise.
*/
private function shouldGenerateValueOnFirstCall($firstCall, $prop)
{
return $firstCall
&& ($this->object->get($prop) === null || $this->object->get($prop) === '')
&& !$this->generatedValue;
}

/**
* Set a generated value for a specified property based on its generation strategy.
*
Expand All @@ -1134,40 +1154,43 @@ private function addGeneratedValue($info, $firstCall)
*/
private function setGeneratedValue($prop, $strategy, $firstCall)
{
if(stripos($strategy, "UUID") !== false)
{
if($firstCall && ($this->object->get($prop) == null || $this->object->get($prop) == "") && !$this->generatedValue)
{
$generatedValue = $this->database->generateNewId();
$this->object->set($prop, $generatedValue);
$this->generatedValue = true;
// Handle IDENTITY strategy separately (DB-driven)
if (stripos($strategy, 'IDENTITY') !== false) {
if ($firstCall) {
$this->requireDbAutoincrement = true;
} elseif ($this->requireDbAutoincrement && !$this->dbAutoinrementCompleted) {
$this->object->set(
$prop,
$this->database->getDatabaseConnection()->lastInsertId()
);
$this->dbAutoinrementCompleted = true;
}

return $this;
}
else if(stripos($strategy, "TIMEBASED") !== false)
{
if($firstCall && ($this->object->get($prop) == null || $this->object->get($prop) == "") && !$this->generatedValue)
{
$generatedValue = $this->database->generateTimeBasedId();
$this->object->set($prop, $generatedValue);

// Map strategy to generator method
$generatorMap = [
'UUID' => 'generateUUID',
'TIMEBASED' => 'generateTimeBasedId',
'LEGACY_TIMEBASED' => 'generateNewId',
];

foreach ($generatorMap as $key => $method) {
if (
stripos($strategy, $key) !== false &&
$this->shouldGenerateValueOnFirstCall($firstCall, $prop)
) {
$this->object->set($prop, $this->database->{$method}());
$this->generatedValue = true;
break;
}
}
else if(stripos($strategy, "IDENTITY") !== false)
{
if($firstCall)
{
$this->requireDbAutoincrement = true;
}
else if($this->requireDbAutoincrement && !$this->dbAutoinrementCompleted)
{
$generatedValue = $this->database->getDatabaseConnection()->lastInsertId();
$this->object->set($prop, $generatedValue);
$this->dbAutoinrementCompleted = true;
}
}

return $this;
}


/**
* Insert the current object's data into the database.
*
Expand Down Expand Up @@ -1275,11 +1298,29 @@ private function fixInsertableValues($values, $info = null)
}

/**
* 1. TABLE - Indicates that the persistence provider must assign primary keys for the entity using an underlying database table to ensure uniqueness.
* 2. SEQUENCE - Indicates that the persistence provider must assign primary keys for the entity using a database sequence.
* 3. IDENTITY - Indicates that the persistence provider must assign primary keys for the entity using a database identity column.
* 4. AUTO - Indicates that the persistence provider should pick an appropriate strategy for the particular database. The AUTO generation strategy may expect a database resource to exist, or it may attempt to create one. A vendor may provide documentation on how to create such resources in the event that it does not support schema generation or cannot create the schema resource at runtime.
* 5. UUID - Indicates that the persistence provider must assign primary keys for the entity with a UUID value.
* 1. TABLE - Indicates that the persistence provider must assign primary keys for the entity
* using an underlying database table to ensure uniqueness.
*
* 2. SEQUENCE - Indicates that the persistence provider must assign primary keys for the entity
* using a database sequence.
*
* 3. IDENTITY - Indicates that the persistence provider must assign primary keys for the entity
* using a database identity column.
*
* 4. AUTO - Indicates that the persistence provider should select an appropriate strategy for
* the specific database. The AUTO generation strategy may expect a database resource to
* already exist, or it may attempt to create one. A vendor may provide documentation on how
* to create such resources if schema generation is not supported or if the resource cannot
* be created at runtime.
*
* 5. UUID - Indicates that the persistence provider must assign primary keys for the entity
* using a standard UUID value.
*
* 6. TIME_BASED - Indicates that the persistence provider must assign primary keys for the entity
* using the current epoch time in nanoseconds, combined with a 3-hex-digit random suffix.
*
* 7. LEGACY_TIMEBASED - Indicates that the persistence provider must assign primary keys for the
* entity using the legacy, non-standard UUID-based strategy from earlier versions.
*/

if($info->getAutoIncrementKeys() != null)
Expand Down Expand Up @@ -1311,7 +1352,7 @@ private function fixInsertableValues($values, $info = null)
*/
private function isRequireGenerateValue($strategy, $propertyName)
{
return stripos($strategy, "UUID") !== false
return (stripos($strategy, "UUID") !== false || stripos($strategy, "TIMEBASED") !== false || stripos($strategy, "LEGACY_TIMEBASED") !== false)
&& ($this->object->get($propertyName) == null || $this->object->get($propertyName) == "")
&& !$this->generatedValue;
}
Expand Down
21 changes: 11 additions & 10 deletions src/Database/PicoDatabaseQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -711,12 +711,10 @@ public function rollback()
*/
public function escapeSQL($query) // NOSONAR
{
// Escape carriage return and newline for all
$query = str_replace(["\r", "\n"], ["\\r", "\\n"], $query);

if (stripos($this->databaseType, PicoDatabaseType::DATABASE_TYPE_MYSQL) !== false
|| stripos($this->databaseType, PicoDatabaseType::DATABASE_TYPE_MARIADB) !== false) {
// MySQL/MariaDB: escape both backslash and single quote

// MySQL / MariaDB
return str_replace(
["\\", "'"],
["\\\\", "\\'"],
Expand All @@ -725,25 +723,28 @@ public function escapeSQL($query) // NOSONAR
}

if (stripos($this->databaseType, PicoDatabaseType::DATABASE_TYPE_PGSQL) !== false) {
// PostgreSQL: double single quotes and backslashes (E'' required at usage site)

// PostgreSQL (without E'')
return str_replace(
["\\", "'"],
["\\\\", "''"],
["'", "\\"],
["''", "\\\\"],
$query
);
}

if (stripos($this->databaseType, PicoDatabaseType::DATABASE_TYPE_SQLITE) !== false) {
// SQLite: only escape single quote

// SQLite
return str_replace("'", "''", $query);
}

if (stripos($this->databaseType, PicoDatabaseType::DATABASE_TYPE_SQLSERVER) !== false) {
// SQL Server: only escape single quote

// SQL Server
return str_replace("'", "''", $query);
}

// Default fallback: treat like MySQL
// Default fallback
return str_replace(
["\\", "'"],
["\\\\", "\\'"],
Expand Down