// Copyright (c) 2004-2008 MySQL AB, 2008-2009 Sun Microsystems, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 as published by // the Free Software Foundation // // There are special exceptions to the terms and conditions of the GPL // as it is applied to this software. View the full text of the // exception in file EXCEPTIONS in the directory of this software // distribution. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA using System; using System.Data; using System.Globalization; using System.Text; using MySql.Data.Types; using MySql.Data.MySqlClient.Properties; namespace MySql.Data.MySqlClient { /// /// Summary description for StoredProcedure. /// internal class StoredProcedure : PreparableStatement { private string outSelect; private DataTable parametersTable; private string resolvedCommandText; // Prefix used for to generate inout or output parameters names internal const string ParameterPrefix = "_cnet_param_"; public StoredProcedure(MySqlCommand cmd, string text) : base(cmd, text) { } private string GetReturnParameter() { if (Parameters != null) foreach (MySqlParameter p in Parameters) if (p.Direction == ParameterDirection.ReturnValue) return p.ParameterName.Substring(1); return null; } public override string ResolvedCommandText { get { return resolvedCommandText; } } private DataSet GetParameters(string procName) { // if we can use mysql.proc, then do so //if (Connection.Settings.UseProcedureBodies) DataSet ds = Connection.ProcedureCache.GetProcedure(Connection, procName); if(ds.Tables.Count == 2) { // if we got our parameters and our user says it is ok to use proc bodies // then just return them if (Connection.Settings.UseProcedureBodies) return ds; // we got the parameters, but ignore them. if(ds.Tables.Contains("Procedure Parameters")) ds.Tables.Remove("Procedure Parameters"); } // we were not able to retrieve parameter data so we have to make do by // adding the parameters from the command object to our table // we use an internal method to create our procedure parameters table. ISSchemaProvider sp = new ISSchemaProvider(Connection); DataTable pTable = sp.CreateParametersTable(); ds.Tables.Add(pTable); // now we run through the parameters that were set and fill in the parameters table // the best we can int pos = 1; foreach (MySqlParameter p in command.Parameters) { // in this mode, all parameters must have their type set if (!p.TypeHasBeenSet) throw new InvalidOperationException(Resources.NoBodiesAndTypeNotSet); DataRow row = pTable.NewRow(); row["PARAMETER_NAME"] = p.ParameterName; row["PARAMETER_MODE"] = "IN"; if (p.Direction == ParameterDirection.InputOutput) row["PARAMETER_MODE"] = "INOUT"; else if (p.Direction == ParameterDirection.Output) row["PARAMETER_MODE"] = "OUT"; else if (p.Direction == ParameterDirection.ReturnValue) { row["PARAMETER_MODE"] = "OUT"; row["ORDINAL_POSITION"] = 0; } else row["ORDINAL_POSITION"] = pos++; pTable.Rows.Add(row); } return ds; } public static string GetFlags(string dtd) { int x = dtd.Length - 1; while (x > 0 && (Char.IsLetterOrDigit(dtd[x]) || dtd[x] == ' ')) x--; return dtd.Substring(x).ToUpper(CultureInfo.InvariantCulture); } private string FixProcedureName(string name) { string[] parts = name.Split('.'); for (int i = 0; i < parts.Length; i++) if (!parts[i].StartsWith("`")) parts[i] = String.Format("`{0}`", parts[i]); if (parts.Length == 1) return parts[0]; return String.Format("{0}.{1}", parts[0], parts[1]); } private MySqlParameter GetAndFixParameter(DataRow param, bool realAsFloat, string returnParameter) { string mode = (string)param["PARAMETER_MODE"]; string pName = (string)param["PARAMETER_NAME"]; if (param["ORDINAL_POSITION"].Equals(0)) pName = returnParameter; if (pName == null) return null; // make sure the parameters given to us have an appropriate // type set if it's not already MySqlParameter p = command.Parameters.GetParameterFlexible(pName, true); if (!p.TypeHasBeenSet) { string datatype = (string)param["DATA_TYPE"]; bool unsigned = GetFlags(param["DTD_IDENTIFIER"].ToString()).IndexOf("UNSIGNED") != -1; p.MySqlDbType = MetaData.NameToType(datatype, unsigned, realAsFloat, Connection); } return p; } public override void Resolve(bool preparing) { // check to see if we are already resolved if (resolvedCommandText != null) return; // first retrieve the procedure definition from our // procedure cache string spName = commandText; if (spName.IndexOf(".") == -1 && !String.IsNullOrEmpty(Connection.Database)) spName = Connection.Database + "." + spName; spName = FixProcedureName(spName); DataSet ds = GetParameters(spName); DataTable procTable = ds.Tables["procedures"]; parametersTable = ds.Tables["procedure parameters"]; if (procTable.Rows.Count == 0) throw new InvalidOperationException(String.Format(Resources.RoutineNotFound, spName)); bool realAsFloat = procTable.Rows[0]["SQL_MODE"].ToString().IndexOf("REAL_AS_FLOAT") != -1; StringBuilder sqlStr = new StringBuilder(); StringBuilder outSql = new StringBuilder(); string sqlDelimiter = ""; string outDelimiter = ""; string retParm = GetReturnParameter(); foreach (DataRow param in parametersTable.Rows) { MySqlParameter p = GetAndFixParameter(param, realAsFloat, retParm); if (p == null) continue; if (param["ORDINAL_POSITION"].Equals(0)) continue; string baseName = p.ParameterName; string pName = baseName; if (baseName.StartsWith("@") || baseName.StartsWith("?")) baseName = baseName.Substring(1); else pName = "@" + pName; string inputVar = pName; if (p.Direction != ParameterDirection.Input && !(Connection.driver.SupportsOutputParameters || preparing)) { // set a user variable to our current value string sql = String.Format("SET @{0}{1}={2}", ParameterPrefix, baseName, pName); MySqlCommand cmd = new MySqlCommand(sql, Connection); cmd.Parameters.Add(p); cmd.ExecuteNonQuery(); inputVar = String.Format("@{0}{1}", ParameterPrefix, baseName); outSql.AppendFormat(CultureInfo.InvariantCulture, "{0}{1}", outDelimiter, inputVar); outDelimiter = ", "; } sqlStr.AppendFormat(CultureInfo.InvariantCulture, "{0}{1}", sqlDelimiter, inputVar); sqlDelimiter = ", "; } string sqlCmd = sqlStr.ToString().TrimEnd(' ', ','); outSelect = outSql.ToString().TrimEnd(' ', ','); if (procTable.Rows[0]["ROUTINE_TYPE"].Equals("PROCEDURE")) sqlCmd = String.Format("call {0} ({1})", spName, sqlCmd); else { if (retParm == null) retParm = ParameterPrefix + "dummy"; else outSelect = String.Format("@{0}{1}", ParameterPrefix, retParm); sqlCmd = String.Format("SET @{0}{1}={2}({3})", ParameterPrefix, retParm, spName, sqlCmd); } resolvedCommandText = sqlCmd; } private MySqlDataReader GetHackedOuputParameters() { if (outSelect.Length == 0) return null; MySqlCommand cmd = new MySqlCommand("SELECT " + outSelect, Connection); MySqlDataReader reader = cmd.ExecuteReader(); // since MySQL likes to return user variables as strings // we reset the types of the readers internal value objects // this will allow those value objects to parse the string based // return values ResultSet results = reader.ResultSet; for (int i = 0; i < reader.FieldCount; i++) { string fieldName = reader.GetName(i); fieldName = fieldName.Remove(0, ParameterPrefix.Length + 1); MySqlParameter parameter = Parameters.GetParameterFlexible(fieldName, true); results.SetValueObject(i, MySqlField.GetIMySqlValue(parameter.MySqlDbType)); } if (!reader.Read()) { reader.Close(); return null; } return reader; } public override void Close(MySqlDataReader reader) { base.Close(reader); ResultSet rs = reader.ResultSet; // if our closing reader doesn't have output parameters then we may have to // use the user variable hack if (rs == null || !rs.IsOutputParameters) { MySqlDataReader rdr = GetHackedOuputParameters(); if (rdr == null) return; reader = rdr; } using (reader) { string prefix = "@" + ParameterPrefix; for (int i = 0; i < reader.FieldCount; i++) { string fieldName = reader.GetName(i); if (fieldName.StartsWith(prefix)) fieldName = fieldName.Remove(0, prefix.Length); MySqlParameter parameter = Parameters.GetParameterFlexible(fieldName, true); parameter.Value = reader.GetValue(i); } reader.Close(); } } } }