EagerLoader.php 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  5. *
  6. * Licensed under The MIT License
  7. * For full copyright and license information, please see the LICENSE.txt
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  11. * @link http://cakephp.org CakePHP(tm) Project
  12. * @since 3.0.0
  13. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\ORM;
  16. use Cake\Database\Statement\BufferedStatement;
  17. use Cake\Database\Statement\CallbackStatement;
  18. use Closure;
  19. use InvalidArgumentException;
  20. /**
  21. * Exposes the methods for storing the associations that should be eager loaded
  22. * for a table once a query is provided and delegates the job of creating the
  23. * required joins and decorating the results so that those associations can be
  24. * part of the result set.
  25. */
  26. class EagerLoader
  27. {
  28. /**
  29. * Nested array describing the association to be fetched
  30. * and the options to apply for each of them, if any
  31. *
  32. * @var array
  33. */
  34. protected $_containments = [];
  35. /**
  36. * Contains a nested array with the compiled containments tree
  37. * This is a normalized version of the user provided containments array.
  38. *
  39. * @var array
  40. */
  41. protected $_normalized;
  42. /**
  43. * List of options accepted by associations in contain()
  44. * index by key for faster access
  45. *
  46. * @var array
  47. */
  48. protected $_containOptions = [
  49. 'associations' => 1,
  50. 'foreignKey' => 1,
  51. 'conditions' => 1,
  52. 'fields' => 1,
  53. 'sort' => 1,
  54. 'matching' => 1,
  55. 'queryBuilder' => 1,
  56. 'finder' => 1,
  57. 'joinType' => 1,
  58. 'strategy' => 1,
  59. 'negateMatch' => 1
  60. ];
  61. /**
  62. * A list of associations that should be loaded with a separate query
  63. *
  64. * @var array
  65. */
  66. protected $_loadExternal = [];
  67. /**
  68. * Contains a list of the association names that are to be eagerly loaded
  69. *
  70. * @var array
  71. */
  72. protected $_aliasList = [];
  73. /**
  74. * Another EagerLoader instance that will be used for 'matching' associations.
  75. *
  76. * @var \Cake\ORM\EagerLoader
  77. */
  78. protected $_matching;
  79. /**
  80. * A map of table aliases pointing to the association objects they represent
  81. * for the query.
  82. *
  83. * @var array
  84. */
  85. protected $_joinsMap = [];
  86. /**
  87. * Controls whether or not fields from associated tables
  88. * will be eagerly loaded. When set to false, no fields will
  89. * be loaded from associations.
  90. *
  91. * @var bool
  92. */
  93. protected $_autoFields = true;
  94. /**
  95. * Sets the list of associations that should be eagerly loaded along for a
  96. * specific table using when a query is provided. The list of associated tables
  97. * passed to this method must have been previously set as associations using the
  98. * Table API.
  99. *
  100. * Associations can be arbitrarily nested using dot notation or nested arrays,
  101. * this allows this object to calculate joins or any additional queries that
  102. * must be executed to bring the required associated data.
  103. *
  104. * Accepted options per passed association:
  105. *
  106. * - foreignKey: Used to set a different field to match both tables, if set to false
  107. * no join conditions will be generated automatically
  108. * - fields: An array with the fields that should be fetched from the association
  109. * - queryBuilder: Equivalent to passing a callable instead of an options array
  110. * - matching: Whether to inform the association class that it should filter the
  111. * main query by the results fetched by that class.
  112. * - joinType: For joinable associations, the SQL join type to use.
  113. * - strategy: The loading strategy to use (join, select, subquery)
  114. *
  115. * @param array|string $associations list of table aliases to be queried.
  116. * When this method is called multiple times it will merge previous list with
  117. * the new one.
  118. * @return array Containments.
  119. */
  120. public function contain($associations = [])
  121. {
  122. if (empty($associations)) {
  123. return $this->_containments;
  124. }
  125. $associations = (array)$associations;
  126. $associations = $this->_reformatContain($associations, $this->_containments);
  127. $this->_normalized = $this->_loadExternal = null;
  128. $this->_aliasList = [];
  129. return $this->_containments = $associations;
  130. }
  131. /**
  132. * Remove any existing non-matching based containments.
  133. *
  134. * This will reset/clear out any contained associations that were not
  135. * added via matching().
  136. *
  137. * @return void
  138. */
  139. public function clearContain()
  140. {
  141. $this->_containments = [];
  142. $this->_normalized = $this->_loadExternal = null;
  143. $this->_aliasList = [];
  144. }
  145. /**
  146. * Set whether or not contained associations will load fields automatically.
  147. *
  148. * @param bool $value The value to set.
  149. * @return bool The current value.
  150. */
  151. public function autoFields($value = null)
  152. {
  153. if ($value !== null) {
  154. $this->_autoFields = (bool)$value;
  155. }
  156. return $this->_autoFields;
  157. }
  158. /**
  159. * Adds a new association to the list that will be used to filter the results of
  160. * any given query based on the results of finding records for that association.
  161. * You can pass a dot separated path of associations to this method as its first
  162. * parameter, this will translate in setting all those associations with the
  163. * `matching` option.
  164. *
  165. * If called with no arguments it will return the current tree of associations to
  166. * be matched.
  167. *
  168. * @param string|null $assoc A single association or a dot separated path of associations.
  169. * @param callable|null $builder the callback function to be used for setting extra
  170. * options to the filtering query
  171. * @param array $options Extra options for the association matching, such as 'joinType'
  172. * and 'fields'
  173. * @return array The resulting containments array
  174. */
  175. public function matching($assoc = null, callable $builder = null, $options = [])
  176. {
  177. if ($this->_matching === null) {
  178. $this->_matching = new self();
  179. }
  180. if ($assoc === null) {
  181. return $this->_matching->contain();
  182. }
  183. $assocs = explode('.', $assoc);
  184. $last = array_pop($assocs);
  185. $containments = [];
  186. $pointer =& $containments;
  187. $options += ['joinType' => 'INNER'];
  188. $opts = ['matching' => true] + $options;
  189. unset($opts['negateMatch']);
  190. foreach ($assocs as $name) {
  191. $pointer[$name] = $opts;
  192. $pointer =& $pointer[$name];
  193. }
  194. $pointer[$last] = ['queryBuilder' => $builder, 'matching' => true] + $options;
  195. return $this->_matching->contain($containments);
  196. }
  197. /**
  198. * Returns the fully normalized array of associations that should be eagerly
  199. * loaded for a table. The normalized array will restructure the original array
  200. * by sorting all associations under one key and special options under another.
  201. *
  202. * Each of the levels of the associations tree will converted to a Cake\ORM\EagerLoadable
  203. * object, that contains all the information required for the association objects
  204. * to load the information from the database.
  205. *
  206. * Additionally it will set an 'instance' key per association containing the
  207. * association instance from the corresponding source table
  208. *
  209. * @param \Cake\ORM\Table $repository The table containing the association that
  210. * will be normalized
  211. * @return array
  212. */
  213. public function normalized(Table $repository)
  214. {
  215. if ($this->_normalized !== null || empty($this->_containments)) {
  216. return (array)$this->_normalized;
  217. }
  218. $contain = [];
  219. foreach ($this->_containments as $alias => $options) {
  220. if (!empty($options['instance'])) {
  221. $contain = (array)$this->_containments;
  222. break;
  223. }
  224. $contain[$alias] = $this->_normalizeContain(
  225. $repository,
  226. $alias,
  227. $options,
  228. ['root' => null]
  229. );
  230. }
  231. return $this->_normalized = $contain;
  232. }
  233. /**
  234. * Formats the containments array so that associations are always set as keys
  235. * in the array. This function merges the original associations array with
  236. * the new associations provided
  237. *
  238. * @param array $associations user provided containments array
  239. * @param array $original The original containments array to merge
  240. * with the new one
  241. * @return array
  242. */
  243. protected function _reformatContain($associations, $original)
  244. {
  245. $result = $original;
  246. foreach ((array)$associations as $table => $options) {
  247. $pointer =& $result;
  248. if (is_int($table)) {
  249. $table = $options;
  250. $options = [];
  251. }
  252. if ($options instanceof EagerLoadable) {
  253. $options = $options->asContainArray();
  254. $table = key($options);
  255. $options = current($options);
  256. }
  257. if (isset($this->_containOptions[$table])) {
  258. $pointer[$table] = $options;
  259. continue;
  260. }
  261. if (strpos($table, '.')) {
  262. $path = explode('.', $table);
  263. $table = array_pop($path);
  264. foreach ($path as $t) {
  265. $pointer += [$t => []];
  266. $pointer =& $pointer[$t];
  267. }
  268. }
  269. if (is_array($options)) {
  270. $options = isset($options['config']) ?
  271. $options['config'] + $options['associations'] :
  272. $options;
  273. $options = $this->_reformatContain(
  274. $options,
  275. isset($pointer[$table]) ? $pointer[$table] : []
  276. );
  277. }
  278. if ($options instanceof Closure) {
  279. $options = ['queryBuilder' => $options];
  280. }
  281. $pointer += [$table => []];
  282. $pointer[$table] = $options + $pointer[$table];
  283. }
  284. return $result;
  285. }
  286. /**
  287. * Modifies the passed query to apply joins or any other transformation required
  288. * in order to eager load the associations described in the `contain` array.
  289. * This method will not modify the query for loading external associations, i.e.
  290. * those that cannot be loaded without executing a separate query.
  291. *
  292. * @param \Cake\ORM\Query $query The query to be modified
  293. * @param \Cake\ORM\Table $repository The repository containing the associations
  294. * @param bool $includeFields whether to append all fields from the associations
  295. * to the passed query. This can be overridden according to the settings defined
  296. * per association in the containments array
  297. * @return void
  298. */
  299. public function attachAssociations(Query $query, Table $repository, $includeFields)
  300. {
  301. if (empty($this->_containments) && $this->_matching === null) {
  302. return;
  303. }
  304. foreach ($this->attachableAssociations($repository) as $loadable) {
  305. $config = $loadable->config() + [
  306. 'aliasPath' => $loadable->aliasPath(),
  307. 'propertyPath' => $loadable->propertyPath(),
  308. 'includeFields' => $includeFields,
  309. ];
  310. $loadable->instance()->attachTo($query, $config);
  311. }
  312. }
  313. /**
  314. * Returns an array with the associations that can be fetched using a single query,
  315. * the array keys are the association aliases and the values will contain an array
  316. * with Cake\ORM\EagerLoadable objects.
  317. *
  318. * @param \Cake\ORM\Table $repository The table containing the associations to be
  319. * attached
  320. * @return array
  321. */
  322. public function attachableAssociations(Table $repository)
  323. {
  324. $contain = $this->normalized($repository);
  325. $matching = $this->_matching ? $this->_matching->normalized($repository) : [];
  326. $this->_fixStrategies();
  327. return $this->_resolveJoins($contain, $matching);
  328. }
  329. /**
  330. * Returns an array with the associations that need to be fetched using a
  331. * separate query, each array value will contain a Cake\ORM\EagerLoadable object.
  332. *
  333. * @param \Cake\ORM\Table $repository The table containing the associations
  334. * to be loaded
  335. * @return array
  336. */
  337. public function externalAssociations(Table $repository)
  338. {
  339. if ($this->_loadExternal) {
  340. return $this->_loadExternal;
  341. }
  342. $this->attachableAssociations($repository);
  343. return $this->_loadExternal;
  344. }
  345. /**
  346. * Auxiliary function responsible for fully normalizing deep associations defined
  347. * using `contain()`
  348. *
  349. * @param Table $parent owning side of the association
  350. * @param string $alias name of the association to be loaded
  351. * @param array $options list of extra options to use for this association
  352. * @param array $paths An array with two values, the first one is a list of dot
  353. * separated strings representing associations that lead to this `$alias` in the
  354. * chain of associations to be loaded. The second value is the path to follow in
  355. * entities' properties to fetch a record of the corresponding association.
  356. * @return array normalized associations
  357. * @throws \InvalidArgumentException When containments refer to associations that do not exist.
  358. */
  359. protected function _normalizeContain(Table $parent, $alias, $options, $paths)
  360. {
  361. $defaults = $this->_containOptions;
  362. $instance = $parent->association($alias);
  363. if (!$instance) {
  364. throw new InvalidArgumentException(
  365. sprintf('%s is not associated with %s', $parent->alias(), $alias)
  366. );
  367. }
  368. if ($instance->alias() !== $alias) {
  369. throw new InvalidArgumentException(sprintf(
  370. "You have contained '%s' but that association was bound as '%s'.",
  371. $alias,
  372. $instance->alias()
  373. ));
  374. }
  375. $paths += ['aliasPath' => '', 'propertyPath' => '', 'root' => $alias];
  376. $paths['aliasPath'] .= '.' . $alias;
  377. $paths['propertyPath'] .= '.' . $instance->property();
  378. $table = $instance->target();
  379. $extra = array_diff_key($options, $defaults);
  380. $config = [
  381. 'associations' => [],
  382. 'instance' => $instance,
  383. 'config' => array_diff_key($options, $extra),
  384. 'aliasPath' => trim($paths['aliasPath'], '.'),
  385. 'propertyPath' => trim($paths['propertyPath'], '.')
  386. ];
  387. $config['canBeJoined'] = $instance->canBeJoined($config['config']);
  388. $eagerLoadable = new EagerLoadable($alias, $config);
  389. if ($config['canBeJoined']) {
  390. $this->_aliasList[$paths['root']][$alias][] = $eagerLoadable;
  391. } else {
  392. $paths['root'] = $config['aliasPath'];
  393. }
  394. foreach ($extra as $t => $assoc) {
  395. $eagerLoadable->addAssociation(
  396. $t,
  397. $this->_normalizeContain($table, $t, $assoc, $paths)
  398. );
  399. }
  400. return $eagerLoadable;
  401. }
  402. /**
  403. * Iterates over the joinable aliases list and corrects the fetching strategies
  404. * in order to avoid aliases collision in the generated queries.
  405. *
  406. * This function operates on the array references that were generated by the
  407. * _normalizeContain() function.
  408. *
  409. * @return void
  410. */
  411. protected function _fixStrategies()
  412. {
  413. foreach ($this->_aliasList as $aliases) {
  414. foreach ($aliases as $configs) {
  415. if (count($configs) < 2) {
  416. continue;
  417. }
  418. foreach ($configs as $loadable) {
  419. if (strpos($loadable->aliasPath(), '.')) {
  420. $this->_correctStrategy($loadable);
  421. }
  422. }
  423. }
  424. }
  425. }
  426. /**
  427. * Changes the association fetching strategy if required because of duplicate
  428. * under the same direct associations chain
  429. *
  430. * @param \Cake\ORM\EagerLoadable $loadable The association config
  431. * @return void
  432. */
  433. protected function _correctStrategy($loadable)
  434. {
  435. $config = $loadable->config();
  436. $currentStrategy = isset($config['strategy']) ?
  437. $config['strategy'] :
  438. 'join';
  439. if (!$loadable->canBeJoined() || $currentStrategy !== 'join') {
  440. return;
  441. }
  442. $config['strategy'] = Association::STRATEGY_SELECT;
  443. $loadable->config($config);
  444. $loadable->canBeJoined(false);
  445. }
  446. /**
  447. * Helper function used to compile a list of all associations that can be
  448. * joined in the query.
  449. *
  450. * @param array $associations list of associations from which to obtain joins.
  451. * @param array $matching list of associations that should be forcibly joined.
  452. * @return array
  453. */
  454. protected function _resolveJoins($associations, $matching = [])
  455. {
  456. $result = [];
  457. foreach ($matching as $table => $loadable) {
  458. $result[$table] = $loadable;
  459. $result += $this->_resolveJoins($loadable->associations(), []);
  460. }
  461. foreach ($associations as $table => $loadable) {
  462. $inMatching = isset($matching[$table]);
  463. if (!$inMatching && $loadable->canBeJoined()) {
  464. $result[$table] = $loadable;
  465. $result += $this->_resolveJoins($loadable->associations(), []);
  466. continue;
  467. }
  468. if ($inMatching) {
  469. $this->_correctStrategy($loadable);
  470. }
  471. $loadable->canBeJoined(false);
  472. $this->_loadExternal[] = $loadable;
  473. }
  474. return $result;
  475. }
  476. /**
  477. * Decorates the passed statement object in order to inject data from associations
  478. * that cannot be joined directly.
  479. *
  480. * @param \Cake\ORM\Query $query The query for which to eager load external
  481. * associations
  482. * @param \Cake\Database\StatementInterface $statement The statement created after executing the $query
  483. * @return CallbackStatement statement modified statement with extra loaders
  484. */
  485. public function loadExternal($query, $statement)
  486. {
  487. $external = $this->externalAssociations($query->repository());
  488. if (empty($external)) {
  489. return $statement;
  490. }
  491. $driver = $query->connection()->driver();
  492. list($collected, $statement) = $this->_collectKeys($external, $query, $statement);
  493. foreach ($external as $meta) {
  494. $contain = $meta->associations();
  495. $instance = $meta->instance();
  496. $config = $meta->config();
  497. $alias = $instance->source()->alias();
  498. $path = $meta->aliasPath();
  499. $requiresKeys = $instance->requiresKeys($config);
  500. if ($requiresKeys && empty($collected[$path][$alias])) {
  501. continue;
  502. }
  503. $keys = isset($collected[$path][$alias]) ? $collected[$path][$alias] : null;
  504. $f = $instance->eagerLoader(
  505. $config + [
  506. 'query' => $query,
  507. 'contain' => $contain,
  508. 'keys' => $keys,
  509. 'nestKey' => $meta->aliasPath()
  510. ]
  511. );
  512. $statement = new CallbackStatement($statement, $driver, $f);
  513. }
  514. return $statement;
  515. }
  516. /**
  517. * Returns an array having as keys a dotted path of associations that participate
  518. * in this eager loader. The values of the array will contain the following keys
  519. *
  520. * - alias: The association alias
  521. * - instance: The association instance
  522. * - canBeJoined: Whether or not the association will be loaded using a JOIN
  523. * - entityClass: The entity that should be used for hydrating the results
  524. * - nestKey: A dotted path that can be used to correctly insert the data into the results.
  525. * - matching: Whether or not it is an association loaded through `matching()`.
  526. *
  527. * @param \Cake\ORM\Table $table The table containing the association that
  528. * will be normalized
  529. * @return array
  530. */
  531. public function associationsMap($table)
  532. {
  533. $map = [];
  534. if (!$this->matching() && !$this->contain() && empty($this->_joinsMap)) {
  535. return $map;
  536. }
  537. $visitor = function ($level, $matching = false) use (&$visitor, &$map) {
  538. foreach ($level as $assoc => $meta) {
  539. $canBeJoined = $meta->canBeJoined();
  540. $instance = $meta->instance();
  541. $associations = $meta->associations();
  542. $forMatching = $meta->forMatching();
  543. $map[] = [
  544. 'alias' => $assoc,
  545. 'instance' => $instance,
  546. 'canBeJoined' => $canBeJoined,
  547. 'entityClass' => $instance->target()->entityClass(),
  548. 'nestKey' => $canBeJoined ? $assoc : $meta->aliasPath(),
  549. 'matching' => $forMatching !== null ? $forMatching : $matching
  550. ];
  551. if ($canBeJoined && $associations) {
  552. $visitor($associations, $matching);
  553. }
  554. }
  555. };
  556. $visitor($this->_matching->normalized($table), true);
  557. $visitor($this->normalized($table));
  558. $visitor($this->_joinsMap);
  559. return $map;
  560. }
  561. /**
  562. * Registers a table alias, typically loaded as a join in a query, as belonging to
  563. * an association. This helps hydrators know what to do with the columns coming
  564. * from such joined table.
  565. *
  566. * @param string $alias The table alias as it appears in the query.
  567. * @param \Cake\ORM\Association $assoc The association object the alias represents;
  568. * will be normalized
  569. * @param bool $asMatching Whether or not this join results should be treated as a
  570. * 'matching' association.
  571. * @return void
  572. */
  573. public function addToJoinsMap($alias, Association $assoc, $asMatching = false)
  574. {
  575. $this->_joinsMap[$alias] = new EagerLoadable($alias, [
  576. 'aliasPath' => $alias,
  577. 'instance' => $assoc,
  578. 'canBeJoined' => true,
  579. 'forMatching' => $asMatching,
  580. ]);
  581. }
  582. /**
  583. * Helper function used to return the keys from the query records that will be used
  584. * to eagerly load associations.
  585. *
  586. * @param array $external the list of external associations to be loaded
  587. * @param \Cake\ORM\Query $query The query from which the results where generated
  588. * @param BufferedStatement $statement The statement to work on
  589. * @return array
  590. */
  591. protected function _collectKeys($external, $query, $statement)
  592. {
  593. $collectKeys = [];
  594. foreach ($external as $meta) {
  595. $instance = $meta->instance();
  596. if (!$instance->requiresKeys($meta->config())) {
  597. continue;
  598. }
  599. $source = $instance->source();
  600. $keys = $instance->type() === Association::MANY_TO_ONE ?
  601. (array)$instance->foreignKey() :
  602. (array)$instance->bindingKey();
  603. $alias = $source->alias();
  604. $pkFields = [];
  605. foreach ($keys as $key) {
  606. $pkFields[] = key($query->aliasField($key, $alias));
  607. }
  608. $collectKeys[$meta->aliasPath()] = [$alias, $pkFields, count($pkFields) === 1];
  609. }
  610. if (empty($collectKeys)) {
  611. return [[], $statement];
  612. }
  613. if (!($statement instanceof BufferedStatement)) {
  614. $statement = new BufferedStatement($statement, $query->connection()->driver());
  615. }
  616. return [$this->_groupKeys($statement, $collectKeys), $statement];
  617. }
  618. /**
  619. * Helper function used to iterate a statement and extract the columns
  620. * defined in $collectKeys
  621. *
  622. * @param \Cake\Database\StatementInterface $statement The statement to read from.
  623. * @param array $collectKeys The keys to collect
  624. * @return array
  625. */
  626. protected function _groupKeys($statement, $collectKeys)
  627. {
  628. $keys = [];
  629. while ($result = $statement->fetch('assoc')) {
  630. foreach ($collectKeys as $nestKey => $parts) {
  631. // Missed joins will have null in the results.
  632. if ($parts[2] === true && !isset($result[$parts[1][0]])) {
  633. continue;
  634. }
  635. if ($parts[2] === true) {
  636. $value = $result[$parts[1][0]];
  637. $keys[$nestKey][$parts[0]][$value] = $value;
  638. continue;
  639. }
  640. // Handle composite keys.
  641. $collected = [];
  642. foreach ($parts[1] as $key) {
  643. $collected[] = $result[$key];
  644. }
  645. $keys[$nestKey][$parts[0]][implode(';', $collected)] = $collected;
  646. }
  647. }
  648. $statement->rewind();
  649. return $keys;
  650. }
  651. }