Make System.Web.Script.Serialization.JavaScriptSerializer.ConvertToType(Type, object...
[mono.git] / mcs / class / Npgsql / Npgsql / NpgsqlCommand.cs
index f9454d37ff9930c8f6efa5cafae8471757db13cf..4a536daec4ed9373808cb85e8dbd90fdc0ba10a4 100644 (file)
@@ -29,6 +29,7 @@ using System.Text;
 using System.Resources;
 using System.ComponentModel;
 using System.Collections;
+using System.IO;
 
 using NpgsqlTypes;
 
@@ -52,6 +53,7 @@ namespace Npgsql
         // Logging related values
         private static readonly String CLASSNAME = "NpgsqlCommand";
         private static ResourceManager resman = new ResourceManager(typeof(NpgsqlCommand));
+        private static readonly Regex parameterReplace = new Regex(@"([:@][\w\.]*)", RegexOptions.Singleline);
 
         private NpgsqlConnection            connection;
         private NpgsqlConnector             connector;
@@ -69,6 +71,8 @@ namespace Npgsql
         
         private CommandBehavior             commandBehavior;
 
+        private Int64                       lastInsertedOID = 0;
+
         // Constructors
 
         /// <summary>
@@ -107,10 +111,11 @@ namespace Npgsql
                 this.connector = connection.Connector;
 
             parameters = new NpgsqlParameterCollection();
-            timeout = 20;
             type = CommandType.Text;
             this.Transaction = transaction;
             commandBehavior = CommandBehavior.Default;
+
+            SetCommandTimeout();
             
             
         }
@@ -131,6 +136,8 @@ namespace Npgsql
             commandBehavior = CommandBehavior.Default;
             
             parameters = new NpgsqlParameterCollection();
+
+            // Internal commands aren't affected by command timeout value provided by user.
             timeout = 20;
         }
 
@@ -165,7 +172,8 @@ namespace Npgsql
         /// <value>The time (in seconds) to wait for the command to execute.
         /// The default is 20 seconds.</value>
         [DefaultValue(20)]
