packages feed

odbc 0.0.5 → 0.1.0

raw patch · 6 files changed

+283/−40 lines, 6 filesdep +parsecdep +template-haskellPVP ok

version bump matches the API change (PVP)

Dependencies added: parsec, template-haskell

API changes (from Hackage documentation)

- Database.ODBC.SQLServer: instance Database.ODBC.SQLServer.ToSql Data.Time.LocalTime.LocalTime.LocalTime
+ Database.ODBC.Internal: withConnection :: MonadUnliftIO m => Text -> (Connection -> m a) -> m a
+ Database.ODBC.SQLServer: Datetime2 :: LocalTime -> Datetime2
+ Database.ODBC.SQLServer: Smalldatetime :: LocalTime -> Smalldatetime
+ Database.ODBC.SQLServer: [unDatetime2] :: Datetime2 -> LocalTime
+ Database.ODBC.SQLServer: [unSmalldatetime] :: Smalldatetime -> LocalTime
+ Database.ODBC.SQLServer: instance (TypeError ...) => Database.ODBC.SQLServer.ToSql Data.Time.Clock.UTC.UTCTime
+ Database.ODBC.SQLServer: instance (TypeError ...) => Database.ODBC.SQLServer.ToSql Data.Time.LocalTime.LocalTime.LocalTime
+ Database.ODBC.SQLServer: instance Data.Data.Data Database.ODBC.SQLServer.Datetime2
+ Database.ODBC.SQLServer: instance Data.Data.Data Database.ODBC.SQLServer.Smalldatetime
+ Database.ODBC.SQLServer: instance Database.ODBC.Conversion.FromValue Database.ODBC.SQLServer.Datetime2
+ Database.ODBC.SQLServer: instance Database.ODBC.Conversion.FromValue Database.ODBC.SQLServer.Smalldatetime
+ Database.ODBC.SQLServer: instance Database.ODBC.SQLServer.ToSql Database.ODBC.SQLServer.Datetime2
+ Database.ODBC.SQLServer: instance Database.ODBC.SQLServer.ToSql Database.ODBC.SQLServer.Smalldatetime
+ Database.ODBC.SQLServer: instance GHC.Classes.Eq Database.ODBC.SQLServer.Datetime2
+ Database.ODBC.SQLServer: instance GHC.Classes.Eq Database.ODBC.SQLServer.Smalldatetime
+ Database.ODBC.SQLServer: instance GHC.Classes.Ord Database.ODBC.SQLServer.Datetime2
+ Database.ODBC.SQLServer: instance GHC.Classes.Ord Database.ODBC.SQLServer.Smalldatetime
+ Database.ODBC.SQLServer: instance GHC.Generics.Generic Database.ODBC.SQLServer.Datetime2
+ Database.ODBC.SQLServer: instance GHC.Generics.Generic Database.ODBC.SQLServer.Smalldatetime
+ Database.ODBC.SQLServer: instance GHC.Show.Show Database.ODBC.SQLServer.Datetime2
+ Database.ODBC.SQLServer: instance GHC.Show.Show Database.ODBC.SQLServer.Smalldatetime
+ Database.ODBC.SQLServer: newtype Datetime2
+ Database.ODBC.SQLServer: newtype Smalldatetime
+ Database.ODBC.TH: ParamName :: !String -> Part
+ Database.ODBC.TH: SqlPart :: !String -> Part
+ Database.ODBC.TH: data Part
+ Database.ODBC.TH: instance GHC.Classes.Eq Database.ODBC.TH.Part
+ Database.ODBC.TH: instance GHC.Show.Show Database.ODBC.TH.Part
+ Database.ODBC.TH: partsParser :: Parser [Part]
+ Database.ODBC.TH: sql :: QuasiQuoter
+ Database.ODBC.TH: sqlFile :: FilePath -> Q Exp

Files

