schema.inc 61 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726
  1. <?php
  2. include_once 'fastcache.inc';
  3. /**
  4. * @filefe
  5. * Database schema code for Microsoft SQL Server database servers.
  6. */
  7. /**
  8. * @ingroup schemaapi
  9. * @{
  10. */
  11. class DatabaseSchema_sqlsrv extends DatabaseSchema {
  12. // Override to take into account the
  13. // default schema for MS SQL.
  14. public function __construct($connection) {
  15. $this->LoadDefaultSchema($connection);
  16. parent::__construct($connection);
  17. }
  18. /**
  19. * Get the current database schema.
  20. *
  21. * @return string
  22. */
  23. public function GetDefaultSchema() {
  24. return $this->defaultSchema;
  25. }
  26. /**
  27. * Default recommended collation for SQL Server.
  28. */
  29. const DEFAULT_COLLATION_CI = 'Latin1_General_CI_AI';
  30. /**
  31. * Default recommended collation for SQL Server.
  32. * when case sensitivity is required.
  33. */
  34. const DEFAULT_COLLATION_CS = 'Latin1_General_CS_AI';
  35. // Name for the technical column used for computed keys
  36. // or technical primary key.
  37. // IMPORTANT: They both start with "__" because the
  38. // statement class will remove those columns from the final
  39. // result set.
  40. // This should be constants, but we are using variable to ease
  41. // their use in inline strings.
  42. var $COMPUTED_PK_COLUMN_NAME = '__pkc';
  43. var $COMPUTED_PK_COLUMN_INDEX = '__ix_pkc';
  44. var $TECHNICAL_PK_COLUMN_NAME = '__pk';
  45. /**
  46. * Returns a list of functions that are not
  47. * available by default on SQL Server, but used
  48. * in Drupal Core or contributed modules
  49. * because they are available in other databases
  50. * such as MySQL.
  51. */
  52. public function DrupalSpecificFunctions() {
  53. if ($cache = fastcache::cache_get('drupal_specific_functions', 'schema')) {
  54. return $cache->data;
  55. }
  56. $functions = array(
  57. 'SUBSTRING',
  58. 'SUBSTRING_INDEX',
  59. 'GREATEST',
  60. 'MD5',
  61. 'LPAD',
  62. 'GROUP_CONCAT',
  63. 'CONCAT',
  64. 'IF',
  65. 'CONNECTION_ID'
  66. );
  67. // Since SQL Server 2012 (11), there
  68. // is a native CONCAT implementation
  69. if ($this->EngineVersionNumber() >= 11) {
  70. $functions = array_diff($functions, array('CONCAT'));
  71. }
  72. fastcache::cache_set('drupal_specific_functions', $functions, 'schema');
  73. return $functions;
  74. }
  75. /**
  76. * Load the real default schema name for this connection.
  77. */
  78. protected function LoadDefaultSchema($connection) {
  79. if ($cache = fastcache::cache_get('default_schema', 'schema')) {
  80. $this->defaultSchema = $cache->data;
  81. return;
  82. }
  83. $result = $connection->query_direct("SELECT SCHEMA_NAME()")->fetchField();
  84. fastcache::cache_set('default_schema', $result, 'schema');
  85. $this->defaultSchema = $result;
  86. }
  87. /**
  88. * Clear introspection cache for a specific table.
  89. *
  90. * @param mixed $table
  91. */
  92. protected function queryColumnInformationInvalidate($table) {
  93. fastcache::cache_clear_all('queryColumnInformation:' . $table, 'schema_queryColumnInformation');
  94. }
  95. /**
  96. * Database introspection: fetch technical information about a table.
  97. *
  98. * @return
  99. * An array with the following structure:
  100. * - blobs[]: Array of column names that should be treated as blobs in this table.
  101. * - identities[]: Array of column names that are identities in this table.
  102. * - identity: The name of the identity column
  103. * - columns[]: An array of specification details for the columns
  104. * - name: Column name.
  105. * - max_length: Maximum length.
  106. * - precision: Precision.
  107. * - collation_name: Collation.
  108. * - is_nullable: Is nullable.
  109. * - is_ansi_padded: Is ANSI padded.
  110. * - is_identity: Is identity.
  111. * - definition: If a computed column, the computation formulae.
  112. * - default_value: Default value for the column (if any).
  113. */
  114. public function queryColumnInformation($table, $refresh = FALSE) {
  115. // No worry for the tableExists() check, results
  116. // are cached.
  117. if (empty($table) || !$this->tableExists($table)) {
  118. return array();
  119. }
  120. $table_info = $this->getPrefixInfo($table);
  121. // We could adapt the current code to support temporary table introspection, but
  122. // for now this is not supported.
  123. if ($table_info['table'][0] == '#') {
  124. throw new Exception('Temporary table introspection is not supported.');
  125. }
  126. if ($cache = fastcache::cache_get('queryColumnInformation:' . $table, 'schema_queryColumnInformation')) {
  127. return $cache->data;
  128. }
  129. $info = array();
  130. // Don't use {} around information_schema.columns table.
  131. $result = $this->connection->query_direct("SELECT sysc.name, sysc.max_length, sysc.precision, sysc.collation_name,
  132. sysc.is_nullable, sysc.is_ansi_padded, sysc.is_identity, sysc.is_computed, TYPE_NAME(sysc.user_type_id) as type,
  133. syscc.definition,
  134. sm.[text] as default_value
  135. FROM sys.columns AS sysc
  136. INNER JOIN sys.syscolumns AS sysc2 ON sysc.object_id = sysc2.id and sysc.name = sysc2.name
  137. LEFT JOIN sys.computed_columns AS syscc ON sysc.object_id = syscc.object_id AND sysc.name = syscc.name
  138. LEFT JOIN sys.syscomments sm ON sm.id = sysc2.cdefault
  139. WHERE sysc.object_id = OBJECT_ID(:table)
  140. ",
  141. array(':table' => $table_info['schema'] . '.' . $table_info['table']));
  142. foreach ($result as $column) {
  143. if ($column->type == 'varbinary') {
  144. $info['blobs'][$column->name] = TRUE;
  145. }
  146. $info['columns'][$column->name] = (array) $column;
  147. // Provide a clean list of columns that excludes the ones internally created by the
  148. // database driver.
  149. if (!(isset($column->name[1]) && substr($column->name, 0, 2) == "__")) {
  150. $info['columns_clean'][$column->name] = (array) $column;
  151. }
  152. }
  153. // If we have computed columns, it is important to know what other columns they depend on!
  154. $column_names = array_keys($info['columns']);
  155. $column_regex = implode('|', $column_names);
  156. foreach($info['columns'] as &$column) {
  157. $dependencies = array();
  158. if (!empty($column['definition'])) {
  159. $matches = array();
  160. if (preg_match_all("/\[[{$column_regex}\]]*\]/", $column['definition'], $matches) > 0) {
  161. $dependencies = array_map(function($m) { return trim($m, "[]"); }, array_shift($matches));
  162. }
  163. }
  164. $column['dependencies'] = array_flip($dependencies);
  165. }
  166. // Don't use {} around system tables.
  167. $result = $this->connection->query_direct('SELECT name FROM sys.identity_columns WHERE object_id = OBJECT_ID(:table)', array(':table' => $table_info['schema'] . '.' . $table_info['table']));
  168. unset($column);
  169. $info['identities'] = array();
  170. $info['identity'] = NULL;
  171. foreach ($result as $column) {
  172. $info['identities'][$column->name] = $column->name;
  173. $info['identity'] = $column->name;
  174. }
  175. // Now introspect information about indexes
  176. $result = $this->connection->query_direct("select tab.[name] as [table_name],
  177. idx.[name] as [index_name],
  178. allc.[name] as [column_name],
  179. idx.[type_desc],
  180. idx.[is_unique],
  181. idx.[data_space_id],
  182. idx.[ignore_dup_key],
  183. idx.[is_primary_key],
  184. idx.[is_unique_constraint],
  185. idx.[fill_factor],
  186. idx.[is_padded],
  187. idx.[is_disabled],
  188. idx.[is_hypothetical],
  189. idx.[allow_row_locks],
  190. idx.[allow_page_locks],
  191. idxc.[is_descending_key],
  192. idxc.[is_included_column],
  193. idxc.[index_column_id],
  194. idxc.[key_ordinal]
  195. FROM sys.[tables] as tab
  196. INNER join sys.[indexes] idx ON tab.[object_id] = idx.[object_id]
  197. INNER join sys.[index_columns] idxc ON idx.[object_id] = idxc.[object_id] and idx.[index_id] = idxc.[index_id]
  198. INNER join sys.[all_columns] allc ON tab.[object_id] = allc.[object_id] and idxc.[column_id] = allc.[column_id]
  199. WHERE tab.object_id = OBJECT_ID(:table)
  200. ORDER BY tab.[name], idx.[index_id], idxc.[index_column_id]
  201. ",
  202. array(':table' => $table_info['schema'] . '.' . $table_info['table']));
  203. foreach ($result as $index_column) {
  204. if (!isset($info['indexes'][$index_column->index_name])) {
  205. $ic = clone $index_column;
  206. // Only retain index specific details.
  207. unset($ic->column_name);
  208. unset($ic->index_column_id);
  209. unset($ic->is_descending_key);
  210. unset($ic->table_name);
  211. unset($ic->key_ordinal);
  212. $info['indexes'][$index_column->index_name] = (array) $ic;
  213. if ($index_column->is_primary_key) {
  214. $info['primary_key_index'] = $ic->index_name;
  215. }
  216. }
  217. $index = &$info['indexes'][$index_column->index_name];
  218. $index['columns'][$index_column->key_ordinal] = array(
  219. 'name' => $index_column->column_name,
  220. 'is_descending_key' => $index_column->is_descending_key,
  221. 'key_ordinal' => $index_column->key_ordinal,
  222. );
  223. // Every columns keeps track of what indexes it is part of.
  224. $info['columns'][$index_column->column_name]['indexes'][] = $index_column->index_name;
  225. if (isset($info['columns_clean'][$index_column->column_name])) {
  226. $info['columns_clean'][$index_column->column_name]['indexes'][] = $index_column->index_name;
  227. }
  228. }
  229. fastcache::cache_set('queryColumnInformation:' . $table, $info, 'schema_queryColumnInformation');
  230. return $info;
  231. }
  232. /**
  233. * {@Inheritdoc}
  234. */
  235. public function createTable($name, $table) {
  236. if ($this->tableExists($name, FALSE)) {
  237. throw new DatabaseSchemaObjectExistsException(t('Table %name already exists.', array('%name' => $name)));
  238. }
  239. // Reset caches after calling tableExists() otherwise it's results get cached again before
  240. // the table is created.
  241. $this->queryColumnInformationInvalidate($name);
  242. fastcache::cache_clear_all('*', 'tableExists', TRUE);
  243. // Build the table and its unique keys in a transaction, and fail the whole
  244. // creation in case of an error.
  245. $transaction = $this->connection->startTransaction(NULL, DatabaseTransactionSettings::GetDDLCompatibleDefaults());
  246. // Create the table with a default technical primary key.
  247. // $this->createTableSql already prefixes the table name, and we must inhibit prefixing at the query level
  248. // because field default _context_menu_block_active_values definitions can contain string literals with braces.
  249. $this->connection->query_direct($this->createTableSql($name, $table), array(), array('prefix_tables' => FALSE));
  250. // If the spec had a primary key, set it now after all fields have been created.
  251. // We are creating the keys after creating the table so that createPrimaryKey
  252. // is able to introspect column definition from the database to calculate index sizes
  253. // This adds quite quite some overhead, but is only noticeable during table creation.
  254. if (isset($table['primary key']) && is_array($table['primary key'])) {
  255. $this->createPrimaryKey($name, $table['primary key']);
  256. }
  257. // Otherwise use a technical primary key.
  258. else {
  259. $this->createTechnicalPrimaryColumn($name);
  260. }
  261. // Now all the unique keys.
  262. if (isset($table['unique keys']) && is_array($table['unique keys'])) {
  263. foreach ($table['unique keys'] as $key_name => $key) {
  264. $this->addUniqueKey($name, $key_name, $key);
  265. }
  266. }
  267. // Commit changes until now.
  268. $transaction->commit();
  269. // Create the indexes but ignore any error during the creation. We do that
  270. // do avoid pulling the carpet under modules that try to implement indexes
  271. // with invalid data types (long columns), before we come up with a better
  272. // solution.
  273. if (isset($table['indexes']) && is_array($table['indexes'])) {
  274. foreach ($table['indexes'] as $key_name => $key) {
  275. try {
  276. $this->addIndex($name, $key_name, $key);
  277. }
  278. catch (Exception $e) {
  279. // Log the exception but do not rollback the transaction.
  280. watchdog_exception('database', $e);
  281. }
  282. }
  283. }
  284. // Invalidate introspection cache.
  285. $this->queryColumnInformationInvalidate($name);
  286. }
  287. /**
  288. * Remove comments from an SQL statement.
  289. * @see http://stackoverflow.com/questions/9690448/regular-expression-to-remove-comments-from-sql-statement
  290. *
  291. * @param mixed $sql
  292. * SQL statement to remove the comments from.
  293. *
  294. * @param mixed $comments
  295. * Comments removed from the statement
  296. *
  297. * @return string
  298. */
  299. public function removeSQLComments($sql, &$comments = NULL) {
  300. $sqlComments = '@(([\'"]).*?[^\\\]\2)|((?:\#|--).*?$|/\*(?:[^/*]|/(?!\*)|\*(?!/)|(?R))*\*\/)\s*|(?<=;)\s+@ms';
  301. /* Commented version
  302. $sqlComments = '@
  303. (([\'"]).*?[^\\\]\2) # $1 : Skip single & double quoted expressions
  304. |( # $3 : Match comments
  305. (?:\#|--).*?$ # - Single line comments
  306. | # - Multi line (nested) comments
  307. /\* # . comment open marker
  308. (?: [^/*] # . non comment-marker characters
  309. |/(?!\*) # . ! not a comment open
  310. |\*(?!/) # . ! not a comment close
  311. |(?R) # . recursive case
  312. )* # . repeat eventually
  313. \*\/ # . comment close marker
  314. )\s* # Trim after comments
  315. |(?<=;)\s+ # Trim after semi-colon
  316. @msx';
  317. */
  318. $uncommentedSQL = trim(preg_replace($sqlComments, '$1', $sql));
  319. if (is_array($comments)) {
  320. preg_match_all($sqlComments, $sql, $comments);
  321. $comments = array_filter($comments[ 3 ]);
  322. }
  323. return $uncommentedSQL;
  324. }
  325. /**
  326. * Find if a table already exists. Results are cached, use
  327. * $reset = TRUE to get a fresh copy.
  328. *
  329. * @param $table
  330. * Name of the table.
  331. * @return
  332. * True if the table exists, false otherwise.
  333. */
  334. public function tableExists($table, $reset = FALSE) {
  335. // Do not cache temporary tables (#)
  336. if (!$reset && $table[0] != '#' && $cache = fastcache::cache_get($table, 'tableExists')) {
  337. return $cache->data;
  338. }
  339. // Temporary tables and regular tables cannot be verified in the same way.
  340. $query = NULL;
  341. if ($table[0] == '#') {
  342. $query = "SELECT 1 FROM tempdb.sys.tables WHERE name like '" . $this->connection->prefixTables('{' . $table . '}') . "%'";
  343. }
  344. else {
  345. $query = "SELECT 1 FROM INFORMATION_SCHEMA.tables WHERE table_name = '" . $this->connection->prefixTables('{' . $table . '}') . "'";
  346. }
  347. $exists = $this->connection
  348. ->query_direct($query)
  349. ->fetchField() !== FALSE;
  350. if ($table[0] != '#') {
  351. fastcache::cache_set($table, $exists, 'tableExists');
  352. }
  353. return $exists;
  354. }
  355. /**
  356. * Returns an array of current connection user options
  357. *
  358. * textsize 2147483647
  359. * language us_english
  360. * dateformat mdy
  361. * datefirst 7
  362. * lock_timeout -1
  363. * quoted_identifier SET
  364. * arithabort SET
  365. * ansi_null_dflt_on SET
  366. * ansi_warnings SET
  367. * ansi_padding SET
  368. * ansi_nulls SET
  369. * concat_null_yields_null SET
  370. * isolation level read committed
  371. *
  372. * @return mixed
  373. */
  374. public function UserOptions() {
  375. return $this->connection->query_direct('DBCC UserOptions')->fetchAllKeyed();
  376. }
  377. /**
  378. * Retrieve Engine Version information.
  379. */
  380. public function EngineVersion() {
  381. if ($cache = fastcache::cache_get('EngineVersion', 'schema')) {
  382. return $cache->data;
  383. }
  384. $version = $this->connection
  385. ->query_direct(<<< EOF
  386. SELECT CONVERT (varchar,SERVERPROPERTY('productversion')) AS VERSION,
  387. CONVERT (varchar,SERVERPROPERTY('productlevel')) AS LEVEL,
  388. CONVERT (varchar,SERVERPROPERTY('edition')) AS EDITION
  389. EOF
  390. )->fetchAssoc();
  391. fastcache::cache_set('EngineVersion', $version, 'schema');
  392. return $version;
  393. }
  394. /**
  395. * Retrieve Major Engine Version Number as integer.
  396. */
  397. public function EngineVersionNumber() {
  398. $version = $this->EngineVersion();
  399. $start = strpos($version['VERSION'], '.');
  400. return intval(substr($version['VERSION'], 0, $start));
  401. }
  402. /**
  403. * Find if a table function exists.
  404. *
  405. * @param $function
  406. * Name of the function.
  407. * @return
  408. * True if the function exists, false otherwise.
  409. */
  410. public function functionExists($function) {
  411. // FN = Scalar Function
  412. // IF = Inline Table Function
  413. // TF = Table Function
  414. // FS | AF = Assembly (CLR) Scalar Function
  415. // FT | AT = Assembly (CLR) Table Valued Function
  416. return $this->connection
  417. ->query_direct("SELECT 1 FROM sys.objects WHERE object_id = OBJECT_ID('" . $function . "') AND type in (N'FN', N'IF', N'TF', N'FS', N'FT', N'AF')")
  418. ->fetchField() !== FALSE;
  419. }
  420. /**
  421. * Check if CLR is enabled, required
  422. * to run GROUP_CONCAT.
  423. */
  424. public function CLREnabled() {
  425. return $this->connection
  426. ->query_direct("SELECT CONVERT(int, [value]) as [enabled] FROM sys.configurations WHERE name = 'clr enabled'")
  427. ->fetchField() !== 1;
  428. }
  429. /**
  430. * Check if a column is of variable length.
  431. */
  432. private function isVariableLengthType($type) {
  433. $types = array('nvarchar' => TRUE, 'ntext' => TRUE, 'varchar' => TRUE, 'varbinary' => TRUE, 'image' => TRUE);
  434. return isset($types[$type]);
  435. }
  436. /**
  437. * Retrieve an array of field specs from
  438. * an array of field names.
  439. *
  440. * @param array $fields
  441. * @param mixed $table
  442. */
  443. private function loadFieldsSpec(array $fields, $table) {
  444. $result = array();
  445. $info = $this->queryColumnInformation($table);
  446. foreach ($fields as $field) {
  447. $result[$field] = $info['columns'][$field];
  448. }
  449. return $result;
  450. }
  451. /**
  452. * Estimates the row size of a clustered index.
  453. * @see https://msdn.microsoft.com/en-us/library/ms178085.aspx
  454. */
  455. public function calculateClusteredIndexRowSizeBytes($table, $fields, $unique = TRUE) {
  456. // The fields must already be in the database to retrieve their real size.
  457. $info = $this->queryColumnInformation($table);
  458. // Specify the number of fixed-length and variable-length columns
  459. // and calculate the space that is required for their storage.
  460. $num_cols = count($fields);
  461. $num_variable_cols = 0;
  462. $max_var_size = 0;
  463. $max_fixed_size = 0;
  464. foreach ($fields as $field) {
  465. if ($this->isVariableLengthType($info['columns'][$field]['type'])) {
  466. $num_variable_cols++;
  467. $max_var_size += $info['columns'][$field]['max_length'];
  468. }
  469. else {
  470. $max_fixed_size += $info['columns'][$field]['max_length'];
  471. }
  472. }
  473. // If the clustered index is nonunique, account for the uniqueifier column.
  474. if (!$unique) {
  475. $num_cols++;
  476. $num_variable_cols++;
  477. $max_var_size += 4;
  478. }
  479. // Part of the row, known as the null bitmap, is reserved to manage column nullability. Calculate its size.
  480. $null_bitmap = 2 + (($num_cols + 7) / 8);
  481. // Calculate the variable-length data size.
  482. $variable_data_size = empty($num_variable_cols) ? 0 : 2 + ($num_variable_cols * 2) + $max_var_size;
  483. // Calculate total row size.
  484. $row_size = $max_fixed_size + $variable_data_size + $null_bitmap + 4;
  485. return $row_size;
  486. }
  487. /**
  488. * Change Database recovery model.
  489. */
  490. public function setRecoveryModel($model) {
  491. $this->connection->query_direct("ALTER " . $this->connection->options['name'] . " model SET RECOVERY " . $model);
  492. }
  493. /**
  494. * Drops the current primary key and creates
  495. * a new one. If the previous primary key
  496. * was an internal primary key, it tries to cleant it up.
  497. *
  498. * @param mixed $table
  499. * @param mixed $primary_key_sql
  500. */
  501. protected function recreatePrimaryKey($table, $fields) {
  502. // Drop the existing primary key if exists, if it was a TPK
  503. // it will get completely dropped.
  504. $this->cleanUpPrimaryKey($table);
  505. $this->createPrimaryKey($table, $fields);
  506. }
  507. /**
  508. * Create a Primary Key for the table, does not drop
  509. * any prior primary keys neither it takes care of cleaning
  510. * technical primary column. Only call this if you are sure
  511. * the table does not currently hold a primary key.
  512. *
  513. * @param string $table
  514. * @param mixed $fields
  515. * @param int $limit
  516. */
  517. private function createPrimaryKey($table, $fields, $limit = 900) {
  518. // To be on the safe side, on the most restrictive use case the limit
  519. // for a primary key clustered index is of 128 bytes (usually 900).
  520. // @see http://blogs.msdn.com/b/jgalla/archive/2005/08/18/453189.aspx
  521. // If that is going to be exceeded, use a computed column.
  522. $csv_fields = $this->createKeySql($fields);
  523. $size = $this->calculateClusteredIndexRowSizeBytes($table, $this->createKeySql($fields, TRUE));
  524. $result = array();
  525. $index = FALSE;
  526. // Add support for nullable columns in a primary key.
  527. $nullable = FALSE;
  528. $field_specs = $this->loadFieldsSpec($fields, $table);
  529. foreach ($field_specs as $field) {
  530. if ($field['is_nullable'] == TRUE) {
  531. $nullable = TRUE;
  532. break;
  533. }
  534. }
  535. if ($nullable || $size >= $limit) {
  536. // Use a computed column instead, and create a custom index.
  537. $result[] = "{$this->COMPUTED_PK_COLUMN_NAME} AS (CONVERT(VARCHAR(32), HASHBYTES('MD5', CONCAT('',{$csv_fields})), 2)) PERSISTED NOT NULL";
  538. $result[] = "CONSTRAINT {{$table}}_pkey PRIMARY KEY CLUSTERED ({$this->COMPUTED_PK_COLUMN_NAME})";
  539. $index = TRUE;
  540. }
  541. else {
  542. $result[] = "CONSTRAINT {{$table}}_pkey PRIMARY KEY CLUSTERED ({$csv_fields})";
  543. }
  544. $this->connection->query_direct('ALTER TABLE [{' . $table . '}] ADD ' . implode(' ', $result));
  545. // If we relied on a computed column for the Primary Key,
  546. // at least index the fields with a regular index.
  547. if ($index) {
  548. $this->addIndex($table, $this->COMPUTED_PK_COLUMN_INDEX, $fields);
  549. }
  550. // Invalidate current introspection.
  551. $this->queryColumnInformationInvalidate($table);
  552. }
  553. /**
  554. * Create the SQL needed to add a new technical primary key based on a
  555. * computed column.
  556. */
  557. private function createTechnicalPrimaryKeyIndexSql($table) {
  558. $result = array();
  559. $result[] = "{$this->TECHNICAL_PK_COLUMN_NAME} UNIQUEIDENTIFIER DEFAULT NEWID() NOT NULL";
  560. $result[] = "CONSTRAINT {{$table}}_pkey_technical PRIMARY KEY CLUSTERED ({$this->TECHNICAL_PK_COLUMN_NAME})";
  561. return implode(' ', $result);
  562. }
  563. /**
  564. * Generate SQL to create a new table from a Drupal schema definition.
  565. *
  566. * @param $name
  567. * The name of the table to create.
  568. * @param $table
  569. * A Schema API table definition array.
  570. * @return
  571. * The SQL statement to create the table.
  572. */
  573. protected function createTableSql($name, $table) {
  574. $sql_fields = array();
  575. foreach ($table['fields'] as $field_name => $field) {
  576. $sql_fields[] = $this->createFieldSql($name, $field_name, $this->processField($field));
  577. }
  578. // Use already prefixed table name.
  579. $table_prefixed = $this->connection->prefixTables('{' . $name . '}');
  580. $sql = "CREATE TABLE [{$table_prefixed}] (" . PHP_EOL;
  581. $sql .= implode("," . PHP_EOL, $sql_fields);
  582. $sql .= PHP_EOL . ")";
  583. return $sql;
  584. }
  585. /**
  586. * Create an SQL string for a field to be used in table creation or
  587. * alteration.
  588. *
  589. * Before passing a field out of a schema definition into this
  590. * function it has to be processed by _db_process_field().
  591. *
  592. *
  593. *
  594. * @param $table
  595. * The name of the table.
  596. * @param $name
  597. * Name of the field.
  598. * @param $spec
  599. * The field specification, as per the schema data structure format.
  600. */
  601. protected function createFieldSql($table, $name, $spec, $skip_checks = FALSE) {
  602. // Use a prefixed table.
  603. $table_prefixed = $this->connection->prefixTables('{' . $table . '}');
  604. $sql = $this->connection->quoteIdentifier($name) . ' ' . $spec['sqlsrv_type'];
  605. $is_text = in_array($spec['sqlsrv_type'], array('char', 'varchar', 'text', 'nchar', 'nvarchar', 'ntext'));
  606. if ($is_text === TRUE && !empty($spec['length'])) {
  607. $sql .= '(' . $spec['length'] . ')';
  608. }
  609. elseif (in_array($spec['sqlsrv_type'], array('numeric', 'decimal')) && isset($spec['precision']) && isset($spec['scale'])) {
  610. // Maximum precision for SQL Server 2008 orn greater is 38.
  611. // For previous versions it's 28.
  612. if ($spec['precision'] > 38) {
  613. watchdog('SQL Server Driver', "Field '@field' in table '@table' has had it's precision dropped from @precision to 38",
  614. array('@field' => $name,
  615. '@table' => $table,
  616. '@precision' => $spec['precision']
  617. )
  618. );
  619. $spec['precision'] = 38;
  620. }
  621. $sql .= '(' . $spec['precision'] . ', ' . $spec['scale'] . ')';
  622. }
  623. // When binary is true, case sensitivity is requested.
  624. if ($is_text === TRUE && isset($spec['binary']) && $spec['binary'] === TRUE) {
  625. $sql .= ' COLLATE ' . self::DEFAULT_COLLATION_CS;
  626. }
  627. if (isset($spec['not null']) && $spec['not null']) {
  628. $sql .= ' NOT NULL';
  629. }
  630. if (!$skip_checks) {
  631. if (isset($spec['default'])) {
  632. $default = $this->defaultValueExpression($spec['sqlsrv_type'], $spec['default']);
  633. $sql .= " CONSTRAINT {$table_prefixed}_{$name}_df DEFAULT $default";
  634. }
  635. if (!empty($spec['identity'])) {
  636. $sql .= ' IDENTITY';
  637. }
  638. if (!empty($spec['unsigned'])) {
  639. $sql .= ' CHECK (' . $this->connection->quoteIdentifier($name) . ' >= 0)';
  640. }
  641. }
  642. return $sql;
  643. }
  644. /**
  645. * Get the SQL expression for a default value.
  646. *
  647. * @param mixed $table
  648. * @param mixed $field
  649. * @param mixed $default
  650. */
  651. private function defaultValueExpression($sqlsr_type, $default) {
  652. // The actual expression depends on the target data type as it might require conversions.
  653. $result = is_string($default) ? "'" . addslashes($default) . "'" : $default;
  654. if (\DatabaseUtils::GetMSSQLType($sqlsr_type) == 'varbinary') {
  655. $default = addslashes($default);
  656. $result = "CONVERT({$sqlsr_type}, '{$default}')";
  657. }
  658. return $result;
  659. }
  660. /**
  661. * Returns a list of field names coma separated ready
  662. * to be used in a SQL Statement.
  663. *
  664. * @param array $fields
  665. * @param boolean $as_array
  666. * @return array|string
  667. */
  668. protected function createKeySql($fields, $as_array = FALSE) {
  669. $ret = array();
  670. foreach ($fields as $field) {
  671. if (is_array($field)) {
  672. $ret[] = $field[0];
  673. }
  674. else {
  675. $ret[] = $field;
  676. }
  677. }
  678. if ($as_array) {
  679. return $ret;
  680. }
  681. return implode(', ', $ret);
  682. }
  683. /**
  684. * Returns the SQL needed (incomplete) to create and index. Supports XML indexes.
  685. *
  686. * @param string $table
  687. * Table to create the index on.
  688. *
  689. * @param string $name
  690. * Name of the index.
  691. *
  692. * @param array $fields
  693. * Fields to be included in the Index.
  694. *
  695. * @return string
  696. */
  697. protected function createIndexSql($table, $name, $fields, &$xml_field) {
  698. // Get information about current columns.
  699. $info = $this->queryColumnInformation($table);
  700. // Flatten $fields array if neccesary.
  701. $fields = $this->createKeySql($fields, TRUE);
  702. // Look if an XML column is present in the fields list.
  703. $xml_field = NULL;
  704. foreach ($fields as $field) {
  705. if (isset($info['columns'][$field]['type']) && $info['columns'][$field]['type'] == 'xml') {
  706. $xml_field = $field;
  707. break;
  708. }
  709. }
  710. // XML indexes can only have 1 column.
  711. if (!empty($xml_field) && isset($fields[1])) {
  712. throw new Exception("Cannot include an XML field on a multiple column index.");
  713. }
  714. // No more than one XML index per table.
  715. if ($xml_field && $this->tableHasXmlIndex($table)) {
  716. throw new Exception("Only one primary clustered XML index is allowed per table.");
  717. }
  718. if (empty($xml_field)) {
  719. // TODO: As we are already doing with primary keys, when a user requests
  720. // an index that is too big for SQL Server (> 900 bytes) this could be dependant
  721. // on a computed hash column.
  722. $fields_csv = implode(', ', $fields);
  723. return "CREATE INDEX {$name}_idx ON [{{$table}}] ({$fields_csv})";
  724. }
  725. else {
  726. return "CREATE PRIMARY XML INDEX {$name}_idx ON [{{$table}}] ({$xml_field})";
  727. }
  728. }
  729. /**
  730. * Set database-engine specific properties for a field.
  731. *
  732. * @param $field
  733. * A field description array, as specified in the schema documentation.
  734. */
  735. protected function processField($field) {
  736. if (!isset($field['size'])) {
  737. $field['size'] = 'normal';
  738. }
  739. // Set the correct database-engine specific datatype.
  740. if (!isset($field['sqlsrv_type'])) {
  741. $map = $this->getFieldTypeMap();
  742. $field['sqlsrv_type'] = $map[$field['type'] . ':' . $field['size']];
  743. }
  744. if ($field['type'] == 'serial') {
  745. $field['identity'] = TRUE;
  746. }
  747. return $field;
  748. }
  749. /**
  750. * This maps a generic data type in combination with its data size
  751. * to the engine-specific data type.
  752. */
  753. function getFieldTypeMap() {
  754. // Put :normal last so it gets preserved by array_flip. This makes
  755. // it much easier for modules (such as schema.module) to map
  756. // database types back into schema types.
  757. return array(
  758. 'varchar:normal' => 'nvarchar',
  759. 'char:normal' => 'nchar',
  760. 'text:tiny' => 'nvarchar(max)',
  761. 'text:small' => 'nvarchar(max)',
  762. 'text:medium' => 'nvarchar(max)',
  763. 'text:big' => 'nvarchar(max)',
  764. 'text:normal' => 'nvarchar(max)',
  765. 'serial:tiny' => 'smallint',
  766. 'serial:small' => 'smallint',
  767. 'serial:medium' => 'int',
  768. 'serial:big' => 'bigint',
  769. 'serial:normal' => 'int',
  770. 'int:tiny' => 'smallint',
  771. 'int:small' => 'smallint',
  772. 'int:medium' => 'int',
  773. 'int:big' => 'bigint',
  774. 'int:normal' => 'int',
  775. 'float:tiny' => 'real',
  776. 'float:small' => 'real',
  777. 'float:medium' => 'real',
  778. 'float:big' => 'float(53)',
  779. 'float:normal' => 'real',
  780. 'numeric:normal' => 'numeric',
  781. 'blob:big' => 'varbinary(max)',
  782. 'blob:normal' => 'varbinary(max)',
  783. 'datetime:normal' => 'timestamp',
  784. 'date:normal' => 'date',
  785. 'datetime:normal' => 'datetime2(0)',
  786. 'time:normal' => 'time(0)',
  787. );
  788. }
  789. /**
  790. * Override DatabaseSchema::renameTable().
  791. *
  792. * @status complete
  793. */
  794. public function renameTable($table, $new_name) {
  795. if (!$this->tableExists($table, TRUE)) {
  796. throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot rename %table to %table_new: table %table doesn't exist.", array('%table' => $table, '%table_new' => $new_name)));
  797. }
  798. if ($this->tableExists($new_name, TRUE)) {
  799. throw new DatabaseSchemaObjectExistsException(t("Cannot rename %table to %table_new: table %table_new already exists.", array('%table' => $table, '%table_new' => $new_name)));
  800. }
  801. $old_table_info = $this->getPrefixInfo($table);
  802. $new_table_info = $this->getPrefixInfo($new_name);
  803. // We don't support renaming tables across schemas (yet).
  804. if ($old_table_info['schema'] != $new_table_info['schema']) {
  805. throw new PDOException(t('Cannot rename a table across schema.'));
  806. }
  807. // Borrar la caché de table_exists
  808. fastcache::cache_clear_all('*', 'tableExists', TRUE);
  809. $this->connection->query_direct('EXEC sp_rename :old, :new', array(
  810. ':old' => $old_table_info['schema'] . '.' . $old_table_info['table'],
  811. ':new' => $new_table_info['table'],
  812. ));
  813. // Constraint names are global in SQL Server, so we need to rename them
  814. // when renaming the table. For some strange reason, indexes are local to
  815. // a table.
  816. $objects = $this->connection->query_direct('SELECT name FROM sys.objects WHERE parent_object_id = OBJECT_ID(:table)', array(':table' => $new_table_info['schema'] . '.' . $new_table_info['table']));
  817. foreach ($objects as $object) {
  818. if (preg_match('/^' . preg_quote($old_table_info['table']) . '_(.*)$/', $object->name, $matches)) {
  819. $this->connection->query_direct('EXEC sp_rename :old, :new, :type', array(
  820. ':old' => $old_table_info['schema'] . '.' . $object->name,
  821. ':new' => $new_table_info['table'] . '_' . $matches[1],
  822. ':type' => 'OBJECT',
  823. ));
  824. }
  825. }
  826. }
  827. /**
  828. * Override DatabaseSchema::dropTable().
  829. *
  830. * @status tested
  831. */
  832. public function dropTable($table) {
  833. if (!$this->tableExists($table, TRUE)) {
  834. return FALSE;
  835. }
  836. $this->connection->query_direct('DROP TABLE {' . $table . '}');
  837. fastcache::cache_clear_all('*', 'tableExists', TRUE);
  838. return TRUE;
  839. }
  840. public function fieldExists($table, $field) {
  841. return $this->connection
  842. ->query_direct("SELECT 1 FROM INFORMATION_SCHEMA.columns WHERE table_name = '" . $this->connection->prefixTables('{' . $table . '}') . "' AND column_name = '" . $field . "'")
  843. ->fetchField() !== FALSE;
  844. }
  845. /**
  846. * Override DatabaseSchema::addField().
  847. *
  848. * @status complete
  849. */
  850. public function addField($table, $field, $spec, $new_keys = array()) {
  851. if (!$this->tableExists($table, TRUE)) {
  852. throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add field %table.%field: table doesn't exist.", array('%field' => $field, '%table' => $table)));
  853. }
  854. if ($this->fieldExists($table, $field)) {
  855. throw new DatabaseSchemaObjectExistsException(t("Cannot add field %table.%field: field already exists.", array('%field' => $field, '%table' => $table)));
  856. }
  857. /** @var DatabaseTransaction_sqlsrv $transaction */
  858. $transaction = $this->connection->startTransaction(NULL, DatabaseTransactionSettings::GetDDLCompatibleDefaults());
  859. // Prepare the specifications.
  860. $spec = $this->processField($spec);
  861. // Clear column information for table.
  862. $this->queryColumnInformationInvalidate($table);
  863. // Use already prefixed table name.
  864. $table_prefixed = $this->connection->prefixTables('{' . $table . '}');
  865. // If the field is declared NOT NULL, we have to first create it NULL insert
  866. // the initial data (or populate default values) and then switch to NOT NULL.
  867. $fixnull = FALSE;
  868. if (!empty($spec['not null'])) {
  869. $fixnull = TRUE;
  870. $spec['not null'] = FALSE;
  871. }
  872. // Create the field.
  873. // Because the default values of fields can contain string literals
  874. // with braces, we CANNOT allow the driver to prefix tables because the algorithm
  875. // to do so is a crappy str_replace.
  876. $query = "ALTER TABLE {$table_prefixed} ADD ";
  877. $query .= $this->createFieldSql($table, $field, $spec);
  878. $this->connection->query_direct($query, array(), array('prefix_tables' => FALSE));
  879. // Clear column information for table.
  880. $this->queryColumnInformationInvalidate($table);
  881. // Load the initial data.
  882. if (isset($spec['initial'])) {
  883. $this->connection->update($table)
  884. ->fields(array($field => $spec['initial']))
  885. ->execute();
  886. }
  887. // Switch to NOT NULL now.
  888. if ($fixnull === TRUE) {
  889. // There is no warranty that the old data did not have NULL values, we need to populate
  890. // nulls with the default value because this won't be done by MSSQL by default.
  891. if (!empty($spec['default'])) {
  892. $default_expression = $this->defaultValueExpression($spec['sqlsrv_type'], $spec['default']);
  893. $this->connection->query_direct("UPDATE [{{$table}}] SET [{$field}] = {$default_expression} WHERE [{$field}] IS NULL");
  894. }
  895. // Now it's time to make this non-nullable.
  896. $spec['not null'] = TRUE;
  897. $this->connection->query_direct('ALTER TABLE {' . $table . '} ALTER COLUMN ' . $this->createFieldSql($table, $field, $spec, TRUE));
  898. }
  899. // Add the new keys.
  900. if (isset($new_keys)) {
  901. $this->recreateTableKeys($table, $new_keys);
  902. }
  903. // Commit.
  904. $transaction->commit();
  905. // Clear column information for table.
  906. $this->queryColumnInformationInvalidate($table);
  907. }
  908. /**
  909. * Sometimes the size of a table's primary key index needs
  910. * to be reduced to allow for Primary XML Indexes.
  911. *
  912. * @param string $table
  913. * @param int $limit
  914. */
  915. public function compressPrimaryKeyIndex($table, $limit = 900) {
  916. // Introspect the schema and save the current primary key if the column
  917. // we are modifying is part of it.
  918. $primary_key_fields = $this->introspectPrimaryKeyFields($table);
  919. // SQL Server supports transactional DDL, so we can just start a transaction
  920. // here and pray for the best.
  921. $transaction = $this->connection->startTransaction(NULL, DatabaseTransactionSettings::GetDDLCompatibleDefaults());
  922. // Clear current Primary Key.
  923. $this->cleanUpPrimaryKey($table);
  924. // Recreate the Primary Key with the given limit size.
  925. $this->createPrimaryKey($table, $primary_key_fields, $limit);
  926. $transaction->commit();
  927. // Refresh introspection for this table.
  928. $this->queryColumnInformation($table, TRUE);
  929. }
  930. /**
  931. * Override DatabaseSchema::changeField().
  932. *
  933. * @status complete
  934. */
  935. public function changeField($table, $field, $field_new, $spec, $new_keys = array()) {
  936. if (!$this->fieldExists($table, $field)) {
  937. throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot change the definition of field %table.%name: field doesn't exist.", array('%table' => $table, '%name' => $field)));
  938. }
  939. if (($field != $field_new) && $this->fieldExists($table, $field_new)) {
  940. throw new DatabaseSchemaObjectExistsException(t("Cannot rename field %table.%name to %name_new: target field already exists.", array('%table' => $table, '%name' => $field, '%name_new' => $field_new)));
  941. }
  942. // SQL Server supports transactional DDL, so we can just start a transaction
  943. // here and pray for the best.
  944. /** @var DatabaseTransaction_sqlsrv $transaction */
  945. $transaction = $this->connection->startTransaction(NULL, DatabaseTransactionSettings::GetDDLCompatibleDefaults());
  946. // Prepare the specifications.
  947. $spec = $this->processField($spec);
  948. // IMPORTANT NOTE: To maintain database portability, you have to explicitly recreate all indices and primary keys that are using the changed field.
  949. // That means that you have to drop all affected keys and indexes with db_drop_{primary_key,unique_key,index}() before calling db_change_field().
  950. // @see https://api.drupal.org/api/drupal/includes!database!database.inc/function/db_change_field/7
  951. //
  952. // What we are going to do in the SQL Server Driver is a best-effort try to preserve original keys if they do not conflict
  953. // with the new_keys parameter, and if the callee has done it's job (droping constraints/keys) then they will of course not be recreated.
  954. // Introspect the schema and save the current primary key if the column
  955. // we are modifying is part of it. Make sure the schema is FRESH.
  956. $this->queryColumnInformationInvalidate($table);
  957. $primary_key_fields = $this->introspectPrimaryKeyFields($table);
  958. if (in_array($field, $primary_key_fields)) {
  959. // Let's drop the PK
  960. $this->cleanUpPrimaryKey($table);
  961. }
  962. // If there is a generated unique key for this field, we will need to
  963. // add it back in when we are done
  964. $unique_key = $this->uniqueKeyExists($table, $field);
  965. // Drop the related objects.
  966. $this->dropFieldRelatedObjects($table, $field);
  967. // Start by renaming the current column.
  968. $this->connection->query_direct('EXEC sp_rename :old, :new, :type', array(
  969. ':old' => $this->connection->prefixTables('{' . $table . '}.' . $field),
  970. ':new' => $field . '_old',
  971. ':type' => 'COLUMN',
  972. ));
  973. // If the new column does not allow nulls, we need to
  974. // create it first as nullable, then either migrate
  975. // data from previous column or populate default values.
  976. $fixnull = FALSE;
  977. if (!empty($spec['not null'])) {
  978. $fixnull = TRUE;
  979. $spec['not null'] = FALSE;
  980. }
  981. // Create a new field.
  982. $this->addField($table, $field_new, $spec);
  983. // Migrate the data over.
  984. // Explicitly cast the old value to the new value to avoid conversion errors.
  985. $this->connection->query_direct("UPDATE [{{$table}}] SET [{$field_new}] = CAST([{$field}_old] AS {$spec['sqlsrv_type']})");
  986. // Switch to NOT NULL now.
  987. if ($fixnull === TRUE) {
  988. // There is no warranty that the old data did not have NULL values, we need to populate
  989. // nulls with the default value because this won't be done by MSSQL by default.
  990. if (!empty($spec['default'])) {
  991. $default_expression = $this->defaultValueExpression($spec['sqlsrv_type'], $spec['default']);
  992. $this->connection->query_direct("UPDATE [{{$table}}] SET [{$field_new}] = {$default_expression} WHERE [{$field_new}] IS NULL");
  993. }
  994. // Now it's time to make this non-nullable.
  995. $spec['not null'] = TRUE;
  996. $this->connection->query_direct('ALTER TABLE {' . $table . '} ALTER COLUMN ' . $this->createFieldSql($table, $field_new, $spec, TRUE));
  997. }
  998. // Initialize new keys.
  999. if (!isset($new_keys)) {
  1000. $new_keys = array(
  1001. 'unique keys' => array(),
  1002. 'primary keys' => array()
  1003. );
  1004. }
  1005. // Recreate the primary key if no new primary key
  1006. // has been sent along with the change field.
  1007. if (in_array($field, $primary_key_fields) && (!isset($new_keys['primary keys']) || empty($new_keys['primary keys']))) {
  1008. // The new primary key needs to have
  1009. // the new column name.
  1010. unset($primary_key_fields[$field]);
  1011. $primary_key_fields[$field_new] = $field_new;
  1012. $new_keys['primary key'] = $primary_key_fields;
  1013. }
  1014. // Recreate the unique constraint if it existed.
  1015. if ($unique_key && !isset($new_keys['unique keys']) && !in_array($field_new, $new_keys['unique keys'])) {
  1016. $new_keys['unique keys'][] = $field_new;
  1017. }
  1018. // Drop the old field.
  1019. $this->dropField($table, $field . '_old');
  1020. // Add the new keys.
  1021. $this->recreateTableKeys($table, $new_keys);
  1022. // Refresh introspection for this table.
  1023. $this->queryColumnInformationInvalidate($table);
  1024. // Commit.
  1025. $transaction->commit();
  1026. }
  1027. /**
  1028. * Return size information for current database.
  1029. */
  1030. public function getSizeInfo() {
  1031. $sql = <<< EOF
  1032. SELECT
  1033. DB_NAME(db.database_id) DatabaseName,
  1034. (CAST(mfrows.RowSize AS FLOAT)*8)/1024 RowSizeMB,
  1035. (CAST(mflog.LogSize AS FLOAT)*8)/1024 LogSizeMB,
  1036. (CAST(mfstream.StreamSize AS FLOAT)*8)/1024 StreamSizeMB,
  1037. (CAST(mftext.TextIndexSize AS FLOAT)*8)/1024 TextIndexSizeMB
  1038. FROM sys.databases db
  1039. LEFT JOIN (SELECT database_id, SUM(size) RowSize FROM sys.master_files WHERE type = 0 GROUP BY database_id, type) mfrows ON mfrows.database_id = db.database_id
  1040. LEFT JOIN (SELECT database_id, SUM(size) LogSize FROM sys.master_files WHERE type = 1 GROUP BY database_id, type) mflog ON mflog.database_id = db.database_id
  1041. LEFT JOIN (SELECT database_id, SUM(size) StreamSize FROM sys.master_files WHERE type = 2 GROUP BY database_id, type) mfstream ON mfstream.database_id = db.database_id
  1042. LEFT JOIN (SELECT database_id, SUM(size) TextIndexSize FROM sys.master_files WHERE type = 4 GROUP BY database_id, type) mftext ON mftext.database_id = db.database_id
  1043. WHERE DB_NAME(db.database_id) = :database
  1044. EOF
  1045. ;
  1046. // Database is defaulted from active connection.
  1047. $options = $this->connection->getConnectionOptions();
  1048. $database = $options['database'];
  1049. return $this->connection->query_direct($sql, array(':database' => $database))->fetchObject();
  1050. }
  1051. /**
  1052. * Get database information from sys.databases
  1053. *
  1054. * @return mixed
  1055. */
  1056. public function getDatabaseInfo() {
  1057. static $result;
  1058. if (isset($result)) {
  1059. return $result;
  1060. }
  1061. $sql = <<< EOF
  1062. select name
  1063. , db.snapshot_isolation_state
  1064. , db.snapshot_isolation_state_desc
  1065. , db.is_read_committed_snapshot_on
  1066. , db.recovery_model
  1067. , db.recovery_model_desc
  1068. , db.collation_name
  1069. from sys.databases db
  1070. WHERE DB_NAME(db.database_id) = :database
  1071. EOF
  1072. ;
  1073. // Database is defaulted from active connection.
  1074. $options = $this->connection->getConnectionOptions();
  1075. $database = $options['database'];
  1076. $result = $this->connection->query_direct($sql, array(':database' => $database))->fetchObject();
  1077. return $result;
  1078. }
  1079. /**
  1080. * Get the collation of current connection wether
  1081. * it has or not a database defined in it.
  1082. *
  1083. * @param string $table
  1084. * @param string $column
  1085. *
  1086. * @return string
  1087. */
  1088. public function getCollation($table = NULL, $column = NULL) {
  1089. // No table or column provided, then get info about
  1090. // database (if exists) or server defaul collation.
  1091. if (empty($table) && empty($column)) {
  1092. // Database is defaulted from active connection.
  1093. $options = $this->connection->getConnectionOptions();
  1094. $database = $options['database'];
  1095. if (!empty($database)) {
  1096. // Default collation for specific table.
  1097. $sql = "SELECT CONVERT (varchar, DATABASEPROPERTYEX('$database', 'collation'))";
  1098. return $this->connection->query_direct($sql)->fetchField();
  1099. }
  1100. else {
  1101. // Server default collation.
  1102. $sql = "SELECT SERVERPROPERTY ('collation') as collation";
  1103. return $this->connection->query_direct($sql)->fetchField();
  1104. }
  1105. }
  1106. $sql = <<< EOF
  1107. SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, COLLATION_NAME, DATA_TYPE
  1108. FROM INFORMATION_SCHEMA.COLUMNS
  1109. WHERE TABLE_SCHEMA = ':schema'
  1110. AND TABLE_NAME = ':table'
  1111. AND COLUMN_NAME = ':column'
  1112. EOF
  1113. ;
  1114. $params = array();
  1115. $params[':schema'] = $this->defaultSchema;
  1116. $params[':table'] = $table;
  1117. $params[':column'] = $column;
  1118. $result = $this->connection->query_direct($sql, $params)->fetchObject();
  1119. return $result->COLLATION_NAME;
  1120. }
  1121. /**
  1122. * Get the list of fields participating in the Primary Key
  1123. *
  1124. * @param string $table
  1125. * @param string $field
  1126. *
  1127. * @return string[]
  1128. */
  1129. public function introspectPrimaryKeyFields($table) {
  1130. $data = $this->queryColumnInformation($table, TRUE);
  1131. // All primary keys have a default index,
  1132. // use that to see if we have a primary key
  1133. // before iterating.
  1134. if (!isset($data['primary_key_index']) || !isset($data['indexes'][$data['primary_key_index']])) {
  1135. return array();
  1136. }
  1137. $result = array();
  1138. $index = $data['indexes'][$data['primary_key_index']];
  1139. foreach ($index['columns'] as $column) {
  1140. if ($column['name'] != $this->COMPUTED_PK_COLUMN_NAME) {
  1141. $result[$column['name']] = $column['name'];
  1142. }
  1143. // Get full column definition
  1144. $c = $data['columns'][$column['name']];
  1145. // If this column depends on other columns
  1146. // the other columns are also part of the index!
  1147. // We don't support nested computed columns here.
  1148. foreach ($c['dependencies'] as $name => $order) {
  1149. $result[$name] = $name;
  1150. }
  1151. }
  1152. return $result;
  1153. }
  1154. /**
  1155. * Re-create keys associated to a table.
  1156. */
  1157. protected function recreateTableKeys($table, $new_keys) {
  1158. if (isset($new_keys['primary key'])) {
  1159. $this->addPrimaryKey($table, $new_keys['primary key']);
  1160. }
  1161. if (isset($new_keys['unique keys'])) {
  1162. foreach ($new_keys['unique keys'] as $name => $fields) {
  1163. $this->addUniqueKey($table, $name, $fields);
  1164. }
  1165. }
  1166. if (isset($new_keys['indexes'])) {
  1167. foreach ($new_keys['indexes'] as $name => $fields) {
  1168. $this->addIndex($table, $name, $fields);
  1169. }
  1170. }
  1171. }
  1172. /**
  1173. * Override DatabaseSchema::dropField().
  1174. *
  1175. * @status complete
  1176. */
  1177. public function dropField($table, $field) {
  1178. if (!$this->fieldExists($table, $field)) {
  1179. return FALSE;
  1180. }
  1181. // Drop the related objects.
  1182. $this->dropFieldRelatedObjects($table, $field);
  1183. $this->connection->query_direct('ALTER TABLE {' . $table . '} DROP COLUMN ' . $field);
  1184. // Clear introspection cache.
  1185. $this->queryColumnInformationInvalidate($table);
  1186. return TRUE;
  1187. }
  1188. /**
  1189. * Drop the related objects of a column (indexes, constraints, etc.).
  1190. *
  1191. * @status complete
  1192. */
  1193. protected function dropFieldRelatedObjects($table, $field) {
  1194. // Fetch the list of indexes referencing this column.
  1195. $indexes = $this->connection->query_direct('SELECT DISTINCT i.name FROM sys.columns c INNER JOIN sys.index_columns ic ON ic.object_id = c.object_id AND ic.column_id = c.column_id INNER JOIN sys.indexes i ON i.object_id = ic.object_id AND i.index_id = ic.index_id WHERE i.is_primary_key = 0 AND i.is_unique_constraint = 0 AND c.object_id = OBJECT_ID(:table) AND c.name = :name', array(
  1196. ':table' => $this->connection->prefixTables('{' . $table . '}'),
  1197. ':name' => $field,
  1198. ));
  1199. foreach ($indexes as $index) {
  1200. $this->connection->query_direct('DROP INDEX [' . $index->name . '] ON [{' . $table . '}]');
  1201. }
  1202. // Fetch the list of check constraints referencing this column.
  1203. $constraints = $this->connection->query_direct('SELECT DISTINCT cc.name FROM sys.columns c INNER JOIN sys.check_constraints cc ON cc.parent_object_id = c.object_id AND cc.parent_column_id = c.column_id WHERE c.object_id = OBJECT_ID(:table) AND c.name = :name', array(
  1204. ':table' => $this->connection->prefixTables('{' . $table . '}'),
  1205. ':name' => $field,
  1206. ));
  1207. foreach ($constraints as $constraint) {
  1208. $this->connection->query_direct('ALTER TABLE [{' . $table . '}] DROP CONSTRAINT [' . $constraint->name . ']');
  1209. }
  1210. // Fetch the list of default constraints referencing this column.
  1211. $constraints = $this->connection->query_direct('SELECT DISTINCT dc.name FROM sys.columns c INNER JOIN sys.default_constraints dc ON dc.parent_object_id = c.object_id AND dc.parent_column_id = c.column_id WHERE c.object_id = OBJECT_ID(:table) AND c.name = :name', array(
  1212. ':table' => $this->connection->prefixTables('{' . $table . '}'),
  1213. ':name' => $field,
  1214. ));
  1215. foreach ($constraints as $constraint) {
  1216. $this->connection->query_direct('ALTER TABLE [{' . $table . '}] DROP CONSTRAINT [' . $constraint->name . ']');
  1217. }
  1218. // Drop any indexes on related computed columns when we have some.
  1219. if ($this->uniqueKeyExists($table, $field)) {
  1220. $this->dropUniqueKey($table, $field);
  1221. }
  1222. // If this column is part of a computed primary key, drop the key.
  1223. $data = $this->queryColumnInformation($table, TRUE);
  1224. if (isset($data['columns'][$this->COMPUTED_PK_COLUMN_NAME]['dependencies'][$field])) {
  1225. $this->cleanUpPrimaryKey($table);
  1226. }
  1227. }
  1228. /**
  1229. * Override DatabaseSchema::fieldSetDefault().
  1230. *
  1231. * @status complete
  1232. */
  1233. public function fieldSetDefault($table, $field, $default) {
  1234. if (!$this->fieldExists($table, $field)) {
  1235. throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot set default value of field %table.%field: field doesn't exist.", array('%table' => $table, '%field' => $field)));
  1236. }
  1237. if ($default === NULL) {
  1238. $default = 'NULL';
  1239. }
  1240. elseif (is_string($default)) {
  1241. $default = "'" . addslashes($spec['default']) . "'";
  1242. }
  1243. // Try to remove any existing default first.
  1244. try { $this->fieldSetNoDefault($table, $field); } catch (Exception $e) {}
  1245. // Create the new default.
  1246. $this->connection->query_direct('ALTER TABLE [{' . $table . '}] ADD CONSTRAINT {' . $table . '}_' . $field . '_df DEFAULT ' . $default . ' FOR [' . $field . ']');
  1247. }
  1248. /**
  1249. * Override DatabaseSchema::fieldSetNoDefault().
  1250. *
  1251. * @status complete
  1252. */
  1253. public function fieldSetNoDefault($table, $field) {
  1254. if (!$this->fieldExists($table, $field)) {
  1255. throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot remove default value of field %table.%field: field doesn't exist.", array('%table' => $table, '%field' => $field)));
  1256. }
  1257. $this->connection->query_direct('ALTER TABLE [{' . $table . '}] DROP CONSTRAINT {' . $table . '}_' . $field . '_df');
  1258. }
  1259. /**
  1260. * Override DatabaseSchema::addPrimaryKey().
  1261. *
  1262. * @status tested
  1263. */
  1264. public function addPrimaryKey($table, $fields) {
  1265. if (!$this->tableExists($table, TRUE)) {
  1266. throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add primary key to table %table: table doesn't exist.", array('%table' => $table)));
  1267. }
  1268. if ($primary_key_name = $this->primaryKeyName($table)) {
  1269. if ($this->isTechnicalPrimaryKey($primary_key_name)) {
  1270. // Destroy the existing technical primary key.
  1271. $this->connection->query_direct('ALTER TABLE [{' . $table . '}] DROP CONSTRAINT [' . $primary_key_name . ']');
  1272. $this->cleanUpTechnicalPrimaryColumn($table);
  1273. }
  1274. else {
  1275. throw new DatabaseSchemaObjectExistsException(t("Cannot add primary key to table %table: primary key already exists.", array('%table' => $table)));
  1276. }
  1277. }
  1278. // The size limit of the primary key depends on the
  1279. // cohexistance with an XML field.
  1280. if ($this->tableHasXmlIndex($table)) {
  1281. $this->createPrimaryKey($table, $fields, 128);
  1282. }
  1283. else {
  1284. $this->createPrimaryKey($table, $fields);
  1285. }
  1286. return TRUE;
  1287. }
  1288. /**
  1289. * Override DatabaseSchema::dropPrimaryKey().
  1290. *
  1291. * @status tested
  1292. */
  1293. public function dropPrimaryKey($table) {
  1294. if (!$this->primaryKeyName($table)) {
  1295. return FALSE;
  1296. }
  1297. $this->cleanUpPrimaryKey($table);
  1298. $this->createTechnicalPrimaryColumn($table);
  1299. $this->connection->query_direct("ALTER TABLE [{{$table}}] ADD CONSTRAINT {{$table}}_pkey_technical PRIMARY KEY CLUSTERED ({$this->TECHNICAL_PK_COLUMN_NAME})");
  1300. return TRUE;
  1301. }
  1302. /**
  1303. * Return the name of the primary key of a table if it exists.
  1304. */
  1305. protected function primaryKeyName($table) {
  1306. $table = $this->connection->prefixTables('{' . $table . '}');
  1307. return $this->connection->query_direct('SELECT name FROM sys.key_constraints WHERE parent_object_id = OBJECT_ID(:table) AND type = :type', array(
  1308. ':table' => $table,
  1309. ':type' => 'PK',
  1310. ))->fetchField();
  1311. }
  1312. /**
  1313. * Check if a key is a technical primary key.
  1314. */
  1315. protected function isTechnicalPrimaryKey($key_name) {
  1316. return $key_name && preg_match('/_pkey_technical$/', $key_name);
  1317. }
  1318. /**
  1319. * Add a primary column to the table.
  1320. */
  1321. protected function createTechnicalPrimaryColumn($table) {
  1322. if (!$this->fieldExists($table, $this->TECHNICAL_PK_COLUMN_NAME)) {
  1323. $this->connection->query_direct("ALTER TABLE {{$table}} ADD {$this->TECHNICAL_PK_COLUMN_NAME} UNIQUEIDENTIFIER DEFAULT NEWID() NOT NULL");
  1324. }
  1325. }
  1326. /**
  1327. * Drop the primary key constraint.
  1328. * @param mixed $table
  1329. */
  1330. protected function cleanUpPrimaryKey($table) {
  1331. // We are droping the constraint, but not the column.
  1332. if ($existing_primary_key = $this->primaryKeyName($table)) {
  1333. $this->connection->query_direct("ALTER TABLE [{{$table}}] DROP CONSTRAINT {$existing_primary_key}");
  1334. }
  1335. // We are using computed columns to store primary keys,
  1336. // try to remove it if it exists.
  1337. if ($this->fieldExists($table, $this->COMPUTED_PK_COLUMN_NAME)) {
  1338. // The TCPK has compensation indexes that need to be cleared.
  1339. $this->dropIndex($table, $this->COMPUTED_PK_COLUMN_INDEX);
  1340. $this->dropField($table, $this->COMPUTED_PK_COLUMN_NAME);
  1341. }
  1342. // Try to get rid of the TPC
  1343. $this->cleanUpTechnicalPrimaryColumn($table);
  1344. }
  1345. /**
  1346. * Tries to clean up the technical primary column. It will
  1347. * be deleted if
  1348. * (a) It is not being used as the current primary key and...
  1349. * (b) There is no unique constraint because they depend on this column (see addUniqueKey())
  1350. *
  1351. * @param string $table
  1352. */
  1353. protected function cleanUpTechnicalPrimaryColumn($table) {
  1354. // Get the number of remaining unique indexes on the table, that
  1355. // are not primary keys and prune the technical primary column if possible.
  1356. $unique_indexes = $this->connection->query_direct('SELECT COUNT(*) FROM sys.indexes WHERE object_id = OBJECT_ID(:table) AND is_unique = 1 AND is_primary_key = 0', array(':table' => $this->connection->prefixTables('{' . $table . '}')))->fetchField();
  1357. $primary_key_is_technical = $this->isTechnicalPrimaryKey($this->primaryKeyName($table));
  1358. if (!$unique_indexes && !$primary_key_is_technical) {
  1359. $this->dropField($table, $this->TECHNICAL_PK_COLUMN_NAME);
  1360. }
  1361. }
  1362. /**
  1363. * Override DatabaseSchema::addUniqueKey().
  1364. *
  1365. * @status tested
  1366. */
  1367. public function addUniqueKey($table, $name, $fields) {
  1368. if (!$this->tableExists($table, TRUE)) {
  1369. throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add unique key %name to table %table: table doesn't exist.", array('%table' => $table, '%name' => $name)));
  1370. }
  1371. if ($this->uniqueKeyExists($table, $name)) {
  1372. throw new DatabaseSchemaObjectExistsException(t("Cannot add unique key %name to table %table: unique key already exists.", array('%table' => $table, '%name' => $name)));
  1373. }
  1374. $this->createTechnicalPrimaryColumn($table);
  1375. // Then, build a expression based on the columns.
  1376. $column_expression = array();
  1377. foreach ($fields as $field) {
  1378. if (is_array($field)) {
  1379. $column_expression[] = 'SUBSTRING(CAST(' . $this->connection->escapeField($field[0]) . ' AS varbinary(max)), 1,' . $field[1] . ')';
  1380. }
  1381. else {
  1382. $column_expression[] = 'CAST(' . $this->connection->escapeField($field) . ' AS varbinary(max))';
  1383. }
  1384. }
  1385. $column_expression = implode(' + ', $column_expression);
  1386. // Build a computed column based on the expression that replaces NULL
  1387. // values with the globally unique identifier generated previously.
  1388. // This is (very) unlikely to result in a collision with any actual value
  1389. // in the columns of the unique key.
  1390. $this->connection->query_direct("ALTER TABLE {{$table}} ADD __unique_{$name} AS CAST(HashBytes('MD4', COALESCE({$column_expression}, CAST({$this->TECHNICAL_PK_COLUMN_NAME} AS varbinary(max)))) AS varbinary(16))");
  1391. $this->connection->query_direct("CREATE UNIQUE INDEX {$name}_unique ON [{{$table}}] (__unique_{$name})");
  1392. }
  1393. /**
  1394. * Override DatabaseSchema::dropUniqueKey().
  1395. */
  1396. public function dropUniqueKey($table, $name) {
  1397. if (!$this->uniqueKeyExists($table, $name)) {
  1398. return FALSE;
  1399. }
  1400. $this->connection->query_direct('DROP INDEX ' . $name . '_unique ON [{' . $table . '}]');
  1401. $this->connection->query_direct('ALTER TABLE [{' . $table . '}] DROP COLUMN __unique_' . $name);
  1402. // Try to clean-up the technical primary key if possible.
  1403. $this->cleanUpTechnicalPrimaryColumn($table);
  1404. return TRUE;
  1405. }
  1406. /**
  1407. * Find if an unique key exists.
  1408. *
  1409. * @status tested
  1410. */
  1411. protected function uniqueKeyExists($table, $name) {
  1412. $table = $this->connection->prefixTables('{' . $table . '}');
  1413. return (bool) $this->connection->query_direct('SELECT 1 FROM sys.indexes WHERE object_id = OBJECT_ID(:table) AND name = :name', array(
  1414. ':table' => $table,
  1415. ':name' => $name . '_unique',
  1416. ))->fetchField();
  1417. }
  1418. /**
  1419. * Override DatabaseSchema::addIndex().
  1420. *
  1421. * @status tested
  1422. */
  1423. public function addIndex($table, $name, $fields) {
  1424. if (!$this->tableExists($table, TRUE)) {
  1425. throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add index %name to table %table: table doesn't exist.", array('%table' => $table, '%name' => $name)));
  1426. }
  1427. if ($this->indexExists($table, $name)) {
  1428. throw new DatabaseSchemaObjectExistsException(t("Cannot add index %name to table %table: index already exists.", array('%table' => $table, '%name' => $name)));
  1429. }
  1430. $xml_field = NULL;
  1431. $sql = $this->createIndexSql($table, $name, $fields, $xml_field);
  1432. if (!empty($xml_field)) {
  1433. // We can create an XML field, but the current primary key index
  1434. // size needs to be under 128bytes.
  1435. $pk_fields = $this->introspectPrimaryKeyFields($table);
  1436. $size = $this->calculateClusteredIndexRowSizeBytes($table, $pk_fields, TRUE);
  1437. if ($size > 128) {
  1438. // Alright the compress the index.
  1439. $this->compressPrimaryKeyIndex($table, 128);
  1440. }
  1441. }
  1442. $this->connection->query_direct($sql);
  1443. $this->queryColumnInformationInvalidate($table);
  1444. }
  1445. /**
  1446. * Override DatabaseSchema::dropIndex().
  1447. *
  1448. * @status tested
  1449. */
  1450. public function dropIndex($table, $name) {
  1451. if (!$this->indexExists($table, $name)) {
  1452. return FALSE;
  1453. }
  1454. $expand = FALSE;
  1455. if (($index = $this->tableHasXmlIndex($table)) && $index == ($name . '_idx')) {
  1456. $expand = TRUE;
  1457. }
  1458. $this->connection->query_direct('DROP INDEX ' . $name . '_idx ON [{' . $table . '}]');
  1459. // If we just dropped an XML index, we can re-expand the original primary key index.
  1460. if ($expand) {
  1461. $this->compressPrimaryKeyIndex($table);
  1462. }
  1463. $this->queryColumnInformationInvalidate($table);
  1464. return TRUE;
  1465. }
  1466. /**
  1467. * Override DatabaseSchema::indexExists().
  1468. *
  1469. * @status tested
  1470. */
  1471. public function indexExists($table, $name) {
  1472. // Indexes created through the database schema
  1473. // have the '_idx' prefix.
  1474. return $this->_ExistsIndex($table, $name . '_idx');
  1475. }
  1476. /**
  1477. * Check if a table already has an XML index.
  1478. *
  1479. * @param string $table
  1480. * @param string $name
  1481. */
  1482. public function tableHasXmlIndex($table) {
  1483. $info = $this->queryColumnInformation($table);
  1484. if (isset($info['indexes']) && is_array($info['indexes'])) {
  1485. foreach ($info['indexes'] as $name => $index) {
  1486. if (strcasecmp($index['type_desc'], 'XML') == 0) {
  1487. return $name;
  1488. }
  1489. }
  1490. }
  1491. return FALSE;
  1492. }
  1493. #region Helper Functions
  1494. // These are a set of non Drupal specific SQL Server Helperf functions
  1495. // to deal with tables, indexes, etc... creation/verification/deletion.
  1496. #region Index
  1497. /**
  1498. * Verify if a in index exists in the database.
  1499. *
  1500. * @param mixed $table
  1501. * @param mixed $name
  1502. * @return bool
  1503. */
  1504. public function _ExistsIndex($table, $index) {
  1505. $table = $this->connection->prefixTables('{' . $table . '}');
  1506. return (bool) $this->connection->query_direct('SELECT 1 FROM sys.indexes WHERE object_id = OBJECT_ID(:table) AND name = :name', array(
  1507. ':table' => $table,
  1508. ':name' => $index
  1509. ))->fetchField();
  1510. }
  1511. /**
  1512. * Drop an index, nothing to to if the index does not exists.
  1513. *
  1514. * @param mixed $table
  1515. * @param mixed $index
  1516. * @return void
  1517. */
  1518. public function _DropIndex($table, $index) {
  1519. if (!$this->_ExistsIndex($table, $index)) {
  1520. // Nothing to do....
  1521. return;
  1522. }
  1523. $table = $this->connection->prefixTables('{' . $table . '}');
  1524. $this->connection->query_direct('DROP INDEX :index ON :table',
  1525. array(
  1526. ':index' => $index,
  1527. ':table' => $table,
  1528. )
  1529. );
  1530. }
  1531. #endregion
  1532. #endregion
  1533. }
  1534. /**
  1535. * @} End of "ingroup schemaapi".
  1536. */