Skip to content

Commit c099447

Browse files
committed
Add Upsert/UpsertBatch to builder
1 parent 44524ee commit c099447

File tree

16 files changed

+1285
-31
lines changed

16 files changed

+1285
-31
lines changed

system/Database/BaseBuilder.php

Lines changed: 193 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,16 @@ class BaseBuilder
153153
*/
154154
protected $QBIgnore = false;
155155

156+
/**
157+
* QB Options data
158+
* Holds additional data used to render SQL
159+
*
160+
* @phpstan-var array{updateFields?: array, constraints?: array, tableIdentity?: string}
161+
*
162+
* @var array
163+
*/
164+
protected $QBOptions;
165+
156166
/**
157167
* A reference to the database connection.
158168
*
@@ -1720,13 +1730,13 @@ public function getWhere($where = null, ?int $limit = null, ?int $offset = 0, bo
17201730
}
17211731

17221732
/**
1723-
* Compiles batch insert strings and runs the queries
1733+
* Compiles batch insert/upsert strings and runs the queries
17241734
*
17251735
* @throws DatabaseException
17261736
*
17271737
* @return false|int|string[] Number of rows inserted or FALSE on failure, SQL array when testMode
17281738
*/
1729-
public function insertBatch(?array $set = null, ?bool $escape = null, int $batchSize = 100)
1739+
protected function batchExecute(string $renderMethod, ?array $set = null, ?bool $escape = null, int $batchSize = 100)
17301740
{
17311741
if ($set === null) {
17321742
if (empty($this->QBSet)) {
@@ -1738,15 +1748,15 @@ public function insertBatch(?array $set = null, ?bool $escape = null, int $batch
17381748
}
17391749
} elseif (empty($set)) {
17401750
if ($this->db->DBDebug) {
1741-
throw new DatabaseException('insertBatch() called with no data');
1751+
throw new DatabaseException('insertBatch()/upsertBatch called with no data');
17421752
}
17431753

17441754
return false; // @codeCoverageIgnore
17451755
}
17461756

17471757
$hasQBSet = $set === null;
17481758

1749-
$table = $this->QBFrom[0];
1759+
$table = $this->db->protectIdentifiers($this->QBFrom[0], true, null, false);
17501760

17511761
$affectedRows = 0;
17521762
$savedSQL = [];
@@ -1759,10 +1769,14 @@ public function insertBatch(?array $set = null, ?bool $escape = null, int $batch
17591769
if ($hasQBSet) {
17601770
$QBSet = array_slice($this->QBSet, $i, $batchSize);
17611771
} else {
1762-
$this->setInsertBatch(array_slice($set, $i, $batchSize), '', $escape);
1772+
$this->setBatch(array_slice($set, $i, $batchSize), $escape);
17631773
$QBSet = $this->QBSet;
17641774
}
1765-
$sql = $this->_insertBatch($this->db->protectIdentifiers($table, true, null, false), $this->QBKeys, $QBSet);
1775+
$sql = $this->{$renderMethod}($table, $this->QBKeys, $QBSet);
1776+
1777+
if ($sql === '') {
1778+
return false; // @codeCoverageIgnore
1779+
}
17661780

17671781
if ($this->testMode) {
17681782
$savedSQL[] = $sql;
@@ -1785,37 +1799,25 @@ public function insertBatch(?array $set = null, ?bool $escape = null, int $batch
17851799
}
17861800

17871801
/**
1788-
* Generates a platform-specific insert string from the supplied data.
1789-
*/
1790-
protected function _insertBatch(string $table, array $keys, array $values): string
1791-
{
1792-
return 'INSERT ' . $this->compileIgnore('insert') . 'INTO ' . $table . ' (' . implode(', ', $keys) . ') VALUES ' . implode(', ', $values);
1793-
}
1794-
1795-
/**
1796-
* Allows key/value pairs to be set for batch inserts
1802+
* Allows key/value pairs to be set for batch inserts/upserts
17971803
*
1798-
* @param mixed $key
1804+
* @param array|object $set
17991805
*
18001806
* @return $this|null
18011807
*/
1802-
public function setInsertBatch($key, string $value = '', ?bool $escape = null)
1808+
public function setBatch($set, ?bool $escape = null)
18031809
{
1804-
$key = $this->batchObjectToArray($key);
1805-
1806-
if (! is_array($key)) {
1807-
$key = [$key => $value];
1808-
}
1810+
$set = $this->batchObjectToArray($set);
18091811

18101812
$escape = is_bool($escape) ? $escape : $this->db->protectIdentifiers;
18111813

1812-
$keys = array_keys($this->objectToArray(current($key)));
1814+
$keys = array_keys($this->objectToArray(current($set)));
18131815
sort($keys);
18141816

1815-
foreach ($key as $row) {
1817+
foreach ($set as $row) {
18161818
$row = $this->objectToArray($row);
18171819
if (array_diff($keys, array_keys($row)) !== [] || array_diff(array_keys($row), $keys) !== []) {
1818-
// batch function above returns an error on an empty array
1820+
// batchExecute() function returns an error on an empty array
18191821
$this->QBSet[] = [];
18201822

18211823
return null;
@@ -1831,7 +1833,7 @@ public function setInsertBatch($key, string $value = '', ?bool $escape = null)
18311833

18321834
$row = $clean;
18331835

1834-
$this->QBSet[] = '(' . implode(',', $row) . ')';
1836+
$this->QBSet[] = $row;
18351837
}
18361838

18371839
foreach ($keys as $k) {
@@ -1841,6 +1843,170 @@ public function setInsertBatch($key, string $value = '', ?bool $escape = null)
18411843
return $this;
18421844
}
18431845

1846+
/**
1847+
* Compiles an upsert query and returns the sql
1848+
*
1849+
* @throws DatabaseException
1850+
*
1851+
* @return string
1852+
*/
1853+
public function getCompiledUpsert()
1854+
{
1855+
$currentTestMode = $this->testMode;
1856+
1857+
$this->testMode = true;
1858+
1859+
$sql = implode(";\n", $this->upsert());
1860+
1861+
$this->testMode = $currentTestMode;
1862+
1863+
return $this->compileFinalQuery($sql);
1864+
}
1865+
1866+
/**
1867+
* Converts call to batchUpsert
1868+
*
1869+
* @param array|object|null $set
1870+
*
1871+
* @throws DatabaseException
1872+
*
1873+
* @return false|int|string[] Number of affected rows or FALSE on failure, SQL array when testMode
1874+
*/
1875+
public function upsert($set = null, ?bool $escape = null)
1876+
{
1877+
if ($set === null) {
1878+
$set = empty($this->binds) ? null : [array_map(static fn ($columnName) => $columnName[0], $this->binds)];
1879+
1880+
$this->binds = [];
1881+
1882+
$this->resetRun([
1883+
'QBSet' => [],
1884+
'QBKeys' => [],
1885+
]);
1886+
} else {
1887+
$set = [$set];
1888+
}
1889+
1890+
return $this->batchExecute('_upsertBatch', $set, $escape, 1);
1891+
}
1892+
1893+
/**
1894+
* Compiles batch upsert strings and runs the queries
1895+
*
1896+
* @throws DatabaseException
1897+
*
1898+
* @return false|int|string[] Number of affected rows or FALSE on failure, SQL array when testMode
1899+
*/
1900+
public function upsertBatch(?array $set = null, ?bool $escape = null, int $batchSize = 100)
1901+
{
1902+
return $this->batchExecute('_upsertBatch', $set, $escape, $batchSize);
1903+
}
1904+
1905+
/**
1906+
* Generates a platform-specific upsertBatch string from the supplied data
1907+
*/
1908+
protected function _upsertBatch(string $table, array $keys, array $values): string
1909+
{
1910+
$fieldNames = array_map(static fn ($columnName) => trim($columnName, '`'), $keys);
1911+
1912+
$updateFields = $this->QBOptions['updateFields'] ?? $fieldNames;
1913+
1914+
$sql = 'INSERT INTO ' . $table . ' (' . implode(', ', $keys) . ')' . "\n";
1915+
1916+
$sql .= 'VALUES ' . implode(', ', $this->getValues($values)) . "\n";
1917+
1918+
$sql .= 'ON DUPLICATE KEY UPDATE' . "\n";
1919+
1920+
return $sql . implode(
1921+
",\n",
1922+
array_map(
1923+
static fn ($columnName) => '`' . $columnName . '` = VALUES(`' . $columnName . '`)',
1924+
$updateFields
1925+
)
1926+
);
1927+
}
1928+
1929+
/**
1930+
* Sets constraints for upsert
1931+
*
1932+
* @param string|string[] $keys
1933+
*
1934+
* @return $this
1935+
*/
1936+
public function onConstraint($keys)
1937+
{
1938+
if (! is_array($keys)) {
1939+
$keys = explode(',', $keys);
1940+
}
1941+
1942+
$this->QBOptions['constraints'] = array_map(static fn ($key) => trim($key), $keys);
1943+
1944+
return $this;
1945+
}
1946+
1947+
/**
1948+
* Sets update fields for upsert
1949+
*
1950+
* @param string|string[] $keys
1951+
*
1952+
* @return $this
1953+
*/
1954+
public function updateFields($keys)
1955+
{
1956+
if (! is_array($keys)) {
1957+
$keys = explode(',', $keys);
1958+
}
1959+
1960+
$this->QBOptions['updateFields'] = array_map(static fn ($key) => trim($key), $keys);
1961+
1962+
return $this;
1963+
}
1964+
1965+
/**
1966+
* Converts value array of array to array of strings
1967+
*/
1968+
protected function getValues(array $values): array
1969+
{
1970+
return array_map(static fn ($index) => '(' . implode(',', $index) . ')', $values);
1971+
}
1972+
1973+
/**
1974+
* Compiles batch insert strings and runs the queries
1975+
*
1976+
* @throws DatabaseException
1977+
*
1978+
* @return false|int|string[] Number of rows inserted or FALSE on failure, SQL array when testMode
1979+
*/
1980+
public function insertBatch(?array $set = null, ?bool $escape = null, int $batchSize = 100)
1981+
{
1982+
return $this->batchExecute('_insertBatch', $set, $escape, $batchSize);
1983+
}
1984+
1985+
/**
1986+
* Generates a platform-specific insert string from the supplied data.
1987+
*/
1988+
protected function _insertBatch(string $table, array $keys, array $values): string
1989+
{
1990+
return 'INSERT ' . $this->compileIgnore('insert') . 'INTO ' . $table
1991+
. ' (' . implode(', ', $keys) . ') VALUES ' . implode(', ', $this->getValues($values));
1992+
}
1993+
1994+
/**
1995+
* Alias for setBatch()
1996+
*
1997+
* @param mixed $key
1998+
*
1999+
* @return $this|null
2000+
*/
2001+
public function setInsertBatch($key, string $value = '', ?bool $escape = null)
2002+
{
2003+
if (! is_array($key)) {
2004+
$key = [[$key => $value]];
2005+
}
2006+
2007+
return $this->setBatch($key, $escape);
2008+
}
2009+
18442010
/**
18452011
* Compiles an insert query and returns the sql
18462012
*
@@ -2828,6 +2994,7 @@ protected function resetWrite()
28282994
'QBKeys' => [],
28292995
'QBLimit' => false,
28302996
'QBIgnore' => false,
2997+
'QBOptions' => [],
28312998
]);
28322999
}
28333000

0 commit comments

Comments
 (0)