haskell-opaleye icon indicating copy to clipboard operation
haskell-opaleye copied to clipboard

PGNumeric support

Open lippling opened this issue 8 years ago • 45 comments

I only saw a PGNumeric instance for IsSqlType. Am I missing something or is the PGNumeric support incomplete?

lippling avatar Nov 05 '16 23:11 lippling

There's instance PGOrd T.PGNumeric here: https://github.com/tomjaguarpaw/haskell-opaleye/blob/7b5e780513ba77d51f43aa78899a78d87fa93ac5/src/Opaleye/Order.hs#L79

But I guess you are saying that we are missing

pgNumeric :: Integer -> Column PGNumeric

instance C.PGNum PGNumeric where
  pgFromInteger = pgNumeric

Do we need anything else? Please feel free to submit a PR!

tomjaguarpaw avatar Nov 06 '16 09:11 tomjaguarpaw

I'm pretty new to Haskell and even newer to Opaleye, so sorry for sticking in like this. Shouldn't Numeric be arbitrary precision decimal numbers? Considering that, wouldn't it be better to use Rational in Haskell as the equivalent type for postgresql's Numeric? I was thinking something like

pgNumeric :: (Integral a) => Ratio a -> Column PGnumeric

https://www.postgresql.org/docs/9.2/static/datatype-numeric.html

mzabani avatar Nov 16 '16 21:11 mzabani

Sure, that sounds better.

tomjaguarpaw avatar Nov 16 '16 21:11 tomjaguarpaw

Just got bitten by this. Is the Default instance (or is it QueryRunnerColumnDefault?) absent because there's no real consensus on what PGNumeric should map to on the Haskell side?

saurabhnanda avatar Dec 27 '16 11:12 saurabhnanda

It's absent because no one's ever got round to doing anything about it. According to the postgresql-simple documentation PGNumeric can map to Scientific and Ratio Integer. Does anyone want to submit a PR?

https://hackage.haskell.org/package/postgresql-simple-0.5.0.0/docs/Database-PostgreSQL-Simple-FromField.html#v:pgArrayFieldParser

tomjaguarpaw avatar Dec 27 '16 12:12 tomjaguarpaw

Will submit a PR after getting it to work in our app.

saurabhnanda avatar Dec 27 '16 13:12 saurabhnanda

So, we need both instances, right? Default AND QueryRunnerColumnDefault?

saurabhnanda avatar Dec 27 '16 13:12 saurabhnanda

which HPQ.Literal should I be using to complete the following:

pgRatioToNumeric :: Ratio Integer -> Column PGNumeric
pgRatioToNumeric r = undefined

instance ProDef.Default Constant (Ratio Integer) (Column PGNumeric) where
  def = Constant pgRatioToNumeric

saurabhnanda avatar Dec 27 '16 14:12 saurabhnanda

You need QueryRunnerColumnDefault and Default Constant, I guess.

You can try DoubleLit or OtherLit.

tomjaguarpaw avatar Dec 27 '16 15:12 tomjaguarpaw

Perhaps ideally you need to create NumericLit but you can try one of those two to start with.

tomjaguarpaw avatar Dec 27 '16 15:12 tomjaguarpaw

I've gotten it to work with the following, but I'm not happy:

instance QueryRunnerColumnDefault PGNumeric (Ratio Integer) where
  queryRunnerColumnDefault = fieldQueryRunnerColumn


pgRatioToNumeric :: Ratio Integer -> Column PGNumeric
pgRatioToNumeric r = IPT.literalColumn $ HPQ.DoubleLit (nr/dr)
  where
    nr :: Double
    nr = fromIntegral $ numerator r

    dr:: Double
    dr = fromIntegral $ denominator r

Problems:

  1. The double conversion is not have any fixed precision and may blow-up if the PG column has a fixed precision+scale
  2. Ratio Integer is horrible to work with. Especially for monetary/currency values, which is the biggest reason why we're using NUMERIC columns in our schema.

How about https://hackage.haskell.org/package/Decimal-0.4.2 ? Anything better?

saurabhnanda avatar Dec 27 '16 18:12 saurabhnanda

The double conversion is not have any fixed precision and may blow-up if the PG column has a fixed precision+scale

That's fair enough. Use OtherLit then (probably combined with the show of the Ratio Integer).

Ratio Integer is horrible to work with. Especially for monetary/currency values, which is the biggest reason why we're using NUMERIC columns in our schema.

Why not Scientific?

tomjaguarpaw avatar Dec 27 '16 19:12 tomjaguarpaw

Announcement

NB I'm taking a holiday from my open source projects until at least 16th January :) Good luck getting this to work. If you really need help from someone before then perhaps you can contact some of the other users who have been posting in the issues pages.

tomjaguarpaw avatar Dec 27 '16 19:12 tomjaguarpaw

@tomjaguarpaw one last question before you go :)

Is this error related?

convertAmountToINR :: Query PaymentPGRead -> Query PaymentPGRead
convertAmountToINR payQuery = proc () -> do
  p <- payQuery -< ()
  fx <- queryTable fxRateTable -< ()
  -- NOTE: This is an inner-join and in the edge-case that paymentCurrency=>INR
  -- rate is not available, it might give incorrect results
  restrict -< ((p ^. currency) .== (fx ^. fromCurrency)
               .&& (fx ^. toCurrency) .== (constant "INR"))
  returnA -< p{payCurrency=(constant "INR"), payAmount=((p ^. amount) * (fx ^. rate))}

   314  57 error           error:
     • No instance for (Opaleye.Internal.Column.PGNum PGNumeric)
         arising from a use of ‘*’
     • In the ‘payAmount’ field of a record
       In the expression:
         p {payCurrency = (constant "INR"),
            payAmount = ((p ^. amount) * (fx ^. rate))}
       In the command: returnA -< p {payCurrency = (constant "INR"),
                                     payAmount = ((p ^. amount) * (fx ^. rate))} (intero)

