-- | Read and write stuff in hstore columns.
--
-- Insert crap into table:
--
-- @
-- let myStuff = [(\"test\", \"shmest\"), (\"spam\", \"eggs\"), (\"sausage\", \"salad\")]
-- withTransaction conn $ \trans -> run trans (\"INSERT INTO test_hstore (stuff) VALUES (\" ++ hsQuery myStuff  ++ \");\") (hsParams myStuff)
-- @
--
-- Parse hstore data:
--
-- @
-- rows <- quickQuery conn \"SELECT * FROM test_hstore;\" []
-- forM_ rows $ \[pk, hstuff] -> print $ (fromSql pk :: Integer, hsParse hstuff)
-- @
--

module Database.HDBC.PostgreSQL.HStore where

import Database.HDBC

import Data.Text (Text)
import Data.Text.Encoding (decodeUtf8)
import qualified Data.Text as T
import qualified Data.Attoparsec.Text as AP

import qualified Data.List as L
import qualified Data.Map as M

-- * Make a query

-- | Generate a placeholder string.
hsQuery :: [(String, String)] -> String
hsQuery = L.intercalate " || " . map (\(_, _) -> "hstore(?, ?)")

-- | Generate a parameter list.
hsParams :: [(String, String)] -> [SqlValue]
hsParams = L.concatMap (\(k, v) -> [toSql k, toSql v])

-- * Parse results

-- | Parse a SqlByteString with hstore data to a Map Text Text.
hsParse :: SqlValue -> M.Map Text Text
hsParse (SqlByteString bs) = case hstoreParser `AP.parseOnly` decodeUtf8 bs of
                                 Left err -> error err
                                 Right val -> val

-- ** Parser internals

-- | Parse hstore-formatted value.
hstoreParser :: AP.Parser (M.Map Text Text)
hstoreParser = do
    pairs <- kvPair `AP.sepBy` AP.string (T.pack ", ")
    return $! M.fromList pairs

-- | Parse one key-value pair.
kvPair :: AP.Parser (Text, Text)
kvPair = do
    key <- doubleQuoted
    AP.string $ T.pack "=>"
    value <- doubleQuoted
    return (key, value)

-- | Grab a value, unquote, unslash.
doubleQuoted :: AP.Parser Text
doubleQuoted = do
    AP.char '"'
    str <- AP.scan False $ \s c -> if s then Just False
                                        else if c == '"'
                                             then Nothing
                                             else Just (c == '\\')
    AP.char '"'

    return $! T.replace (T.pack "\\\"") (T.singleton '"') str