packages feed

sqlite-simple-interpolate (empty) → 0.1

raw patch · 7 files changed

+310/−0 lines, 7 filesdep +basedep +haskell-src-metadep +mtlsetup-changed

Dependencies added: base, haskell-src-meta, mtl, parsec, sqlite-simple, template-haskell

Files

+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Changelog++## 0.1.0++* Initial version.
+ LICENSE view
@@ -0,0 +1,31 @@+Copyright (c) 2019, Elliot Cameron+Copyright (c) 2022, ruby0b++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Taylor Hedberg nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,35 @@+# `sqlite-simple-interpolate`++Write natural SQL statements in Haskell using QuasiQuoters!++```haskell+{-# LANGUAGE QuasiQuotes #-}+module Main where++import Data.Char (toLower)+import qualified Database.SQLite.Simple as SQL+import Database.SQLite.Simple.QQ.Interpolated+import Control.Exception (bracket)++(&) = flip ($)+infixl 1 &++main :: IO ()+main = bracket (SQL.open ":memory:") SQL.close $ \conn -> do+  conn & [iexecute|CREATE TABLE people (name TEXT, age INTEGER)|]+  conn & [iexecute|INSERT INTO people VALUES ("clive", 40)|]+  -- you can always use 'isql' directly but you'll have to use uncurry:+  (uncurry $ SQL.execute conn) [isql|INSERT INTO people VALUES ("clive", 32)|]++  ageSum <- conn & [ifold|SELECT age FROM people|] 0 (\acc (SQL.Only x) -> pure (acc + x))+  print ageSum++  let limit = 1+  ages <- conn & [iquery|SELECT age FROM people WHERE name = ${map toLower "CLIVE"} LIMIT ${limit}|]+  print (ages :: [SQL.Only Int])+```++## Acknowledgements+This library is a fork of [`postgresql-simple-interpolate`](https://github.com/3noch/postgresql-simple-interpolate), adapted for use with `sqlite-simple`.++The original itself is basically just a copy of the [`here` package](https://github.com/tmhedberg/here) by Taylor M. Hedberg with slight modifications!
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ sqlite-simple-interpolate.cabal view
@@ -0,0 +1,41 @@+cabal-version:      3.0+name:               sqlite-simple-interpolate+version:            0.1+synopsis:           Interpolated SQLite queries via quasiquotation+description:+  This package provides Quasiquoters for writing SQLite queries with inline interpolation of values.+  The values are interpolated using toField from sqlite-simple.+  See the README for more details.++license:            BSD-3-Clause+license-file:       LICENSE+author:             ruby0b+maintainer:         ruby0b+copyright:          ©2022 ruby0b ©2019 Elliot Cameron+homepage:           https://github.com/ruby0b/sqlite-simple-interpolate+category:           Database+tested-with:        GHC ==9.0.2+extra-source-files:+  CHANGELOG.md+  README.md++extra-doc-files:    README.md++source-repository head+  type:     git+  location: git://github.com/ruby0b/sqlite-simple-interpolate.git++library+  hs-source-dirs:   src+  exposed-modules:  Database.SQLite.Simple.QQ.Interpolated+  other-modules:    Database.SQLite.Simple.QQ.Interpolated.Parser+  build-depends:+    , base              >=4.5  && <5+    , haskell-src-meta  >=0.6  && <0.9+    , mtl               >=2.1  && <2.3+    , parsec            ^>=3.1+    , sqlite-simple     >=0.1+    , template-haskell  >=2.17 && <2.19++  ghc-options:      -Wall+  default-language: Haskell2010
+ src/Database/SQLite/Simple/QQ/Interpolated.hs view
@@ -0,0 +1,93 @@+{-# LANGUAGE TemplateHaskell #-}++-- | Interpolated SQL queries+module Database.SQLite.Simple.QQ.Interpolated+  ( isql+  , quoteInterpolatedSql+  , iquery+  , iexecute+  , ifold+  ) where++import Language.Haskell.TH (Exp, Q, appE, listE, sigE, tupE, varE)+import Language.Haskell.TH.Quote (QuasiQuoter (..))+import Database.SQLite.Simple.ToField (toField)+import Database.SQLite.Simple.QQ (sql)+import Text.Parsec (ParseError)+import Database.SQLite.Simple++import Database.SQLite.Simple.QQ.Interpolated.Parser (StringPart (..), parseInterpolated)++-- | Quote a SQL statement with embedded antiquoted expressions.+--+-- The result of the quasiquoter is a tuple, containing the statement string and a list+-- of parameters. For example:+--+-- @[isql|SELECT field FROM table WHERE name = ${map toLower "ELLIOT"} LIMIT ${10}|]@+--+-- produces+--+-- @("SELECT field FROM table WHERE name = ? LIMIT ?", [toField ((map toLower) "ELLIOT"), toField 10])@+--+-- How the parser works:+--+-- Any expression occurring between @${@ and @}@ will be replaced with a @?@+-- and passed as a query parameter.+--+-- Characters preceded by a backslash are treated literally. This enables the+-- inclusion of the literal substring @${@ within your quoted text by writing+-- it as @\\${@. The literal sequence @\\${@ may be written as @\\\\${@.+--+-- Note: This quasiquoter is a wrapper around 'Database.SQLite.Simple.QQ.sql'.+--+-- This quasiquoter only works in expression contexts and will throw an error+-- at compile time if used in any other context.+isql :: QuasiQuoter+isql = QuasiQuoter+  { quoteExp = quoteInterpolatedSql+  , quotePat = error "isql quasiquoter does not support usage in patterns"+  , quoteType = error "isql quasiquoter does not support usage in types"+  , quoteDec = error "isql quasiquoter does not support usage in declarations"+  }++combineParts :: [StringPart] -> (String, [Q Exp])+combineParts = foldr step ("", [])+  where+    step subExpr (s, exprs) = case subExpr of+      Lit str -> (str <> s, exprs)+      Esc c -> (c : s, exprs)+      Anti e -> ('?' : s, e : exprs)++applySql :: [StringPart] -> Q Exp+applySql parts =+  let+    (s', exps) = combineParts parts+  in+  tupE [quoteExp sql s', sigE (listE $ map (appE (varE 'toField)) exps) [t| [SQLData] |]]++-- | The internal parser used by 'isql'.+quoteInterpolatedSql :: String -> Q Exp+quoteInterpolatedSql s = either (handleError s) applySql (parseInterpolated s)++handleError :: String -> ParseError -> Q Exp+handleError expStr parseError = error $ mconcat+  [ "Failed to parse interpolated expression in string: "+  , expStr+  , "\n"+  , show parseError+  ]++-- | Invokes 'query' with arguments provided by 'isql'.+-- The result is of type '(Connection -> IO [r])'.+iquery :: QuasiQuoter+iquery = isql { quoteExp = appE [| \(q, qs) c -> query c q qs |] . quoteInterpolatedSql }++-- | Invokes 'execute' with arguments provided by 'isql'+-- The result is of type '(Connection -> IO ())'.+iexecute :: QuasiQuoter+iexecute = isql { quoteExp = appE [| \(q, qs) c -> execute c q qs |] . quoteInterpolatedSql }++-- | Invokes 'fold' with arguments provided by 'isql'.+-- The result is of type 'a -> (a -> row -> IO a) -> Connection -> IO a'.+ifold :: QuasiQuoter+ifold = isql { quoteExp = appE [| \(q, qs) acc f c -> fold c q qs acc f |] . quoteInterpolatedSql }
+ src/Database/SQLite/Simple/QQ/Interpolated/Parser.hs view
@@ -0,0 +1,103 @@+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE FlexibleContexts #-}++-- | Parsers for antiquoted Haskell expressions inside strings.+--+-- This module was largely copied from+-- https://github.com/tmhedberg/here/blob/8a616b358bcc16bd215a78a8f6192ad9df8224b6/src/Data/String/Here/Interpolated.hs+module Database.SQLite.Simple.QQ.Interpolated.Parser where++import Data.Char (isDigit, isLetter)+import Data.Functor (($>))+import Control.Monad (unless)+import Control.Monad.State (evalStateT, get, modify)+import Control.Monad.Trans (lift)+import Language.Haskell.Meta (parseExp)+import Language.Haskell.TH (Exp, Q)+import Text.Parsec ( ParseError, anyChar, between, char, eof, incSourceColumn+                   , getInput, lookAhead, manyTill, noneOf, parse, setInput, statePos+                   , string, try, updateParserState, (<|>)+                   )+import Text.Parsec.String (Parser)++data StringPart = Lit String | Esc Char | Anti (Q Exp)++data HsChompState = HsChompState { quoteState :: QuoteState+                                 , braceCt :: Int+                                 , consumed :: String+                                 , prevCharWasIdentChar :: Bool+                                 }++data QuoteState = None | Single EscapeState | Double EscapeState deriving (Eq, Ord, Show)++data EscapeState = Escaped | Unescaped deriving (Bounded, Enum, Eq, Ord, Show)++parseInterpolated :: String -> Either ParseError [StringPart]+parseInterpolated = parse pInterp ""++pInterp :: Parser [StringPart]+pInterp = manyTill pStringPart eof++pStringPart :: Parser StringPart+pStringPart = pAnti <|> pEsc <|> pLit++pAnti :: Parser StringPart+pAnti = Anti <$> between (try pAntiOpen) pAntiClose pAntiExpr++pAntiOpen :: Parser String+pAntiOpen = string "${"++pAntiClose :: Parser String+pAntiClose = string "}"++pAntiExpr :: Parser (Q Exp)+pAntiExpr = pUntilUnbalancedCloseBrace >>= either fail (pure . pure) . parseExp++pUntilUnbalancedCloseBrace :: Parser String+pUntilUnbalancedCloseBrace = evalStateT go $ HsChompState None 0 "" False+  where+    go = do+      c <- lift anyChar+      modify $ \st@HsChompState {consumed} -> st {consumed = c:consumed}+      HsChompState{..} <- get+      let next = setIdentifierCharState c *> go+      case quoteState of+        None -> case c of+          '{' -> incBraceCt 1 *> next+          '}' | braceCt > 0 -> incBraceCt (-1) *> next+              | otherwise -> stepBack $> reverse (tail consumed)+          '\'' -> unless prevCharWasIdentChar (setQuoteState $ Single Unescaped)+               *> next+          '"' -> setQuoteState (Double Unescaped) *> next+          _ -> next+        Single Unescaped -> do case c of '\\' -> setQuoteState (Single Escaped)+                                         '\'' -> setQuoteState None+                                         _ -> pure ()+                               next+        Single Escaped -> setQuoteState (Single Unescaped) *> next+        Double Unescaped -> do case c of '\\' -> setQuoteState (Double Escaped)+                                         '"' -> setQuoteState None+                                         _ -> pure ()+                               next+        Double Escaped -> setQuoteState (Double Unescaped) *> next+    stepBack = lift $+      updateParserState+        (\s -> s {statePos = incSourceColumn (statePos s) (-1)})+        *> getInput+        >>= setInput . ('}':)+    incBraceCt n = modify $ \st@HsChompState {braceCt} ->+      st {braceCt = braceCt + n}+    setQuoteState qs = modify $ \st -> st {quoteState = qs}+    setIdentifierCharState c = modify $ \st ->+      st+        {prevCharWasIdentChar = or [isLetter c, isDigit c, c == '_', c == '\'']}++pEsc :: Parser StringPart+pEsc = Esc <$> (char '\\' *> anyChar)++pLit :: Parser StringPart+pLit = fmap Lit $+  try (litCharTil $ try $ lookAhead pAntiOpen <|> lookAhead (string "\\"))+    <|> litCharTil eof+  where litCharTil = manyTill $ noneOf ['\\']