README.md view
@@ -66,6 +66,13 @@  ## Common issues +Compilation on Linux/OS X may require a `odbcss.h` header file for type/constant definitions. To get this install the freetds package:++* [Linux example](https://github.com/fpco/odbc/blob/efe81f7c17f5ff4c1cf8937577b32f049c0dd62b/Dockerfile#L15).+* On OS X you can use `brew install freetds`.++Windows should already have this file.+ If you see an error like this:      [unixODBC][Driver Manager]Can't open lib 'ODBC Driver 13 for SQL Server' : file not found@@ -82,3 +89,13 @@ environment variable or argument, check that your input string isn't quoted e.g. `"Driver=.."` instead of `Driver=..` due to silly shell scripting quoting issues.++If you see an error like this on OS X with driver version 17,++```+libc++abi.dylib: terminating with uncaught exception of type+std::runtime_error: collate_byname::collate_byname failed to construct+for C/en_AU.UTF-8/C/C/C/C+```++use driver 13 or [see here for more detail](https://github.com/fpco/odbc/issues/17).
odbc.cabal view
@@ -5,7 +5,7 @@              suite runs on OS X, Windows and Linux. copyright: FP Complete 2018 maintainer: chrisdone@fpcomplete.com-version:             0.0.5+version:             0.1.0 license:             BSD3 license-file:        LICENSE build-type:          Simple@@ -21,6 +21,7 @@    Database.ODBC.Internal    Database.ODBC.SQLServer    Database.ODBC.Conversion+   Database.ODBC.TH   default-language:  Haskell2010   if os(mingw32) || os(win32)     extra-libraries: odbc32@@ -39,7 +40,9 @@     containers,     time,     semigroups,-    transformers+    transformers,+    template-haskell,+    parsec  executable odbc   hs-source-dirs:    app@@ -56,7 +59,7 @@ test-suite test   default-language:  Haskell2010   type: exitcode-stdio-1.0-  build-depends: base, text, odbc, hspec, QuickCheck, bytestring, time+  build-depends: base, text, odbc, hspec, QuickCheck, bytestring, time, parsec   ghc-options: -threaded   hs-source-dirs: test   main-is: Main.hs
src/Database/ODBC/Internal.hs view
@@ -24,6 +24,7 @@   ( -- * Connect/disconnect     connect   , close+  , withConnection   , Connection     -- * Executing queries   , exec@@ -209,6 +210,16 @@         -- the connection and the env.         maybe (throwIO DatabaseAlreadyClosed) finalizeForeignPtr mstate) ++-- | Memory bracket around 'connect' and 'close'.+withConnection :: MonadUnliftIO m =>+               Text  -- ^ An ODBC connection string.+            -> (Connection -> m a) -- ^ Program that uses the ODBC connection.+            -> m a+withConnection str inner = withRunInIO $ \io ->+  withBound $ bracket (connect str) close (\h -> io (inner h))++ -- | Execute a statement on the database. exec ::      MonadIO m@@ -627,6 +638,7 @@                      (fmap                         (\frac -> fromIntegral frac / 1000000000)                         (odbc_TIMESTAMP_STRUCT_fraction timestampPtr))))))+     | colType == sql_guid -> getGuid dbc stmt i      | otherwise ->        throwIO          (UnknownDataType@@ -636,25 +648,44 @@   where     colType = columnType col +-- | Get a GUID as a binary value.+getGuid :: Ptr EnvAndDbc -> SQLHSTMT s -> SQLUSMALLINT -> IO (Maybe Value)+getGuid dbc stmt column =+  uninterruptibleMask_+    (do bufferp <- callocBytes odbcGuidBytes+        void+          (getTypedData+             dbc+             stmt+             sql_c_binary+             column+             (coerce bufferp)+             (SQLLEN odbcGuidBytes))+        !bs <- S.unsafePackMallocCStringLen (bufferp, odbcGuidBytes)+        evaluate (Just (BinaryValue (Binary bs))))+ -- | Get the column's data as a vector of CHAR. getBytesData :: Ptr EnvAndDbc -> SQLHSTMT s -> SQLUSMALLINT -> IO (Maybe Value) getBytesData dbc stmt column = do   mavailableBytes <- getSize dbc stmt sql_c_binary column   case mavailableBytes of     Just 0 -> pure (Just (ByteStringValue mempty))-    Just availableBytes -> do-      let allocBytes = availableBytes + 1-      bufferp <- callocBytes (fromIntegral allocBytes)-      void-        (getTypedData-           dbc-           stmt-           sql_c_binary-           column-           (coerce bufferp)-           (SQLLEN (fromIntegral allocBytes)))-      bs <- S.unsafePackMallocCStringLen (bufferp, fromIntegral availableBytes)-      evaluate (Just (ByteStringValue bs))+    Just availableBytes ->+      uninterruptibleMask_+        (do let allocBytes = availableBytes + 1+            bufferp <- callocBytes (fromIntegral allocBytes)+            void+              (getTypedData+                 dbc+                 stmt+                 sql_c_binary+                 column+                 (coerce bufferp)+                 (SQLLEN (fromIntegral allocBytes)))+            bs <-+              S.unsafePackMallocCStringLen+                (bufferp, fromIntegral availableBytes)+            evaluate (Just (ByteStringValue bs)))     Nothing -> pure Nothing  -- | Get the column's data as raw binary.@@ -663,19 +694,22 @@   mavailableBinary <- getSize dbc stmt sql_c_binary column   case mavailableBinary of     Just 0 -> pure (Just (BinaryValue (Binary mempty)))-    Just availableBinary -> do-      let allocBinary = availableBinary-      bufferp <- callocBytes (fromIntegral allocBinary)-      void-        (getTypedData-           dbc-           stmt-           sql_c_binary-           column-           (coerce bufferp)-           (SQLLEN (fromIntegral allocBinary)))-      bs <- S.unsafePackMallocCStringLen (bufferp, fromIntegral availableBinary)-      evaluate (Just (BinaryValue (Binary bs)))+    Just availableBinary ->+      uninterruptibleMask_+        (do let allocBinary = availableBinary+            bufferp <- callocBytes (fromIntegral allocBinary)+            void+              (getTypedData+                 dbc+                 stmt+                 sql_c_binary+                 column+                 (coerce bufferp)+                 (SQLLEN (fromIntegral allocBinary)))+            bs <-+              S.unsafePackMallocCStringLen+                (bufferp, fromIntegral availableBinary)+            evaluate (Just (BinaryValue (Binary bs))))     Nothing -> pure Nothing  -- | Get the column's data as a text string.@@ -958,6 +992,11 @@ -------------------------------------------------------------------------------- -- SQL constants +-- | hardcoded size for a GUID value,+-- https://technet.microsoft.com/en-us/library/ms172424(v=sql.110).aspx+odbcGuidBytes :: Integral a => a+odbcGuidBytes = 16+ -- https://github.com/Microsoft/ODBC-Specification/blob/753d7e714b7eab9eaab4ad6105fdf4267d6ad6f6/Windows/inc/sql.h#L50..L51 sql_success :: RETCODE sql_success = RETCODE 0@@ -1065,8 +1104,8 @@ sql_bit :: SQLSMALLINT sql_bit = (-7) --- sql_guid :: SQLSMALLINT--- sql_guid = (-11)+sql_guid :: SQLSMALLINT+sql_guid = (-11)  -------------------------------------------------------------------------------- -- C type constants
src/Database/ODBC/SQLServer.hs view
@@ -1,3 +1,7 @@+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE CPP #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE DeriveDataTypeable #-}@@ -28,6 +32,8 @@   , FromValue(..)   , FromRow(..)   , Internal.Binary(..)+  , Datetime2(..)+  , Smalldatetime(..)      -- * Streaming results     -- $streaming@@ -76,6 +82,10 @@ import           GHC.Generics import           Text.Printf +#if MIN_VERSION_base(4,9,0)+import           GHC.TypeLits+#endif+ -- $building -- -- You have to compile your projects using the @-threaded@ flag to@@ -223,6 +233,27 @@ instance IsString Part where   fromString = TextPart . T.pack +-- The 'LocalTime' type has more accuracy than the @datetime@ type and+-- the @datetime2@ types can hold; so you will lose precision when you+-- insert. Use this type to indicate that you are aware of the+-- precision loss and fine with it.+--+-- <https://docs.microsoft.com/en-us/sql/t-sql/data-types/datetime2-transact-sql?view=sql-server-2017>+--+-- If you are using @smalldatetime@ in SQL Server, use instead the+-- 'Smalldatetime' type.+newtype Datetime2 = Datetime2+  { unDatetime2 :: LocalTime+  } deriving (Eq, Ord, Show, Typeable, Generic, Data, FromValue)++-- Use this type to discard higher precision than seconds in your+-- 'LocalTime' values for a schema using @smalldatetime@.+--+-- <https://docs.microsoft.com/en-us/sql/t-sql/data-types/smalldatetime-transact-sql?view=sql-server-2017>+newtype Smalldatetime = Smalldatetime+  { unSmalldatetime :: LocalTime+  } deriving (Eq, Ord, Show, Typeable, Generic, Data, FromValue)+ -------------------------------------------------------------------------------- -- Conversion to SQL @@ -309,13 +340,32 @@ instance ToSql TimeOfDay where   toSql = toSql . TimeOfDayValue +#if MIN_VERSION_base(4,9,0)+-- | You cannot use this instance. Wrap your value in either+-- 'Datetime2' or 'Smalldatetime'.+instance GHC.TypeLits.TypeError ('GHC.TypeLits.Text "Instance for LocalTime is disabled:" 'GHC.TypeLits.:$$: 'GHC.TypeLits.Text "Wrap your value in either (Datetime2 foo) or (Smalldatetime foo).") =>+         ToSql LocalTime where+  toSql = toSql++-- | You cannot use this instance. Wrap your value in either+-- 'Datetime2' or 'Smalldatetime'.+instance GHC.TypeLits.TypeError ('GHC.TypeLits.Text "Instance for UTCTime is not possible:" 'GHC.TypeLits.:$$: 'GHC.TypeLits.Text "SQL Server does not support time zones. "'GHC.TypeLits.:$$: 'GHC.TypeLits.Text "You can use utcToLocalTime to make a LocalTime, and" 'GHC.TypeLits.:$$: 'GHC.TypeLits.Text "wrap your value in either (Datetime2 foo) or (Smalldatetime foo).") =>+         ToSql UTCTime where+  toSql = toSql+#endif+ -- | Corresponds to DATETIME/DATETIME2 type of SQL Server. ----- The 'LocalTime' type has more accuracy than the @datetime@ type and+-- The 'Datetime2' type has more accuracy than the @datetime@ type and -- the @datetime2@ types can hold; so you will lose precision when you -- insert.-instance ToSql LocalTime where-  toSql = toSql . LocalTimeValue+instance ToSql Datetime2 where+  toSql = toSql . LocalTimeValue . unDatetime2++-- | Corresponds to SMALLDATETIME type of SQL Server. Precision up to+-- seconds, nothing smaller.+instance ToSql Smalldatetime where+  toSql = toSql . LocalTimeValue . unSmalldatetime  -------------------------------------------------------------------------------- -- Top-level functions
+ src/Database/ODBC/TH.hs view
@@ -0,0 +1,88 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE BangPatterns #-}+module Database.ODBC.TH+  ( sql+  , sqlFile+  , partsParser+  , Part(..)+  ) where++import           Control.DeepSeq+import           Data.Char+import           Data.List (foldl1')+import           Data.Monoid ((<>))+import           Language.Haskell.TH (Q, Exp)+import qualified Language.Haskell.TH as TH+import           Language.Haskell.TH.Quote (QuasiQuoter(..))+import           Text.Parsec+import           Text.Parsec.String++data Part+    = SqlPart !String+    | ParamName !String+    deriving (Show, Eq)++partsParser :: Parser [Part]+partsParser = many1 (self <|> param <|> part)+  where+    self = try (SqlPart "$" <$ string "$$") <?> "escaped dollar $$"+    param =+      (char '$' *>+       (ParamName <$>+        (many1 (satisfy isAlphaNum)) <?> "variable name (alpha-numeric only)")) <?>+      "parameter (e.g. $foo123)"+    part = (SqlPart <$> many1 (satisfy (/= '$'))) <?> "SQL code"+++{- | Allows SQL parameters interpolation from a SQL query. Only 'quoteExp' is+     implemented because this quote can only be used at the expression level.++@+select_some_stuff :: Text -> Int -> Query+select_some_stuff name age = [sql|select * from user where age = $age AND name = $name|]+@++     In this case, 'sql' quote will generate the code below:++@+"select * from user where age = " 'Data.Monoid.<>' 'Database.ODBC.SQLServer.toSql' age 'Data.Monoid.<>' " AND name = " 'Data.Monoid.<>' 'Database.ODBC.SQLServer.toSql' name+@+-}+sql :: QuasiQuoter+sql = QuasiQuoter { quoteExp  = buildSqlQuery "<expression>"+                  , quotePat  = ignored+                  , quoteType = ignored+                  , quoteDec  = ignored+                  }+  where+    ignored :: x -> Q a+    ignored _ = fail "sql quote can be used at the expression level only"++{- | Loads the content of a SQL query file and allows SQL parameters interpolation+     from it.++@+select_some_stuff :: Text -> Int -> Query+select_some_stuff name age = $(sqlFile "path\/to\/my\/sql\/file.sql")+@++     See 'sql' for more details.+-}+sqlFile :: FilePath -> Q Exp+sqlFile fp = do+  !str <- fmap force (TH.runIO (readFile fp))+  buildSqlQuery fp str++buildSqlQuery :: FilePath -> String -> Q Exp+buildSqlQuery fp input = do+  case parse partsParser fp input of+    Left err    -> fail $ "Parse error in SQL: " <> show err+    Right parts -> pure $ buildExp parts++buildExp :: [Part] -> Exp+buildExp = foldl1' go . fmap toExp+  where+    toExp (SqlPart s) = TH.LitE $ TH.StringL s+    toExp (ParamName name) =+      TH.AppE (TH.VarE $ TH.mkName "toSql") (TH.VarE $ TH.mkName name)+    go a b = TH.UInfixE a (TH.VarE '(<>)) b
test/Main.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE BangPatterns #-} {-# LANGUAGE TypeApplications #-}@@ -17,6 +18,7 @@ import           Control.Exception (try, onException, SomeException, catch, throwIO) import           Control.Monad import           Control.Monad.IO.Class+import           Data.Bifunctor import           Data.ByteString (ByteString) import qualified Data.ByteString as S import qualified Data.ByteString.Char8 as S8@@ -34,12 +36,14 @@ import           Database.ODBC.Conversion (FromValue(..)) import           Database.ODBC.Internal (Value (..), Connection, ODBCException(..), Step(..), Binary) import qualified Database.ODBC.Internal as Internal-import           Database.ODBC.SQLServer (ToSql(..))+import           Database.ODBC.SQLServer (Datetime2(..), Smalldatetime(..), ToSql(..)) import qualified Database.ODBC.SQLServer as SQLServer+import           Database.ODBC.TH (partsParser, Part(..)) import           System.Environment import           Test.Hspec import           Test.QuickCheck import           Test.QuickCheck.Monadic+import qualified Text.Parsec as Parsec import           Text.Printf  --------------------------------------------------------------------------------@@ -59,9 +63,35 @@         describe "Regression tests" regressions         describe "Data retrieval" dataRetrieval         describe "Big data" bigData)+  describe "Database.ODBC.SQLServer" (describe "Conversion to SQL" conversionTo)+  describe "Database.ODBC.TH" thparser++thparser :: SpecWith ()+thparser =   describe-    "Database.ODBC.SQLServer"-    (describe "Conversion to SQL" conversionTo)+    "Parser"+    (do it+          "Basic range of syntax"+          (shouldBe+             (Parsec.parse partsParser "" "foo $bar3*123 zot '$$12.4' $wibble")+             (Right+                [ SqlPart "foo "+                , ParamName "bar3"+                , SqlPart "*123 zot '"+                , SqlPart "$"+                , SqlPart "12.4' "+                , ParamName "wibble"+                ]))+        it+          "Incomplete parameter"+          (shouldBe+             (first (const ()) (Parsec.parse partsParser "" "foo$"))+             (Left ()))+        it+          "Invalid parameter character"+          (shouldBe+             (first (const ()) (Parsec.parse partsParser "" "foo$."))+             (Left ())))  regressions :: Spec regressions = do@@ -110,7 +140,8 @@         roundtrip @Int "minBound(Int16)" "Int" "smallint" (fromIntegral (minBound :: Int16))         roundtrip @Word8 "minBound(Word8)" "Word8" "tinyint" minBound)   quickCheckRoundtrip @Day "Day" "date"-  quickCheckRoundtrip @LocalTime "LocalTime" "datetime2"+  quickCheckRoundtrip @Datetime2 "LocalTime" "datetime2"+  quickCheckRoundtrip @Smalldatetime "LocalTime" "datetime2"   quickCheckRoundtrip @TestDateTime "TestDateTime" "datetime"   quickCheckOneway @TimeOfDay "TimeOfDay" "time"   quickCheckRoundtrip @TestTimeOfDay "TimeOfDay" "time"@@ -128,6 +159,7 @@   quickCheckRoundtrip @ByteString "ByteString" ("varchar(" <>  (show maxStringLen) <> ")")   quickCheckRoundtrip @TestBinary "ByteString" ("binary(" <>  (show maxStringLen) <> ")")   quickCheckRoundtrip @Binary "ByteString" ("varbinary(" <>  (show maxStringLen) <> ")")+  quickCheckRoundtrip @TestGUID "GUID" "uniqueidentifier"  connectivity :: Spec connectivity = do@@ -493,6 +525,17 @@          (SQLServer.Binary             (S.take maxStringLen (bytes <> S.replicate maxStringLen 0)))) +newtype TestGUID = TestGUID Binary+  deriving (Show, Eq, Ord, SQLServer.ToSql, FromValue)++instance Arbitrary TestGUID where+  arbitrary = do+    bytes <- arbitrary+    pure+      (TestGUID+         (SQLServer.Binary+            (S.take 16 (bytes <> S.replicate maxStringLen 0))))+ -- | The maximum total number of decimal digits that will be stored, -- both to the left and to the right of the decimal point. The -- precision must be a value from 1 through the maximum precision of@@ -567,7 +610,7 @@           (timeToTimeOfDay              (secondsToDiffTime seconds + (fromRational (fractional % 10000000)))) -newtype TestDateTime = TestDateTime LocalTime+newtype TestDateTime = TestDateTime Datetime2   deriving (Eq, Ord, Show, SQLServer.ToSql, FromValue)  newtype TestTimeOfDay = TestTimeOfDay TimeOfDay@@ -579,7 +622,7 @@     pure (TestTimeOfDay (timeToTimeOfDay (secondsToDiffTime seconds)))  instance Arbitrary TestDateTime where-  arbitrary = fmap TestDateTime (LocalTime <$> arbitrary <*> arbitraryLimited)+  arbitrary = fmap (TestDateTime . Datetime2) (LocalTime <$> arbitrary <*> arbitraryLimited)     where       arbitraryLimited = do         fractional <- elements [993, 003, 497, 007, 000, 127] :: Gen Integer@@ -589,3 +632,6 @@         pure           (timeToTimeOfDay              (secondsToDiffTime seconds + (fromRational (fractional % 1000))))++deriving instance Arbitrary Datetime2+deriving instance Arbitrary Smalldatetime