Search code examples
c#sqlpostgresqlnpgsql

multiple cursor using Npgsql?


I'm used to (from Ada using libpq) to

  • open a cursor

  • get some key values

  • use those key values as bind parameters for other statements.

But I get a Npgsql.NpgsqlOperationInProgressException instead.

I once got this working with sql-server, but solved it via adding 'MARS' to the connect string (Multiple Active Record Sets)

Can I do something like that here to?

Here what I'm trying to do:

            conn.Open();

        // Define a query
        NpgsqlCommand cmdGetSelectionIds = new NpgsqlCommand("select distinct(R.SELECTIONID) from ARUNNERS R, AMARKETS M " +
                                              "where true " +
                                              "and M.MARKETID = R.MARKETID " +
                                              "and M.MARKETTYPE = 'WIN' " +
                                              "and R.STATUS <> 'REMOVED'", conn);


        NpgsqlCommand cmdNumWins = new NpgsqlCommand("select count('a') from ARUNNERS R, AMARKETS M " +
                                              "where true " +
                                              "and M.MARKETID = R.MARKETID " +
                                              "and R.SELECTIONID = @selid " +
                                              "and M.MARKETTYPE = 'WIN' " +
                                              "and R.STATUS = 'WINNER'", conn);

        NpgsqlCommand cmdNumPlcs = new NpgsqlCommand("select count('a') from ARUNNERS R, AMARKETS M " +
                                              "where true " +
                                              "and M.MARKETID = R.MARKETID " +
                                              "and R.SELECTIONID = @selid " +
                                              "and M.MARKETTYPE = 'PLACE' " +
                                              "and R.STATUS = 'WINNER'", conn);

        NpgsqlCommand cmdNumLosses = new NpgsqlCommand("select count('a') from ARUNNERS R, AMARKETS M " +
                                              "where true " +
                                              "and M.MARKETID = R.MARKETID " +
                                              "and R.SELECTIONID = @selid " +
                                              "and M.MARKETTYPE = 'WIN' " +
                                              "and R.STATUS = 'LOSER'", conn);

        NpgsqlDataReader drGetSelectionIds = cmdGetSelectionIds.ExecuteReader();

        while (drGetSelectionIds.Read())
        {
            selid = drGetSelectionIds.GetInt32(0);


            cmdNumWins.Parameters.AddWithValue("selid", selid);
            using (NpgsqlDataReader drNumWins = cmdNumWins.ExecuteReader())
            { 
                if (drNumWins.Read()) numWins = drNumWins.GetInt32(0);
            }

            using (NpgsqlDataReader drNumPlcs = cmdNumPlcs.ExecuteReader())
            {
                if (drNumPlcs.Read()) numPlcs = drNumPlcs.GetInt32(0);
            }

            using (NpgsqlDataReader drNumLosses = cmdNumLosses.ExecuteReader()) {
                if (drNumLosses.Read()) numPlcs = drNumLosses.GetInt32(0);
            }

            Console.Write("selid : {0} \t num W {1} \t num P {2} num L {3} \t points {4}\n", selid, numWins, numPlcs, numLosses, (3.0*numWins + numPlcs )/(numWins+numLosses));


        }
        // Close connection
        conn.Close();

Yes, I could read the first statement into a list and loop over that, but I do have got quite some legacy code that is build like the above.

/Björn


Solution

  • The problem is NpgSql only allows a single data reader to be open at a time. If you have a situation where you need more than one data reader open against the same connection at the same time, I would argue there may be a better way of reaching your end goal.

    So, disclaimer... don't do this. BUT, if you wanted to do what you described above, this would be a way to accomplish it:

    cmdNumWins.Parameters.Add(new NpgsqlParameter("@selid", NpgsqlDbType.Integer));
    cmdNumPlcs.Parameters.Add(new NpgsqlParameter("@selid", NpgsqlDbType.Integer));
    cmdNumLosses.Parameters.Add(new NpgsqlParameter("@selid", NpgsqlDbType.Integer));
    
    List<int> idList = new List<int>();
    
    using (NpgsqlDataReader drGetSelectionIds = cmdGetSelectionIds.ExecuteReader())
    {
        while (drGetSelectionIds.Read())
            idList.Add(drGetSelectionIds.GetInt32(0));
    
        drGetSelectionIds.Close();
    }
    
    foreach (int selid in idList)
    {
        cmdNumWins.Parameters[0].Value = selid;
        cmdNumPlcs.Parameters[0].Value = selid;
        cmdNumLosses.Parameters[0].Value = selid;
    
        numWins = Convert.ToInt32(cmdNumWins.ExecuteScalar());
        numPlcs = Convert.ToInt32(cmdNumPlcs.ExecuteScalar());
        numLosses = Convert.ToInt32(cmdNumLosses.ExecuteScalar());
    
        Console.Write("selid : {0} \t num W {1} \t num P {2} num L {3} \t points {4}\n", selid, 
            numWins, numPlcs, numLosses, (3.0 * numWins + numPlcs) / (numWins + numLosses));
    }
    

    The better way would be to do this all as a single query:

    select
      r.selectionid,
      count (1) filter (where m.markettype = 'WIN' and r.status = 'WINNER') as win,
      count (1) fitler (where m.markettype = 'PLACE' and r.status = 'WINNER') as place,
      count (1) filter (where m.markettype = 'WIN' and r.status = 'LOSER') as lose
    from
      arunners r
      join amarkets m on m.marketid = r.marketid
    where
      r.status != 'REMOVED'
    group by
      r.selectionid
    

    I may have missed some nuances about your logic, but hopefully you get the idea. This should do in one fell swoop, quickly and efficiently, what your C# logic is doing. It will be much friendlier to the database and for large datasets should be quite a bit quicker.