saurabhnanda avatar Dec 27 '16 19:12 saurabhnanda

As this comment says https://github.com/tomjaguarpaw/haskell-opaleye/issues/230#issuecomment-258666597 you need a PGNum instance in PGTypes.hs

tomjaguarpaw avatar Dec 27 '16 19:12 tomjaguarpaw

You'll have to work out how to implement pgNumeric. It should be no more difficult than your pgRatioToNumeric.

tomjaguarpaw avatar Dec 27 '16 19:12 tomjaguarpaw

Now I really am out of here! Bye.

tomjaguarpaw avatar Dec 27 '16 19:12 tomjaguarpaw

(If you need any more help with PGNumeric I'm sure @lippling can help you)

tomjaguarpaw avatar Dec 27 '16 19:12 tomjaguarpaw

Closing as stale. Feel free to reopen if necessary. I'd like to add PGNumeric support to Opaleye but I'm not going to do it unless someone's going to use it.

tomjaguarpaw avatar Aug 23 '17 12:08 tomjaguarpaw

@saurabhnanda What's the status on this?

ruhatch avatar Oct 10 '17 18:10 ruhatch

@ruhatch I don't think anyone attempted it but it should be very easy.

tomjaguarpaw avatar Oct 13 '17 11:10 tomjaguarpaw

We've got a version working in our project, but it's not correct. Creating a Decimal requires the knowledge of a precision, IIRC.

      instance FromField Decimal where
        fromField field maybebs = (realToFrac :: Rational -> Decimal)  <$> fromField field maybebs
      instance QueryRunnerColumnDefault PGNumeric Decimal where
        queryRunnerColumnDefault = fieldQueryRunnerColumn
      instance Default Constant Decimal (Column PGNumeric) where
        def = Constant f1
          where
          f1 :: Decimal -> (Column PGNumeric)
          f1 x = unsafeCoerceColumn $ pgDouble $ realToFrac x

saurabhnanda avatar Oct 13 '17 13:10 saurabhnanda

@saurabhnanda I don't understand what's difficult about this. Could you explain? @ruhatch Perhaps you'd like to open a new issue with a specific request. I've never fully grasped what was being asked for in this thread.

tomjaguarpaw avatar Oct 15 '17 09:10 tomjaguarpaw

@tomjaguarpaw can you please reopen this issue. I'd like to pick this up and submit a PR.

saurabhnanda avatar Nov 06 '17 13:11 saurabhnanda

Sure, though perhaps you could clarify exactly what this issue is about.

tomjaguarpaw avatar Nov 06 '17 13:11 tomjaguarpaw

This PR isn't going to be easy, and may need some brainstorming. Quoting from the relevant PG documentation:


The type numeric can store numbers with a very large number of digits. It is especially recommended for storing monetary amounts and other quantities where exactness is required. Calculations with numeric values yield exact results where possible, e.g. addition, subtraction, multiplication. However, calculations on numeric values are very slow compared to the integer types, or to the floating-point types described in the next section.

We use the following terms below: The scale of a numeric is the count of decimal digits in the fractional part, to the right of the decimal point. The precision of a numeric is the total count of significant digits in the whole number, that is, the number of digits to both sides of the decimal point. So the number 23.5141 has a precision of 6 and a scale of 4. Integers can be considered to have a scale of zero.

Both the maximum precision and the maximum scale of a numeric column can be configured. To declare a column of type numeric use the syntax:

NUMERIC(precision, scale)

The precision must be positive, the scale zero or positive. Alternatively:

NUMERIC(precision)

selects a scale of 0. Specifying:

NUMERIC

without any precision or scale creates a column in which numeric values of any precision and scale can be stored, up to the implementation limit on precision. A column of this kind will not coerce input values to any particular scale, whereas numeric columns with a declared scale will coerce input values to that scale. (The SQL standard requires a default scale of 0, i.e., coercion to integer precision. We find this a bit useless. If you're concerned about portability, always specify the precision and scale explicitly.)


If I'm reading this correctly, a NUMERIC column can behave like one of the following three things:

  • Arbitrary precision (mapping to a Scientific, Double, or Float on the Haskell side)
  • Zero precision (mapping to an Integral type on the Haskell side)
  • Fixed precision and scale (most probably mapping only to Decimal on the Haskell side)

saurabhnanda avatar Nov 06 '17 13:11 saurabhnanda

Sure, though perhaps you could clarify exactly what this issue is about.

It seems that the Default Constant and Default QueryRunner instances for PGNumeric are missing.

saurabhnanda avatar Nov 06 '17 13:11 saurabhnanda

I see, thanks.

tomjaguarpaw avatar Nov 06 '17 13:11 tomjaguarpaw

I think the lowest common denominator here is to treat Opaleye PGNumeric as Postgres NUMERIC, i.e. arbitrary precision, and probably map it to Scientific. Double and Float seem like bad matches for this datatype.

tomjaguarpaw avatar Nov 06 '17 13:11 tomjaguarpaw

Later one we can consider type-level Natural indexing for the types with specific precision and scale, but I think we should solve the simple case first.

tomjaguarpaw avatar Nov 06 '17 13:11 tomjaguarpaw