1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Nette\Database\Table;
9:
10: use Nette;
11: use Nette\Database\ISupplementalDriver;
12:
13:
14: 15: 16: 17: 18: 19: 20: 21: 22:
23: class Selection extends Nette\Object implements \Iterator, IRowContainer, \ArrayAccess, \Countable
24: {
25:
26: protected $connection;
27:
28:
29: protected $reflection;
30:
31:
32: protected $cache;
33:
34:
35: protected $sqlBuilder;
36:
37:
38: protected $name;
39:
40:
41: protected $primary;
42:
43:
44: protected $primarySequence = FALSE;
45:
46:
47: protected $rows;
48:
49:
50: protected $data;
51:
52:
53: protected $dataRefreshed = FALSE;
54:
55:
56: protected $globalRefCache;
57:
58:
59: protected $refCache;
60:
61:
62: protected $generalCacheKey;
63:
64:
65: protected $specificCacheKey;
66:
67:
68: protected $aggregation = array();
69:
70:
71: protected $accessedColumns;
72:
73:
74: protected $previousAccessedColumns;
75:
76:
77: protected $observeCache = FALSE;
78:
79:
80: protected $keys = array();
81:
82:
83: 84: 85: 86: 87:
88: public function __construct(Nette\Database\Connection $connection, $table, Nette\Database\IReflection $reflection, Nette\Caching\IStorage $cacheStorage = NULL)
89: {
90: $this->name = $table;
91: $this->connection = $connection;
92: $this->reflection = $reflection;
93: $this->cache = $cacheStorage ? new Nette\Caching\Cache($cacheStorage, 'Nette.Database.' . md5($connection->getDsn())) : NULL;
94: $this->primary = $reflection->getPrimary($table);
95: $this->sqlBuilder = new SqlBuilder($table, $connection, $reflection);
96: $this->refCache = & $this->getRefTable($refPath)->globalRefCache[$refPath];
97: }
98:
99:
100: public function __destruct()
101: {
102: $this->saveCacheState();
103: }
104:
105:
106: public function __clone()
107: {
108: $this->sqlBuilder = clone $this->sqlBuilder;
109: }
110:
111:
112: 113: 114:
115: public function getConnection()
116: {
117: return $this->connection;
118: }
119:
120:
121: 122: 123:
124: public function getDatabaseReflection()
125: {
126: return $this->reflection;
127: }
128:
129:
130: 131: 132:
133: public function getName()
134: {
135: return $this->name;
136: }
137:
138:
139: 140: 141: 142:
143: public function getPrimary($need = TRUE)
144: {
145: if ($this->primary === NULL && $need) {
146: throw new \LogicException("Table '{$this->name}' does not have a primary key.");
147: }
148: return $this->primary;
149: }
150:
151:
152: 153: 154:
155: public function getPrimarySequence()
156: {
157: if ($this->primarySequence === FALSE) {
158: $this->primarySequence = NULL;
159: $driver = $this->connection->getSupplementalDriver();
160: if ($driver->isSupported(ISupplementalDriver::SUPPORT_SEQUENCE) && $this->primary !== NULL) {
161: foreach ($driver->getColumns($this->name) as $column) {
162: if ($column['name'] === $this->primary) {
163: $this->primarySequence = $column['vendor']['sequence'];
164: break;
165: }
166: }
167: }
168: }
169:
170: return $this->primarySequence;
171: }
172:
173:
174: 175: 176: 177:
178: public function setPrimarySequence($sequence)
179: {
180: $this->primarySequence = $sequence;
181: return $this;
182: }
183:
184:
185: 186: 187:
188: public function getSql()
189: {
190: return $this->sqlBuilder->buildSelectQuery($this->getPreviousAccessedColumns());
191: }
192:
193:
194: 195: 196: 197: 198:
199: public function getPreviousAccessedColumns()
200: {
201: if ($this->cache && $this->previousAccessedColumns === NULL) {
202: $this->accessedColumns = $this->previousAccessedColumns = $this->cache->load($this->getGeneralCacheKey());
203: if ($this->previousAccessedColumns === NULL) {
204: $this->previousAccessedColumns = array();
205: }
206: }
207:
208: return array_keys(array_filter((array) $this->previousAccessedColumns));
209: }
210:
211:
212: 213: 214: 215:
216: public function getSqlBuilder()
217: {
218: return $this->sqlBuilder;
219: }
220:
221:
222:
223:
224:
225: 226: 227: 228: 229:
230: public function get($key)
231: {
232: $clone = clone $this;
233: return $clone->wherePrimary($key)->fetch();
234: }
235:
236:
237: 238: 239:
240: public function fetch()
241: {
242: $this->execute();
243: $return = current($this->data);
244: next($this->data);
245: return $return;
246: }
247:
248:
249: 250: 251:
252: public function fetchPairs($key = NULL, $value = NULL)
253: {
254: return Nette\Database\Helpers::toPairs(iterator_to_array($this), $key, $value);
255: }
256:
257:
258: 259: 260:
261: public function fetchAll()
262: {
263: return iterator_to_array($this);
264: }
265:
266:
267:
268:
269:
270: 271: 272: 273: 274:
275: public function select($columns)
276: {
277: $this->emptyResultSet();
278: call_user_func_array(array($this->sqlBuilder, 'addSelect'), func_get_args());
279: return $this;
280: }
281:
282:
283: 284: 285: 286: 287:
288: public function wherePrimary($key)
289: {
290: if (is_array($this->primary) && Nette\Utils\Arrays::isList($key)) {
291: if (isset($key[0]) && is_array($key[0])) {
292: $this->where($this->primary, $key);
293: } else {
294: foreach ($this->primary as $i => $primary) {
295: $this->where($this->name . '.' . $primary, $key[$i]);
296: }
297: }
298: } elseif (is_array($key) && !Nette\Utils\Arrays::isList($key)) {
299: $this->where($key);
300: } else {
301: $this->where($this->name . '.' . $this->getPrimary(), $key);
302: }
303:
304: return $this;
305: }
306:
307:
308: 309: 310: 311: 312: 313: 314:
315: public function where($condition, $parameters = array())
316: {
317: if (is_array($condition) && $parameters === array()) {
318: foreach ($condition as $key => $val) {
319: if (is_int($key)) {
320: $this->where($val);
321: } else {
322: $this->where($key, $val);
323: }
324: }
325: return $this;
326: }
327:
328: $this->emptyResultSet();
329: call_user_func_array(array($this->sqlBuilder, 'addWhere'), func_get_args());
330: return $this;
331: }
332:
333:
334: 335: 336: 337: 338:
339: public function order($columns)
340: {
341: $this->emptyResultSet();
342: call_user_func_array(array($this->sqlBuilder, 'addOrder'), func_get_args());
343: return $this;
344: }
345:
346:
347: 348: 349: 350: 351: 352:
353: public function limit($limit, $offset = NULL)
354: {
355: $this->emptyResultSet();
356: $this->sqlBuilder->setLimit($limit, $offset);
357: return $this;
358: }
359:
360:
361: 362: 363: 364: 365: 366:
367: public function page($page, $itemsPerPage, & $numOfPages = NULL)
368: {
369: if (func_num_args() > 2) {
370: $numOfPages = (int) ceil($this->count('*') / $itemsPerPage);
371: }
372: return $this->limit($itemsPerPage, ($page - 1) * $itemsPerPage);
373: }
374:
375:
376: 377: 378: 379: 380:
381: public function group($columns)
382: {
383: $this->emptyResultSet();
384: call_user_func_array(array($this->sqlBuilder, 'setGroup'), func_get_args());
385: return $this;
386: }
387:
388:
389: 390: 391: 392: 393:
394: public function having($having)
395: {
396: $this->emptyResultSet();
397: call_user_func_array(array($this->sqlBuilder, 'setHaving'), func_get_args());
398: return $this;
399: }
400:
401:
402:
403:
404:
405: 406: 407: 408: 409:
410: public function aggregation($function)
411: {
412: $selection = $this->createSelectionInstance();
413: $selection->getSqlBuilder()->importConditions($this->getSqlBuilder());
414: $selection->select($function);
415: foreach ($selection->fetch() as $val) {
416: return $val;
417: }
418: }
419:
420:
421: 422: 423: 424: 425:
426: public function count($column = NULL)
427: {
428: if (!$column) {
429: $this->execute();
430: return count($this->data);
431: }
432: return $this->aggregation("COUNT($column)");
433: }
434:
435:
436: 437: 438: 439: 440:
441: public function min($column)
442: {
443: return $this->aggregation("MIN($column)");
444: }
445:
446:
447: 448: 449: 450: 451:
452: public function max($column)
453: {
454: return $this->aggregation("MAX($column)");
455: }
456:
457:
458: 459: 460: 461: 462:
463: public function sum($column)
464: {
465: return $this->aggregation("SUM($column)");
466: }
467:
468:
469:
470:
471:
472: protected function execute()
473: {
474: if ($this->rows !== NULL) {
475: return;
476: }
477:
478: $this->observeCache = $this;
479:
480: if ($this->primary === NULL && $this->sqlBuilder->getSelect() === NULL) {
481: throw new Nette\InvalidStateException('Table with no primary key requires an explicit select clause.');
482: }
483:
484: try {
485: $result = $this->query($this->getSql());
486:
487: } catch (\PDOException $exception) {
488: if (!$this->sqlBuilder->getSelect() && $this->previousAccessedColumns) {
489: $this->previousAccessedColumns = FALSE;
490: $this->accessedColumns = array();
491: $result = $this->query($this->getSql());
492: } else {
493: throw $exception;
494: }
495: }
496:
497: $this->rows = array();
498: $usedPrimary = TRUE;
499: foreach ($result->getPdoStatement() as $key => $row) {
500: $row = $this->createRow($result->normalizeRow($row));
501: $primary = $row->getSignature(FALSE);
502: $usedPrimary = $usedPrimary && $primary;
503: $this->rows[$primary ?: $key] = $row;
504: }
505: $this->data = $this->rows;
506:
507: if ($usedPrimary && $this->accessedColumns !== FALSE) {
508: foreach ((array) $this->primary as $primary) {
509: $this->accessedColumns[$primary] = TRUE;
510: }
511: }
512: }
513:
514:
515: protected function createRow(array $row)
516: {
517: return new ActiveRow($row, $this);
518: }
519:
520:
521: public function createSelectionInstance($table = NULL)
522: {
523: return new self($this->connection, $table ?: $this->name, $this->reflection, $this->cache ? $this->cache->getStorage() : NULL);
524: }
525:
526:
527: protected function createGroupedSelectionInstance($table, $column)
528: {
529: return new GroupedSelection($this, $table, $column);
530: }
531:
532:
533: protected function query($query)
534: {
535: return $this->connection->queryArgs($query, $this->sqlBuilder->getParameters());
536: }
537:
538:
539: protected function emptyResultSet($saveCache = TRUE)
540: {
541: if ($this->rows !== NULL && $saveCache) {
542: $this->saveCacheState();
543: }
544:
545: $this->rows = NULL;
546: $this->specificCacheKey = NULL;
547: $this->generalCacheKey = NULL;
548: $this->refCache['referencingPrototype'] = array();
549: }
550:
551:
552: protected function saveCacheState()
553: {
554: if ($this->observeCache === $this && $this->cache && !$this->sqlBuilder->getSelect() && $this->accessedColumns !== $this->previousAccessedColumns) {
555: $previousAccessed = $this->cache->load($this->getGeneralCacheKey());
556: $accessed = $this->accessedColumns;
557: $needSave = is_array($accessed) && is_array($previousAccessed)
558: ? array_intersect_key($accessed, $previousAccessed) !== $accessed
559: : $accessed !== $previousAccessed;
560:
561: if ($needSave) {
562: $save = is_array($accessed) && is_array($previousAccessed) ? $previousAccessed + $accessed : $accessed;
563: $this->cache->save($this->getGeneralCacheKey(), $save);
564: $this->previousAccessedColumns = NULL;
565: }
566: }
567: }
568:
569:
570: 571: 572: 573:
574: protected function getRefTable(& $refPath)
575: {
576: return $this;
577: }
578:
579:
580: 581: 582:
583: protected function loadRefCache()
584: {
585: }
586:
587:
588: 589: 590: 591: 592:
593: protected function getGeneralCacheKey()
594: {
595: if ($this->generalCacheKey) {
596: return $this->generalCacheKey;
597: }
598:
599: return $this->generalCacheKey = md5(serialize(array(__CLASS__, $this->name, $this->sqlBuilder->getConditions())));
600: }
601:
602:
603: 604: 605: 606: 607:
608: protected function getSpecificCacheKey()
609: {
610: if ($this->specificCacheKey) {
611: return $this->specificCacheKey;
612: }
613:
614: return $this->specificCacheKey = md5($this->getSql() . json_encode($this->sqlBuilder->getParameters()));
615: }
616:
617:
618: 619: 620: 621: 622:
623: public function accessColumn($key, $selectColumn = TRUE)
624: {
625: if (!$this->cache) {
626: return;
627: }
628:
629: if ($key === NULL) {
630: $this->accessedColumns = FALSE;
631: $currentKey = key((array) $this->data);
632: } elseif ($this->accessedColumns !== FALSE) {
633: $this->accessedColumns[$key] = $selectColumn;
634: }
635:
636: if ($selectColumn && !$this->sqlBuilder->getSelect() && $this->previousAccessedColumns && ($key === NULL || !isset($this->previousAccessedColumns[$key]))) {
637: $this->previousAccessedColumns = array();
638:
639: if ($this->sqlBuilder->getLimit()) {
640: $generalCacheKey = $this->generalCacheKey;
641: $sqlBuilder = $this->sqlBuilder;
642:
643: $primaryValues = array();
644: foreach ((array) $this->rows as $row) {
645: $primary = $row->getPrimary();
646: $primaryValues[] = is_array($primary) ? array_values($primary) : $primary;
647: }
648:
649: $this->emptyResultSet(FALSE);
650: $this->sqlBuilder = clone $this->sqlBuilder;
651: $this->sqlBuilder->setLimit(NULL, NULL);
652: $this->wherePrimary($primaryValues);
653:
654: $this->generalCacheKey = $generalCacheKey;
655: $this->execute();
656: $this->sqlBuilder = $sqlBuilder;
657: } else {
658: $this->emptyResultSet(FALSE);
659: $this->execute();
660: }
661:
662: $this->dataRefreshed = TRUE;
663:
664:
665: if (isset($currentKey)) {
666: while (key($this->data) !== $currentKey) {
667: next($this->data);
668: }
669: }
670: }
671: }
672:
673:
674: 675: 676: 677:
678: public function removeAccessColumn($key)
679: {
680: if ($this->cache && is_array($this->accessedColumns)) {
681: $this->accessedColumns[$key] = FALSE;
682: }
683: }
684:
685:
686: 687: 688: 689:
690: public function getDataRefreshed()
691: {
692: return $this->dataRefreshed;
693: }
694:
695:
696:
697:
698:
699: 700: 701: 702: 703:
704: public function insert($data)
705: {
706: if ($data instanceof self) {
707: $data = new Nette\Database\SqlLiteral($data->getSql(), $data->getSqlBuilder()->getParameters());
708:
709: } elseif ($data instanceof \Traversable) {
710: $data = iterator_to_array($data);
711: }
712:
713: $return = $this->connection->query($this->sqlBuilder->buildInsertQuery(), $data);
714: $this->loadRefCache();
715:
716: if ($data instanceof Nette\Database\SqlLiteral || $this->primary === NULL) {
717: unset($this->refCache['referencing'][$this->getGeneralCacheKey()][$this->getSpecificCacheKey()]);
718: return $return->getRowCount();
719: }
720:
721: $primaryKey = $this->connection->getInsertId($this->getPrimarySequence());
722: if ($primaryKey === FALSE) {
723: unset($this->refCache['referencing'][$this->getGeneralCacheKey()][$this->getSpecificCacheKey()]);
724: return $return->getRowCount();
725: }
726:
727: if (is_array($this->getPrimary())) {
728: $primaryKey = array();
729:
730: foreach ((array) $this->getPrimary() as $key) {
731: if (!isset($data[$key])) {
732: return $data;
733: }
734:
735: $primaryKey[$key] = $data[$key];
736: }
737: if (count($primaryKey) === 1) {
738: $primaryKey = reset($primaryKey);
739: }
740: }
741:
742: $row = $this->createSelectionInstance()
743: ->select('*')
744: ->wherePrimary($primaryKey)
745: ->fetch();
746:
747: if ($this->rows !== NULL) {
748: if ($signature = $row->getSignature(FALSE)) {
749: $this->rows[$signature] = $row;
750: $this->data[$signature] = $row;
751: } else {
752: $this->rows[] = $row;
753: $this->data[] = $row;
754: }
755: }
756:
757: return $row;
758: }
759:
760:
761: 762: 763: 764: 765: 766:
767: public function update($data)
768: {
769: if ($data instanceof \Traversable) {
770: $data = iterator_to_array($data);
771:
772: } elseif (!is_array($data)) {
773: throw new Nette\InvalidArgumentException;
774: }
775:
776: if (!$data) {
777: return 0;
778: }
779:
780: return $this->connection->queryArgs(
781: $this->sqlBuilder->buildUpdateQuery(),
782: array_merge(array($data), $this->sqlBuilder->getParameters())
783: )->getRowCount();
784: }
785:
786:
787: 788: 789: 790:
791: public function delete()
792: {
793: return $this->query($this->sqlBuilder->buildDeleteQuery())->getRowCount();
794: }
795:
796:
797:
798:
799:
800: 801: 802: 803: 804: 805: 806:
807: public function getReferencedTable($table, $column, $checkPrimaryKey)
808: {
809: $referenced = & $this->refCache['referenced'][$this->getSpecificCacheKey()]["$table.$column"];
810: $selection = & $referenced['selection'];
811: $cacheKeys = & $referenced['cacheKeys'];
812: if ($selection === NULL || ($checkPrimaryKey !== NULL && !isset($cacheKeys[$checkPrimaryKey]))) {
813: $this->execute();
814: $cacheKeys = array();
815: foreach ($this->rows as $row) {
816: if ($row[$column] === NULL) {
817: continue;
818: }
819:
820: $key = $row[$column];
821: $cacheKeys[$key] = TRUE;
822: }
823:
824: if ($cacheKeys) {
825: $selection = $this->createSelectionInstance($table);
826: $selection->where($selection->getPrimary(), array_keys($cacheKeys));
827: } else {
828: $selection = array();
829: }
830: }
831:
832: return $selection;
833: }
834:
835:
836: 837: 838: 839: 840: 841: 842:
843: public function getReferencingTable($table, $column, $active = NULL)
844: {
845: $prototype = & $this->refCache['referencingPrototype'][$this->getSpecificCacheKey()]["$table.$column"];
846: if (!$prototype) {
847: $prototype = $this->createGroupedSelectionInstance($table, $column);
848: $prototype->where("$table.$column", array_keys((array) $this->rows));
849: }
850:
851: $clone = clone $prototype;
852: $clone->setActive($active);
853: return $clone;
854: }
855:
856:
857:
858:
859:
860: public function rewind()
861: {
862: $this->execute();
863: $this->keys = array_keys($this->data);
864: reset($this->keys);
865: }
866:
867:
868:
869: public function current()
870: {
871: if (($key = current($this->keys)) !== FALSE) {
872: return $this->data[$key];
873: } else {
874: return FALSE;
875: }
876: }
877:
878:
879: 880: 881:
882: public function key()
883: {
884: return current($this->keys);
885: }
886:
887:
888: public function next()
889: {
890: next($this->keys);
891: }
892:
893:
894: public function valid()
895: {
896: return current($this->keys) !== FALSE;
897: }
898:
899:
900:
901:
902:
903: 904: 905: 906: 907: 908:
909: public function offsetSet($key, $value)
910: {
911: $this->execute();
912: $this->rows[$key] = $value;
913: }
914:
915:
916: 917: 918: 919: 920:
921: public function offsetGet($key)
922: {
923: $this->execute();
924: return $this->rows[$key];
925: }
926:
927:
928: 929: 930: 931: 932:
933: public function offsetExists($key)
934: {
935: $this->execute();
936: return isset($this->rows[$key]);
937: }
938:
939:
940: 941: 942: 943: 944:
945: public function offsetUnset($key)
946: {
947: $this->execute();
948: unset($this->rows[$key], $this->data[$key]);
949: }
950:
951: }
952: