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
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -1526,3 +1526,29 @@ $secretConfig->retrieve('database', 'credentials', 'username');


With this fix, MagicObject now handles optional or missing fields more reliably during insert operations.


# MagicObject Version 3.22.2

## Bug Fixes

Several bug fixes and codesmell

## Update Documentation

Update docblock


# MagicObject Version 3.22.2

### Added

- `insertAll()` and `deleteAll()` for bulk database operation
- Added parameter to PicoSpecification for operation:
- GREATER_THAN
- GREATER_THAN_OR_EQUALS
- LESS_THAN
- LESS_THAN_OR_EQUALS
- NOT_EQUALS
- LIKE
- IN
2,352 changes: 1,535 additions & 817 deletions docs/doc.html

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/DataTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use MagicObject\Util\PicoStringUtil;
use MagicObject\Util\PicoTableUtil;
use ReflectionClass;
use ReflectionProperty;
use stdClass;

/**
Expand Down
12 changes: 8 additions & 4 deletions src/Database/PicoDatabase.php
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,6 @@ public function __construct($databaseCredentials, $callbackExecuteQuery = null,
* Establishes a connection to the specified database type. Optionally selects a database if the
* connection is to an RDMS and the flag is set.
*
* @param int $timeout Connection timeout in seconds.
* @param bool $withDatabase Flag to select the database when connected (default is true).
* @return bool true if the connection is successful, false if it fails.
*/
Expand Down Expand Up @@ -441,7 +440,12 @@ private function connectRDMS($withDatabase = true, $timeout = 0)
}

// Get charset from the database credentials
$charset = addslashes($this->databaseCredentials->getCharset());
$charset = $this->databaseCredentials->getCharset();
if(isset($charset)) {
$charset = addslashes($charset);
} else {
$charset = 'utf8';
}

