BuildsQueries.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  1. <?php
  2. namespace Illuminate\Database\Concerns;
  3. use Illuminate\Container\Container;
  4. use Illuminate\Database\Eloquent\Builder;
  5. use Illuminate\Database\MultipleRecordsFoundException;
  6. use Illuminate\Database\Query\Expression;
  7. use Illuminate\Database\RecordsNotFoundException;
  8. use Illuminate\Pagination\Cursor;
  9. use Illuminate\Pagination\CursorPaginator;
  10. use Illuminate\Pagination\LengthAwarePaginator;
  11. use Illuminate\Pagination\Paginator;
  12. use Illuminate\Support\Collection;
  13. use Illuminate\Support\LazyCollection;
  14. use Illuminate\Support\Str;
  15. use Illuminate\Support\Traits\Conditionable;
  16. use InvalidArgumentException;
  17. use RuntimeException;
  18. trait BuildsQueries
  19. {
  20. use Conditionable;
  21. /**
  22. * Chunk the results of the query.
  23. *
  24. * @param int $count
  25. * @param callable $callback
  26. * @return bool
  27. */
  28. public function chunk($count, callable $callback)
  29. {
  30. $this->enforceOrderBy();
  31. $page = 1;
  32. do {
  33. // We'll execute the query for the given page and get the results. If there are
  34. // no results we can just break and return from here. When there are results
  35. // we will call the callback with the current chunk of these results here.
  36. $results = $this->forPage($page, $count)->get();
  37. $countResults = $results->count();
  38. if ($countResults == 0) {
  39. break;
  40. }
  41. // On each chunk result set, we will pass them to the callback and then let the
  42. // developer take care of everything within the callback, which allows us to
  43. // keep the memory low for spinning through large result sets for working.
  44. if ($callback($results, $page) === false) {
  45. return false;
  46. }
  47. unset($results);
  48. $page++;
  49. } while ($countResults == $count);
  50. return true;
  51. }
  52. /**
  53. * Run a map over each item while chunking.
  54. *
  55. * @param callable $callback
  56. * @param int $count
  57. * @return \Illuminate\Support\Collection
  58. */
  59. public function chunkMap(callable $callback, $count = 1000)
  60. {
  61. $collection = Collection::make();
  62. $this->chunk($count, function ($items) use ($collection, $callback) {
  63. $items->each(function ($item) use ($collection, $callback) {
  64. $collection->push($callback($item));
  65. });
  66. });
  67. return $collection;
  68. }
  69. /**
  70. * Execute a callback over each item while chunking.
  71. *
  72. * @param callable $callback
  73. * @param int $count
  74. * @return bool
  75. *
  76. * @throws \RuntimeException
  77. */
  78. public function each(callable $callback, $count = 1000)
  79. {
  80. return $this->chunk($count, function ($results) use ($callback) {
  81. foreach ($results as $key => $value) {
  82. if ($callback($value, $key) === false) {
  83. return false;
  84. }
  85. }
  86. });
  87. }
  88. /**
  89. * Chunk the results of a query by comparing IDs.
  90. *
  91. * @param int $count
  92. * @param callable $callback
  93. * @param string|null $column
  94. * @param string|null $alias
  95. * @return bool
  96. */
  97. public function chunkById($count, callable $callback, $column = null, $alias = null)
  98. {
  99. return $this->orderedChunkById($count, $callback, $column, $alias);
  100. }
  101. /**
  102. * Chunk the results of a query by comparing IDs in descending order.
  103. *
  104. * @param int $count
  105. * @param callable $callback
  106. * @param string|null $column
  107. * @param string|null $alias
  108. * @return bool
  109. */
  110. public function chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
  111. {
  112. return $this->orderedChunkById($count, $callback, $column, $alias, descending: true);
  113. }
  114. /**
  115. * Chunk the results of a query by comparing IDs in a given order.
  116. *
  117. * @param int $count
  118. * @param callable $callback
  119. * @param string|null $column
  120. * @param string|null $alias
  121. * @param bool $descending
  122. * @return bool
  123. *
  124. * @throws \RuntimeException
  125. */
  126. public function orderedChunkById($count, callable $callback, $column = null, $alias = null, $descending = false)
  127. {
  128. $column ??= $this->defaultKeyName();
  129. $alias ??= $column;
  130. $lastId = null;
  131. $page = 1;
  132. do {
  133. $clone = clone $this;
  134. // We'll execute the query for the given page and get the results. If there are
  135. // no results we can just break and return from here. When there are results
  136. // we will call the callback with the current chunk of these results here.
  137. if ($descending) {
  138. $results = $clone->forPageBeforeId($count, $lastId, $column)->get();
  139. } else {
  140. $results = $clone->forPageAfterId($count, $lastId, $column)->get();
  141. }
  142. $countResults = $results->count();
  143. if ($countResults == 0) {
  144. break;
  145. }
  146. // On each chunk result set, we will pass them to the callback and then let the
  147. // developer take care of everything within the callback, which allows us to
  148. // keep the memory low for spinning through large result sets for working.
  149. if ($callback($results, $page) === false) {
  150. return false;
  151. }
  152. $lastId = data_get($results->last(), $alias);
  153. if ($lastId === null) {
  154. throw new RuntimeException("The chunkById operation was aborted because the [{$alias}] column is not present in the query result.");
  155. }
  156. unset($results);
  157. $page++;
  158. } while ($countResults == $count);
  159. return true;
  160. }
  161. /**
  162. * Execute a callback over each item while chunking by ID.
  163. *
  164. * @param callable $callback
  165. * @param int $count
  166. * @param string|null $column
  167. * @param string|null $alias
  168. * @return bool
  169. */
  170. public function eachById(callable $callback, $count = 1000, $column = null, $alias = null)
  171. {
  172. return $this->chunkById($count, function ($results, $page) use ($callback, $count) {
  173. foreach ($results as $key => $value) {
  174. if ($callback($value, (($page - 1) * $count) + $key) === false) {
  175. return false;
  176. }
  177. }
  178. }, $column, $alias);
  179. }
  180. /**
  181. * Query lazily, by chunks of the given size.
  182. *
  183. * @param int $chunkSize
  184. * @return \Illuminate\Support\LazyCollection
  185. *
  186. * @throws \InvalidArgumentException
  187. */
  188. public function lazy($chunkSize = 1000)
  189. {
  190. if ($chunkSize < 1) {
  191. throw new InvalidArgumentException('The chunk size should be at least 1');
  192. }
  193. $this->enforceOrderBy();
  194. return LazyCollection::make(function () use ($chunkSize) {
  195. $page = 1;
  196. while (true) {
  197. $results = $this->forPage($page++, $chunkSize)->get();
  198. foreach ($results as $result) {
  199. yield $result;
  200. }
  201. if ($results->count() < $chunkSize) {
  202. return;
  203. }
  204. }
  205. });
  206. }
  207. /**
  208. * Query lazily, by chunking the results of a query by comparing IDs.
  209. *
  210. * @param int $chunkSize
  211. * @param string|null $column
  212. * @param string|null $alias
  213. * @return \Illuminate\Support\LazyCollection
  214. *
  215. * @throws \InvalidArgumentException
  216. */
  217. public function lazyById($chunkSize = 1000, $column = null, $alias = null)
  218. {
  219. return $this->orderedLazyById($chunkSize, $column, $alias);
  220. }
  221. /**
  222. * Query lazily, by chunking the results of a query by comparing IDs in descending order.
  223. *
  224. * @param int $chunkSize
  225. * @param string|null $column
  226. * @param string|null $alias
  227. * @return \Illuminate\Support\LazyCollection
  228. *
  229. * @throws \InvalidArgumentException
  230. */
  231. public function lazyByIdDesc($chunkSize = 1000, $column = null, $alias = null)
  232. {
  233. return $this->orderedLazyById($chunkSize, $column, $alias, true);
  234. }
  235. /**
  236. * Query lazily, by chunking the results of a query by comparing IDs in a given order.
  237. *
  238. * @param int $chunkSize
  239. * @param string|null $column
  240. * @param string|null $alias
  241. * @param bool $descending
  242. * @return \Illuminate\Support\LazyCollection
  243. *
  244. * @throws \InvalidArgumentException
  245. */
  246. protected function orderedLazyById($chunkSize = 1000, $column = null, $alias = null, $descending = false)
  247. {
  248. if ($chunkSize < 1) {
  249. throw new InvalidArgumentException('The chunk size should be at least 1');
  250. }
  251. $column ??= $this->defaultKeyName();
  252. $alias ??= $column;
  253. return LazyCollection::make(function () use ($chunkSize, $column, $alias, $descending) {
  254. $lastId = null;
  255. while (true) {
  256. $clone = clone $this;
  257. if ($descending) {
  258. $results = $clone->forPageBeforeId($chunkSize, $lastId, $column)->get();
  259. } else {
  260. $results = $clone->forPageAfterId($chunkSize, $lastId, $column)->get();
  261. }
  262. foreach ($results as $result) {
  263. yield $result;
  264. }
  265. if ($results->count() < $chunkSize) {
  266. return;
  267. }
  268. $lastId = $results->last()->{$alias};
  269. if ($lastId === null) {
  270. throw new RuntimeException("The lazyById operation was aborted because the [{$alias}] column is not present in the query result.");
  271. }
  272. }
  273. });
  274. }
  275. /**
  276. * Execute the query and get the first result.
  277. *
  278. * @param array|string $columns
  279. * @return \Illuminate\Database\Eloquent\Model|object|static|null
  280. */
  281. public function first($columns = ['*'])
  282. {
  283. return $this->take(1)->get($columns)->first();
  284. }
  285. /**
  286. * Execute the query and get the first result if it's the sole matching record.
  287. *
  288. * @param array|string $columns
  289. * @return \Illuminate\Database\Eloquent\Model|object|static|null
  290. *
  291. * @throws \Illuminate\Database\RecordsNotFoundException
  292. * @throws \Illuminate\Database\MultipleRecordsFoundException
  293. */
  294. public function sole($columns = ['*'])
  295. {
  296. $result = $this->take(2)->get($columns);
  297. $count = $result->count();
  298. if ($count === 0) {
  299. throw new RecordsNotFoundException;
  300. }
  301. if ($count > 1) {
  302. throw new MultipleRecordsFoundException($count);
  303. }
  304. return $result->first();
  305. }
  306. /**
  307. * Paginate the given query using a cursor paginator.
  308. *
  309. * @param int $perPage
  310. * @param array|string $columns
  311. * @param string $cursorName
  312. * @param \Illuminate\Pagination\Cursor|string|null $cursor
  313. * @return \Illuminate\Contracts\Pagination\CursorPaginator
  314. */
  315. protected function paginateUsingCursor($perPage, $columns = ['*'], $cursorName = 'cursor', $cursor = null)
  316. {
  317. if (! $cursor instanceof Cursor) {
  318. $cursor = is_string($cursor)
  319. ? Cursor::fromEncoded($cursor)
  320. : CursorPaginator::resolveCurrentCursor($cursorName, $cursor);
  321. }
  322. $orders = $this->ensureOrderForCursorPagination(! is_null($cursor) && $cursor->pointsToPreviousItems());
  323. if (! is_null($cursor)) {
  324. // Reset the union bindings so we can add the cursor where in the correct position...
  325. $this->setBindings([], 'union');
  326. $addCursorConditions = function (self $builder, $previousColumn, $originalColumn, $i) use (&$addCursorConditions, $cursor, $orders) {
  327. $unionBuilders = $builder->getUnionBuilders();
  328. if (! is_null($previousColumn)) {
  329. $originalColumn ??= $this->getOriginalColumnNameForCursorPagination($this, $previousColumn);
  330. $builder->where(
  331. Str::contains($originalColumn, ['(', ')']) ? new Expression($originalColumn) : $originalColumn,
  332. '=',
  333. $cursor->parameter($previousColumn)
  334. );
  335. $unionBuilders->each(function ($unionBuilder) use ($previousColumn, $cursor) {
  336. $unionBuilder->where(
  337. $this->getOriginalColumnNameForCursorPagination($unionBuilder, $previousColumn),
  338. '=',
  339. $cursor->parameter($previousColumn)
  340. );
  341. $this->addBinding($unionBuilder->getRawBindings()['where'], 'union');
  342. });
  343. }
  344. $builder->where(function (self $secondBuilder) use ($addCursorConditions, $cursor, $orders, $i, $unionBuilders) {
  345. ['column' => $column, 'direction' => $direction] = $orders[$i];
  346. $originalColumn = $this->getOriginalColumnNameForCursorPagination($this, $column);
  347. $secondBuilder->where(
  348. Str::contains($originalColumn, ['(', ')']) ? new Expression($originalColumn) : $originalColumn,
  349. $direction === 'asc' ? '>' : '<',
  350. $cursor->parameter($column)
  351. );
  352. if ($i < $orders->count() - 1) {
  353. $secondBuilder->orWhere(function (self $thirdBuilder) use ($addCursorConditions, $column, $originalColumn, $i) {
  354. $addCursorConditions($thirdBuilder, $column, $originalColumn, $i + 1);
  355. });
  356. }
  357. $unionBuilders->each(function ($unionBuilder) use ($column, $direction, $cursor, $i, $orders, $addCursorConditions) {
  358. $unionWheres = $unionBuilder->getRawBindings()['where'];
  359. $originalColumn = $this->getOriginalColumnNameForCursorPagination($unionBuilder, $column);
  360. $unionBuilder->where(function ($unionBuilder) use ($column, $direction, $cursor, $i, $orders, $addCursorConditions, $originalColumn, $unionWheres) {
  361. $unionBuilder->where(
  362. $originalColumn,
  363. $direction === 'asc' ? '>' : '<',
  364. $cursor->parameter($column)
  365. );
  366. if ($i < $orders->count() - 1) {
  367. $unionBuilder->orWhere(function (self $fourthBuilder) use ($addCursorConditions, $column, $originalColumn, $i) {
  368. $addCursorConditions($fourthBuilder, $column, $originalColumn, $i + 1);
  369. });
  370. }
  371. $this->addBinding($unionWheres, 'union');
  372. $this->addBinding($unionBuilder->getRawBindings()['where'], 'union');
  373. });
  374. });
  375. });
  376. };
  377. $addCursorConditions($this, null, null, 0);
  378. }
  379. $this->limit($perPage + 1);
  380. return $this->cursorPaginator($this->get($columns), $perPage, $cursor, [
  381. 'path' => Paginator::resolveCurrentPath(),
  382. 'cursorName' => $cursorName,
  383. 'parameters' => $orders->pluck('column')->toArray(),
  384. ]);
  385. }
  386. /**
  387. * Get the original column name of the given column, without any aliasing.
  388. *
  389. * @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $builder
  390. * @param string $parameter
  391. * @return string
  392. */
  393. protected function getOriginalColumnNameForCursorPagination($builder, string $parameter)
  394. {
  395. $columns = $builder instanceof Builder ? $builder->getQuery()->getColumns() : $builder->getColumns();
  396. if (! is_null($columns)) {
  397. foreach ($columns as $column) {
  398. if (($position = strripos($column, ' as ')) !== false) {
  399. $original = substr($column, 0, $position);
  400. $alias = substr($column, $position + 4);
  401. if ($parameter === $alias || $builder->getGrammar()->wrap($parameter) === $alias) {
  402. return $original;
  403. }
  404. }
  405. }
  406. }
  407. return $parameter;
  408. }
  409. /**
  410. * Create a new length-aware paginator instance.
  411. *
  412. * @param \Illuminate\Support\Collection $items
  413. * @param int $total
  414. * @param int $perPage
  415. * @param int $currentPage
  416. * @param array $options
  417. * @return \Illuminate\Pagination\LengthAwarePaginator
  418. */
  419. protected function paginator($items, $total, $perPage, $currentPage, $options)
  420. {
  421. return Container::getInstance()->makeWith(LengthAwarePaginator::class, compact(
  422. 'items', 'total', 'perPage', 'currentPage', 'options'
  423. ));
  424. }
  425. /**
  426. * Create a new simple paginator instance.
  427. *
  428. * @param \Illuminate\Support\Collection $items
  429. * @param int $perPage
  430. * @param int $currentPage
  431. * @param array $options
  432. * @return \Illuminate\Pagination\Paginator
  433. */
  434. protected function simplePaginator($items, $perPage, $currentPage, $options)
  435. {
  436. return Container::getInstance()->makeWith(Paginator::class, compact(
  437. 'items', 'perPage', 'currentPage', 'options'
  438. ));
  439. }
  440. /**
  441. * Create a new cursor paginator instance.
  442. *
  443. * @param \Illuminate\Support\Collection $items
  444. * @param int $perPage
  445. * @param \Illuminate\Pagination\Cursor $cursor
  446. * @param array $options
  447. * @return \Illuminate\Pagination\CursorPaginator
  448. */
  449. protected function cursorPaginator($items, $perPage, $cursor, $options)
  450. {
  451. return Container::getInstance()->makeWith(CursorPaginator::class, compact(
  452. 'items', 'perPage', 'cursor', 'options'
  453. ));
  454. }
  455. /**
  456. * Pass the query to a given callback.
  457. *
  458. * @param callable($this): mixed $callback
  459. * @return $this
  460. */
  461. public function tap($callback)
  462. {
  463. $callback($this);
  464. return $this;
  465. }
  466. }