-        public Int32 CommandTimeout {
+        public Int32 CommandTimeout
+        {
             get
             {
                 return timeout;
@@ -187,7 +195,8 @@ namespace Npgsql
         /// </summary>
         /// <value>One of the <see cref="System.Data.CommandType">CommandType</see> values. The default is <see cref="System.Data.CommandType">CommandType.Text</see>.</value>
         [Category("Data"), DefaultValue(CommandType.Text)]
-        public CommandType CommandType {
+        public CommandType CommandType
+        {
             get
             {
                 return type;
@@ -220,7 +229,8 @@ namespace Npgsql
         /// </summary>
         /// <value>The connection to a data source. The default value is a null reference.</value>
         [Category("Behavior"), DefaultValue(null)]
-        public NpgsqlConnection Connection {
+        public NpgsqlConnection Connection
+        {
             get
             {
                 NpgsqlEventLog.LogPropertyGet(LogLevel.Debug, CLASSNAME, "Connection");
@@ -244,11 +254,14 @@ namespace Npgsql
                 if (this.connection != null)
                     connector = this.connection.Connector;
 
+                SetCommandTimeout();
+                
                 NpgsqlEventLog.LogPropertySet(LogLevel.Debug, CLASSNAME, "Connection", value);
             }
         }
 
-        internal NpgsqlConnector Connector {
+        internal NpgsqlConnector Connector
+        {
             get
             {
                 if (this.connection != null)
@@ -273,7 +286,8 @@ namespace Npgsql
         [Category("Data"), DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
         #endif
         
-        public NpgsqlParameterCollection Parameters {
+        public NpgsqlParameterCollection Parameters
+        {
             get
             {
                 NpgsqlEventLog.LogPropertyGet(LogLevel.Debug, CLASSNAME, "Parameters");
@@ -347,10 +361,22 @@ namespace Npgsql
 
             set
             {
-                throw new NotImplementedException();
             }
         }
 
+        /// <summary>
+        /// Returns oid of inserted row. This is only updated when using executenonQuery and when command inserts just a single row. If table is created without oids, this will always be 0.
+        /// </summary>
+
+       public Int64 LastInsertedOID
+        {
+            get
+            {
+                return lastInsertedOID;
+            }
+        }
+
+
         /// <summary>
         /// Attempts to cancel the execution of a <see cref="Npgsql.NpgsqlCommand">NpgsqlCommand</see>.
         /// </summary>
@@ -359,8 +385,23 @@ namespace Npgsql
         {
             NpgsqlEventLog.LogMethodEnter(LogLevel.Debug, CLASSNAME, "Cancel");
 
-            // [TODO] Finish method implementation.
-            throw new NotImplementedException();
+            try
+            {
+                // get copy for thread safety of null test
+                NpgsqlConnector connector = Connector;
+                if (connector != null)
+                {
+                    connector.CancelRequest();
+                }
+            }
+            catch (IOException)
+            {
+                Connection.ClearPool();
+            }   
+            catch (NpgsqlException)
+            {
+                // Cancel documentation says the Cancel doesn't throw on failure
+            }
         }
         
         /// <summary>
@@ -413,6 +454,9 @@ namespace Npgsql
         {
             NpgsqlEventLog.LogMethodEnter(LogLevel.Debug, CLASSNAME, "ExecuteNonQuery");
 
+            // Initialize lastInsertOID
+            lastInsertedOID = 0;
+
             ExecuteCommand();
             
             UpdateOutputParameters();
@@ -433,18 +477,27 @@ namespace Npgsql
             String[] ret_string_tokens = firstCompletedResponse.Split(null);        // whitespace separator.
 
 
-            // Check if the command was insert, delete or update.
+            // Check if the command was insert, delete, update, fetch or move.
             // Only theses commands return rows affected.
             // [FIXME] Is there a better way to check this??
             if ((String.Compare(ret_string_tokens[0], "INSERT", true) == 0) ||
                     (String.Compare(ret_string_tokens[0], "UPDATE", true) == 0) ||
-                    (String.Compare(ret_string_tokens[0], "DELETE", true) == 0))
-
+                    (String.Compare(ret_string_tokens[0], "DELETE", true) == 0) ||
+                    (String.Compare(ret_string_tokens[0], "FETCH", true) == 0) ||
+                    (String.Compare(ret_string_tokens[0], "MOVE", true) == 0))
+                
+                
+            {
+                if (String.Compare(ret_string_tokens[0], "INSERT", true) == 0)
+                    // Get oid of inserted row.
+                    lastInsertedOID = Int32.Parse(ret_string_tokens[1]);
+                
                 // The number of rows affected is in the third token for insert queries
                 // and in the second token for update and delete queries.
                 // In other words, it is the last token in the 0-based array.
 
                 return Int32.Parse(ret_string_tokens[ret_string_tokens.Length - 1]);
+            }
             else
                 return -1;
         }
@@ -475,16 +528,12 @@ namespace Npgsql
                     
                     foreach (NpgsqlParameter p in Parameters)
                     {
-                        try
+                        if (nrs.RowDescription.FieldIndex(p.ParameterName.Substring(1)) > -1)
                         {
-                            if (nrs.RowDescription.FieldIndex(p.ParameterName.Substring(1)) > -1)
-                            {
-                                hasMapping = true;
-                                break;
-                            }
+                            hasMapping = true;
+                            break;
                         }
-                        catch(ArgumentOutOfRangeException)
-                        {}
+                        
                     }
                                         
                     
@@ -495,13 +544,14 @@ namespace Npgsql
                             if (((p.Direction == ParameterDirection.Output) ||
                                 (p.Direction == ParameterDirection.InputOutput)) && (i < nrs.RowDescription.NumFields ))
                             {
-                                try
+                                Int32 fieldIndex = nrs.RowDescription.FieldIndex(p.ParameterName.Substring(1));
+                                
+                                if (fieldIndex > -1)
                                 {
-                                    p.Value = nar[nrs.RowDescription.FieldIndex(p.ParameterName.Substring(1))];
+                                    p.Value = nar[fieldIndex];
                                     i++;
                                 }
-                                catch(ArgumentOutOfRangeException)
-                                {}
+                                
                             }
                         }
                         
@@ -598,19 +648,37 @@ namespace Npgsql
             if (parameters.Count != 0)
             {
                 Object[] parameterValues = new Object[parameters.Count];
+                Int16[] parameterFormatCodes = bind.ParameterFormatCodes;
+                
                 for (Int32 i = 0; i < parameters.Count; i++)
                 {
                     // Do not quote strings, or escape existing quotes - this will be handled by the backend.
                     // DBNull or null values are returned as null.
                     // TODO: Would it be better to remove this null special handling out of ConvertToBackend??
-                    parameterValues[i] = parameters[i].TypeInfo.ConvertToBackend(parameters[i].Value, true);
+                    
+                    // Do special handling of bytea values. They will be send in binary form.
+                    // TODO: Add binary format support for all supported types. Not only bytea.
+                    if (parameters[i].TypeInfo.NpgsqlDbType != NpgsqlDbType.Bytea)
+                    {
+                        
+                        parameterValues[i] = parameters[i].TypeInfo.ConvertToBackend(parameters[i].Value, true);
+                    }
+                    else
+                    {
+                        parameterFormatCodes[i] = (Int16) FormatCode.Binary;
+                        parameterValues[i]=(byte[])parameters[i].Value;
+                    }
                 }
                 bind.ParameterValues = parameterValues;
+                bind.ParameterFormatCodes = parameterFormatCodes;
             }
 
             Connector.Bind(bind);
+            
+            // See Prepare() method for a discussion of this.
             Connector.Mediator.RequireReadyForQuery = false;
             Connector.Flush();
+            
 
             connector.CheckErrorsAndNotifications();
         }
@@ -625,20 +693,9 @@ namespace Npgsql
         {
             NpgsqlEventLog.LogMethodEnter(LogLevel.Debug, CLASSNAME, "ExecuteScalar");
 
-            /*if ((type == CommandType.Text) || (type == CommandType.StoredProcedure))
-              if (parse == null)
-                               connection.Query(this); 
-               else
-               {
-                 BindParameters();
-                 connection.Execute(new NpgsqlExecute(bind.PortalName, 0));
-               }
-            else
-               throw new NotImplementedException(resman.GetString("Exception_CommandTypeTableDirect"));
-            */
-
             ExecuteCommand();
 
+
             // Now get the results.
             // Only the first column of the first row must be returned.
 
@@ -673,8 +730,14 @@ namespace Npgsql
 
             // Check the connection state.
             CheckConnectionState();
+            
+            // reset any responses just before getting new ones
+            Connector.Mediator.ResetResponses();
+            
+            // Set command timeout.
+            connector.Mediator.CommandTimeout = CommandTimeout;
 
-            if (! Connector.SupportsPrepare)
+            if (! connector.SupportsPrepare)
             {
                 return;        // Do nothing.
             }
@@ -686,21 +749,90 @@ namespace Npgsql
             }
             else
             {
-                // Use the extended query parsing...
-                //planName = "NpgsqlPlan" + Connector.NextPlanIndex();
-                planName = Connector.NextPlanName();
-                String portalName = Connector.NextPortalName();
-
-                parse = new NpgsqlParse(planName, GetParseCommandText(), new Int32[] {});
-
-                Connector.Parse(parse);
-                Connector.Mediator.RequireReadyForQuery = false;
-                Connector.Flush();
-
-                // Check for errors and/or notifications and do the Right Thing.
-                connector.CheckErrorsAndNotifications();
-
-                bind = new NpgsqlBind("", planName, new Int16[] {0}, null, new Int16[] {0});
+                try
+                {
+                    
+                    connector.StopNotificationThread();
+                    
+                    // Use the extended query parsing...
+                    planName = connector.NextPlanName();
+                    String portalName = connector.NextPortalName();
+    
+                    parse = new NpgsqlParse(planName, GetParseCommandText(), new Int32[] {});
+    
+                    connector.Parse(parse);
+                    
+                    // We need that because Flush() doesn't cause backend to send
+                    // ReadyForQuery on error. Without ReadyForQuery, we don't return 
+                    // from query extended processing.
+                    
+                    // We could have used Connector.Flush() which sends us back a
+                    // ReadyForQuery, but on postgresql server below 8.1 there is an error
+                    // with extended query processing which hinders us from using it.
+                    connector.Mediator.RequireReadyForQuery = false;
+                    connector.Flush();
+                    
+                    // Check for errors and/or notifications and do the Right Thing.
+                    connector.CheckErrorsAndNotifications();
+    
+                    
+                    // Description...
+                    NpgsqlDescribe describe = new NpgsqlDescribe('S', planName);
+                
+                
+                    connector.Describe(describe);
+                    
+                    connector.Sync();
+                    
+                    Npgsql.NpgsqlRowDescription returnRowDesc = connector.Mediator.LastRowDescription;
+                
+                    Int16[] resultFormatCodes;
+                    
+                    
+                    if (returnRowDesc != null)
+                    {
+                        resultFormatCodes = new Int16[returnRowDesc.NumFields];
+                        
+                        for (int i=0; i < returnRowDesc.NumFields; i++)
+                        {
+                            Npgsql.NpgsqlRowDescriptionFieldData returnRowDescData = returnRowDesc[i];
+                            
+                            
+                            if (returnRowDescData.type_info != null && returnRowDescData.type_info.NpgsqlDbType == NpgsqlTypes.NpgsqlDbType.Bytea)
+                            {
+                            // Binary format
+                                resultFormatCodes[i] = (Int16)FormatCode.Binary;
+                            }
+                            else 
+                            // Text Format
+                                resultFormatCodes[i] = (Int16)FormatCode.Text;
+                        }
+                    
+                        
+                    }
+                    else
+                        resultFormatCodes = new Int16[]{0};
+                    
+                    bind = new NpgsqlBind("", planName, new Int16[Parameters.Count], null, resultFormatCodes);
+                }    
+                catch (IOException e)
+                {
+                    ClearPoolAndThrowException(e);
+                }
+                catch
+                {
+                    // See ExecuteCommand method for a discussion of this.
+                    connector.Sync();
+                    
+                    throw;
+                }
+                finally
+                {
+                    connector.ResumeNotificationThread();
+                }
+                
+                
+                
             }
         }
 
@@ -831,28 +963,22 @@ namespace Npgsql
             // If parenthesis don't need to be added, they were added by user with parameter names. Replace them.
             if (!addProcedureParenthesis)
             {
-
-                Regex a = new Regex(@"(:[\w]*)|(@[\w]*)|(.)", RegexOptions.Singleline);
-
-                //CheckParameters();
-
                 StringBuilder sb = new StringBuilder();
+                NpgsqlParameter p;
+                string[] queryparts = parameterReplace.Split(result);
 
-                for ( Match m = a.Match(result); m.Success; m = m.NextMatch() )
+                foreach (String s in queryparts)
                 {
-                    String s = m.Groups[0].ToString();
+                    if (s == string.Empty)
+                        continue;
 
-                    if ((s.StartsWith(":") ||
-                         s.StartsWith("@")) &&
-                         Parameters.Contains(s))
+                    if ((s[0] == ':' || s[0] == '@') &&
+                        Parameters.TryGetValue(s, out p))
                     {
                         // It's a parameter. Lets handle it.
-
-                        NpgsqlParameter p = Parameters[s];
                         if ((p.Direction == ParameterDirection.Input) ||
                              (p.Direction == ParameterDirection.InputOutput))
                         {
-
                             // FIXME DEBUG ONLY
                             // adding the '::<datatype>' on the end of a parameter is a highly
                             // questionable practice, but it is great for debugging!
@@ -873,13 +999,11 @@ namespace Npgsql
                     }
                     else
                         sb.Append(s);
-
                 }
 
                 result = sb.ToString();
             }
 
-
             else
             {
 
@@ -925,11 +1049,17 @@ namespace Npgsql
         
         private Boolean CheckFunctionReturn(String ReturnType)
         {
+            // Updated after 0.99.3 to support the optional existence of a name qualifying schema and allow for case insensitivity
+            // when the schema or procedure name do not contain a quote.
+            // The hard-coded schema name 'public' was replaced with code that uses schema as a qualifier, only if it is provided.
 
-            String returnRecordQuery = "select count(*) > 0 from pg_proc where prorettype = ( select oid from pg_type where typname = :typename ) and proargtypes=:proargtypes and proname=:proname;";
+            String returnRecordQuery;
 
             StringBuilder parameterTypes = new StringBuilder("");
 
+            
+            // Process parameters
+            
             foreach(NpgsqlParameter p in Parameters)
             {
                 if ((p.Direction == ParameterDirection.Input) ||
@@ -939,6 +1069,33 @@ namespace Npgsql
                 }
             }
 
+            
+            // Process schema name.
+            
+            String schemaName = String.Empty;
+            String procedureName = String.Empty;
+            
+            
+            String[] fullName = CommandText.Split('.');
+            
+            if (fullName.Length == 2)
+            {
+                returnRecordQuery = "select count(*) > 0 from pg_proc p left join pg_namespace n on p.pronamespace = n.oid where prorettype = ( select oid from pg_type where typname = :typename ) and proargtypes=:proargtypes and proname=:proname and n.nspname=:nspname";
+
+                schemaName = (fullName[0].IndexOf("\"") != -1) ? fullName[0] : fullName[0].ToLower();
+                procedureName = (fullName[1].IndexOf("\"") != -1) ? fullName[1] : fullName[1].ToLower();
+            }
+            else
+            {
+                // Instead of defaulting don't use the nspname, as an alternative, query pg_proc and pg_namespace to try and determine the nspname.
+                //schemaName = "public"; // This was removed after build 0.99.3 because the assumption that a function is in public is often incorrect.
+                returnRecordQuery = "select count(*) > 0 from pg_proc p where prorettype = ( select oid from pg_type where typname = :typename ) and proargtypes=:proargtypes and proname=:proname";
+                
+                procedureName = (CommandText.IndexOf("\"") != -1) ? CommandText : CommandText.ToLower();
+            }
+                
+            
+            
 
             NpgsqlCommand c = new NpgsqlCommand(returnRecordQuery, Connection);
             
@@ -948,13 +1105,23 @@ namespace Npgsql
             
             c.Parameters[0].Value = ReturnType;
             c.Parameters[1].Value = parameterTypes.ToString();
-            c.Parameters[2].Value = CommandText;
+            c.Parameters[2].Value = procedureName;
+
+            if (schemaName != null && schemaName.Length > 0)
+            {
+                c.Parameters.Add(new NpgsqlParameter("nspname", NpgsqlDbType.Text));
+                c.Parameters[3].Value = schemaName;
+            }
             
 
             Boolean ret = (Boolean) c.ExecuteScalar();
 
             // reset any responses just before getting new ones
             connector.Mediator.ResetResponses();
+            
+            // Set command timeout.
+            connector.Mediator.CommandTimeout = CommandTimeout;
+            
             return ret;
 
 
@@ -1017,6 +1184,9 @@ namespace Npgsql
             // reset any responses just before getting new ones
             connector.Mediator.ResetResponses();
             
+            // Set command timeout.
+            connector.Mediator.CommandTimeout = CommandTimeout;
+            
             return sb.ToString();
                     
             
@@ -1161,7 +1331,7 @@ namespace Npgsql
 
                 }
 
-                //[TODO] Check if there is any missing parameters in the query.
+                //[TODO] Check if there are any missing parameters in the query.
                 // For while, an error is thrown saying about the ':' char.
 
                 command.Append('(');
@@ -1191,41 +1361,51 @@ namespace Npgsql
         }
 
 
-        private String ReplaceParameterValue(String result, String parameterName, String paramVal)
+        private static String ReplaceParameterValue(String result, String parameterName, String paramVal)
         {
-            Int32 resLen = result.Length;
-            Int32 paramStart = result.IndexOf(parameterName);
-            Int32 paramLen = parameterName.Length;
-            Int32 paramEnd = paramStart + paramLen;
+        
+            String quote_pattern = @"['][^']*[']";
+            String pattern = "[- |\n\r\t,)(;=+/]" + parameterName + "([- |\n\r\t,)(;=+/]|$)";
+            Int32 start, end;
+            String withoutquote = result;
             Boolean found = false;
-
-
-            while(paramStart > -1)
+            // First of all
+            // Suppress quoted string from query (because we ave to ignore them)
+            MatchCollection results = Regex.Matches(result,quote_pattern);
+            foreach (Match match in results)
             {
-                if((resLen > paramEnd) && !Char.IsLetterOrDigit(result, paramEnd))
-                {
-                    result = result.Substring(0, paramStart) + paramVal + result.Substring(paramEnd);
-                    found = true;
-                }
-                else if(resLen == paramEnd)
-                {
-                    result = result.Substring(0, paramStart)+ paramVal;
-                    found = true;
-                }
-                else
+                start = match.Index;
+                end = match.Index + match.Length;
+                String spaces = new String(' ', match.Length-2);
+                withoutquote = withoutquote.Substring(0,start + 1) + spaces + withoutquote.Substring(end - 1);
+            }
+            do
+            {
+                // Now we look for the searched parameters on the "withoutquote" string
+                results = Regex.Matches(withoutquote,pattern);
+                if (results.Count == 0)
+                // If no parameter is found, go out!
                     break;
-                resLen = result.Length;
-                paramStart = result.IndexOf(parameterName, paramStart);
-                paramEnd = paramStart + paramLen;
-
-            }//while
-            if(!found)
-                throw new IndexOutOfRangeException (String.Format(resman.GetString("Exception_ParamNotInQuery"), parameterName));
-
-
+                // We take the first parameter found
+                found = true;
+                Match match = results[0];
+                start = match.Index;
+                if ((match.Length - parameterName.Length) == 2)
+                    // If the found string is not the end of the string
+                    end = match.Index + match.Length - 1;
+                else
+                    // If the found string is the end of the string
+                    end = match.Index + match.Length;
+                result = result.Substring(0, start + 1) + paramVal + result.Substring(end);
+                withoutquote = withoutquote.Substring(0,start + 1) + paramVal + withoutquote.Substring(end);
+            }
+            while (true);
+            if (!found)
+                throw new IndexOutOfRangeException (String.Format(resman.GetString("Exception_ParamNotInQuery"),
+                    parameterName));
             return result;
-        }//ReplaceParameterValue
-        
+        }
+
         
         private String AddSingleRowBehaviorSupport(String ResultCommandText)
         {
@@ -1272,19 +1452,34 @@ namespace Npgsql
 
         private void ExecuteCommand()
         {
+            try
+            {
+                
             // Check the connection state first.
             CheckConnectionState();
 
             // reset any responses just before getting new ones
             Connector.Mediator.ResetResponses();
+            
+            // Set command timeout.
+            connector.Mediator.CommandTimeout = CommandTimeout;
+            
+            
+            connector.StopNotificationThread();
 
 
             if (parse == null)
             {
-                Connector.Query(this);
+                connector.Query(this);
+
 
+                connector.ResumeNotificationThread();
+                
                 // Check for errors and/or notifications and do the Right Thing.
                 connector.CheckErrorsAndNotifications();
+                
+                
+                
             }
             else
             {
@@ -1298,7 +1493,7 @@ namespace Npgsql
                     // Check for errors and/or notifications and do the Right Thing.
                     connector.CheckErrorsAndNotifications();
                 }
-                finally
+                catch
                 {
                     // As per documentation:
                     // "[...] When an error is detected while processing any extended-query message,
@@ -1307,10 +1502,39 @@ namespace Npgsql
                     // So, send a sync command if we get any problems.
 
                     connector.Sync();
+                    
+                    throw;
+                }
+                finally
+                {
+                    connector.ResumeNotificationThread();
                 }
             }
+
+            }
+
+            catch(IOException e)
+            {
+                ClearPoolAndThrowException(e);
+            }
+
         }
-        
+
+        private void SetCommandTimeout()
+        {
+            if (Connector != null)
+                timeout = Connector.CommandTimeout;
+            else
+                timeout = ConnectionStringDefaults.CommandTimeout;
+        }
+
+        private void ClearPoolAndThrowException(Exception e)
+        {
+            Connection.ClearPool();
+            throw new NpgsqlException(resman.GetString("Exception_ConnectionBroken"), e);
+
+        }
+