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