// Handle PostgreSQL-specific connection settings
if ($this->getDatabaseType() == PicoDatabaseType::DATABASE_TYPE_PGSQL) {
Expand Down Expand Up @@ -811,7 +815,7 @@ public function setTimeZoneOffset($timeZoneOffset)
* The method generates the appropriate SQL command based on the type of the database (e.g., PostgreSQL, MySQL, etc.)
* and executes it to apply the time zone setting.
*
* @param string $timeZoneOffset The time zone offset to set for the session. It can either be a valid UTC offset (e.g., '+00:00')
* @param string $timezone The time zone offset to set for the session. It can either be a valid UTC offset (e.g., '+00:00')
* or a named time zone (e.g., 'Europe/London').
* @return self Returns the current instance for method chaining.
*/
Expand Down Expand Up @@ -1404,7 +1408,7 @@ public function getDatabaseTimeZoneOffset()
*/
public function __toString()
{
$val = new stdClass;
$val = new stdClass();
$val->databaseType = $this->databaseType;
$val->autocommit = $this->autocommit;
$val->connected = $this->connected;
Expand Down
304 changes: 304 additions & 0 deletions src/Database/PicoDatabasePersistence.php
Original file line number Diff line number Diff line change
Expand Up @@ -1268,6 +1268,288 @@ private function _insertQuery($info = null, $queryBuilder = null)
->values($this->createStatementValues($fixValues));
}

/**
* Insert multiple objects into the database.
*
* When $returnIds is false, the method uses multi-row INSERT statements
* and never calls insert() for individual records. The number of rows
* in each SQL statement is limited by $batchSize.
*
* When $returnIds is true, the existing insert() mechanism is used so
* that generated database IDs can be retrieved.
*
* @param array $objects Array of entity objects, associative arrays, or stdClass objects.
* @param bool $includeNull Whether to include NULL values in the insert operation.
* @param bool $returnIds Whether to return generated primary key values.
* @param int $batchSize Maximum number of rows per INSERT statement.
* @return int|array Number of inserted records, or generated IDs.
* @throws EntityException If the entity or insert operation is invalid.
*/
public function insertAll(
$objects,
$includeNull = false,
$returnIds = false,
$batchSize = 1000
)
{
if(!is_array($objects))
{
throw new InvalidParameterException(
"Objects must be an array"
);
}

if(empty($objects))
{
return $returnIds ? array() : 0;
}

$batchSize = (int) $batchSize;

if($batchSize < 1)
{
throw new InvalidParameterException(
"Batch size must be greater than zero"
);
}

$info = $this->getTableInfo();

/*
* Normalize all input data into entity objects.
*/
$entities = array();

foreach($objects as $object)
{
if($object instanceof $this->className)
{
$entity = $object;
}
else
{
$entity = new $this->className(
$object,
$this->database
);
}

if(!($entity instanceof MagicObject))
{
throw new InvalidParameterException(
"Invalid entity object: " . get_class($entity)
);
}

$entity->currentDatabase($this->database);

$entities[] = $entity;
}

/*
* ID retrieval is intentionally not part of the fast bulk path.
*
* When requested, use the existing insert() mechanism because it
* already handles database-generated primary keys.
*/
if($returnIds)
{
$ids = array();

foreach($entities as $entity)
{
$persist = new self(
$this->database,
$entity
);

$persist->insert($includeNull);

$primaryKeys = $info->getPrimaryKeys();

if(count($primaryKeys) == 1)
{
$propertyName = key($primaryKeys);

$ids[] = $entity->get($propertyName);
}
else
{
$primaryKeyValues = array();

foreach($primaryKeys as $propertyName=>$primaryKey)
{
$primaryKeyValues[$propertyName] =
$entity->get($propertyName);
}

$ids[] = $primaryKeyValues;
}
}

return $ids;
}

/*
* Fast bulk-insert path.
*
* Rows with the same column structure are grouped together.
* This allows includeNull=false to preserve database defaults.
*/
$groups = array();

foreach($entities as $entity)
{
$persist = new self(
$this->database,
$entity
);

$persist->flagIncludeNull = $includeNull;

/*
* Generate UUID/TIMEBASED/LEGACY_TIMEBASED values before
* collecting the values.
*
* IDENTITY values remain database-generated.
*/
$persist->addGeneratedValue(
$info,
true
);

$queryBuilder = new PicoDatabaseQueryBuilder(
$this->database
);

$values = $persist->getValues(
$info,
$queryBuilder
);

$values = $persist->fixInsertableValues(
$values,
$info
);

if(empty($values))
{
throw new NoInsertableColumnException(
"No insertable column"
);
}

/*
* The keys represent the actual columns that will be inserted.
*
* Two rows can only be placed in the same INSERT statement when
* they have exactly the same column structure.
*/
$groupKey = implode(
"\x1F",
array_keys($values)
);

if(!isset($groups[$groupKey]))
{
$groups[$groupKey] = array();
}

$groups[$groupKey][] = $values;
}

/*
* Execute each group in batches.
*/
$inserted = 0;

foreach($groups as $rows)
{
$totalRows = count($rows);

for(
$offset = 0;
$offset < $totalRows;
$offset += $batchSize
)
{
$batch = array_slice(
$rows,
$offset,
$batchSize
);

if(empty($batch))
{
continue;
}

$queryBuilder = new PicoDatabaseQueryBuilder(
$this->database
);

$queryBuilder = $this->createBulkInsertQuery(
$queryBuilder,
$info,
$batch
);

/*
* Execute exactly one multi-row INSERT for this batch.
*/
$this->database->executeInsert(
$queryBuilder
);

$inserted += count($batch);
}
}

return $inserted;
}

/**
* Create a bulk INSERT query.
*
* The values are expected to have already been escaped by the
* persistence layer.
*
* @param PicoDatabaseQueryBuilder $queryBuilder Query builder.
* @param PicoTableInfo $info Table information.
* @param array $rows Rows of associative column/value pairs.
* @return PicoDatabaseQueryBuilder The generated query.
*/
private function createBulkInsertQuery(
$queryBuilder,
$info,
$rows
)
{
if(empty($rows))
{
throw new NoInsertableColumnException(
"No insertable column"
);
}

$firstRow = reset($rows);

$queryBuilder
->newQuery()
->insert()
->into($info->getTableName())
->fields($this->createStatementFields($firstRow));

foreach($rows as $values)
{
$queryBuilder->values(
$this->createStatementValues($values)
);
}

return $queryBuilder;
}

/**
* Filter the values to only include those that are insertable based on table info.
*
Expand Down Expand Up @@ -4131,6 +4413,28 @@ private function _deleteQuery($info = null, $queryBuilder = null, $where = null)
;
}

/**
* Delete all record by specification
*
* @param PicoSpecification $specification Specification
* @return PDOStatement
*/
public function deleteAll($specification)
{
if($specification === null)
{
throw new InvalidParameterException(
"Specification is required"
);
}

$persistence = $this->whereWithSpecification(
$specification
);

return $persistence->delete();
}

/**
* Retrieves a MagicObject with a WHERE specification.
*
Expand Down
Loading
Loading