init
This commit is contained in:
@@ -0,0 +1,506 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Concerns;
|
||||
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\MultipleRecordsFoundException;
|
||||
use Illuminate\Database\Query\Expression;
|
||||
use Illuminate\Database\RecordsNotFoundException;
|
||||
use Illuminate\Pagination\Cursor;
|
||||
use Illuminate\Pagination\CursorPaginator;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Pagination\Paginator;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\LazyCollection;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Traits\Conditionable;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
trait BuildsQueries
|
||||
{
|
||||
use Conditionable;
|
||||
|
||||
/**
|
||||
* Chunk the results of the query.
|
||||
*
|
||||
* @param int $count
|
||||
* @param callable $callback
|
||||
* @return bool
|
||||
*/
|
||||
public function chunk($count, callable $callback)
|
||||
{
|
||||
$this->enforceOrderBy();
|
||||
|
||||
$page = 1;
|
||||
|
||||
do {
|
||||
// We'll execute the query for the given page and get the results. If there are
|
||||
// no results we can just break and return from here. When there are results
|
||||
// we will call the callback with the current chunk of these results here.
|
||||
$results = $this->forPage($page, $count)->get();
|
||||
|
||||
$countResults = $results->count();
|
||||
|
||||
if ($countResults == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
// On each chunk result set, we will pass them to the callback and then let the
|
||||
// developer take care of everything within the callback, which allows us to
|
||||
// keep the memory low for spinning through large result sets for working.
|
||||
if ($callback($results, $page) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
unset($results);
|
||||
|
||||
$page++;
|
||||
} while ($countResults == $count);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a map over each item while chunking.
|
||||
*
|
||||
* @param callable $callback
|
||||
* @param int $count
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
public function chunkMap(callable $callback, $count = 1000)
|
||||
{
|
||||
$collection = Collection::make();
|
||||
|
||||
$this->chunk($count, function ($items) use ($collection, $callback) {
|
||||
$items->each(function ($item) use ($collection, $callback) {
|
||||
$collection->push($callback($item));
|
||||
});
|
||||
});
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a callback over each item while chunking.
|
||||
*
|
||||
* @param callable $callback
|
||||
* @param int $count
|
||||
* @return bool
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function each(callable $callback, $count = 1000)
|
||||
{
|
||||
return $this->chunk($count, function ($results) use ($callback) {
|
||||
foreach ($results as $key => $value) {
|
||||
if ($callback($value, $key) === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Chunk the results of a query by comparing IDs.
|
||||
*
|
||||
* @param int $count
|
||||
* @param callable $callback
|
||||
* @param string|null $column
|
||||
* @param string|null $alias
|
||||
* @return bool
|
||||
*/
|
||||
public function chunkById($count, callable $callback, $column = null, $alias = null)
|
||||
{
|
||||
$column ??= $this->defaultKeyName();
|
||||
|
||||
$alias ??= $column;
|
||||
|
||||
$lastId = null;
|
||||
|
||||
$page = 1;
|
||||
|
||||
do {
|
||||
$clone = clone $this;
|
||||
|
||||
// We'll execute the query for the given page and get the results. If there are
|
||||
// no results we can just break and return from here. When there are results
|
||||
// we will call the callback with the current chunk of these results here.
|
||||
$results = $clone->forPageAfterId($count, $lastId, $column)->get();
|
||||
|
||||
$countResults = $results->count();
|
||||
|
||||
if ($countResults == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
// On each chunk result set, we will pass them to the callback and then let the
|
||||
// developer take care of everything within the callback, which allows us to
|
||||
// keep the memory low for spinning through large result sets for working.
|
||||
if ($callback($results, $page) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$lastId = data_get($results->last(), $alias);
|
||||
|
||||
if ($lastId === null) {
|
||||
throw new RuntimeException("The chunkById operation was aborted because the [{$alias}] column is not present in the query result.");
|
||||
}
|
||||
|
||||
unset($results);
|
||||
|
||||
$page++;
|
||||
} while ($countResults == $count);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a callback over each item while chunking by ID.
|
||||
*
|
||||
* @param callable $callback
|
||||
* @param int $count
|
||||
* @param string|null $column
|
||||
* @param string|null $alias
|
||||
* @return bool
|
||||
*/
|
||||
public function eachById(callable $callback, $count = 1000, $column = null, $alias = null)
|
||||
{
|
||||
return $this->chunkById($count, function ($results, $page) use ($callback, $count) {
|
||||
foreach ($results as $key => $value) {
|
||||
if ($callback($value, (($page - 1) * $count) + $key) === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}, $column, $alias);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query lazily, by chunks of the given size.
|
||||
*
|
||||
* @param int $chunkSize
|
||||
* @return \Illuminate\Support\LazyCollection
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function lazy($chunkSize = 1000)
|
||||
{
|
||||
if ($chunkSize < 1) {
|
||||
throw new InvalidArgumentException('The chunk size should be at least 1');
|
||||
}
|
||||
|
||||
$this->enforceOrderBy();
|
||||
|
||||
return LazyCollection::make(function () use ($chunkSize) {
|
||||
$page = 1;
|
||||
|
||||
while (true) {
|
||||
$results = $this->forPage($page++, $chunkSize)->get();
|
||||
|
||||
foreach ($results as $result) {
|
||||
yield $result;
|
||||
}
|
||||
|
||||
if ($results->count() < $chunkSize) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Query lazily, by chunking the results of a query by comparing IDs.
|
||||
*
|
||||
* @param int $chunkSize
|
||||
* @param string|null $column
|
||||
* @param string|null $alias
|
||||
* @return \Illuminate\Support\LazyCollection
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function lazyById($chunkSize = 1000, $column = null, $alias = null)
|
||||
{
|
||||
return $this->orderedLazyById($chunkSize, $column, $alias);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query lazily, by chunking the results of a query by comparing IDs in descending order.
|
||||
*
|
||||
* @param int $chunkSize
|
||||
* @param string|null $column
|
||||
* @param string|null $alias
|
||||
* @return \Illuminate\Support\LazyCollection
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function lazyByIdDesc($chunkSize = 1000, $column = null, $alias = null)
|
||||
{
|
||||
return $this->orderedLazyById($chunkSize, $column, $alias, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query lazily, by chunking the results of a query by comparing IDs in a given order.
|
||||
*
|
||||
* @param int $chunkSize
|
||||
* @param string|null $column
|
||||
* @param string|null $alias
|
||||
* @param bool $descending
|
||||
* @return \Illuminate\Support\LazyCollection
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
protected function orderedLazyById($chunkSize = 1000, $column = null, $alias = null, $descending = false)
|
||||
{
|
||||
if ($chunkSize < 1) {
|
||||
throw new InvalidArgumentException('The chunk size should be at least 1');
|
||||
}
|
||||
|
||||
$column ??= $this->defaultKeyName();
|
||||
|
||||
$alias ??= $column;
|
||||
|
||||
return LazyCollection::make(function () use ($chunkSize, $column, $alias, $descending) {
|
||||
$lastId = null;
|
||||
|
||||
while (true) {
|
||||
$clone = clone $this;
|
||||
|
||||
if ($descending) {
|
||||
$results = $clone->forPageBeforeId($chunkSize, $lastId, $column)->get();
|
||||
} else {
|
||||
$results = $clone->forPageAfterId($chunkSize, $lastId, $column)->get();
|
||||
}
|
||||
|
||||
foreach ($results as $result) {
|
||||
yield $result;
|
||||
}
|
||||
|
||||
if ($results->count() < $chunkSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
$lastId = $results->last()->{$alias};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the query and get the first result.
|
||||
*
|
||||
* @param array|string $columns
|
||||
* @return \Illuminate\Database\Eloquent\Model|object|static|null
|
||||
*/
|
||||
public function first($columns = ['*'])
|
||||
{
|
||||
return $this->take(1)->get($columns)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the query and get the first result if it's the sole matching record.
|
||||
*
|
||||
* @param array|string $columns
|
||||
* @return \Illuminate\Database\Eloquent\Model|object|static|null
|
||||
*
|
||||
* @throws \Illuminate\Database\RecordsNotFoundException
|
||||
* @throws \Illuminate\Database\MultipleRecordsFoundException
|
||||
*/
|
||||
public function sole($columns = ['*'])
|
||||
{
|
||||
$result = $this->take(2)->get($columns);
|
||||
|
||||
$count = $result->count();
|
||||
|
||||
if ($count === 0) {
|
||||
throw new RecordsNotFoundException;
|
||||
}
|
||||
|
||||
if ($count > 1) {
|
||||
throw new MultipleRecordsFoundException($count);
|
||||
}
|
||||
|
||||
return $result->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Paginate the given query using a cursor paginator.
|
||||
*
|
||||
* @param int $perPage
|
||||
* @param array|string $columns
|
||||
* @param string $cursorName
|
||||
* @param \Illuminate\Pagination\Cursor|string|null $cursor
|
||||
* @return \Illuminate\Contracts\Pagination\CursorPaginator
|
||||
*/
|
||||
protected function paginateUsingCursor($perPage, $columns = ['*'], $cursorName = 'cursor', $cursor = null)
|
||||
{
|
||||
if (! $cursor instanceof Cursor) {
|
||||
$cursor = is_string($cursor)
|
||||
? Cursor::fromEncoded($cursor)
|
||||
: CursorPaginator::resolveCurrentCursor($cursorName, $cursor);
|
||||
}
|
||||
|
||||
$orders = $this->ensureOrderForCursorPagination(! is_null($cursor) && $cursor->pointsToPreviousItems());
|
||||
|
||||
if (! is_null($cursor)) {
|
||||
$addCursorConditions = function (self $builder, $previousColumn, $i) use (&$addCursorConditions, $cursor, $orders) {
|
||||
$unionBuilders = isset($builder->unions) ? collect($builder->unions)->pluck('query') : collect();
|
||||
|
||||
if (! is_null($previousColumn)) {
|
||||
$originalColumn = $this->getOriginalColumnNameForCursorPagination($this, $previousColumn);
|
||||
|
||||
$builder->where(
|
||||
Str::contains($originalColumn, ['(', ')']) ? new Expression($originalColumn) : $originalColumn,
|
||||
'=',
|
||||
$cursor->parameter($previousColumn)
|
||||
);
|
||||
|
||||
$unionBuilders->each(function ($unionBuilder) use ($previousColumn, $cursor) {
|
||||
$unionBuilder->where(
|
||||
$this->getOriginalColumnNameForCursorPagination($this, $previousColumn),
|
||||
'=',
|
||||
$cursor->parameter($previousColumn)
|
||||
);
|
||||
|
||||
$this->addBinding($unionBuilder->getRawBindings()['where'], 'union');
|
||||
});
|
||||
}
|
||||
|
||||
$builder->where(function (self $builder) use ($addCursorConditions, $cursor, $orders, $i, $unionBuilders) {
|
||||
['column' => $column, 'direction' => $direction] = $orders[$i];
|
||||
|
||||
$originalColumn = $this->getOriginalColumnNameForCursorPagination($this, $column);
|
||||
|
||||
$builder->where(
|
||||
Str::contains($originalColumn, ['(', ')']) ? new Expression($originalColumn) : $originalColumn,
|
||||
$direction === 'asc' ? '>' : '<',
|
||||
$cursor->parameter($column)
|
||||
);
|
||||
|
||||
if ($i < $orders->count() - 1) {
|
||||
$builder->orWhere(function (self $builder) use ($addCursorConditions, $column, $i) {
|
||||
$addCursorConditions($builder, $column, $i + 1);
|
||||
});
|
||||
}
|
||||
|
||||
$unionBuilders->each(function ($unionBuilder) use ($column, $direction, $cursor, $i, $orders, $addCursorConditions) {
|
||||
$unionBuilder->where(function ($unionBuilder) use ($column, $direction, $cursor, $i, $orders, $addCursorConditions) {
|
||||
$unionBuilder->where(
|
||||
$this->getOriginalColumnNameForCursorPagination($this, $column),
|
||||
$direction === 'asc' ? '>' : '<',
|
||||
$cursor->parameter($column)
|
||||
);
|
||||
|
||||
if ($i < $orders->count() - 1) {
|
||||
$unionBuilder->orWhere(function (self $builder) use ($addCursorConditions, $column, $i) {
|
||||
$addCursorConditions($builder, $column, $i + 1);
|
||||
});
|
||||
}
|
||||
|
||||
$this->addBinding($unionBuilder->getRawBindings()['where'], 'union');
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$addCursorConditions($this, null, 0);
|
||||
}
|
||||
|
||||
$this->limit($perPage + 1);
|
||||
|
||||
return $this->cursorPaginator($this->get($columns), $perPage, $cursor, [
|
||||
'path' => Paginator::resolveCurrentPath(),
|
||||
'cursorName' => $cursorName,
|
||||
'parameters' => $orders->pluck('column')->toArray(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the original column name of the given column, without any aliasing.
|
||||
*
|
||||
* @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $builder
|
||||
* @param string $parameter
|
||||
* @return string
|
||||
*/
|
||||
protected function getOriginalColumnNameForCursorPagination($builder, string $parameter)
|
||||
{
|
||||
$columns = $builder instanceof Builder ? $builder->getQuery()->columns : $builder->columns;
|
||||
|
||||
if (! is_null($columns)) {
|
||||
foreach ($columns as $column) {
|
||||
if (($position = strripos($column, ' as ')) !== false) {
|
||||
$original = substr($column, 0, $position);
|
||||
|
||||
$alias = substr($column, $position + 4);
|
||||
|
||||
if ($parameter === $alias || $builder->getGrammar()->wrap($parameter) === $alias) {
|
||||
return $original;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $parameter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new length-aware paginator instance.
|
||||
*
|
||||
* @param \Illuminate\Support\Collection $items
|
||||
* @param int $total
|
||||
* @param int $perPage
|
||||
* @param int $currentPage
|
||||
* @param array $options
|
||||
* @return \Illuminate\Pagination\LengthAwarePaginator
|
||||
*/
|
||||
protected function paginator($items, $total, $perPage, $currentPage, $options)
|
||||
{
|
||||
return Container::getInstance()->makeWith(LengthAwarePaginator::class, compact(
|
||||
'items', 'total', 'perPage', 'currentPage', 'options'
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new simple paginator instance.
|
||||
*
|
||||
* @param \Illuminate\Support\Collection $items
|
||||
* @param int $perPage
|
||||
* @param int $currentPage
|
||||
* @param array $options
|
||||
* @return \Illuminate\Pagination\Paginator
|
||||
*/
|
||||
protected function simplePaginator($items, $perPage, $currentPage, $options)
|
||||
{
|
||||
return Container::getInstance()->makeWith(Paginator::class, compact(
|
||||
'items', 'perPage', 'currentPage', 'options'
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new cursor paginator instance.
|
||||
*
|
||||
* @param \Illuminate\Support\Collection $items
|
||||
* @param int $perPage
|
||||
* @param \Illuminate\Pagination\Cursor $cursor
|
||||
* @param array $options
|
||||
* @return \Illuminate\Pagination\CursorPaginator
|
||||
*/
|
||||
protected function cursorPaginator($items, $perPage, $cursor, $options)
|
||||
{
|
||||
return Container::getInstance()->makeWith(CursorPaginator::class, compact(
|
||||
'items', 'perPage', 'cursor', 'options'
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass the query to a given callback.
|
||||
*
|
||||
* @param callable $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function tap($callback)
|
||||
{
|
||||
$callback($this);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Concerns;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
trait CompilesJsonPaths
|
||||
{
|
||||
/**
|
||||
* Split the given JSON selector into the field and the optional path and wrap them separately.
|
||||
*
|
||||
* @param string $column
|
||||
* @return array
|
||||
*/
|
||||
protected function wrapJsonFieldAndPath($column)
|
||||
{
|
||||
$parts = explode('->', $column, 2);
|
||||
|
||||
$field = $this->wrap($parts[0]);
|
||||
|
||||
$path = count($parts) > 1 ? ', '.$this->wrapJsonPath($parts[1], '->') : '';
|
||||
|
||||
return [$field, $path];
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap the given JSON path.
|
||||
*
|
||||
* @param string $value
|
||||
* @param string $delimiter
|
||||
* @return string
|
||||
*/
|
||||
protected function wrapJsonPath($value, $delimiter = '->')
|
||||
{
|
||||
$value = preg_replace("/([\\\\]+)?\\'/", "''", $value);
|
||||
|
||||
$jsonPath = collect(explode($delimiter, $value))
|
||||
->map(fn ($segment) => $this->wrapJsonPathSegment($segment))
|
||||
->join('.');
|
||||
|
||||
return "'$".(str_starts_with($jsonPath, '[') ? '' : '.').$jsonPath."'";
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap the given JSON path segment.
|
||||
*
|
||||
* @param string $segment
|
||||
* @return string
|
||||
*/
|
||||
protected function wrapJsonPathSegment($segment)
|
||||
{
|
||||
if (preg_match('/(\[[^\]]+\])+$/', $segment, $parts)) {
|
||||
$key = Str::beforeLast($segment, $parts[0]);
|
||||
|
||||
if (! empty($key)) {
|
||||
return '"'.$key.'"'.$parts[0];
|
||||
}
|
||||
|
||||
return $parts[0];
|
||||
}
|
||||
|
||||
return '"'.$segment.'"';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Concerns;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
trait ExplainsQueries
|
||||
{
|
||||
/**
|
||||
* Explains the query.
|
||||
*
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
public function explain()
|
||||
{
|
||||
$sql = $this->toSql();
|
||||
|
||||
$bindings = $this->getBindings();
|
||||
|
||||
$explanation = $this->getConnection()->select('EXPLAIN '.$sql, $bindings);
|
||||
|
||||
return new Collection($explanation);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Concerns;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Database\DeadlockException;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
trait ManagesTransactions
|
||||
{
|
||||
/**
|
||||
* Execute a Closure within a transaction.
|
||||
*
|
||||
* @param \Closure $callback
|
||||
* @param int $attempts
|
||||
* @return mixed
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function transaction(Closure $callback, $attempts = 1)
|
||||
{
|
||||
for ($currentAttempt = 1; $currentAttempt <= $attempts; $currentAttempt++) {
|
||||
$this->beginTransaction();
|
||||
|
||||
// We'll simply execute the given callback within a try / catch block and if we
|
||||
// catch any exception we can rollback this transaction so that none of this
|
||||
// gets actually persisted to a database or stored in a permanent fashion.
|
||||
try {
|
||||
$callbackResult = $callback($this);
|
||||
}
|
||||
|
||||
// If we catch an exception we'll rollback this transaction and try again if we
|
||||
// are not out of attempts. If we are out of attempts we will just throw the
|
||||
// exception back out, and let the developer handle an uncaught exception.
|
||||
catch (Throwable $e) {
|
||||
$this->handleTransactionException(
|
||||
$e, $currentAttempt, $attempts
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
if ($this->transactions == 1) {
|
||||
$this->fireConnectionEvent('committing');
|
||||
$this->getPdo()->commit();
|
||||
}
|
||||
|
||||
$this->transactions = max(0, $this->transactions - 1);
|
||||
|
||||
if ($this->afterCommitCallbacksShouldBeExecuted()) {
|
||||
$this->transactionsManager?->commit($this->getName());
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$this->handleCommitTransactionException(
|
||||
$e, $currentAttempt, $attempts
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->fireConnectionEvent('committed');
|
||||
|
||||
return $callbackResult;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an exception encountered when running a transacted statement.
|
||||
*
|
||||
* @param \Throwable $e
|
||||
* @param int $currentAttempt
|
||||
* @param int $maxAttempts
|
||||
* @return void
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
protected function handleTransactionException(Throwable $e, $currentAttempt, $maxAttempts)
|
||||
{
|
||||
// On a deadlock, MySQL rolls back the entire transaction so we can't just
|
||||
// retry the query. We have to throw this exception all the way out and
|
||||
// let the developer handle it in another way. We will decrement too.
|
||||
if ($this->causedByConcurrencyError($e) &&
|
||||
$this->transactions > 1) {
|
||||
$this->transactions--;
|
||||
|
||||
$this->transactionsManager?->rollback(
|
||||
$this->getName(), $this->transactions
|
||||
);
|
||||
|
||||
throw new DeadlockException($e->getMessage(), is_int($e->getCode()) ? $e->getCode() : 0, $e);
|
||||
}
|
||||
|
||||
// If there was an exception we will rollback this transaction and then we
|
||||
// can check if we have exceeded the maximum attempt count for this and
|
||||
// if we haven't we will return and try this query again in our loop.
|
||||
$this->rollBack();
|
||||
|
||||
if ($this->causedByConcurrencyError($e) &&
|
||||
$currentAttempt < $maxAttempts) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new database transaction.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function beginTransaction()
|
||||
{
|
||||
$this->createTransaction();
|
||||
|
||||
$this->transactions++;
|
||||
|
||||
$this->transactionsManager?->begin(
|
||||
$this->getName(), $this->transactions
|
||||
);
|
||||
|
||||
$this->fireConnectionEvent('beganTransaction');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a transaction within the database.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
protected function createTransaction()
|
||||
{
|
||||
if ($this->transactions == 0) {
|
||||
$this->reconnectIfMissingConnection();
|
||||
|
||||
try {
|
||||
$this->getPdo()->beginTransaction();
|
||||
} catch (Throwable $e) {
|
||||
$this->handleBeginTransactionException($e);
|
||||
}
|
||||
} elseif ($this->transactions >= 1 && $this->queryGrammar->supportsSavepoints()) {
|
||||
$this->createSavepoint();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a save point within the database.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
protected function createSavepoint()
|
||||
{
|
||||
$this->getPdo()->exec(
|
||||
$this->queryGrammar->compileSavepoint('trans'.($this->transactions + 1))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an exception from a transaction beginning.
|
||||
*
|
||||
* @param \Throwable $e
|
||||
* @return void
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
protected function handleBeginTransactionException(Throwable $e)
|
||||
{
|
||||
if ($this->causedByLostConnection($e)) {
|
||||
$this->reconnect();
|
||||
|
||||
$this->getPdo()->beginTransaction();
|
||||
} else {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit the active database transaction.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function commit()
|
||||
{
|
||||
if ($this->transactionLevel() == 1) {
|
||||
$this->fireConnectionEvent('committing');
|
||||
$this->getPdo()->commit();
|
||||
}
|
||||
|
||||
$this->transactions = max(0, $this->transactions - 1);
|
||||
|
||||
if ($this->afterCommitCallbacksShouldBeExecuted()) {
|
||||
$this->transactionsManager?->commit($this->getName());
|
||||
}
|
||||
|
||||
$this->fireConnectionEvent('committed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if after commit callbacks should be executed.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function afterCommitCallbacksShouldBeExecuted()
|
||||
{
|
||||
return $this->transactions == 0 ||
|
||||
($this->transactionsManager &&
|
||||
$this->transactionsManager->callbackApplicableTransactions()->count() === 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an exception encountered when committing a transaction.
|
||||
*
|
||||
* @param \Throwable $e
|
||||
* @param int $currentAttempt
|
||||
* @param int $maxAttempts
|
||||
* @return void
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
protected function handleCommitTransactionException(Throwable $e, $currentAttempt, $maxAttempts)
|
||||
{
|
||||
$this->transactions = max(0, $this->transactions - 1);
|
||||
|
||||
if ($this->causedByConcurrencyError($e) && $currentAttempt < $maxAttempts) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->causedByLostConnection($e)) {
|
||||
$this->transactions = 0;
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback the active database transaction.
|
||||
*
|
||||
* @param int|null $toLevel
|
||||
* @return void
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function rollBack($toLevel = null)
|
||||
{
|
||||
// We allow developers to rollback to a certain transaction level. We will verify
|
||||
// that this given transaction level is valid before attempting to rollback to
|
||||
// that level. If it's not we will just return out and not attempt anything.
|
||||
$toLevel = is_null($toLevel)
|
||||
? $this->transactions - 1
|
||||
: $toLevel;
|
||||
|
||||
if ($toLevel < 0 || $toLevel >= $this->transactions) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Next, we will actually perform this rollback within this database and fire the
|
||||
// rollback event. We will also set the current transaction level to the given
|
||||
// level that was passed into this method so it will be right from here out.
|
||||
try {
|
||||
$this->performRollBack($toLevel);
|
||||
} catch (Throwable $e) {
|
||||
$this->handleRollBackException($e);
|
||||
}
|
||||
|
||||
$this->transactions = $toLevel;
|
||||
|
||||
$this->transactionsManager?->rollback(
|
||||
$this->getName(), $this->transactions
|
||||
);
|
||||
|
||||
$this->fireConnectionEvent('rollingBack');
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a rollback within the database.
|
||||
*
|
||||
* @param int $toLevel
|
||||
* @return void
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
protected function performRollBack($toLevel)
|
||||
{
|
||||
if ($toLevel == 0) {
|
||||
$pdo = $this->getPdo();
|
||||
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
} elseif ($this->queryGrammar->supportsSavepoints()) {
|
||||
$this->getPdo()->exec(
|
||||
$this->queryGrammar->compileSavepointRollBack('trans'.($toLevel + 1))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an exception from a rollback.
|
||||
*
|
||||
* @param \Throwable $e
|
||||
* @return void
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
protected function handleRollBackException(Throwable $e)
|
||||
{
|
||||
if ($this->causedByLostConnection($e)) {
|
||||
$this->transactions = 0;
|
||||
|
||||
$this->transactionsManager?->rollback(
|
||||
$this->getName(), $this->transactions
|
||||
);
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of active transactions.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function transactionLevel()
|
||||
{
|
||||
return $this->transactions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the callback after a transaction commits.
|
||||
*
|
||||
* @param callable $callback
|
||||
* @return void
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function afterCommit($callback)
|
||||
{
|
||||
if ($this->transactionsManager) {
|
||||
return $this->transactionsManager->addCallback($callback);
|
||||
}
|
||||
|
||||
throw new RuntimeException('Transactions Manager has not been set.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Database\Concerns;
|
||||
|
||||
trait ParsesSearchPath
|
||||
{
|
||||
/**
|
||||
* Parse the Postgres "search_path" configuration value into an array.
|
||||
*
|
||||
* @param string|array|null $searchPath
|
||||
* @return array
|
||||
*/
|
||||
protected function parseSearchPath($searchPath)
|
||||
{
|
||||
if (is_string($searchPath)) {
|
||||
preg_match_all('/[^\s,"\']+/', $searchPath, $matches);
|
||||
|
||||
$searchPath = $matches[0];
|
||||
}
|
||||
|
||||
return array_map(function ($schema) {
|
||||
return trim($schema, '\'"');
|
||||
}, $searchPath ?? []);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user