1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17:
18:
19: App::uses('DboSource', 'Model/Datasource');
20:
21: 22: 23: 24: 25: 26: 27: 28: 29: 30:
31: class Sqlserver extends DboSource {
32:
33: 34: 35: 36: 37:
38: public $description = "SQL Server DBO Driver";
39:
40: 41: 42: 43: 44:
45: public $startQuote = "[";
46:
47: 48: 49: 50: 51:
52: public $endQuote = "]";
53:
54: 55: 56: 57: 58: 59:
60: protected $_fieldMappings = array();
61:
62: 63: 64: 65: 66:
67: protected $_lastAffected = false;
68:
69: 70: 71: 72: 73:
74: protected $_baseConfig = array(
75: 'host' => 'localhost\SQLEXPRESS',
76: 'login' => '',
77: 'password' => '',
78: 'database' => 'cake',
79: 'schema' => '',
80: 'flags' => array()
81: );
82:
83: 84: 85: 86: 87: 88:
89: public $columns = array(
90: 'primary_key' => array('name' => 'IDENTITY (1, 1) NOT NULL'),
91: 'string' => array('name' => 'nvarchar', 'limit' => '255'),
92: 'text' => array('name' => 'nvarchar', 'limit' => 'MAX'),
93: 'integer' => array('name' => 'int', 'formatter' => 'intval'),
94: 'smallinteger' => array('name' => 'smallint', 'formatter' => 'intval'),
95: 'tinyinteger' => array('name' => 'tinyint', 'formatter' => 'intval'),
96: 'biginteger' => array('name' => 'bigint'),
97: 'numeric' => array('name' => 'decimal', 'formatter' => 'floatval'),
98: 'decimal' => array('name' => 'decimal', 'formatter' => 'floatval'),
99: 'float' => array('name' => 'float', 'formatter' => 'floatval'),
100: 'real' => array('name' => 'float', 'formatter' => 'floatval'),
101: 'datetime' => array('name' => 'datetime', 'format' => 'Y-m-d H:i:s', 'formatter' => 'date'),
102: 'timestamp' => array('name' => 'timestamp', 'format' => 'Y-m-d H:i:s', 'formatter' => 'date'),
103: 'time' => array('name' => 'datetime', 'format' => 'H:i:s', 'formatter' => 'date'),
104: 'date' => array('name' => 'datetime', 'format' => 'Y-m-d', 'formatter' => 'date'),
105: 'binary' => array('name' => 'varbinary'),
106: 'boolean' => array('name' => 'bit')
107: );
108:
109: 110: 111: 112: 113: 114:
115: const ROW_COUNTER = '_cake_page_rownum_';
116:
117: 118: 119: 120: 121: 122: 123: 124: 125: 126: 127: 128:
129: public function connect() {
130: $config = $this->config;
131: $this->connected = false;
132:
133: if (isset($config['persistent']) && $config['persistent']) {
134: throw new InvalidArgumentException('Config setting "persistent" cannot be set to true, as the Sqlserver PDO driver does not support PDO::ATTR_PERSISTENT');
135: }
136:
137: $flags = $config['flags'] + array(
138: PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
139: );
140:
141: if (!empty($config['encoding'])) {
142: $flags[PDO::SQLSRV_ATTR_ENCODING] = $config['encoding'];
143: }
144:
145: try {
146: $this->_connection = new PDO(
147: "sqlsrv:server={$config['host']};Database={$config['database']}",
148: $config['login'],
149: $config['password'],
150: $flags
151: );
152: $this->connected = true;
153: if (!empty($config['settings'])) {
154: foreach ($config['settings'] as $key => $value) {
155: $this->_execute("SET $key $value");
156: }
157: }
158: } catch (PDOException $e) {
159: throw new MissingConnectionException(array(
160: 'class' => get_class($this),
161: 'message' => $e->getMessage()
162: ));
163: }
164:
165: return $this->connected;
166: }
167:
168: 169: 170: 171: 172:
173: public function enabled() {
174: return in_array('sqlsrv', PDO::getAvailableDrivers());
175: }
176:
177: 178: 179: 180: 181: 182:
183: public function listSources($data = null) {
184: $cache = parent::listSources();
185: if ($cache !== null) {
186: return $cache;
187: }
188: $result = $this->_execute("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES");
189:
190: if (!$result) {
191: $result->closeCursor();
192: return array();
193: }
194: $tables = array();
195:
196: while ($line = $result->fetch(PDO::FETCH_NUM)) {
197: $tables[] = $line[0];
198: }
199:
200: $result->closeCursor();
201: parent::listSources($tables);
202: return $tables;
203: }
204:
205: 206: 207: 208: 209: 210: 211:
212: public function describe($model) {
213: $table = $this->fullTableName($model, false, false);
214: $fulltable = $this->fullTableName($model, false, true);
215:
216: $cache = parent::describe($fulltable);
217: if ($cache) {
218: return $cache;
219: }
220:
221: $fields = array();
222: $schema = is_object($model) ? $model->schemaName : false;
223:
224: $cols = $this->_execute(
225: "SELECT
226: COLUMN_NAME as Field,
227: DATA_TYPE as Type,
228: COL_LENGTH('" . ($schema ? $fulltable : $table) . "', COLUMN_NAME) as Length,
229: IS_NULLABLE As [Null],
230: COLUMN_DEFAULT as [Default],
231: COLUMNPROPERTY(OBJECT_ID('" . ($schema ? $fulltable : $table) . "'), COLUMN_NAME, 'IsIdentity') as [Key],
232: NUMERIC_SCALE as Size
233: FROM INFORMATION_SCHEMA.COLUMNS
234: WHERE TABLE_NAME = '" . $table . "'" . ($schema ? " AND TABLE_SCHEMA = '" . $schema . "'" : '')
235: );
236:
237: if (!$cols) {
238: throw new CakeException(__d('cake_dev', 'Could not describe table for %s', $table));
239: }
240:
241: while ($column = $cols->fetch(PDO::FETCH_OBJ)) {
242: $field = $column->Field;
243: $fields[$field] = array(
244: 'type' => $this->column($column),
245: 'null' => ($column->Null === 'YES' ? true : false),
246: 'default' => $column->Default,
247: 'length' => $this->length($column),
248: 'key' => ($column->Key == '1') ? 'primary' : false
249: );
250:
251: if ($fields[$field]['default'] === 'null') {
252: $fields[$field]['default'] = null;
253: }
254: if ($fields[$field]['default'] !== null) {
255: $fields[$field]['default'] = preg_replace(
256: "/^[(]{1,2}'?([^')]*)?'?[)]{1,2}$/",
257: "$1",
258: $fields[$field]['default']
259: );
260: $this->value($fields[$field]['default'], $fields[$field]['type']);
261: }
262:
263: if ($fields[$field]['key'] !== false && $fields[$field]['type'] === 'integer') {
264: $fields[$field]['length'] = 11;
265: } elseif ($fields[$field]['key'] === false) {
266: unset($fields[$field]['key']);
267: }
268: if (in_array($fields[$field]['type'], array('date', 'time', 'datetime', 'timestamp'))) {
269: $fields[$field]['length'] = null;
270: }
271: if ($fields[$field]['type'] === 'float' && !empty($column->Size)) {
272: $fields[$field]['length'] = $fields[$field]['length'] . ',' . $column->Size;
273: }
274: }
275: $this->_cacheDescription($table, $fields);
276: $cols->closeCursor();
277: return $fields;
278: }
279:
280: 281: 282: 283: 284: 285: 286: 287: 288:
289: public function fields(Model $model, $alias = null, $fields = array(), $quote = true) {
290: if (empty($alias)) {
291: $alias = $model->alias;
292: }
293: $fields = parent::fields($model, $alias, $fields, false);
294: $count = count($fields);
295:
296: if ($count >= 1 && strpos($fields[0], 'COUNT(*)') === false) {
297: $result = array();
298: for ($i = 0; $i < $count; $i++) {
299: $prepend = '';
300:
301: if (strpos($fields[$i], 'DISTINCT') !== false && strpos($fields[$i], 'COUNT') === false) {
302: $prepend = 'DISTINCT ';
303: $fields[$i] = trim(str_replace('DISTINCT', '', $fields[$i]));
304: }
305: if (strpos($fields[$i], 'COUNT(DISTINCT') !== false) {
306: $prepend = 'COUNT(DISTINCT ';
307: $fields[$i] = trim(str_replace('COUNT(DISTINCT', '', $this->_quoteFields($fields[$i])));
308: }
309:
310: if (!preg_match('/\s+AS\s+/i', $fields[$i])) {
311: if (substr($fields[$i], -1) === '*') {
312: if (strpos($fields[$i], '.') !== false && $fields[$i] != $alias . '.*') {
313: $build = explode('.', $fields[$i]);
314: $AssociatedModel = $model->{$build[0]};
315: } else {
316: $AssociatedModel = $model;
317: }
318:
319: $_fields = $this->fields($AssociatedModel, $AssociatedModel->alias, array_keys($AssociatedModel->schema()));
320: $result = array_merge($result, $_fields);
321: continue;
322: }
323:
324: if (strpos($fields[$i], '.') === false) {
325: $this->_fieldMappings[$alias . '__' . $fields[$i]] = $alias . '.' . $fields[$i];
326: $fieldName = $this->name($alias . '.' . $fields[$i]);
327: $fieldAlias = $this->name($alias . '__' . $fields[$i]);
328: } else {
329: $build = explode('.', $fields[$i]);
330: $build[0] = trim($build[0], '[]');
331: $build[1] = trim($build[1], '[]');
332: $name = $build[0] . '.' . $build[1];
333: $alias = $build[0] . '__' . $build[1];
334:
335: $this->_fieldMappings[$alias] = $name;
336: $fieldName = $this->name($name);
337: $fieldAlias = $this->name($alias);
338: }
339: if ($model->getColumnType($fields[$i]) === 'datetime') {
340: $fieldName = "CONVERT(VARCHAR(20), {$fieldName}, 20)";
341: }
342: $fields[$i] = "{$fieldName} AS {$fieldAlias}";
343: }
344: $result[] = $prepend . $fields[$i];
345: }
346: return $result;
347: }
348: return $fields;
349: }
350:
351: 352: 353: 354: 355: 356: 357: 358: 359: 360:
361: public function create(Model $model, $fields = null, $values = null) {
362: if (!empty($values)) {
363: $fields = array_combine($fields, $values);
364: }
365: $primaryKey = $this->_getPrimaryKey($model);
366:
367: if (array_key_exists($primaryKey, $fields)) {
368: if (empty($fields[$primaryKey])) {
369: unset($fields[$primaryKey]);
370: } else {
371: $this->_execute('SET IDENTITY_INSERT ' . $this->fullTableName($model) . ' ON');
372: }
373: }
374: $result = parent::create($model, array_keys($fields), array_values($fields));
375: if (array_key_exists($primaryKey, $fields) && !empty($fields[$primaryKey])) {
376: $this->_execute('SET IDENTITY_INSERT ' . $this->fullTableName($model) . ' OFF');
377: }
378: return $result;
379: }
380:
381: 382: 383: 384: 385: 386: 387: 388: 389: 390:
391: public function update(Model $model, $fields = array(), $values = null, $conditions = null) {
392: if (!empty($values)) {
393: $fields = array_combine($fields, $values);
394: }
395: if (isset($fields[$model->primaryKey])) {
396: unset($fields[$model->primaryKey]);
397: }
398: if (empty($fields)) {
399: return true;
400: }
401: return parent::update($model, array_keys($fields), array_values($fields), $conditions);
402: }
403:
404: 405: 406: 407: 408: 409: 410:
411: public function limit($limit, $offset = null) {
412: if ($limit) {
413: $rt = '';
414: if (!strpos(strtolower($limit), 'top') || strpos(strtolower($limit), 'top') === 0) {
415: $rt = ' TOP';
416: }
417: $rt .= sprintf(' %u', $limit);
418: if ((is_int($offset) || ctype_digit($offset)) && $offset > 0) {
419: $rt = sprintf(' OFFSET %u ROWS FETCH FIRST %u ROWS ONLY', $offset, $limit);
420: }
421: return $rt;
422: }
423: return null;
424: }
425:
426: 427: 428: 429: 430: 431: 432:
433: public function column($real) {
434: $limit = null;
435: $col = $real;
436: if (is_object($real) && isset($real->Field)) {
437: $limit = $real->Length;
438: $col = $real->Type;
439: }
440:
441: if ($col === 'datetime2') {
442: return 'datetime';
443: }
444: if (in_array($col, array('date', 'time', 'datetime', 'timestamp'))) {
445: return $col;
446: }
447: if ($col === 'bit') {
448: return 'boolean';
449: }
450: if (strpos($col, 'bigint') !== false) {
451: return 'biginteger';
452: }
453: if (strpos($col, 'smallint') !== false) {
454: return 'smallinteger';
455: }
456: if (strpos($col, 'tinyint') !== false) {
457: return 'tinyinteger';
458: }
459: if (strpos($col, 'int') !== false) {
460: return 'integer';
461: }
462: if (strpos($col, 'char') !== false && $limit == -1) {
463: return 'text';
464: }
465: if (strpos($col, 'char') !== false) {
466: return 'string';
467: }
468: if (strpos($col, 'text') !== false) {
469: return 'text';
470: }
471: if (strpos($col, 'binary') !== false || $col === 'image') {
472: return 'binary';
473: }
474: if (in_array($col, array('float', 'real'))) {
475: return 'float';
476: }
477: if (in_array($col, array('decimal', 'numeric'))) {
478: return 'decimal';
479: }
480: return 'text';
481: }
482:
483: 484: 485: 486: 487: 488: 489:
490: public function length($length) {
491: if (is_object($length) && isset($length->Length)) {
492: if ($length->Length == -1 && strpos($length->Type, 'char') !== false) {
493: return null;
494: }
495: if (in_array($length->Type, array('nchar', 'nvarchar'))) {
496: return floor($length->Length / 2);
497: }
498: if ($length->Type === 'text') {
499: return null;
500: }
501: return $length->Length;
502: }
503: return parent::length($length);
504: }
505:
506: 507: 508: 509: 510: 511:
512: public function resultSet($results) {
513: $this->map = array();
514: $numFields = $results->columnCount();
515: $index = 0;
516:
517: while ($numFields-- > 0) {
518: $column = $results->getColumnMeta($index);
519: $name = $column['name'];
520:
521: if (strpos($name, '__')) {
522: if (isset($this->_fieldMappings[$name]) && strpos($this->_fieldMappings[$name], '.')) {
523: $map = explode('.', $this->_fieldMappings[$name]);
524: } elseif (isset($this->_fieldMappings[$name])) {
525: $map = array(0, $this->_fieldMappings[$name]);
526: } else {
527: $map = array(0, $name);
528: }
529: } else {
530: $map = array(0, $name);
531: }
532: $map[] = ($column['sqlsrv:decl_type'] === 'bit') ? 'boolean' : $column['native_type'];
533: $this->map[$index++] = $map;
534: }
535: }
536:
537: 538: 539: 540: 541: 542: 543:
544: public function renderStatement($type, $data) {
545: switch (strtolower($type)) {
546: case 'select':
547: extract($data);
548: $fields = trim($fields);
549:
550: $having = !empty($having) ? " $having" : '';
551: $lock = !empty($lock) ? " $lock" : '';
552:
553: if (strpos($limit, 'TOP') !== false && strpos($fields, 'DISTINCT ') === 0) {
554: $limit = 'DISTINCT ' . trim($limit);
555: $fields = substr($fields, 9);
556: }
557:
558:
559: if ($limit && !$order) {
560: $order = 'ORDER BY (SELECT NULL)';
561: }
562:
563:
564: if (version_compare($this->getVersion(), '11', '<') && preg_match('/FETCH\sFIRST\s+([0-9]+)/i', $limit, $offset)) {
565: preg_match('/OFFSET\s*(\d+)\s*.*?(\d+)\s*ROWS/', $limit, $limitOffset);
566:
567: $limit = 'TOP ' . (int)$limitOffset[2];
568: $page = (int)($limitOffset[1] / $limitOffset[2]);
569: $offset = (int)($limitOffset[2] * $page);
570:
571: $rowCounter = static::ROW_COUNTER;
572: $sql = "SELECT {$limit} * FROM (
573: SELECT {$fields}, ROW_NUMBER() OVER ({$order}) AS {$rowCounter}
574: FROM {$table} {$alias}{$lock} {$joins} {$conditions} {$group}{$having}
575: ) AS _cake_paging_
576: WHERE _cake_paging_.{$rowCounter} > {$offset}
577: ORDER BY _cake_paging_.{$rowCounter}
578: ";
579: return trim($sql);
580: }
581: if (strpos($limit, 'FETCH') !== false) {
582: return trim("SELECT {$fields} FROM {$table} {$alias}{$lock} {$joins} {$conditions} {$group}{$having} {$order} {$limit}");
583: }
584: return trim("SELECT {$limit} {$fields} FROM {$table} {$alias}{$lock} {$joins} {$conditions} {$group}{$having} {$order}");
585: case "schema":
586: extract($data);
587:
588: foreach ($indexes as $i => $index) {
589: if (preg_match('/PRIMARY KEY/', $index)) {
590: unset($indexes[$i]);
591: break;
592: }
593: }
594:
595: foreach (array('columns', 'indexes') as $var) {
596: if (is_array(${$var})) {
597: ${$var} = "\t" . implode(",\n\t", array_filter(${$var}));
598: }
599: }
600: return trim("CREATE TABLE {$table} (\n{$columns});\n{$indexes}");
601: default:
602: return parent::renderStatement($type, $data);
603: }
604: }
605:
606: 607: 608: 609: 610: 611: 612: 613:
614: public function value($data, $column = null, $null = true) {
615: if ($data === null || is_array($data) || is_object($data)) {
616: return parent::value($data, $column, $null);
617: }
618: if (in_array($data, array('{$__cakeID__$}', '{$__cakeForeignKey__$}'), true)) {
619: return $data;
620: }
621:
622: if (empty($column)) {
623: $column = $this->introspectType($data);
624: }
625:
626: switch ($column) {
627: case 'string':
628: case 'text':
629: return 'N' . $this->_connection->quote($data, PDO::PARAM_STR);
630: default:
631: return parent::value($data, $column, $null);
632: }
633: }
634:
635: 636: 637: 638: 639: 640: 641: 642: 643:
644: public function read(Model $model, $queryData = array(), $recursive = null) {
645: $results = parent::read($model, $queryData, $recursive);
646: $this->_fieldMappings = array();
647: return $results;
648: }
649:
650: 651: 652: 653: 654: 655:
656: public function fetchResult() {
657: if ($row = $this->_result->fetch(PDO::FETCH_NUM)) {
658: $resultRow = array();
659: foreach ($this->map as $col => $meta) {
660: list($table, $column, $type) = $meta;
661: if ($table === 0 && $column === static::ROW_COUNTER) {
662: continue;
663: }
664: $resultRow[$table][$column] = $row[$col];
665: if ($type === 'boolean' && $row[$col] !== null) {
666: $resultRow[$table][$column] = $this->boolean($resultRow[$table][$column]);
667: }
668: }
669: return $resultRow;
670: }
671: $this->_result->closeCursor();
672: return false;
673: }
674:
675: 676: 677: 678: 679: 680: 681: 682:
683: public function insertMulti($table, $fields, $values) {
684: $primaryKey = $this->_getPrimaryKey($table);
685: $hasPrimaryKey = $primaryKey && (
686: (is_array($fields) && in_array($primaryKey, $fields)
687: || (is_string($fields) && strpos($fields, $this->startQuote . $primaryKey . $this->endQuote) !== false))
688: );
689:
690: if ($hasPrimaryKey) {
691: $this->_execute('SET IDENTITY_INSERT ' . $this->fullTableName($table) . ' ON');
692: }
693:
694: parent::insertMulti($table, $fields, $values);
695:
696: if ($hasPrimaryKey) {
697: $this->_execute('SET IDENTITY_INSERT ' . $this->fullTableName($table) . ' OFF');
698: }
699: }
700:
701: 702: 703: 704: 705: 706: 707: 708:
709: public function buildColumn($column) {
710: $result = parent::buildColumn($column);
711: $result = preg_replace('/(bigint|int|integer)\([0-9]+\)/i', '$1', $result);
712: $result = preg_replace('/(bit)\([0-9]+\)/i', '$1', $result);
713: if (strpos($result, 'DEFAULT NULL') !== false) {
714: if (isset($column['default']) && $column['default'] === '') {
715: $result = str_replace('DEFAULT NULL', "DEFAULT ''", $result);
716: } else {
717: $result = str_replace('DEFAULT NULL', 'NULL', $result);
718: }
719: } elseif (array_keys($column) === array('type', 'name')) {
720: $result .= ' NULL';
721: } elseif (strpos($result, "DEFAULT N'")) {
722: $result = str_replace("DEFAULT N'", "DEFAULT '", $result);
723: }
724: return $result;
725: }
726:
727: 728: 729: 730: 731: 732: 733:
734: public function buildIndex($indexes, $table = null) {
735: $join = array();
736:
737: foreach ($indexes as $name => $value) {
738: if ($name === 'PRIMARY') {
739: $join[] = 'PRIMARY KEY (' . $this->name($value['column']) . ')';
740: } elseif (isset($value['unique']) && $value['unique']) {
741: $out = "ALTER TABLE {$table} ADD CONSTRAINT {$name} UNIQUE";
742:
743: if (is_array($value['column'])) {
744: $value['column'] = implode(', ', array_map(array(&$this, 'name'), $value['column']));
745: } else {
746: $value['column'] = $this->name($value['column']);
747: }
748: $out .= "({$value['column']});";
749: $join[] = $out;
750: }
751: }
752: return $join;
753: }
754:
755: 756: 757: 758: 759: 760:
761: protected function _getPrimaryKey($model) {
762: $schema = $this->describe($model);
763: foreach ($schema as $field => $props) {
764: if (isset($props['key']) && $props['key'] === 'primary') {
765: return $field;
766: }
767: }
768: return null;
769: }
770:
771: 772: 773: 774: 775: 776: 777:
778: public function lastAffected($source = null) {
779: $affected = parent::lastAffected();
780: if ($affected === null && $this->_lastAffected !== false) {
781: return $this->_lastAffected;
782: }
783: return $affected;
784: }
785:
786: 787: 788: 789: 790: 791: 792: 793: 794: 795:
796: protected function _execute($sql, $params = array(), $prepareOptions = array()) {
797: $this->_lastAffected = false;
798: $sql = trim($sql);
799: if (strncasecmp($sql, 'SELECT', 6) === 0 || preg_match('/^EXEC(?:UTE)?\s/mi', $sql) > 0) {
800: $prepareOptions += array(PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL);
801: return parent::_execute($sql, $params, $prepareOptions);
802: }
803: try {
804: $this->_lastAffected = $this->_connection->exec($sql);
805: if ($this->_lastAffected === false) {
806: $this->_results = null;
807: $error = $this->_connection->errorInfo();
808: $this->error = $error[2];
809: return false;
810: }
811: return true;
812: } catch (PDOException $e) {
813: if (isset($query->queryString)) {
814: $e->queryString = $query->queryString;
815: } else {
816: $e->queryString = $sql;
817: }
818: throw $e;
819: }
820: }
821:
822: 823: 824: 825: 826: 827:
828: protected function _dropTable($table) {
829: return "IF OBJECT_ID('" . $this->fullTableName($table, false) . "', 'U') IS NOT NULL DROP TABLE " . $this->fullTableName($table) . ";";
830: }
831:
832: 833: 834: 835: 836:
837: public function getSchemaName() {
838: return $this->config['schema'];
839: }
840:
841: 842: 843: 844: 845: 846: 847: 848:
849: public function getLockingHint($mode) {
850: if ($mode !== true) {
851: return null;
852: }
853: return ' WITH (UPDLOCK)';
854: }
855: }
856: