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