sqlite-simple-interpolate 0.1.1 → 0.2.0.0
raw patch · 9 files changed
+205/−284 lines, 9 filesdep +custom-interpolationdep −haskell-src-metadep −mtldep −parsecdep ~basedep ~template-haskellsetup-changed
Dependencies added: custom-interpolation
Dependencies removed: haskell-src-meta, mtl, parsec
Dependency ranges changed: base, template-haskell
Files
- CHANGELOG.md +10/−3
- LICENSE +19/−22
- README.md +37/−14
- Setup.hs +0/−2
- sqlite-simple-interpolate.cabal +16/−18
- src/Database/SQLite/Simple/Interpolate.hs +104/−0
- src/Database/SQLite/Simple/QQ/Interpolated.hs +0/−104
- src/Database/SQLite/Simple/QQ/Interpolated/Parser.hs +0/−112
- tests/Main.hs +19/−9
CHANGELOG.md view
@@ -1,10 +1,17 @@ # Changelog +## 0.2.0.0++- **BREAKING**: field interpolation now uses `{}`, _without_ a dollar sign (before: `${}`)+- Rename the module to `Database.SQLite.Simple.Interpolate` (before: `Database.SQLite.Simple.QQ.Interpolated`)+- Add support for row interpolation (uses `@{}` syntax, see the updated readme for details)+- Refactor the parser into a [custom-interpolation](https://hackage.haskell.org/package/custom-interpolation) package which does most of the work+ ## 0.1.1 -* Add support for textual SQL injection (PR #1)-* Support GHC 8.10.7+- Add support for textual SQL injection (PR #1)+- Support GHC 8.10.7 ## 0.1.0 -* Initial version.+- Initial version.
LICENSE view
@@ -1,31 +1,28 @@-Copyright (c) 2019, Elliot Cameron-Copyright (c) 2022, ruby0b+BSD 3-Clause License -All rights reserved.+Copyright (c) 2023, ruby0b 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.+1. 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.+2. 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.+3. Neither the name of the copyright holder nor the names of its+ 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+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 HOLDER 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
@@ -1,7 +1,20 @@-# `sqlite-simple-interpolate`+<h1 align="center">sqlite-simple-interpolate</h1> +<p align="center">+ <a href="https://hackage.haskell.org/package/sqlite-simple-interpolate"><img src="https://img.shields.io/hackage/v/sqlite-simple-interpolate" alt="Hackage"></a>+ <a href="https://github.com/ruby0b/sqlite-simple-interpolate/actions/workflows/haskell-ci.yml"><img src="https://github.com/ruby0b/sqlite-simple-interpolate/actions/workflows/haskell-ci.yml/badge.svg" alt="Build Status"></a>+ <a href="https://github.com/simmsb/calamity/blob/master/LICENSE"><img src="https://img.shields.io/github/license/ruby0b/sqlite-simple-interpolate" alt="License"></a>+ <a href="https://hackage.haskell.org/package/sqlite-simple-interpolate"><img src="https://img.shields.io/hackage-deps/v/sqlite-simple-interpolate" alt="Hackage-Deps"></a>+</p>+ Write natural SQL statements in Haskell using QuasiQuoters! +The QuasiQuoters support 3 methods of interpolation that can be mixed freely:++- `{}`: injection-safe field interpolation, e.g. `{myName}` gets replaced with `?` which gets substituted with `toField myName`.+- `@{}`: injection-safe row interpolation, e.g. `@{myPerson}` gets replaced with `(?,?)` (assuming `Person` has two fields) which gets substituted with `toRow myPerson`.+- `!{}`: injection-_vulnerable_ raw string interpolation. **Never use this for user input!** Intended for use cases that the anti-injection mechanisms won't allow, e.g. table names: `!{myTableName}` gets replaced with the value of `myTableName :: String`.+ ```haskell {-# LANGUAGE QuasiQuotes #-} @@ -11,28 +24,38 @@ import Data.Char (toLower) import Data.Function ((&)) import qualified Database.SQLite.Simple as SQL-import Database.SQLite.Simple.QQ.Interpolated+import Database.SQLite.Simple.Interpolate +data Person = Person {name :: String, age :: Integer}++instance SQL.ToRow Person where+ toRow p = SQL.toRow (name p, age p)+ table :: String table = "people" main :: IO () main = bracket (SQL.open ":memory:") SQL.close $ \conn -> do+ -- Create a table, interpolating safe string constants like table names with !{} conn & [iexecute|CREATE TABLE !{table} (name TEXT, age INTEGER)|] - conn & [iexecute|INSERT INTO !{table} VALUES ("clive", 40)|]- -- you can always use 'isql' directly but you'll have to use uncurry:- (uncurry $ SQL.execute conn) [isql|INSERT INTO !{table} VALUES ("clara", 32)|]+ -- Insert a person, safely interpolating a field using {}+ let name = "clive"+ conn & [iexecute|INSERT INTO !{table} VALUES ({name}, 40)|] - ageSum <- conn & [ifold|SELECT age FROM !{table}|] 0 (\acc (SQL.Only x) -> pure (acc + x))- print (ageSum :: Int)+ -- Insert a person, safely interpolating an entire row type using @{} (gets replaced with "(?,?)")+ let clara = Person {name = "clara", age = 25}+ conn & [iexecute|INSERT INTO !{table} VALUES @{clara}|] - let limit = 1 :: Int- ages <- conn & [iquery|SELECT age FROM !{table} WHERE name = ${map toLower "CLIVE"} LIMIT ${limit}|]- print (ages :: [SQL.Only Int])-```+ -- Use ifold to fold some rows into their sum in haskell+ ageHaskellSum <- conn & [ifold|SELECT age FROM !{table}|] 0 (\acc (SQL.Only x) -> pure (acc + x))+ print (ageHaskellSum :: Int) -## Acknowledgements-This library is a fork of [`postgresql-simple-interpolate`](https://github.com/3noch/postgresql-simple-interpolate), adapted for use with `sqlite-simple`.+ -- Let's calculate the average age of people that are at least 20 years old+ let minAge = 20 :: Int+ [ageAvg] <- conn & [iquery|SELECT AVG(age) FROM !{table} WHERE age >= {minAge}|]+ print (ageAvg :: SQL.Only Double) -The original itself is basically just a copy of the [`here` package](https://github.com/tmhedberg/here) by Taylor M. Hedberg with slight modifications!+ -- You can always use 'isql' directly but you'll have to use uncurry:+ (uncurry $ SQL.execute conn) [isql|INSERT OR REPLACE INTO !{table} VALUES ({name}, 41)|]+```
− Setup.hs
@@ -1,2 +0,0 @@-import Distribution.Simple-main = defaultMain
sqlite-simple-interpolate.cabal view
@@ -1,20 +1,17 @@ cabal-version: 3.0 name: sqlite-simple-interpolate-version: 0.1.1+version: 0.2.0.0 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.+ Please see the readme at https://github.com/ruby0b/sqlite-simple-interpolate#readme. 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 ==8.10.7 || ==9.0.2+tested-with: GHC ==8.10.7 || ==9.2.5 || ==9.4.4 extra-source-files: CHANGELOG.md README.md@@ -26,20 +23,21 @@ 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+ hs-source-dirs: src+ exposed-modules: Database.SQLite.Simple.Interpolate 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.16 && <2.19+ , base >=4.14 && <5+ , custom-interpolation ^>=0.1+ , sqlite-simple >=0.1+ , template-haskell >=2.16 && <2.20 - ghc-options: -Wall- default-language: Haskell2010+ default-extensions:+ TemplateHaskell+ QuasiQuotes + ghc-options: -Wall+ default-language: Haskell2010+ flag tests manual: False default: True@@ -49,7 +47,7 @@ main-is: Main.hs hs-source-dirs: tests build-depends:- , base >=4.7 && <5+ , base , sqlite-simple , sqlite-simple-interpolate
+ src/Database/SQLite/Simple/Interpolate.hs view
@@ -0,0 +1,104 @@+-- | Interpolated SQLite queries+module Database.SQLite.Simple.Interpolate (+ isql,+ iquery,+ iexecute,+ ifold,+ quoteInterpolatedSql,+) where++import CustomInterpolation+import Data.Maybe (catMaybes)+import Data.String (fromString)+import Database.SQLite.Simple+import Database.SQLite.Simple.ToField (toField)+import GHC.OldList (intercalate)+import Language.Haskell.TH (Exp, Q, appE, listE, sigE, stringE, tupE, varE)+import Language.Haskell.TH.Quote (QuasiQuoter (..))++{- | Quote an 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:++>>> import Data.Char (toLower)+>>> [isql|SELECT field FROM !{map toLower "PEOPLE"} WHERE name = {map toLower "ELLIOT"} AND access IN @{["admin", "employee"]} LIMIT {10 :: Int}|]+("SELECT field FROM people WHERE name = ? AND access IN (?,?) LIMIT ?",[SQLText "elliot",SQLText "admin",SQLText "employee",SQLInteger 10])++The generated code is:++@("SELECT field FROM people WHERE name = ? AND access IN (?,?) LIMIT ?", ['toField' (map toLower "ELLIOT")] ++ 'toRow' ["admin", "employee"] ++ ['toField' (10 :: Int)])@++How the parser works:++* Any expression occurring between @{@ and @}@ will be replaced with a @?@+and passed as a query parameter using 'toField'.+* Any expression occuring between @\@{@ and @}@ will be replaced with the right amount of @?@, separated by commas and surrounded by parentheses (e.g. @(?,?,?)@ for a `ToRow` instance with 3 fields).+The expression gets converted to query parameters using 'toRow'.+* Any expression occurring between @!{@ and @}@ will be replaced with its value, bypassing the anti-injection mechanisms. /Never use this one for user input!/++Characters preceded by a backslash are treated literally. This enables the+inclusion of the literal character @{@ within your quoted text by writing+it as @\\{@. The literal sequence @\\{@ may be written as @\\\\{@.+-}+isql :: QuasiQuoter+isql =+ QuasiQuoter+ { quoteExp = quoteInterpolatedSql,+ quotePat = error "This quasiquoter does not support usage in patterns",+ quoteType = error "This quasiquoter does not support usage in types",+ quoteDec = error "This quasiquoter does not support usage in declarations"+ }++-- | The Template Haskell function used by 'isql'.+quoteInterpolatedSql :: String -> Q Exp+quoteInterpolatedSql =+ interpolate+ defaultConfig+ { finalize = consumeInterpolated,+ handlers =+ [ simpleInterpolator+ { prefix = "",+ handler = \sqlFieldExpr -> (Just $ listE [appE (varE 'toField) sqlFieldExpr], [|"?"|])+ },+ simpleInterpolator+ { prefix = "!",+ handler = \stringExpr -> (Nothing, stringExpr)+ },+ simpleInterpolator+ { prefix = "@",+ handler = \sqlRowExpr ->+ let sqlRow = appE (varE 'toRow) sqlRowExpr+ rowLength = appE (varE 'length) sqlRow+ intercalateCommas = appE $ appE (varE 'intercalate) $ stringE ","+ questionMarks = intercalateCommas $ appE (appE (varE 'replicate) rowLength) [|"?"|]+ questionMarksWithParens = appE (varE 'concat) $ listE [stringE "(", questionMarks, stringE ")"]+ in (Just sqlRow, questionMarksWithParens)+ }+ ]+ }++consumeInterpolated :: ([Maybe (Q Exp)], Q Exp) -> Q Exp+consumeInterpolated (sqlDataExprs, queryStrExpr) = tupE [queryStr, sqlData]+ where+ queryStr = appE [|fromString :: String -> Query|] queryStrExpr+ sqlData = sigE (appE (varE 'concat) (listE sqlDataExprs')) [t|[SQLData]|]+ sqlDataExprs' = catMaybes sqlDataExprs++{- | 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.hs
@@ -1,104 +0,0 @@-{-# 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, litE)-import Language.Haskell.TH.Quote (QuasiQuoter (..))-import Language.Haskell.TH.Syntax (Lit(..), lookupValueName)-import Database.SQLite.Simple.ToField (toField)-import Text.Parsec (ParseError)-import Database.SQLite.Simple-import Data.Foldable (foldrM)-import Data.String (fromString)--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] -> Q ([Q Exp], [Q Exp])-combineParts = foldrM step ([], [])- where- step subExpr (s, exprs) = case subExpr of- AntiInject e -> injectExpr e (s, exprs)- Lit str -> pure (litE (StringL str) : s, exprs)- Esc c -> pure (litE (StringL [c]) : s, exprs)- AntiParam e -> pure (litE (StringL "?") : s, e : exprs)--injectExpr :: String -> ([Q Exp], [Q Exp]) -> Q ([Q Exp], [Q Exp])-injectExpr name (s, exprs) = do- valueName <- lookupValueName name- case valueName of- Nothing ->- fail $ "Value `" ++ name ++ "` is not in scope"- Just found -> do- pure (varE found : s, exprs)--applySql :: [StringPart] -> Q Exp-applySql parts = do- (queryParts, exps) <- combineParts parts- let queryStr = appE [| fromString :: String -> Query |] $ appE [| concat |] $ listE queryParts- tupE [queryStr, 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
@@ -1,112 +0,0 @@-{-# 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 | AntiInject String | AntiParam (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 = pAntiInject <|> pAntiParam <|> pEsc <|> pLit--pAntiInject :: Parser StringPart-pAntiInject = AntiInject <$> between (try pAntiOpenInject) pAntiClose pAntiName--pAntiOpenInject :: Parser String-pAntiOpenInject = string "!{"--pAntiName :: Parser String-pAntiName = manyTill anyChar (lookAhead pAntiClose)--pAntiParam :: Parser StringPart-pAntiParam = AntiParam <$> between (try pAntiOpenParam) pAntiClose pAntiExpr--pAntiOpenParam :: Parser String-pAntiOpenParam = string "${"--pAntiExpr :: Parser (Q Exp)-pAntiExpr = pUntilUnbalancedCloseBrace >>= either fail (pure . pure) . parseExp--pAntiClose :: Parser String-pAntiClose = string "}"--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 pAntiOpenInject <|> lookAhead pAntiOpenParam <|> lookAhead (string "\\"))- <|> litCharTil eof- where litCharTil = manyTill $ noneOf ['\\']
tests/Main.hs view
@@ -6,8 +6,13 @@ import Data.Char (toLower) import Data.Function ((&)) import qualified Database.SQLite.Simple as SQL-import Database.SQLite.Simple.QQ.Interpolated+import Database.SQLite.Simple.Interpolate +data Person = Person {name :: String, age :: Integer}++instance SQL.ToRow Person where+ toRow p = SQL.toRow (name p, age p)+ table :: String table = "people" @@ -16,13 +21,18 @@ print [isql|CREATE TABLE !{table} (name TEXT, age INTEGER)|] conn & [iexecute|CREATE TABLE !{table} (name TEXT, age INTEGER)|] - print [isql|INSERT INTO !{table} VALUES ("clive", 40)|]- conn & [iexecute|INSERT INTO !{table} VALUES ("clive", 40)|]+ let name = "clive"+ print [isql|INSERT INTO !{table} VALUES ({name}, 40)|]+ conn & [iexecute|INSERT INTO !{table} VALUES ({name}, 40)|] - ageSum <- conn & [ifold|SELECT age FROM !{table}|] 0 (\acc (SQL.Only x) -> pure (acc + x))- print (ageSum :: Int)+ let clara = Person {name = "clara", age = 25}+ print [isql|INSERT INTO !{table} VALUES @{clara}|]+ conn & [iexecute|INSERT INTO !{table} VALUES @{clara}|] - let limit = 1 :: Int- print [isql|SELECT age FROM !{table} WHERE name = ${map toLower "CLIVE"} LIMIT ${limit}|]- ages <- conn & [iquery|SELECT age FROM !{table} WHERE name = ${map toLower "CLIVE"} LIMIT ${limit}|]- print (ages :: [SQL.Only Int])+ ageHaskellSum <- conn & [ifold|SELECT age FROM !{table}|] 0 (\acc (SQL.Only x) -> pure (acc + x))+ print (ageHaskellSum :: Int)++ let minAge = 1 :: Int+ print [isql|SELECT AVG(age) FROM !{table} WHERE age >= {minAge}|]+ [ageAvg] <- conn & [iquery|SELECT AVG(age) FROM !{table} WHERE age >= {minAge}|]+ print (ageAvg :: SQL.Only Double)