PageRenderTime 51ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/App/StackExchange.DataExplorer/Helpers/QueryRunner.cs

https://code.google.com/p/stack-exchange-data-explorer/
C# | 731 lines | 616 code | 86 blank | 29 comment | 77 complexity | 417e47437cac9a19bba2ba6dc742db9a MD5 | raw file
Possible License(s): CC-BY-SA-3.0, Apache-2.0
  1. using System;
  2. using System.Collections.Concurrent;
  3. using System.Collections.Generic;
  4. using System.Data.SqlClient;
  5. using System.Diagnostics;
  6. using System.Linq;
  7. using System.Text;
  8. using System.Web;
  9. using StackExchange.DataExplorer.Models;
  10. using Dapper;
  11. namespace StackExchange.DataExplorer.Helpers
  12. {
  13. public class QueryRunner
  14. {
  15. private const int MAX_RESULTS = 50000;
  16. private static readonly Dictionary<Type, ResultColumnType> ColumnTypeMap = new Dictionary<Type, ResultColumnType>
  17. {
  18. {
  19. typeof (int),
  20. ResultColumnType.Number
  21. },
  22. {
  23. typeof (long),
  24. ResultColumnType.Number
  25. },
  26. {
  27. typeof (float),
  28. ResultColumnType.Number
  29. },
  30. {
  31. typeof (double),
  32. ResultColumnType.Number
  33. },
  34. {
  35. typeof (decimal),
  36. ResultColumnType.Number
  37. },
  38. {
  39. typeof (string),
  40. ResultColumnType.Text
  41. },
  42. {
  43. typeof (DateTime),
  44. ResultColumnType.Date
  45. }
  46. };
  47. private static readonly Dictionary<string, Func<SqlConnection, IEnumerable<object>, List<object>>> magic_columns
  48. = GetMagicColumns();
  49. static void AddBody(StringBuilder buffer, QueryResults results, Site site)
  50. {
  51. buffer.AppendLine(site.Name);
  52. buffer.AppendLine("-------------------------------------------------");
  53. buffer.AppendLine(results.Messages);
  54. buffer.AppendLine();
  55. buffer.AppendLine();
  56. buffer.AppendLine();
  57. }
  58. public static void MergePivot(Site site, QueryResults current, QueryResults newResults)
  59. {
  60. int pivotIndex = -1;
  61. foreach (var info in newResults.ResultSets.First().Columns)
  62. {
  63. pivotIndex++;
  64. if (info.Name == "Pivot")
  65. {
  66. break;
  67. }
  68. }
  69. var map = current
  70. .ResultSets
  71. .First()
  72. .Rows
  73. .Select(columns => new
  74. {
  75. key = string.Join("|||", columns.Where((c, i) => i != pivotIndex && i < newResults.ResultSets.First().Columns.Count)),
  76. cols = columns
  77. })
  78. .ToDictionary(r => r.key, r => r.cols);
  79. var newRows = new List<List<object>>();
  80. foreach (var row in newResults.ResultSets.First().Rows)
  81. {
  82. List<object> foundRow;
  83. if (map.TryGetValue(string.Join("|||", row.Where((c, i) => i != pivotIndex)), out foundRow))
  84. {
  85. foundRow.Add(row[pivotIndex]);
  86. }
  87. else
  88. {
  89. newRows.Add(row);
  90. }
  91. }
  92. current.ResultSets.First().Columns.Add(new ResultColumnInfo
  93. {
  94. Name = site.Name + " Pivot",
  95. Type = newResults.ResultSets.First().Columns[pivotIndex].Type
  96. });
  97. var totalColumns = current.ResultSets.First().Columns.Count;
  98. foreach (var row in current.ResultSets.First().Rows)
  99. {
  100. if (row.Count < totalColumns)
  101. {
  102. row.Add(null);
  103. }
  104. }
  105. foreach (var row in newRows)
  106. {
  107. for (int i = pivotIndex+1; i < totalColumns; i++)
  108. {
  109. row.Insert(pivotIndex, null);
  110. }
  111. current.ResultSets.First().Rows.Add(row);
  112. }
  113. }
  114. private static QueryResults GetMultiSiteResults(ParsedQuery parsedQuery, User currentUser, AsyncQueryRunner.AsyncResult result = null)
  115. {
  116. var sites = Current.DB.Sites.All();
  117. if (parsedQuery.TargetSites == TargetSites.AllNonMetaSites)
  118. {
  119. sites = sites.Where(s => !s.Url.Contains("meta.")).ToList();
  120. }
  121. else if (parsedQuery.TargetSites == TargetSites.AllMetaSites)
  122. {
  123. sites = sites.Where(s => s.Url.Contains("meta.")).ToList();
  124. }
  125. var firstSite = sites.First();
  126. var results = QueryRunner.GetSingleSiteResults(parsedQuery, firstSite, currentUser, result);
  127. if (results.ResultSets.First().Columns.Where(c => c.Name == "Pivot").Any())
  128. {
  129. foreach (var info in results.ResultSets.First().Columns)
  130. {
  131. if (info.Name == "Pivot")
  132. {
  133. info.Name = firstSite.Name + " Pivot";
  134. break;
  135. }
  136. }
  137. foreach (var s in sites.Skip(1))
  138. {
  139. try
  140. {
  141. var tmp = QueryRunner.GetSingleSiteResults(parsedQuery, s, currentUser);
  142. results.ExecutionTime += tmp.ExecutionTime;
  143. MergePivot(s, results, tmp);
  144. }
  145. catch (Exception)
  146. {
  147. // don't blow up here ... just skip the site.
  148. }
  149. }
  150. }
  151. else
  152. {
  153. results.ResultSets[0].Columns.Add(new ResultColumnInfo { Name = "Site Name", Type = ResultColumnType.Site });
  154. foreach (var row in results.ResultSets[0].Rows)
  155. {
  156. row.Add(sites.First().SiteInfo);
  157. }
  158. foreach (var s in sites.Skip(1))
  159. {
  160. if (result != null && result.Cancelled)
  161. {
  162. break;
  163. }
  164. try
  165. {
  166. var tmp = QueryRunner.GetSingleSiteResults(parsedQuery, s, currentUser, result);
  167. foreach (var row in tmp.ResultSets[0].Rows)
  168. {
  169. row.Add(s.SiteInfo);
  170. results.ResultSets[0].Rows.Add(row);
  171. }
  172. results.ExecutionTime += tmp.ExecutionTime;
  173. results.Messages += "\n" + tmp.Messages;
  174. }
  175. catch (Exception)
  176. {
  177. // don't blow up ... just skip the site
  178. }
  179. }
  180. }
  181. results.TargetSites = parsedQuery.TargetSites;
  182. return results;
  183. }
  184. public static void LogRevisionExecution(User user, int siteId, int revisionId)
  185. {
  186. int updated = Current.DB.Query<int>(@"
  187. UPDATE RevisionExecutions SET
  188. ExecutionCount = ExecutionCount + 1,
  189. LastRun = @last
  190. WHERE
  191. RevisionId = @revision AND
  192. SiteId = @site AND
  193. UserId " + (user.IsAnonymous ? "IS NULL" : "= @user") + @"
  194. SELECT @@ROWCOUNT",
  195. new
  196. {
  197. revision = revisionId,
  198. site = siteId,
  199. user = user.Id,
  200. last = DateTime.UtcNow
  201. }
  202. ).FirstOrDefault();
  203. if (updated == 0)
  204. {
  205. Current.DB.Execute(@"
  206. INSERT INTO RevisionExecutions(
  207. ExecutionCount, FirstRun, LastRun,
  208. RevisionId, SiteId, UserId
  209. ) VALUES(
  210. 1, @first, @last, @revision, @site, @user
  211. )",
  212. new
  213. {
  214. first = DateTime.UtcNow,
  215. last = DateTime.UtcNow,
  216. revision = revisionId,
  217. site = siteId,
  218. user = (user.IsAnonymous ? (int?)null : user.Id)
  219. }
  220. );
  221. }
  222. }
  223. [System.Diagnostics.CodeAnalysis.SuppressMessage(
  224. "Microsoft.Security",
  225. "CA2100:Review SQL queries for security vulnerabilities",
  226. Justification = "What else can I do, we are allowing users to run sql.")]
  227. public static QueryResults ExecuteNonCached(ParsedQuery query, Site site, User user, AsyncQueryRunner.AsyncResult result)
  228. {
  229. var remoteIP = OData.GetRemoteIP();
  230. var key = "total-" + remoteIP;
  231. var currentCount = (int?)Current.GetCachedObject(key) ?? 0;
  232. currentCount++;
  233. Current.SetCachedObjectSliding(key, currentCount, 60 * 60);
  234. if (currentCount > 130)
  235. {
  236. // clearly a robot, auto black list
  237. Current.DB.BlackList.Insert(new { CreationDate = DateTime.UtcNow, IPAddress = remoteIP });
  238. }
  239. if (currentCount > 100)
  240. {
  241. throw new Exception("You can not run any new queries for another hour, you have exceeded your limit!");
  242. }
  243. if (Current.DB.Query<int>("select count(*) from BlackList where IPAddress = @remoteIP", new { remoteIP }).First() > 0)
  244. {
  245. System.Threading.Thread.Sleep(2000);
  246. throw new Exception("You have been blacklisted due to abuse!");
  247. }
  248. var results = new QueryResults();
  249. using (SqlConnection cnn = site.GetOpenConnection())
  250. {
  251. // well we do not want to risk blocking, if somebody needs to change this we will need to add a setting
  252. cnn.Execute("set transaction isolation level read uncommitted");
  253. var timer = new Stopwatch();
  254. timer.Start();
  255. var messages = new StringBuilder();
  256. var infoHandler = new SqlInfoMessageEventHandler((sender, args) =>
  257. {
  258. // todo handle errors as well
  259. messages.AppendLine(args.Message);
  260. });
  261. try
  262. {
  263. cnn.InfoMessage += infoHandler;
  264. if (query.IncludeExecutionPlan)
  265. {
  266. using (var command = new SqlCommand("SET STATISTICS XML ON", cnn))
  267. {
  268. command.ExecuteNonQuery();
  269. }
  270. }
  271. var plan = new QueryPlan();
  272. foreach (string batch in query.ExecutionSqlBatches)
  273. {
  274. using (var command = new SqlCommand(batch, cnn))
  275. {
  276. if (result != null)
  277. {
  278. result.Command = command;
  279. if (result.Cancelled)
  280. {
  281. continue;
  282. }
  283. }
  284. command.CommandTimeout = AppSettings.QueryTimeout;
  285. PopulateResults(results, command, messages, query.IncludeExecutionPlan);
  286. }
  287. if (query.IncludeExecutionPlan)
  288. {
  289. plan.AppendBatchPlan(results.ExecutionPlan);
  290. results.ExecutionPlan = null;
  291. }
  292. }
  293. results.ExecutionPlan = plan.PlanXml;
  294. }
  295. finally
  296. {
  297. cnn.InfoMessage -= infoHandler;
  298. results.Messages = messages.ToString();
  299. }
  300. timer.Stop();
  301. results.ExecutionTime = timer.ElapsedMilliseconds;
  302. ProcessMagicColumns(results, cnn);
  303. }
  304. return results;
  305. }
  306. /// <summary>
  307. /// Executes an SQL query and populates a given <see cref="QueryResults" /> instance with the results.
  308. /// </summary>
  309. /// <param name="results"><see cref="QueryResults" /> instance to populate with results.</param>
  310. /// <param name="command">SQL command to execute.</param>
  311. /// <param name="messages"><see cref="StringBuilder" /> instance to which to append messages.</param>
  312. /// <param name="IncludeExecutionPlan">If true indciates that the query execution plans are expected to be contained
  313. /// in the results sets; otherwise, false.</param>
  314. private static void PopulateResults(QueryResults results, SqlCommand command, StringBuilder messages, bool IncludeExecutionPlan)
  315. {
  316. QueryPlan plan = new QueryPlan();
  317. using (SqlDataReader reader = command.ExecuteReader())
  318. {
  319. do
  320. {
  321. // Check to see if the resultset is an execution plan
  322. if (IncludeExecutionPlan && reader.FieldCount == 1 && reader.GetName(0) == "Microsoft SQL Server 2005 XML Showplan")
  323. {
  324. if (reader.Read())
  325. {
  326. plan.AppendStatementPlan(reader[0].ToString());
  327. }
  328. }
  329. else
  330. {
  331. if (reader.FieldCount == 0)
  332. {
  333. if (reader.RecordsAffected >= 0)
  334. {
  335. messages.AppendFormat("({0} row(s) affected)\n\n", reader.RecordsAffected);
  336. }
  337. continue;
  338. }
  339. var resultSet = new ResultSet();
  340. resultSet.MessagePosition = messages.Length;
  341. results.ResultSets.Add(resultSet);
  342. for (int i = 0; i < reader.FieldCount; i++)
  343. {
  344. var columnInfo = new ResultColumnInfo();
  345. columnInfo.Name = reader.GetName(i);
  346. ResultColumnType colType;
  347. if (ColumnTypeMap.TryGetValue(reader.GetFieldType(i), out colType))
  348. {
  349. columnInfo.Type = colType;
  350. }
  351. resultSet.Columns.Add(columnInfo);
  352. }
  353. int currentRow = 0;
  354. while (reader.Read())
  355. {
  356. if (currentRow++ >= MAX_RESULTS)
  357. {
  358. results.Truncated = true;
  359. results.MaxResults = MAX_RESULTS;
  360. break;
  361. }
  362. var row = new List<object>();
  363. resultSet.Rows.Add(row);
  364. for (int i = 0; i < reader.FieldCount; i++)
  365. {
  366. object col = reader.GetValue(i);
  367. if (col is DateTime)
  368. {
  369. var date = (DateTime)col;
  370. col = date.ToJavascriptTime();
  371. }
  372. row.Add(col);
  373. }
  374. }
  375. if (results.Truncated)
  376. {
  377. // next result would force ado.net to fast forward
  378. // through the result set, which is way too slow
  379. break;
  380. }
  381. if (reader.RecordsAffected >= 0)
  382. {
  383. messages.AppendFormat("({0} row(s) affected)\n\n", reader.RecordsAffected);
  384. }
  385. messages.AppendFormat("({0} row(s) affected)\n\n", resultSet.Rows.Count);
  386. }
  387. } while (reader.NextResult());
  388. command.Cancel();
  389. }
  390. results.ExecutionPlan = plan.PlanXml;
  391. }
  392. public static QueryResults GetResults(ParsedQuery query, Site site, User user, AsyncQueryRunner.AsyncResult result = null)
  393. {
  394. if (query.TargetSites != TargetSites.Current)
  395. {
  396. return GetMultiSiteResults(query, user, result);
  397. }
  398. else
  399. {
  400. return GetSingleSiteResults(query, site, user, result);
  401. }
  402. }
  403. private static QueryResults GetSingleSiteResults(ParsedQuery query, Site site, User user, AsyncQueryRunner.AsyncResult result = null)
  404. {
  405. QueryResults results = null;
  406. var timer = new Stopwatch();
  407. timer.Start();
  408. var cache = QueryUtil.GetCachedResults(query, site.Id);
  409. if (cache != null)
  410. {
  411. if (!query.IncludeExecutionPlan || cache.ExecutionPlan != null)
  412. {
  413. results = new QueryResults();
  414. results.WithCache(cache);
  415. results.Truncated = cache.Truncated;
  416. results.Messages = cache.Messages;
  417. results.FromCache = true;
  418. // If we didn't ask for the execution plan, don't return it
  419. if (!query.IncludeExecutionPlan)
  420. {
  421. results.ExecutionPlan = null;
  422. }
  423. }
  424. }
  425. timer.Stop();
  426. if (results == null)
  427. {
  428. results = ExecuteNonCached(query, site, user, result);
  429. results.FromCache = false;
  430. AddResultToCache(results, query, site, cache != null);
  431. }
  432. else
  433. {
  434. results.ExecutionTime = timer.ElapsedMilliseconds;
  435. }
  436. results.Url = site.Url;
  437. results.SiteId = site.Id;
  438. results.SiteName = site.Name.ToLower();
  439. return results;
  440. }
  441. /// <summary>
  442. /// Adds the results of a running a particular query for a given site to the database cache
  443. /// </summary>
  444. /// <param name="results">The results of the query</param>
  445. /// <param name="query">The query that was executed</param>
  446. /// <param name="site">The site that the query was run against</param>
  447. /// <param name="planOnly">Whether or not this is just an update to add the cached execution plan</param>
  448. private static void AddResultToCache(QueryResults results, ParsedQuery query, Site site, bool planOnly)
  449. {
  450. if (!planOnly)
  451. {
  452. Current.DB.Execute(@"
  453. INSERT INTO CachedResults(
  454. QueryHash, SiteId, Results, ExecutionPlan,
  455. Messages, Truncated, CreationDate
  456. ) VALUES(
  457. @hash, @site, @results, @plan,
  458. @messages, @truncated, @creation
  459. )",
  460. new
  461. {
  462. hash = query.ExecutionHash,
  463. site = site.Id,
  464. results = results.GetJsonResults(),
  465. plan = results.ExecutionPlan,
  466. messages = results.Messages,
  467. truncated = results.Truncated,
  468. creation = DateTime.UtcNow
  469. }
  470. );
  471. }
  472. else
  473. {
  474. // Should we just update everything in this case? Presumably the only
  475. // thing that changed was the addition of the execution plan, but...
  476. Current.DB.Execute(@"
  477. UPDATE
  478. CachedResults
  479. SET
  480. ExecutionPlan = @plan
  481. WHERE
  482. QueryHash = @hash",
  483. new
  484. {
  485. plan = results.ExecutionPlan,
  486. hash = query.ExecutionHash
  487. }
  488. );
  489. }
  490. }
  491. private static void ProcessMagicColumns(QueryResults results, SqlConnection cnn)
  492. {
  493. int index = 0;
  494. foreach (ResultSet resultSet in results.ResultSets)
  495. {
  496. foreach (ResultColumnInfo column in resultSet.Columns)
  497. {
  498. if (magic_columns.ContainsKey(column.Name))
  499. {
  500. DecorateColumn(column);
  501. // tricky ... multi site has special handling.
  502. if (resultSet.Columns.Any(c => c.Type == ResultColumnType.Site))
  503. {
  504. int siteNameIndex = 0;
  505. foreach (var item in resultSet.Columns)
  506. {
  507. if (item.Type == ResultColumnType.Site) break;
  508. siteNameIndex++;
  509. }
  510. var sites = Current.DB.Sites.All();
  511. foreach (var group in resultSet.Rows.GroupBy(r => r[siteNameIndex]))
  512. {
  513. using (var newConnection = sites.First(s => s.Id == ((SiteInfo)group.First()[siteNameIndex]).Id).GetOpenConnection())
  514. {
  515. ProcessColumn(newConnection, index, group.ToList(), column);
  516. }
  517. }
  518. }
  519. else
  520. {
  521. ProcessColumn(cnn, index, resultSet.Rows, column);
  522. }
  523. }
  524. index++;
  525. }
  526. }
  527. }
  528. private static void ProcessColumn(SqlConnection cnn, int index, List<List<object>> rows, ResultColumnInfo column)
  529. {
  530. IEnumerable<object> values = rows.Select(row => row[index]);
  531. List<object> processedValues = magic_columns[column.Name](cnn, values);
  532. int rowNumber = 0;
  533. foreach (var row in rows)
  534. {
  535. row[index] = processedValues[rowNumber];
  536. rowNumber++;
  537. }
  538. }
  539. private static void DecorateColumn(ResultColumnInfo column)
  540. {
  541. switch (column.Name)
  542. {
  543. case "Post Link":
  544. column.Type = ResultColumnType.Post;
  545. break;
  546. case "User Link":
  547. column.Type = ResultColumnType.User;
  548. break;
  549. case "Comment Link":
  550. column.Type = ResultColumnType.Comment;
  551. break;
  552. case "Suggested Edit Link":
  553. column.Type = ResultColumnType.SuggestedEdit;
  554. break;
  555. default:
  556. break;
  557. }
  558. }
  559. private static Dictionary<string, Func<SqlConnection, IEnumerable<object>, List<object>>> GetMagicColumns()
  560. {
  561. return new Dictionary<string, Func<SqlConnection, IEnumerable<object>, List<object>>>
  562. {
  563. { "Post Link", GetPostLinks },
  564. { "User Link", GetUserLinks },
  565. { "Comment Link", GetCommentLinks },
  566. { "Suggested Edit Link", GetSuggestedEditLinks }
  567. };
  568. }
  569. public static List<object> GetCommentLinks(SqlConnection cnn, IEnumerable<object> items)
  570. {
  571. return LookupIds(cnn, items, @"SELECT Id, Text FROM Comments WHERE Id IN ");
  572. }
  573. public static List<object> GetSuggestedEditLinks(SqlConnection cnn, IEnumerable<object> items)
  574. {
  575. return LookupIds(cnn, items,
  576. @"select Id, case when RejectionDate is not null then 'rejected' when ApprovalDate is not null then 'accepted' else 'pending' end
  577. from SuggestedEdits
  578. where Id in ");
  579. }
  580. public static List<object> GetUserLinks(SqlConnection cnn, IEnumerable<object> items)
  581. {
  582. return LookupIds(cnn, items,
  583. @"select Id, case when DisplayName is null or LEN(DisplayName) = 0 then 'unknown' else DisplayName end from Users where Id in ");
  584. }
  585. public static List<object> GetPostLinks(SqlConnection cnn, IEnumerable<object> items)
  586. {
  587. return LookupIds(cnn, items,
  588. @"select p1.Id, isnull(p1.Title,p2.Title) from Posts p1
  589. left join Posts p2 on p1.ParentId = p2.Id where p1.Id in ");
  590. }
  591. public static List<object> LookupIds(SqlConnection cnn, IEnumerable<object> items, string lookupSql)
  592. {
  593. var rval = new List<object>();
  594. if (items.Count() == 0) return rval;
  595. // safe due to the long cast (not that it matters, it runs read only)
  596. string list = String.Join(" , ",
  597. items.Where(i => i != null && i is int).Select(i => ((int) i).ToString()).ToArray());
  598. if (list == "")
  599. {
  600. return items.ToList();
  601. }
  602. StringBuilder query = new StringBuilder()
  603. .Append(lookupSql)
  604. .Append("( ")
  605. .Append(list)
  606. .Append(" ) ");
  607. var linkMap = new Dictionary<long, object>();
  608. using (SqlCommand cmd = cnn.CreateCommand())
  609. {
  610. cmd.CommandText = query.ToString();
  611. using (SqlDataReader reader = cmd.ExecuteReader())
  612. {
  613. while (reader.Read())
  614. {
  615. var extraInfo = new Dictionary<string, object>();
  616. extraInfo["title"] = reader.IsDBNull(1) ? "unknown" : reader.GetString(1);
  617. extraInfo["id"] = reader.GetInt32(0);
  618. linkMap[reader.GetInt32(0)] = extraInfo;
  619. }
  620. }
  621. }
  622. foreach (object item in items)
  623. {
  624. if (item == null || !(item is int))
  625. {
  626. rval.Add(item);
  627. }
  628. else
  629. {
  630. try
  631. {
  632. rval.Add(linkMap[(int) item]);
  633. }
  634. catch
  635. {
  636. // this is exceptional
  637. rval.Add(item);
  638. }
  639. }
  640. }
  641. return rval;
  642. }
  643. }
  644. }