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