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