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.

483 lines
13 KiB

4 years ago
  1. #if MYSQL_6_9
  2. // Copyright ?2004, 2014, 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 Externals.MySql.Data.Common;
  24. using System.Collections.Generic;
  25. using System.Text;
  26. using System;
  27. using System.Data;
  28. using System.Globalization;
  29. using System.IO;
  30. using Externals.MySql.Data.MySqlClient.Properties;
  31. using System.Threading.Tasks;
  32. using System.Threading;
  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. private MySqlConnection connection;
  43. private string query;
  44. private string delimiter;
  45. public event MySqlStatementExecutedEventHandler StatementExecuted;
  46. public event MySqlScriptErrorEventHandler Error;
  47. public event EventHandler ScriptCompleted;
  48. #region Constructors
  49. /// <summary>
  50. /// Initializes a new instance of the
  51. /// <see cref="MySqlScript"/> class.
  52. /// </summary>
  53. public MySqlScript()
  54. {
  55. Delimiter = ";";
  56. }
  57. /// <summary>
  58. /// Initializes a new instance of the
  59. /// <see cref="MySqlScript"/> class.
  60. /// </summary>
  61. /// <param name="connection">The connection.</param>
  62. public MySqlScript(MySqlConnection connection)
  63. : this()
  64. {
  65. this.connection = connection;
  66. }
  67. /// <summary>
  68. /// Initializes a new instance of the
  69. /// <see cref="MySqlScript"/> class.
  70. /// </summary>
  71. /// <param name="query">The query.</param>
  72. public MySqlScript(string query)
  73. : this()
  74. {
  75. this.query = query;
  76. }
  77. /// <summary>
  78. /// Initializes a new instance of the
  79. /// <see cref="MySqlScript"/> class.
  80. /// </summary>
  81. /// <param name="connection">The connection.</param>
  82. /// <param name="query">The query.</param>
  83. public MySqlScript(MySqlConnection connection, string query)
  84. : this()
  85. {
  86. this.connection = connection;
  87. this.query = query;
  88. }
  89. #endregion
  90. #region Properties
  91. /// <summary>
  92. /// Gets or sets the connection.
  93. /// </summary>
  94. /// <value>The connection.</value>
  95. public MySqlConnection Connection
  96. {
  97. get { return connection; }
  98. set { connection = value; }
  99. }
  100. /// <summary>
  101. /// Gets or sets the query.
  102. /// </summary>
  103. /// <value>The query.</value>
  104. public string Query
  105. {
  106. get { return query; }
  107. set { query = value; }
  108. }
  109. /// <summary>
  110. /// Gets or sets the delimiter.
  111. /// </summary>
  112. /// <value>The delimiter.</value>
  113. public string Delimiter
  114. {
  115. get { return delimiter; }
  116. set { delimiter = value; }
  117. }
  118. #endregion
  119. #region Public Methods
  120. /// <summary>
  121. /// Executes this instance.
  122. /// </summary>
  123. /// <returns>The number of statements executed as part of the script.</returns>
  124. public int Execute()
  125. {
  126. bool openedConnection = false;
  127. if (connection == null)
  128. throw new InvalidOperationException(Resources.ConnectionNotSet);
  129. if (query == null || query.Length == 0)
  130. return 0;
  131. // next we open up the connetion if it is not already open
  132. if (connection.State != ConnectionState.Open)
  133. {
  134. openedConnection = true;
  135. connection.Open();
  136. }
  137. // since we don't allow setting of parameters on a script we can
  138. // therefore safely allow the use of user variables. no one should be using
  139. // this connection while we are using it so we can temporarily tell it
  140. // to allow the use of user variables
  141. bool allowUserVars = connection.Settings.AllowUserVariables;
  142. connection.Settings.AllowUserVariables = true;
  143. try
  144. {
  145. string mode = connection.driver.Property("sql_mode");
  146. mode = StringUtility.ToUpperInvariant(mode);
  147. bool ansiQuotes = mode.IndexOf("ANSI_QUOTES") != -1;
  148. bool noBackslashEscapes = mode.IndexOf("NO_BACKSLASH_ESCAPES") != -1;
  149. // first we break the query up into smaller queries
  150. List<ScriptStatement> statements = BreakIntoStatements(ansiQuotes, noBackslashEscapes);
  151. int count = 0;
  152. MySqlCommand cmd = new MySqlCommand(null, connection);
  153. foreach (ScriptStatement statement in statements)
  154. {
  155. if (String.IsNullOrEmpty(statement.text)) continue;
  156. cmd.CommandText = statement.text;
  157. try
  158. {
  159. cmd.ExecuteNonQuery();
  160. count++;
  161. OnQueryExecuted(statement);
  162. }
  163. catch (Exception ex)
  164. {
  165. if (Error == null)
  166. throw;
  167. if (!OnScriptError(ex))
  168. break;
  169. }
  170. }
  171. OnScriptCompleted();
  172. return count;
  173. }
  174. finally
  175. {
  176. connection.Settings.AllowUserVariables = allowUserVars;
  177. if (openedConnection)
  178. {
  179. connection.Close();
  180. }
  181. }
  182. }
  183. #endregion
  184. private void OnQueryExecuted(ScriptStatement statement)
  185. {
  186. if (StatementExecuted != null)
  187. {
  188. MySqlScriptEventArgs args = new MySqlScriptEventArgs();
  189. args.Statement = statement;
  190. StatementExecuted(this, args);
  191. }
  192. }
  193. private void OnScriptCompleted()
  194. {
  195. if (ScriptCompleted != null)
  196. ScriptCompleted(this, EventArgs.Empty);
  197. }
  198. private bool OnScriptError(Exception ex)
  199. {
  200. if (Error != null)
  201. {
  202. MySqlScriptErrorEventArgs args = new MySqlScriptErrorEventArgs(ex);
  203. Error(this, args);
  204. return args.Ignore;
  205. }
  206. return false;
  207. }
  208. private List<int> BreakScriptIntoLines()
  209. {
  210. List<int> lineNumbers = new List<int>();
  211. StringReader sr = new StringReader(query);
  212. string line = sr.ReadLine();
  213. int pos = 0;
  214. while (line != null)
  215. {
  216. lineNumbers.Add(pos);
  217. pos += line.Length;
  218. line = sr.ReadLine();
  219. }
  220. return lineNumbers;
  221. }
  222. private static int FindLineNumber(int position, List<int> lineNumbers)
  223. {
  224. int i = 0;
  225. while (i < lineNumbers.Count && position < lineNumbers[i])
  226. i++;
  227. return i;
  228. }
  229. private List<ScriptStatement> BreakIntoStatements(bool ansiQuotes, bool noBackslashEscapes)
  230. {
  231. string currentDelimiter = Delimiter;
  232. int startPos = 0;
  233. List<ScriptStatement> statements = new List<ScriptStatement>();
  234. List<int> lineNumbers = BreakScriptIntoLines();
  235. MySqlTokenizer tokenizer = new MySqlTokenizer(query);
  236. tokenizer.AnsiQuotes = ansiQuotes;
  237. tokenizer.BackslashEscapes = !noBackslashEscapes;
  238. string token = tokenizer.NextToken();
  239. while (token != null)
  240. {
  241. if (!tokenizer.Quoted)
  242. {
  243. if (token.ToLower(CultureInfo.InvariantCulture) == "delimiter")
  244. {
  245. tokenizer.NextToken();
  246. AdjustDelimiterEnd(tokenizer);
  247. currentDelimiter = query.Substring(tokenizer.StartIndex,
  248. tokenizer.StopIndex - tokenizer.StartIndex).Trim();
  249. startPos = tokenizer.StopIndex;
  250. }
  251. else
  252. {
  253. // this handles the case where our tokenizer reads part of the
  254. // delimiter
  255. if (currentDelimiter.StartsWith(token, StringComparison.OrdinalIgnoreCase))
  256. {
  257. if ((tokenizer.StartIndex + currentDelimiter.Length) <= query.Length)
  258. {
  259. if (query.Substring(tokenizer.StartIndex, currentDelimiter.Length) == currentDelimiter)
  260. {
  261. token = currentDelimiter;
  262. tokenizer.Position = tokenizer.StartIndex + currentDelimiter.Length;
  263. tokenizer.StopIndex = tokenizer.Position;
  264. }
  265. }
  266. }
  267. int delimiterPos = token.IndexOf(currentDelimiter, StringComparison.OrdinalIgnoreCase);
  268. if (delimiterPos != -1)
  269. {
  270. int endPos = tokenizer.StopIndex - token.Length + delimiterPos;
  271. if (tokenizer.StopIndex == query.Length - 1)
  272. endPos++;
  273. string currentQuery = query.Substring(startPos, endPos - startPos);
  274. ScriptStatement statement = new ScriptStatement();
  275. statement.text = currentQuery.Trim();
  276. statement.line = FindLineNumber(startPos, lineNumbers);
  277. statement.position = startPos - lineNumbers[statement.line];
  278. statements.Add(statement);
  279. startPos = endPos + currentDelimiter.Length;
  280. }
  281. }
  282. }
  283. token = tokenizer.NextToken();
  284. }
  285. // now clean up the last statement
  286. if (startPos < query.Length - 1)
  287. {
  288. string sqlLeftOver = query.Substring(startPos).Trim();
  289. if (!String.IsNullOrEmpty(sqlLeftOver))
  290. {
  291. ScriptStatement statement = new ScriptStatement();
  292. statement.text = sqlLeftOver;
  293. statement.line = FindLineNumber(startPos, lineNumbers);
  294. statement.position = startPos - lineNumbers[statement.line];
  295. statements.Add(statement);
  296. }
  297. }
  298. return statements;
  299. }
  300. private void AdjustDelimiterEnd(MySqlTokenizer tokenizer)
  301. {
  302. if (tokenizer.StopIndex < query.Length)
  303. {
  304. int pos = tokenizer.StopIndex;
  305. char c = query[pos];
  306. while (!Char.IsWhiteSpace(c) && pos < (query.Length - 1))
  307. {
  308. c = query[++pos];
  309. }
  310. tokenizer.StopIndex = pos;
  311. tokenizer.Position = pos;
  312. }
  313. }
  314. #region Async
  315. /// <summary>
  316. /// Async version of Execute
  317. /// </summary>
  318. /// <returns>The number of statements executed as part of the script inside.</returns>
  319. public Task<int> ExecuteAsync()
  320. {
  321. return ExecuteAsync(CancellationToken.None);
  322. }
  323. public Task<int> ExecuteAsync(CancellationToken cancellationToken)
  324. {
  325. var result = new TaskCompletionSource<int>();
  326. if (cancellationToken == CancellationToken.None || !cancellationToken.IsCancellationRequested)
  327. {
  328. try
  329. {
  330. var executeResult = Execute();
  331. result.SetResult(executeResult);
  332. }
  333. catch (Exception ex)
  334. {
  335. result.SetException(ex);
  336. }
  337. }
  338. else
  339. {
  340. result.SetCanceled();
  341. }
  342. return result.Task;
  343. }
  344. #endregion
  345. }
  346. /// <summary>
  347. ///
  348. /// </summary>
  349. internal delegate void MySqlStatementExecutedEventHandler(object sender, MySqlScriptEventArgs args);
  350. /// <summary>
  351. ///
  352. /// </summary>
  353. internal delegate void MySqlScriptErrorEventHandler(object sender, MySqlScriptErrorEventArgs args);
  354. /// <summary>
  355. ///
  356. /// </summary>
  357. internal class MySqlScriptEventArgs : EventArgs
  358. {
  359. private ScriptStatement statement;
  360. internal ScriptStatement Statement
  361. {
  362. set { this.statement = value; }
  363. }
  364. /// <summary>
  365. /// Gets the statement text.
  366. /// </summary>
  367. /// <value>The statement text.</value>
  368. public string StatementText
  369. {
  370. get { return statement.text; }
  371. }
  372. /// <summary>
  373. /// Gets the line.
  374. /// </summary>
  375. /// <value>The line.</value>
  376. public int Line
  377. {
  378. get { return statement.line; }
  379. }
  380. /// <summary>
  381. /// Gets the position.
  382. /// </summary>
  383. /// <value>The position.</value>
  384. public int Position
  385. {
  386. get { return statement.position; }
  387. }
  388. }
  389. /// <summary>
  390. ///
  391. /// </summary>
  392. internal class MySqlScriptErrorEventArgs : MySqlScriptEventArgs
  393. {
  394. private Exception exception;
  395. private bool ignore;
  396. /// <summary>
  397. /// Initializes a new instance of the <see cref="MySqlScriptErrorEventArgs"/> class.
  398. /// </summary>
  399. /// <param name="exception">The exception.</param>
  400. public MySqlScriptErrorEventArgs(Exception exception)
  401. : base()
  402. {
  403. this.exception = exception;
  404. }
  405. /// <summary>
  406. /// Gets the exception.
  407. /// </summary>
  408. /// <value>The exception.</value>
  409. public Exception Exception
  410. {
  411. get { return exception; }
  412. }
  413. /// <summary>
  414. /// Gets or sets a value indicating whether this <see cref="MySqlScriptErrorEventArgs"/> is ignore.
  415. /// </summary>
  416. /// <value><c>true</c> if ignore; otherwise, <c>false</c>.</value>
  417. public bool Ignore
  418. {
  419. get { return ignore; }
  420. set { ignore = value; }
  421. }
  422. }
  423. struct ScriptStatement
  424. {
  425. public string text;
  426. public int line;
  427. public int position;
  428. }
  429. }
  430. #endif