You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

491 lines
17 KiB

4 years ago
  1. #if MYSQL_6_10
  2. // Copyright ?2004, 2018, Oracle and/or its affiliates. All rights reserved.
  3. //
  4. // MySQL Connector/NET is licensed under the terms of the GPLv2
  5. // <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>, like most
  6. // MySQL Connectors. There are special exceptions to the terms and
  7. // conditions of the GPLv2 as it is applied to this software, see the
  8. // FLOSS License Exception
  9. // <http://www.mysql.com/about/legal/licensing/foss-exception.html>.
  10. //
  11. // This program is free software; you can redistribute it and/or modify
  12. // it under the terms of the GNU General Public License as published
  13. // by the Free Software Foundation; version 2 of the License.
  14. //
  15. // This program is distributed in the hope that it will be useful, but
  16. // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
  17. // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
  18. // for more details.
  19. //
  20. // You should have received a copy of the GNU General Public License along
  21. // with this program; if not, write to the Free Software Foundation, Inc.,
  22. // 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
  23. using System;
  24. using System.Collections.Generic;
  25. using System.Data;
  26. using System.Globalization;
  27. using System.IO;
  28. using System.Linq;
  29. using System.Threading.Tasks;
  30. using System.Threading;
  31. using static System.String;
  32. using Externals.MySql.Data.Common;
  33. namespace Externals.MySql.Data.MySqlClient
  34. {
  35. /// <summary>
  36. /// Provides a class capable of executing a SQL script containing
  37. /// multiple SQL statements including CREATE PROCEDURE statements
  38. /// that require changing the delimiter
  39. /// </summary>
  40. internal class MySqlScript
  41. {
  42. /// <summary>
  43. /// Handles the event raised whenever a statement is executed.
  44. /// </summary>
  45. public event MySqlStatementExecutedEventHandler StatementExecuted;
  46. /// <summary>
  47. /// Handles the event raised whenever an error is raised by the execution of a script.
  48. /// </summary>
  49. public event MySqlScriptErrorEventHandler Error;
  50. /// <summary>
  51. /// Handles the event raised whenever a script execution is finished.
  52. /// </summary>
  53. public event EventHandler ScriptCompleted;
  54. #region Constructors
  55. /// <summary>
  56. /// Initializes a new instance of the
  57. /// <see cref="MySqlScript"/> class.
  58. /// </summary>
  59. public MySqlScript()
  60. {
  61. Delimiter = ";";
  62. }
  63. /// <summary>
  64. /// Initializes a new instance of the
  65. /// <see cref="MySqlScript"/> class.
  66. /// </summary>
  67. /// <param name="connection">The connection.</param>
  68. public MySqlScript(MySqlConnection connection)
  69. : this()
  70. {
  71. Connection = connection;
  72. }
  73. /// <summary>
  74. /// Initializes a new instance of the
  75. /// <see cref="MySqlScript"/> class.
  76. /// </summary>
  77. /// <param name="query">The query.</param>
  78. public MySqlScript(string query)
  79. : this()
  80. {
  81. Query = query;
  82. }
  83. /// <summary>
  84. /// Initializes a new instance of the
  85. /// <see cref="MySqlScript"/> class.
  86. /// </summary>
  87. /// <param name="connection">The connection.</param>
  88. /// <param name="query">The query.</param>
  89. public MySqlScript(MySqlConnection connection, string query)
  90. : this()
  91. {
  92. Connection = connection;
  93. Query = query;
  94. }
  95. #endregion
  96. #region Properties
  97. /// <summary>
  98. /// Gets or sets the connection.
  99. /// </summary>
  100. /// <value>The connection.</value>
  101. public MySqlConnection Connection { get; set; }
  102. /// <summary>
  103. /// Gets or sets the query.
  104. /// </summary>
  105. /// <value>The query.</value>
  106. public string Query { get; set; }
  107. /// <summary>
  108. /// Gets or sets the delimiter.
  109. /// </summary>
  110. /// <value>The delimiter.</value>
  111. public string Delimiter { get; set; }
  112. #endregion
  113. #region Public Methods
  114. /// <summary>
  115. /// Executes this instance.
  116. /// </summary>
  117. /// <returns>The number of statements executed as part of the script.</returns>
  118. public int Execute()
  119. {
  120. bool openedConnection = false;
  121. if (Connection == null)
  122. throw new InvalidOperationException(Resources.ConnectionNotSet);
  123. if (IsNullOrEmpty(Query))
  124. return 0;
  125. // next we open up the connetion if it is not already open
  126. if (Connection.State != ConnectionState.Open)
  127. {
  128. openedConnection = true;
  129. Connection.Open();
  130. }
  131. // since we don't allow setting of parameters on a script we can
  132. // therefore safely allow the use of user variables. no one should be using
  133. // this connection while we are using it so we can temporarily tell it
  134. // to allow the use of user variables
  135. bool allowUserVars = Connection.Settings.AllowUserVariables;
  136. Connection.Settings.AllowUserVariables = true;
  137. try
  138. {
  139. string mode = Connection.driver.Property("sql_mode");
  140. mode = StringUtility.ToUpperInvariant(mode);
  141. bool ansiQuotes = mode.IndexOf("ANSI_QUOTES") != -1;
  142. bool noBackslashEscapes = mode.IndexOf("NO_BACKSLASH_ESCAPES") != -1;
  143. // first we break the query up into smaller queries
  144. List<ScriptStatement> statements = BreakIntoStatements(ansiQuotes, noBackslashEscapes);
  145. int count = 0;
  146. MySqlCommand cmd = new MySqlCommand(null, Connection);
  147. foreach (ScriptStatement statement in statements.Where(statement => !IsNullOrEmpty(statement.text)))
  148. {
  149. cmd.CommandText = statement.text;
  150. try
  151. {
  152. cmd.ExecuteNonQuery();
  153. count++;
  154. OnQueryExecuted(statement);
  155. }
  156. catch (Exception ex)
  157. {
  158. if (Error == null)
  159. throw;
  160. if (!OnScriptError(ex))
  161. break;
  162. }
  163. }
  164. OnScriptCompleted();
  165. return count;
  166. }
  167. finally
  168. {
  169. Connection.Settings.AllowUserVariables = allowUserVars;
  170. if (openedConnection)
  171. {
  172. Connection.Close();
  173. }
  174. }
  175. }
  176. #endregion
  177. private void OnQueryExecuted(ScriptStatement statement)
  178. {
  179. if (StatementExecuted == null) return;
  180. MySqlScriptEventArgs args = new MySqlScriptEventArgs { Statement = statement };
  181. StatementExecuted(this, args);
  182. }
  183. private void OnScriptCompleted()
  184. {
  185. ScriptCompleted?.Invoke(this, EventArgs.Empty);
  186. }
  187. private bool OnScriptError(Exception ex)
  188. {
  189. if (Error == null) return false;
  190. MySqlScriptErrorEventArgs args = new MySqlScriptErrorEventArgs(ex);
  191. Error(this, args);
  192. return args.Ignore;
  193. }
  194. private List<int> BreakScriptIntoLines()
  195. {
  196. List<int> lineNumbers = new List<int>();
  197. StringReader sr = new StringReader(Query);
  198. string line = sr.ReadLine();
  199. int pos = 0;
  200. while (line != null)
  201. {
  202. lineNumbers.Add(pos);
  203. pos += line.Length;
  204. line = sr.ReadLine();
  205. }
  206. return lineNumbers;
  207. }
  208. private static int FindLineNumber(int position, List<int> lineNumbers)
  209. {
  210. int i = 0;
  211. while (i < lineNumbers.Count && position < lineNumbers[i])
  212. i++;
  213. return i;
  214. }
  215. private List<ScriptStatement> BreakIntoStatements(bool ansiQuotes, bool noBackslashEscapes)
  216. {
  217. string currentDelimiter = Delimiter;
  218. int startPos = 0;
  219. List<ScriptStatement> statements = new List<ScriptStatement>();
  220. List<int> lineNumbers = BreakScriptIntoLines();
  221. MySqlTokenizer tokenizer = new MySqlTokenizer(Query);
  222. tokenizer.AnsiQuotes = ansiQuotes;
  223. tokenizer.BackslashEscapes = !noBackslashEscapes;
  224. string token = tokenizer.NextToken();
  225. while (token != null)
  226. {
  227. if (!tokenizer.Quoted)
  228. {
  229. #if !NETSTANDARD1_3
  230. if (token.ToLower(CultureInfo.InvariantCulture) == "delimiter")
  231. #else
  232. if (token.ToLowerInvariant() == "delimiter")
  233. #endif
  234. {
  235. tokenizer.NextToken();
  236. AdjustDelimiterEnd(tokenizer);
  237. currentDelimiter = Query.Substring(tokenizer.StartIndex,
  238. tokenizer.StopIndex - tokenizer.StartIndex).Trim();
  239. startPos = tokenizer.StopIndex;
  240. }
  241. else
  242. {
  243. // this handles the case where our tokenizer reads part of the
  244. // delimiter
  245. if (currentDelimiter.StartsWith(token, StringComparison.OrdinalIgnoreCase))
  246. {
  247. if ((tokenizer.StartIndex + currentDelimiter.Length) <= Query.Length)
  248. {
  249. if (Query.Substring(tokenizer.StartIndex, currentDelimiter.Length) == currentDelimiter)
  250. {
  251. token = currentDelimiter;
  252. tokenizer.Position = tokenizer.StartIndex + currentDelimiter.Length;
  253. tokenizer.StopIndex = tokenizer.Position;
  254. }
  255. }
  256. }
  257. int delimiterPos = token.IndexOf(currentDelimiter, StringComparison.OrdinalIgnoreCase);
  258. if (delimiterPos != -1)
  259. {
  260. int endPos = tokenizer.StopIndex - token.Length + delimiterPos;
  261. if (tokenizer.StopIndex == Query.Length - 1)
  262. endPos++;
  263. string currentQuery = Query.Substring(startPos, endPos - startPos);
  264. ScriptStatement statement = new ScriptStatement();
  265. statement.text = currentQuery.Trim();
  266. statement.line = FindLineNumber(startPos, lineNumbers);
  267. statement.position = startPos - lineNumbers[statement.line];
  268. statements.Add(statement);
  269. startPos = endPos + currentDelimiter.Length;
  270. }
  271. }
  272. }
  273. token = tokenizer.NextToken();
  274. }
  275. // now clean up the last statement
  276. if (startPos < Query.Length - 1)
  277. {
  278. string sqlLeftOver = Query.Substring(startPos).Trim();
  279. if (IsNullOrEmpty(sqlLeftOver)) return statements;
  280. ScriptStatement statement = new ScriptStatement
  281. {
  282. text = sqlLeftOver,
  283. line = FindLineNumber(startPos, lineNumbers)
  284. };
  285. statement.position = startPos - lineNumbers[statement.line];
  286. statements.Add(statement);
  287. }
  288. return statements;
  289. }
  290. private void AdjustDelimiterEnd(MySqlTokenizer tokenizer)
  291. {
  292. if (tokenizer.StopIndex >= Query.Length) return;
  293. int pos = tokenizer.StopIndex;
  294. char c = Query[pos];
  295. while (!Char.IsWhiteSpace(c) && pos < (Query.Length - 1))
  296. {
  297. c = Query[++pos];
  298. }
  299. tokenizer.StopIndex = pos;
  300. tokenizer.Position = pos;
  301. }
  302. #region Async
  303. #if NETSTANDARD1_3
  304. /// <summary>
  305. /// Initiates the asynchronous execution of SQL statements.
  306. /// </summary>
  307. /// <returns>The number of statements executed as part of the script inside.</returns>
  308. public async Task<int> ExecuteAsync()
  309. {
  310. return await ExecuteAsync(CancellationToken.None);
  311. }
  312. /// <summary>
  313. /// Initiates the asynchronous execution of SQL statements.
  314. /// </summary>
  315. /// <returns>The number of statements executed as part of the script inside.</returns>
  316. public async Task<int> ExecuteAsync(CancellationToken cancellationToken)
  317. {
  318. var result = new TaskCompletionSource<int>();
  319. if (cancellationToken == CancellationToken.None || !cancellationToken.IsCancellationRequested)
  320. {
  321. try
  322. {
  323. var executeResult = Execute();
  324. result.SetResult(executeResult);
  325. }
  326. catch (Exception ex)
  327. {
  328. result.SetException(ex);
  329. }
  330. }
  331. else
  332. {
  333. result.SetCanceled();
  334. }
  335. return await result.Task;
  336. }
  337. #else
  338. /// <summary>
  339. /// Initiates the asynchronous execution of SQL statements.
  340. /// </summary>
  341. /// <returns>The number of statements executed as part of the script inside.</returns>
  342. public Task<int> ExecuteAsync()
  343. {
  344. return ExecuteAsync(CancellationToken.None);
  345. }
  346. /// <summary>
  347. /// Initiates the asynchronous execution of SQL statements.
  348. /// </summary>
  349. /// <param name="cancellationToken">The cancellation token.</param>
  350. /// <returns>The number of statements executed as part of the script inside.</returns>
  351. public Task<int> ExecuteAsync(CancellationToken cancellationToken)
  352. {
  353. var result = new TaskCompletionSource<int>();
  354. if (cancellationToken == CancellationToken.None || !cancellationToken.IsCancellationRequested)
  355. {
  356. try
  357. {
  358. var executeResult = Execute();
  359. result.SetResult(executeResult);
  360. }
  361. catch (Exception ex)
  362. {
  363. result.SetException(ex);
  364. }
  365. }
  366. else
  367. {
  368. result.SetCanceled();
  369. }
  370. return result.Task;
  371. }
  372. #endif
  373. #endregion
  374. }
  375. /// <summary>
  376. /// Represents the method that will handle errors when executing MySQL statements.
  377. /// </summary>
  378. internal delegate void MySqlStatementExecutedEventHandler(object sender, MySqlScriptEventArgs args);
  379. /// <summary>
  380. /// Represents the method that will handle errors when executing MySQL scripts.
  381. /// </summary>
  382. internal delegate void MySqlScriptErrorEventHandler(object sender, MySqlScriptErrorEventArgs args);
  383. /// <summary>
  384. /// Sets the arguments associated to MySQL scripts.
  385. /// </summary>
  386. internal class MySqlScriptEventArgs : EventArgs
  387. {
  388. internal ScriptStatement Statement { get; set; }
  389. /// <summary>
  390. /// Gets the statement text.
  391. /// </summary>
  392. /// <value>The statement text.</value>
  393. public string StatementText => Statement.text;
  394. /// <summary>
  395. /// Gets the line.
  396. /// </summary>
  397. /// <value>The line.</value>
  398. public int Line => Statement.line;
  399. /// <summary>
  400. /// Gets the position.
  401. /// </summary>
  402. /// <value>The position.</value>
  403. public int Position => Statement.position;
  404. }
  405. /// <summary>
  406. /// Sets the arguments associated to MySQL script errors.
  407. /// </summary>
  408. internal class MySqlScriptErrorEventArgs : MySqlScriptEventArgs
  409. {
  410. /// <summary>
  411. /// Initializes a new instance of the <see cref="MySqlScriptErrorEventArgs"/> class.
  412. /// </summary>
  413. /// <param name="exception">The exception.</param>
  414. public MySqlScriptErrorEventArgs(Exception exception)
  415. {
  416. Exception = exception;
  417. }
  418. /// <summary>
  419. /// Gets the exception.
  420. /// </summary>
  421. /// <value>The exception.</value>
  422. public Exception Exception { get; }
  423. /// <summary>
  424. /// Gets or sets a value indicating whether this <see cref="MySqlScriptErrorEventArgs"/> is ignored.
  425. /// </summary>
  426. /// <value><c>true</c> if ignore; otherwise, <c>false</c>.</value>
  427. public bool Ignore { get; set; }
  428. }
  429. struct ScriptStatement
  430. {
  431. public string text;
  432. public int line;
  433. public int position;
  434. }
  435. }
  436. #endif