qhs 0.3.3 → 0.4.0
raw patch · 106 files changed
+967/−583 lines, 106 filesdep +extradep +qhsdep −splitdep ~basedep ~bytestringdep ~containersbinary-added
Dependencies added: extra, qhs
Dependencies removed: split
Dependency ranges changed: base, bytestring, containers, cryptonite, hspec, optparse-applicative, process, simple-sql-parser, sqlite-simple, syb, text, zlib
Files
- LICENSE +1/−1
- Main.hs +7/−0
- README.md +115/−0
- _qhs +18/−0
- qhs.cabal +60/−56
- src/File.hs +0/−65
- src/Main.hs +0/−120
- src/Option.hs +0/−61
- src/Parser.hs +0/−71
- src/Qhs.hs +3/−0
- src/Qhs/CLI.hs +125/−0
- src/Qhs/File.hs +74/−0
- src/Qhs/Option.hs +70/−0
- src/Qhs/Parser.hs +82/−0
- src/Qhs/SQL.hs +49/−0
- src/Qhs/SQLType.hs +48/−0
- src/SQL.hs +0/−61
- src/SQLType.hs +0/−44
- stack.yaml +3/−0
- stack.yaml.lock +19/−0
- test/FileSpec.hs +19/−17
- test/MainSpec.hs +0/−38
- test/ParserSpec.hs +34/−33
- test/QhsSpec.hs +38/−0
- test/SQLSpec.hs +17/−16
- test/tests/avg.out +1/−0
- test/tests/avg.sh +1/−0
- test/tests/avg_sum.out +2/−0
- test/tests/avg_sum.sh +1/−0
- test/tests/basic.csv +4/−0
- test/tests/basic.csv.gz binary
- test/tests/basic.out +4/−0
- test/tests/basic.sh +1/−0
- test/tests/big.csv +13/−0
- test/tests/columns.out +4/−0
- test/tests/columns.sh +1/−0
- test/tests/concat.out +8/−0
- test/tests/concat.sh +1/−0
- test/tests/count.out +2/−0
- test/tests/count.sh +1/−0
- test/tests/crlf.csv +4/−0
- test/tests/crlf.out +4/−0
- test/tests/crlf.sh +1/−0
- test/tests/delimiters.out +3/−0
- test/tests/delimiters.sh +1/−0
- test/tests/empty.sql +0/−0
- test/tests/empty_query.out +2/−0
- test/tests/empty_query.sh +1/−0
- test/tests/empty_query_file.out +2/−0
- test/tests/empty_query_file.sh +1/−0
- test/tests/file containing spaces.csv +4/−0
- test/tests/file_spaces.out +3/−0
- test/tests/file_spaces.sh +1/−0
- test/tests/group.out +4/−0
- test/tests/group.sh +1/−0
- test/tests/group_sum.out +4/−0
- test/tests/group_sum.sh +1/−0
- test/tests/gzip.out +4/−0
- test/tests/gzip.sh +1/−0
- test/tests/gzip_stdin.out +2/−0
- test/tests/gzip_stdin.sh +1/−0
- test/tests/header.out +3/−0
- test/tests/header.sh +1/−0
- test/tests/invalid.out +7/−0
- test/tests/invalid.sh +1/−0
- test/tests/is_null.out +1/−0
- test/tests/is_null.sh +1/−0
- test/tests/join.out +3/−0
- test/tests/join.sh +1/−0
- test/tests/multiline.csv +4/−0
- test/tests/multiline.out +2/−0
- test/tests/multiline.sh +1/−0
- test/tests/not_null.out +2/−0
- test/tests/not_null.sh +1/−0
- test/tests/output_delimiter.out +4/−0
- test/tests/output_delimiter.sh +1/−0
- test/tests/output_header.out +4/−0
- test/tests/output_header.sh +1/−0
- test/tests/pipe_delimited.csv +4/−0
- test/tests/pipe_delimited.out +3/−0
- test/tests/pipe_delimited.sh +1/−0
- test/tests/pipe_delimited_output.out +4/−0
- test/tests/pipe_delimited_output.sh +1/−0
- test/tests/query.sql +1/−0
- test/tests/query_file.out +3/−0
- test/tests/query_file.sh +1/−0
- test/tests/quote.out +3/−0
- test/tests/quote.sh +1/−0
- test/tests/seq.out +1/−0
- test/tests/seq.sh +1/−0
- test/tests/spaces.csv +4/−0
- test/tests/spaces.out +4/−0
- test/tests/spaces.sh +1/−0
- test/tests/stdin.out +2/−0
- test/tests/stdin.sh +1/−0
- test/tests/sum.out +1/−0
- test/tests/sum.sh +1/−0
- test/tests/tab.csv +4/−0
- test/tests/tab.out +3/−0
- test/tests/tab.sh +1/−0
- test/tests/tab2.out +3/−0
- test/tests/tab2.sh +1/−0
- test/tests/tab_delimited_output.out +4/−0
- test/tests/tab_delimited_output.sh +1/−0
- test/tests/where.out +2/−0
- test/tests/where.sh +1/−0
LICENSE view
@@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2016-2020 itchyny+Copyright (c) 2016-2025 itchyny Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal
+ Main.hs view
@@ -0,0 +1,7 @@+module Main where++import Paths_qhs (version)+import Qhs qualified++main :: IO ()+main = Qhs.main version
+ README.md view
@@ -0,0 +1,115 @@+# qhs+[](https://github.com/itchyny/qhs/actions?query=branch:main)++### SQL queries on CSV and TSV files+This is a Haskell implementation of [q](https://github.com/harelba/q) command.++## Installation+### Homebrew+```shell+brew install itchyny/tap/qhs+```++### Build with stack+```shell+stack install qhs+```++## Usage+In the beginning, `qhs [QUERY]` is the basic usage.+```shell+ $ wc * > wc_out.txt+ $ qhs "SELECT * FROM ./wc_out.txt"+66 471 3131 File.hs+118 649 4962 Main.hs+61 258 2346 Option.hs+51 366 2564 Parser.hs+45 273 1769 SQL.hs+341 2017 14772 total+```+You can specify the file name for the table name.+The column names are automatically assigned as `c1`, `c2` and so on.+```shell+ $ qhs "SELECT c4,c1 FROM ./wc_out.txt WHERE c4 <> 'total' ORDER BY c1 DESC"+Main.hs 118+File.hs 66+Option.hs 61+Parser.hs 51+SQL.hs 45+```++The `qhs` command can read the table from the standard input as well.+```shell+ $ wc * | qhs "SELECT c4,c1 FROM - WHERE c4 <> 'total' ORDER BY c1 DESC"+Main.hs 118+File.hs 66+Option.hs 61+Parser.hs 51+SQL.hs 45+```++You can use `-H` flag to make `qhs` regard the head line as the row of column names.+```shell+ $ cat basic.csv+foo,bar,baz+a0,1,a2+b0,3,b2+c0,,c2+ $ qhs -H "SELECT * FROM basic.csv WHERE bar IS NOT NULL"+a0 1 a2+b0 3 b2+```++You can use the basic SQL operations; `GROUP BY`, `ORDER BY`, `LIMIT` and `COUNT(*)`.+```shell+ $ ps -ef | qhs -H -O "SELECT UID,COUNT(*) cnt FROM - GROUP BY UID ORDER BY cnt DESC LIMIT 3"+UID cnt+503 102+0 86+89 3+```+You can also use other SQL operations like `JOIN`, `UNION` and sub-query.+The command helps you deal with multiple CSV files.++Please refer to `qhs --help` for further options.+The command respects the behaviour of the original [q](https://github.com/harelba/q) command.+```shell+ $ qhs --help+qhs - SQL queries on CSV and TSV files++Usage: qhs [-H|--skip-header] [-O|--output-header] [-d|--delimiter DELIMITER]+ [-t|--tab-delimited] [-p|--pipe-delimited]+ [-D|--output-delimiter OUTPUT_DELIMITER] [-T|--tab-delimited-output]+ [-P|--pipe-delimited-output] [-k|--keep-leading-whitespace]+ [-z|--gzipped] [-q|--query-filename QUERY_FILENAME] [QUERY]++Available options:+ -h,--help Show this help text+ -v,--version Show the version of the command.+ -H,--skip-header Skip the header row for row input and use it for+ column names instead.+ -O,--output-header Output the header line.+ -d,--delimiter DELIMITER Field delimiter. If not specified, automatically+ detected.+ -t,--tab-delimited Same as -d $'\t'.+ -p,--pipe-delimited Same as -d '|'.+ -D,--output-delimiter OUTPUT_DELIMITER+ Field delimiter for output. If not specified, the+ argument of -d DELIMITER is used.+ -T,--tab-delimited-output+ Same as -D $'\t'.+ -P,--pipe-delimited-output+ Same as -D '|'.+ -k,--keep-leading-whitespace+ Keep leading whitespace in values. The leading+ whitespaces are stripped off by default.+ -z,--gzipped Assuming the gzipped input.+ -q,--query-filename QUERY_FILENAME+ Read query from the provided filename.+```++## Author+itchyny (<https://github.com/itchyny>)++## License+This software is released under the MIT License, see LICENSE.
+ _qhs view
@@ -0,0 +1,18 @@+#compdef qhs++_qhs()+{+ _arguments -s -S \+ '(-H --skip-header)'{-H,--skip-header}'[use the first row for column names]' \+ '(-O --output-header)'{-O,--output-header}'[output the header line]' \+ '(-d --delimiter -t --tab-delimited)'{-d,--delimiter=}'[field delimiter]:DELIMITER' \+ '(-d --delimiter -t --tab-delimited)'{-t,--tab-delimited}'[use tab for field delimiter]' \+ '(-D --output-delimiter -T --tab-delimited-output)'{-D,--output-delimiter=}'[field delimiter of output]:OUTPUT_DELIMITER' \+ '(-D --output-delimiter -T --tab-delimited-output)'{-T,--tab-delimited-output}'[use tab for field delimiter of output]' \+ '(-k --keep-leading-whitespace)'{-k,--keep-leading-whitespace}'[keep leading whitespace in values]' \+ '(-z --gzipped)'{-z,--gzipped}'[assuming the gzipped input]' \+ '(-q --query-filename 1)'{-q,--query-filename=}'[read query from file]:QUERY_FILENAME:_files' \+ '(- 1)'{-v,--version}'[print version]' \+ '(- 1)'{-h,--help}'[print help]' \+ '1: :_guard "^-*" "SQL query"'+}
qhs.cabal view
@@ -1,68 +1,72 @@+cabal-version: 3.0 name: qhs-version: 0.3.3-author: itchyny <https://github.com/itchyny>-maintainer: itchyny <https://github.com/itchyny>-license: MIT-license-file: LICENSE+version: 0.4.0 category: Console-build-type: Simple-cabal-version: >=1.10 synopsis: Command line tool qhs, SQL queries on CSV and TSV files.-description: This is a Haskell port of q command (https://github.com/harelba/q).+description: This is a Haskell port of <https://github.com/harelba/q q command>.+author: itchyny <itchyny@cybozu.co.jp>+maintainer: itchyny <itchyny@cybozu.co.jp>+homepage: https://github.com/itchyny/qhs+bug-reports: https://github.com/itchyny/qhs/issues+license: MIT+license-file: LICENSE+extra-source-files: stack.yaml stack.yaml.lock README.md _qhs+ test/tests/*.csv test/tests/*.csv.gz test/tests/*.out+ test/tests/*.sh test/tests/*.sql -executable qhs+common base+ default-language: GHC2021+ default-extensions: BlockArguments+ LambdaCase+ NoFieldSelectors+ OverloadedRecordDot+ build-depends: base >= 4.18 && < 5+ ghc-options: -Wdefault -Wall -Wunused-packages++library qhs-lib+ import: base+ visibility: private hs-source-dirs: src+ exposed-modules: Qhs+ Qhs.CLI+ Qhs.File+ Qhs.Option+ Qhs.Parser+ Qhs.SQL+ Qhs.SQLType+ build-depends: bytestring >= 0.11 && < 0.13+ , containers >= 0.6 && < 0.7+ , cryptonite >= 0.30 && < 0.31+ , extra >= 1.7 && < 1.9+ , optparse-applicative >= 0.18 && < 0.19+ , simple-sql-parser >= 0.8 && < 0.9+ , sqlite-simple >= 0.4 && < 0.5+ , syb >= 0.7 && < 0.8+ , text >= 2.0 && < 2.2+ , zlib >= 0.6 && < 0.8++executable qhs+ import: base main-is: Main.hs- ghc-options: -threaded -Wall- default-language: Haskell2010- other-modules: Parser- , SQL- , SQLType- , File- , Option- , Paths_qhs- build-depends: base >= 4.9 && < 5- , bytestring- , containers- , cryptonite- , optparse-applicative- , simple-sql-parser >= 0.6.0- , split- , sqlite-simple- , syb >= 0.4- , text- , zlib+ other-modules: Paths_qhs+ autogen-modules: Paths_qhs+ build-depends: qhs-lib test-suite spec- hs-source-dirs: test,src- main-is: Spec.hs- ghc-options: -threaded -Wall- other-modules: Parser- , SQL- , SQLType- , File- , Option- , Paths_qhs- , ParserSpec- , SQLSpec- , FileSpec- , MainSpec+ import: base type: exitcode-stdio-1.0- default-language: Haskell2010- build-depends: base >= 4.9 && < 5- , bytestring- , containers- , cryptonite- , hspec- , optparse-applicative- , process- , simple-sql-parser >= 0.6.0- , split- , sqlite-simple- , syb >= 0.4- , text- , zlib+ hs-source-dirs: test+ main-is: Spec.hs+ other-modules: FileSpec+ ParserSpec+ QhsSpec+ SQLSpec+ build-depends: qhs-lib+ , containers >= 0.6 && < 0.7+ , extra >= 1.7 && < 1.8+ , hspec >= 2.11 && < 2.12+ , process >= 1.6 && < 1.7 source-repository head type: git- location: git@github.com:itchyny/qhs.git+ location: https://github.com/itchyny/qhs.git
− src/File.hs
@@ -1,65 +0,0 @@-module File where--import qualified Codec.Compression.GZip as GZip-import Control.Applicative ((<|>))-import Control.Monad (guard, when)-import qualified Data.ByteString.Lazy as ByteString-import qualified Data.ByteString.Lazy.Char8 as Char8-import Data.Char (isSpace)-import System.Exit (exitFailure)-import System.IO--import qualified Option as Option--readFromFile :: Option.Option -> Handle -> IO ([String], [[String]])-readFromFile opts handle = do- contents <- joinMultiLines <$> lines <$>- if Option.gzipped opts- then Char8.unpack <$> GZip.decompress <$> ByteString.hGetContents handle- else hGetContents handle- let (headLine : secondLine : _) = contents ++ [ "", "" ]- let delimiter = guard (Option.tabDelimited opts) *> Just "\t" <|> Option.delimiter opts- when (maybe False ((/=1) . length) delimiter) $ do- hPutStrLn stderr "Invalid delimiter."- exitFailure- let splitter = case delimiter of- Just [c] -> (==) c- _ -> detectSplitter headLine secondLine- let headColumns = splitFixedSize splitter 0 headLine- let size = length headColumns- let columns = if Option.skipHeader opts then headColumns else [ 'c' : show i | i <- [1..size] ]- let skipLine = if Option.skipHeader opts then tail else id- let stripSpaces = if Option.keepLeadingWhiteSpace opts then id else dropWhile isSpace- let body = filter (not . null) $ map (map stripSpaces . splitFixedSize splitter size) (skipLine contents)- return (columns, body)- where joinMultiLines (cs:ds:css) | valid True cs = cs : joinMultiLines (ds:css)- | otherwise = joinMultiLines $ (cs ++ "\n" ++ ds) : css- where valid False ('"':'"':xs) = valid False xs- valid False ('\\':'"':xs) = valid False xs- valid b ('"':xs) = valid (not b) xs- valid b (_:xs) = valid b xs- valid b "" = b- joinMultiLines css = css--detectSplitter :: String -> String -> Char -> Bool-detectSplitter xs ys = head $ [ splitter | (x, y, splitter) <- map splitLines splitters- , 1 < length x && length x <= length y ] ++ splitters- where splitLines f = (splitFixedSize f 0 xs, splitFixedSize f 0 ys, f)- splitters = [ (==','), isSpace ]--splitFixedSize :: (Char -> Bool) -> Int -> String -> [String]-splitFixedSize f n = fill . go n- where go _ "" = []- go k (c:cs@(c':_)) | f c && f c' && not (f ' ' && isSpace c') = "" : go (k - 1) cs- | f c = go k cs- go k ('"':cs) = let (ys, xs) = takeQuotedString cs in xs : go (k - 1) ys- where takeQuotedString ('"':'"':xs) = fmap ('"':) (takeQuotedString xs)- takeQuotedString ('\\':'"':xs) = fmap ('"':) (takeQuotedString xs)- takeQuotedString ('"':xs) = (xs, "")- takeQuotedString (x:xs) = fmap (x:) (takeQuotedString xs)- takeQuotedString "" = ("", "")- go k (c:cs) | f c = go k cs- go 1 cs = [cs]- go k cs = let (xs, ys) = break f cs in xs : go (k - 1) ys- fill [] = []- fill xs = xs ++ replicate (n - length xs) ""
− src/Main.hs
@@ -1,120 +0,0 @@-module Main where--import Control.Applicative-import Control.Monad (forM_, guard, when)-import Data.Char (isSpace)-import Data.List (isSuffixOf, intercalate, transpose)-import qualified Data.Map as Map-import Data.Maybe-import qualified Data.Set as Set-import Data.Set ((\\))-import qualified Database.SQLite.Simple as SQLite-import Options.Applicative (execParser, helper, info, fullDesc, header)-import System.Exit (exitFailure)-import System.IO-import Text.Read (readMaybe)--import qualified File as File-import qualified Option as Option-import qualified Parser as Parser-import qualified SQL as SQL-import qualified SQLType as SQLType--main :: IO ()-main = runCommand =<< execParser opts- where opts = info (helper <*> Option.version <*> Option.options)- (fullDesc <> header "qhs - SQL queries on CSV and TSV files")--runCommand :: Option.Option -> IO ()-runCommand opts = do- queryTableMap <- parseQuery =<< fetchQuery opts- conn <- SQL.open ":memory:"- runQuery opts conn queryTableMap- SQL.close conn--runQuery :: Option.Option -> SQLite.Connection -> (String, Parser.TableNameMap) -> IO ()-runQuery opts conn (query, tableMap) = do- readFilesCreateTables opts conn tableMap- ret <- SQL.execute conn query- case ret of- Right (cs, rs) -> do- let outputDelimiter =- fromMaybe " " $ guard (Option.tabDelimitedOutput opts) *> Just "\t"- <|> Option.outputDelimiter opts- <|> guard (Option.tabDelimited opts) *> Just "\t"- <|> Option.delimiter opts- when (Option.outputHeader opts) $- putStrLn $ intercalate outputDelimiter $ cs- mapM_ (putStrLn . intercalate outputDelimiter . map show) rs- Left err -> do- hPutStrLn stderr err- exitFailure--fetchQuery :: Option.Option -> IO String-fetchQuery opts = do- when (isJust (Option.query opts) && isJust (Option.queryFile opts)) $ do- hPutStrLn stderr "Can't provide both a query file and a query on the command line."- exitFailure- query <- fromMaybe "" <$> case Option.query opts of- Just q -> return (Just q)- Nothing -> mapM readFile (Option.queryFile opts)- when (all isSpace query) $ do- hPutStrLn stderr "Query cannot be empty."- hPutStrLn stderr "For basic information, try the `--help' option."- exitFailure- return query--parseQuery :: String -> IO (String, Parser.TableNameMap)-parseQuery qs = do- let (query, tableMap) = Parser.replaceTableNames qs- case Parser.extractTableNames query "<<query>>" of- Left err -> do- hPutStrLn stderr $ Parser.replaceBackTableNames tableMap $ Parser.errorString err- exitFailure- Right tableNames -> do- let xs = Set.fromList tableNames- let ys = Set.fromList (Map.elems tableMap)- if xs == ys- then return (query, tableMap)- else do- hPutStrLn stderr "Invalid table name:"- hPutStrLn stderr $ " " ++ show (Set.union (xs \\ ys) (ys \\ xs))- hPutStrLn stderr "Probably a bug of qhs. Please submit a issue report."- exitFailure--readFilesCreateTables :: Option.Option -> SQLite.Connection -> Parser.TableNameMap -> IO ()-readFilesCreateTables opts conn tableMap =- forM_ (Map.toList tableMap) $ \(path, name) -> do- let path' = unquote path- handle <- openFile (if path' == "-" then "/dev/stdin" else path') ReadMode- let opts' = opts { Option.gzipped = Option.gzipped opts || ".gz" `isSuffixOf` path' }- (columns, body) <- File.readFromFile opts' handle- when (length columns == 0) $ do- hPutStrLn stderr $ if Option.skipHeader opts- then "Header line is expected but missing in file " ++ path- else "Warning - data is empty"- exitFailure- when (any (elem ',') columns) $ do- hPutStrLn stderr "Column name cannot contain commas"- exitFailure- when (length columns >= 1) $- createTable conn name path columns body- hClose handle- where unquote (x:xs@(_:_)) | x `elem` "\"'`" && x == last xs = init xs- unquote xs = xs--createTable :: SQLite.Connection -> String -> String -> [String] -> [[String]] -> IO ()-createTable conn name path columns body = do- let probablyNumberColumn =- [ all isJust [ readMaybe x :: Maybe Float | x <- xs, not (all isSpace x) ]- | xs <- transpose body ]- let types = [ if b then SQLType.SQLInt else SQLType.SQLChar | b <- probablyNumberColumn ]- ret <- SQL.createTable conn name columns types- case ret of- Just err -> do- putStrLn "Error on creating a new table:"- putStrLn $ " " ++ path ++ " (" ++ name ++ ") " ++ show columns- putStrLn err- Nothing ->- forM_ body $ \entry -> do- mapM_ (hPutStrLn stderr) =<< SQL.insertRow conn name columns types entry
− src/Option.hs
@@ -1,61 +0,0 @@-module Option where--import Data.Version (showVersion)-import Options.Applicative--import qualified Paths_qhs as QHS---- | Command options-data Option = Option { skipHeader :: Bool,- outputHeader :: Bool,- delimiter :: Maybe String,- tabDelimited :: Bool,- outputDelimiter :: Maybe String,- tabDelimitedOutput :: Bool,- keepLeadingWhiteSpace :: Bool,- gzipped :: Bool,- queryFile :: Maybe String,- query :: Maybe String }---- | Option parser-options :: Parser Option-options = Option- <$> switch (long "skip-header"- <> short 'H'- <> help "Skip the header row for row input and use it for column names instead.")- <*> switch (long "output-header"- <> short 'O'- <> help "Output the header line.")- <*> optional (strOption (long "delimiter"- <> short 'd'- <> metavar "DELIMITER"- <> help "Field delimiter. If not specified, automatically detected."))- <*> switch (long "tab-delimited"- <> short 't'- <> help "Same as -d $'\\t'.")- <*> optional (strOption (long "output-delimiter"- <> short 'D'- <> metavar "OUTPUT_DELIMITER"- <> help "Field delimiter for output. If not specified, the argument of -d DELIMITER is used."))- <*> switch (long "tab-delimited-output"- <> short 'T'- <> help "Same as -D $'\\t'.")- <*> switch (long "keep-leading-whitespace"- <> short 'k'- <> help "Keep leading whitespace in values. The leading whitespaces are stripped off by default.")- <*> switch (long "gzipped"- <> short 'z'- <> help "Assuming the gzipped input.")- <*> optional (strOption (long "query-filename"- <> short 'q'- <> metavar "QUERY_FILENAME"- <> help "Read query from the provided filename."))- <*> optional (argument str (metavar "QUERY"))---- | Parser for --version/-v-version :: Parser (a -> a)-version = abortOption (InfoMsg ("qhs " ++ showVersion QHS.version)) $- long "version" <>- short 'v' <>- help "Show the version of the command." <>- hidden
− src/Parser.hs
@@ -1,71 +0,0 @@-module Parser (replaceTableNames, roughlyExtractTableNames, replaceBackTableNames, extractTableNames, errorString, TableNameMap) where--import qualified Crypto.Hash as Crypto-import qualified Data.ByteString.Char8 as Char8-import Data.Char (isNumber, isSpace, toUpper)-import Data.Generics (everything, mkQ)-import Data.List (unfoldr)-import qualified Data.Map as Map-import Data.Tuple (swap)-import qualified Language.SQL.SimpleSQL.Dialect as Dialect-import qualified Language.SQL.SimpleSQL.Parse as Parse-import qualified Language.SQL.SimpleSQL.Syntax as Syntax--type TableNameMap = Map.Map String String---- | Replace all the occurrence of table names (file names, in many cases) into--- valid table names in SQL. The table names are calculated based on the file--- names, keeping its length so that error position stays at the same position--- after we replace them back to the file names. I take `max 5` to avoid conflict.-replaceTableNames :: String -> (String, TableNameMap)-replaceTableNames qs = (replaceQueryWithTableMap tableMap qs, tableMap)- where genTableName xs = take (max 5 (length xs)) $ dropWhile isNumber $ concat $ tail $ iterate sha1Encode xs- tableMap = Map.fromList [ (name, genTableName name) | name <- roughlyExtractTableNames qs ]--sha1Encode :: String -> String-sha1Encode = show . (Crypto.hash :: Char8.ByteString -> Crypto.Digest Crypto.SHA1) . Char8.pack---- | This function roughly extract the table names. We need this function because--- the given query contains the file names so the SQL parser cannot parse.-roughlyExtractTableNames :: String -> [String]-roughlyExtractTableNames qs = [ ys | (xs, ys) <- zip qss (drop 2 qss), isTableNamePrefix xs ]- where qss = splitQuery qs---- | The words after these words are possibly table names.-isTableNamePrefix :: String -> Bool-isTableNamePrefix xs = map toUpper xs `elem` ["FROM", "JOIN"]---- | Replace the table names using the tableMap.-replaceQueryWithTableMap :: TableNameMap -> String -> String-replaceQueryWithTableMap tableMap qs = query- where qss = splitQuery qs- query = concat [ if isTableNamePrefix xs then Map.findWithDefault ys ys tableMap else ys | (xs, ys) <- zip ("" : "" : qss) qss ]---- | Split the query string with spaces, taking the quotes into consideration.-splitQuery :: String -> [String]-splitQuery = unfoldr split'- where split' :: String -> Maybe (String, String)- split' ccs@(c:cs) | c `elem` "\"'`" = Just $ splitAt (1 + countUntil c cs) ccs- | isSpace c = Just $ span isSpace ccs- | otherwise = Just $ break (\d -> isSpace d || d `elem` "\"'`") ccs- split' [] = Nothing- countUntil c ('\\':c':cs) | c == c' = 2 + countUntil c cs- countUntil c (c':cs) | c == c' = 1- | otherwise = 1 + countUntil c cs- countUntil _ [] = 0---- | Replace the generated table names back into the original file names.-replaceBackTableNames :: TableNameMap -> String -> String-replaceBackTableNames tableMap = replaceQueryWithTableMap reverseMap- where reverseMap = Map.fromList $ map swap $ Map.toList tableMap---- | Extracts the table names using the rigid SQL parser.-extractTableNames :: String -> FilePath -> Either Parse.ParseError [String]-extractTableNames query path = everything (++) ([] `mkQ` tableNames)- <$> Parse.parseQueryExpr Dialect.mysql path Nothing query- where tableNames (Syntax.TRSimple (name:_)) = fromName name- tableNames _ = []- fromName (Syntax.Name _ name) = [name]--errorString :: Parse.ParseError -> String-errorString = Parse.peFormattedError
+ src/Qhs.hs view
@@ -0,0 +1,3 @@+module Qhs (main) where++import Qhs.CLI (main)
+ src/Qhs/CLI.hs view
@@ -0,0 +1,125 @@+module Qhs.CLI (main) where++import Control.Applicative ((<|>))+import Control.Monad (forM_, guard, unless, when)+import Data.Char (isSpace)+import Data.List (intercalate, isSuffixOf, transpose)+import Data.Map qualified as Map+import Data.Maybe (fromMaybe, isJust)+import Data.Set as Set (fromList, union, (\\))+import Data.Version (Version)+import Database.SQLite.Simple qualified as SQLite+import Options.Applicative (execParser, fullDesc, header, helper, info)+import System.Exit (exitFailure)+import System.IO+import Text.Read (readMaybe)++import Qhs.File qualified as File+import Qhs.Option as Option+import Qhs.Parser qualified as Parser+import Qhs.SQL qualified as SQL+import Qhs.SQLType++main :: Version -> IO ()+main ver = execParser opts >>= runCommand+ where opts = info (helper <*> Option.version ver <*> Option.options)+ (fullDesc <> header "qhs - SQL queries on CSV and TSV files")++runCommand :: Option -> IO ()+runCommand opts = do+ queryTableMap <- parseQuery =<< fetchQuery opts+ conn <- SQL.open ":memory:"+ runQuery opts conn queryTableMap+ SQL.close conn++runQuery :: Option -> SQLite.Connection -> (String, Parser.TableNameMap) -> IO ()+runQuery opts conn (query, tableMap) = do+ readFilesCreateTables opts conn tableMap+ SQL.execute conn query >>= \case+ Right (cs, rs) -> do+ let outputDelimiter =+ fromMaybe " " $ guard opts.tabDelimitedOutput *> Just "\t" <|>+ guard opts.pipeDelimitedOutput *> Just "|" <|>+ opts.outputDelimiter <|>+ guard opts.tabDelimited *> Just "\t" <|>+ guard opts.pipeDelimited *> Just "|" <|>+ opts.delimiter+ when opts.outputHeader $+ putStrLn $ intercalate outputDelimiter cs+ mapM_ (putStrLn . intercalate outputDelimiter . map show) rs+ Left err -> do+ hPrint stderr err+ exitFailure++fetchQuery :: Option -> IO String+fetchQuery opts = do+ when (isJust opts.query && isJust opts.queryFile) do+ hPutStrLn stderr "Can't provide both a query file and a query on the command line."+ exitFailure+ query <- fromMaybe "" <$> case opts.query of+ Just q -> return (Just q)+ Nothing -> mapM readFile opts.queryFile+ when (all isSpace query) do+ hPutStrLn stderr "Query cannot be empty."+ hPutStrLn stderr "For basic information, try the `--help' option."+ exitFailure+ return query++parseQuery :: String -> IO (String, Parser.TableNameMap)+parseQuery qs = do+ let (query, tableMap) = Parser.replaceTableNames qs+ case Parser.extractTableNames query "<<query>>" of+ Left err -> do+ hPutStrLn stderr $ Parser.replaceBackTableNames tableMap $ Parser.errorString err+ exitFailure+ Right tableNames -> do+ let xs = Set.fromList tableNames+ let ys = Set.fromList (Map.elems tableMap)+ if xs == ys+ then return (query, tableMap)+ else do+ hPutStrLn stderr "Invalid table name:"+ hPutStrLn stderr $ " " ++ show (Set.union (xs \\ ys) (ys \\ xs))+ hPutStrLn stderr "Probably a bug of qhs. Please submit a issue report."+ exitFailure++readFilesCreateTables :: Option -> SQLite.Connection -> Parser.TableNameMap -> IO ()+readFilesCreateTables opts conn tableMap =+ forM_ (Map.toList tableMap) \(path, name) -> do+ let path' = unquote path+ handle <- if path' == "-" then return stdin else openFile path' ReadMode+ let opts' = opts { gzipped = opts.gzipped || ".gz" `isSuffixOf` path' }+ (columns, body) <- File.readFromFile opts' handle+ when (null columns) do+ hPutStrLn stderr $ if opts.skipHeader+ then "Header line is expected but missing in file " ++ path+ else "Warning - data is empty"+ exitFailure+ when (any (elem ',') columns) do+ hPutStrLn stderr "Column name cannot contain commas"+ exitFailure+ unless (null columns) $+ createTable conn name path columns body+ hClose handle+ where unquote (x:xs@(_:_)) | x `elem` "\"'`" && x == last xs = init xs+ unquote xs = xs++createTable :: SQLite.Connection -> String -> String -> [String] -> [[String]] -> IO ()+createTable conn name path columns body = do+ let probablyNumberColumn =+ [ all isJust [ readMaybe x :: Maybe Double | x <- xs, not (all isSpace x) ]+ | xs <- transpose body ]+ let types = [ if b then SQLInt else SQLChar | b <- probablyNumberColumn ]+ SQL.createTable conn name columns types >>= \case+ Left err -> do+ hPutStrLn stderr "Error on creating a new table:"+ hPutStrLn stderr $ " " ++ path ++ " (" ++ name ++ ") " ++ show columns+ hPrint stderr err+ exitFailure+ Right _ -> do+ forM_ body \entry -> do+ SQL.insertRow conn name columns types entry >>= \case+ Left err -> do+ hPrint stderr err+ exitFailure+ Right _ -> return ()
+ src/Qhs/File.hs view
@@ -0,0 +1,74 @@+module Qhs.File (readFromFile, detectSplitter, splitFixedSize) where++import Codec.Compression.GZip qualified as GZip+import Control.Applicative ((<|>))+import Control.Monad (guard, when)+import Data.ByteString.Lazy qualified as ByteString+import Data.ByteString.Lazy.Char8 qualified as Char8+import Data.Char (isSpace)+import Data.List (unsnoc)+import Data.List.NonEmpty (NonEmpty((:|)), head, prependList, tail, toList,+ uncons, (<|))+import Data.Tuple.Extra (second)+import System.Exit (exitFailure)+import System.IO+import Prelude hiding (head, tail)++import Qhs.Option++readFromFile :: Option -> Handle -> IO ([String], [[String]])+readFromFile opts handle = do+ contents <- joinMultiLines . map stripCR . lines <$>+ if opts.gzipped+ then Char8.unpack . GZip.decompress <$> ByteString.hGetContents handle+ else hGetContents handle+ let delimiter = guard opts.tabDelimited *> Just "\t" <|>+ guard opts.pipeDelimited *> Just "|" <|>+ opts.delimiter+ when (maybe False ((/=1) . length) delimiter) do+ hPutStrLn stderr "Invalid delimiter."+ exitFailure+ let splitter = case delimiter of+ Just [c] -> (==) c+ _ -> detectSplitter headLine secondLine+ where (headLine, secondLine) = second (maybe "" head) (uncons contents)+ let headColumns = splitFixedSize splitter 0 $ head contents+ let size = length headColumns+ let columns = if opts.skipHeader then headColumns else [ 'c' : show i | i <- [1..size] ]+ let skipLine = if opts.skipHeader then tail else toList+ let stripSpaces = if opts.keepLeadingWhiteSpace then id else dropWhile isSpace+ let body = filter (not . null) $ map (map stripSpaces . splitFixedSize splitter size) (skipLine contents)+ return (columns, body)+ where joinMultiLines (cs:ds:css) | valid True cs = cs <| joinMultiLines (ds:css)+ | otherwise = joinMultiLines $ (cs ++ "\n" ++ ds) : css+ where valid False ('"':'"':xs) = valid False xs+ valid False ('\\':'"':xs) = valid False xs+ valid b ('"':xs) = valid (not b) xs+ valid b (_:xs) = valid b xs+ valid b "" = b+ joinMultiLines (cs:css) = cs :| css+ joinMultiLines [] = "" :| []+ stripCR cs = case unsnoc cs of Just (ts, '\r') -> ts; _ -> cs++detectSplitter :: String -> String -> Char -> Bool+detectSplitter xs ys = head $ [ s | (x, y, s) <- map splitLines $ toList splitters+ , 1 < length x && length x <= length y ] `prependList` splitters+ where splitLines f = (splitFixedSize f 0 xs, splitFixedSize f 0 ys, f)+ splitters = (==',') :| [isSpace]++splitFixedSize :: (Char -> Bool) -> Int -> String -> [String]+splitFixedSize f n = fill . go n+ where go _ "" = []+ go k (c:cs@(c':_)) | f c && f c' && not (f ' ' && isSpace c') = "" : go (k - 1) cs+ | f c = go k cs+ go k ('"':cs) = let (ys, xs) = takeQuotedString cs in xs : go (k - 1) ys+ where takeQuotedString ('"':'"':xs) = fmap ('"':) (takeQuotedString xs)+ takeQuotedString ('\\':'"':xs) = fmap ('"':) (takeQuotedString xs)+ takeQuotedString ('"':xs) = (xs, "")+ takeQuotedString (x:xs) = fmap (x:) (takeQuotedString xs)+ takeQuotedString "" = ("", "")+ go k (c:cs) | f c = go k cs+ go 1 cs = [cs]+ go k cs = let (xs, ys) = break f cs in xs : go (k - 1) ys+ fill [] = []+ fill xs = xs ++ replicate (n - length xs) ""
+ src/Qhs/Option.hs view
@@ -0,0 +1,70 @@+module Qhs.Option (Option(..), options, version) where++import Data.Version (Version, showVersion)+import Options.Applicative++-- | Command options+data Option =+ Option {+ skipHeader :: Bool,+ outputHeader :: Bool,+ delimiter :: Maybe String,+ tabDelimited :: Bool,+ pipeDelimited :: Bool,+ outputDelimiter :: Maybe String,+ tabDelimitedOutput :: Bool,+ pipeDelimitedOutput :: Bool,+ keepLeadingWhiteSpace :: Bool,+ gzipped :: Bool,+ queryFile :: Maybe String,+ query :: Maybe String+ }++-- | Option parser+options :: Parser Option+options = Option+ <$> switch (long "skip-header"+ <> short 'H'+ <> help "Skip the header row for row input and use it for column names instead.")+ <*> switch (long "output-header"+ <> short 'O'+ <> help "Output the header line.")+ <*> optional (strOption (long "delimiter"+ <> short 'd'+ <> metavar "DELIMITER"+ <> help "Field delimiter. If not specified, automatically detected."))+ <*> switch (long "tab-delimited"+ <> short 't'+ <> help "Same as -d $'\\t'.")+ <*> switch (long "pipe-delimited"+ <> short 'p'+ <> help "Same as -d '|'.")+ <*> optional (strOption (long "output-delimiter"+ <> short 'D'+ <> metavar "OUTPUT_DELIMITER"+ <> help "Field delimiter for output. If not specified, the argument of -d DELIMITER is used."))+ <*> switch (long "tab-delimited-output"+ <> short 'T'+ <> help "Same as -D $'\\t'.")+ <*> switch (long "pipe-delimited-output"+ <> short 'P'+ <> help "Same as -D '|'.")+ <*> switch (long "keep-leading-whitespace"+ <> short 'k'+ <> help "Keep leading whitespace in values. The leading whitespaces are stripped off by default.")+ <*> switch (long "gzipped"+ <> short 'z'+ <> help "Assuming the gzipped input.")+ <*> optional (strOption (long "query-filename"+ <> short 'q'+ <> metavar "QUERY_FILENAME"+ <> help "Read query from the provided filename."))+ <*> optional (argument str (metavar "QUERY"))++-- | Parser for --version/-v+version :: Version -> Parser (a -> a)+version ver = abortOption (InfoMsg ("qhs " ++ showVersion ver)) $+ long "version" <>+ short 'v' <>+ help "Show the version of the command." <>+ hidden
+ src/Qhs/Parser.hs view
@@ -0,0 +1,82 @@+module Qhs.Parser+ (+ replaceTableNames,+ roughlyExtractTableNames,+ replaceBackTableNames,+ extractTableNames,+ errorString,+ TableNameMap+ ) where++import Crypto.Hash qualified as Crypto+import Data.ByteString.Char8 qualified as Char8+import Data.Char (isNumber, isSpace, toUpper)+import Data.Generics (everything, mkQ)+import Data.List (unfoldr)+import Data.List.NonEmpty (iterate, tail)+import Data.Map qualified as Map+import Data.Text qualified as Text+import Data.Tuple (swap)+import Language.SQL.SimpleSQL.Dialect qualified as Dialect+import Language.SQL.SimpleSQL.Parse qualified as Parse+import Language.SQL.SimpleSQL.Syntax qualified as Syntax+import Prelude hiding (iterate, tail)++type TableNameMap = Map.Map String String++-- | Replace all the occurrence of table names (file names, in many cases) into+-- valid table names in SQL. The table names are calculated based on the file+-- names, keeping its length so that error position stays at the same position+-- after we replace them back to the file names. I take `max 5` to avoid conflict.+replaceTableNames :: String -> (String, TableNameMap)+replaceTableNames qs = (replaceQueryWithTableMap tableMap qs, tableMap)+ where genTableName xs = take (max 5 (length xs)) $ dropWhile isNumber $ concat $ tail $ iterate sha1Encode xs+ tableMap = Map.fromList [ (name, genTableName name) | name <- roughlyExtractTableNames qs ]++sha1Encode :: String -> String+sha1Encode = show . (Crypto.hash :: Char8.ByteString -> Crypto.Digest Crypto.SHA1) . Char8.pack++-- | This function roughly extract the table names. We need this function because+-- the given query contains the file names so the SQL parser cannot parse.+roughlyExtractTableNames :: String -> [String]+roughlyExtractTableNames qs = [ ys | (xs, ys) <- zip qss (drop 2 qss), isTableNamePrefix xs ]+ where qss = splitQuery qs++-- | The words after these words are possibly table names.+isTableNamePrefix :: String -> Bool+isTableNamePrefix xs = map toUpper xs `elem` ["FROM", "JOIN"]++-- | Replace the table names using the tableMap.+replaceQueryWithTableMap :: TableNameMap -> String -> String+replaceQueryWithTableMap tableMap qs = query+ where qss = splitQuery qs+ query = concat [ if isTableNamePrefix xs then Map.findWithDefault ys ys tableMap else ys | (xs, ys) <- zip ("" : "" : qss) qss ]++-- | Split the query string with spaces, taking the quotes into consideration.+splitQuery :: String -> [String]+splitQuery = unfoldr split'+ where split' :: String -> Maybe (String, String)+ split' ccs@(c:cs) | c `elem` "\"'`" = Just $ splitAt (1 + countUntil c cs) ccs+ | isSpace c = Just $ span isSpace ccs+ | otherwise = Just $ break (\d -> isSpace d || d `elem` "\"'`") ccs+ split' [] = Nothing+ countUntil c ('\\':c':cs) | c == c' = 2 + countUntil c cs+ countUntil c (c':cs) | c == c' = 1+ | otherwise = 1 + countUntil c cs+ countUntil _ [] = 0++-- | Replace the generated table names back into the original file names.+replaceBackTableNames :: TableNameMap -> String -> String+replaceBackTableNames tableMap = replaceQueryWithTableMap reverseMap+ where reverseMap = Map.fromList $ map swap $ Map.toList tableMap++-- | Extracts the table names using the rigid SQL parser.+extractTableNames :: String -> FilePath -> Either Parse.ParseError [String]+extractTableNames query path = everything (++) ([] `mkQ` tableNames)+ <$> Parse.parseQueryExpr Dialect.mysql (Text.pack path) Nothing (Text.pack query)+ where tableNames (Syntax.TRSimple (name:_)) = fromName name+ tableNames _ = []+ fromName (Syntax.Name _ name) = [Text.unpack name]++errorString :: Parse.ParseError -> String+errorString = Text.unpack . Parse.prettyError
+ src/Qhs/SQL.hs view
@@ -0,0 +1,49 @@+module Qhs.SQL (open, close, createTable, insertRow, execute) where++import Control.Exception (SomeException, try)+import Control.Monad (forM)+import Data.List (intercalate)+import Data.String (fromString)+import Data.Text qualified as Text+import Database.SQLite.Simple qualified as SQLite++import Qhs.SQLType++-- | Open a new database connection.+open :: String -> IO SQLite.Connection+open = SQLite.open++-- | Close a database connection.+close :: SQLite.Connection -> IO ()+close = SQLite.close++-- | Creates a new table.+createTable :: SQLite.Connection -> String -> [String] -> [SQLType] -> IO (Either SomeException ())+createTable conn name columns types = do+ let stmt = "CREATE TABLE " ++ sqlQuote name ++ " "+ ++ tupled (zipWith quote types columns) ++ ";"+ try $ SQLite.execute_ conn (fromString stmt)+ where quote t c = sqlQuote c ++ " " ++ show t++-- | Inserts a row into a table.+insertRow :: SQLite.Connection -> String -> [String] -> [SQLType] -> [String] -> IO (Either SomeException ())+insertRow conn name columns types entry = do+ let stmt = "INSERT INTO " ++ sqlQuote name ++ tupled (map sqlQuote columns)+ ++ " VALUES " ++ tupled (replicate (length columns) "?") ++ ";"+ try $ SQLite.execute conn (fromString stmt) (zip types entry)++-- | Executes a SQL statement.+execute :: SQLite.Connection -> String -> IO (Either SomeException ([String], [[Any]]))+execute conn query = do+ e <- try $ SQLite.query_ conn (fromString query)+ columns <- SQLite.withStatement conn (fromString query) \stmt -> do+ cnt <- toInteger <$> SQLite.columnCount stmt+ forM [0..cnt-1] \i ->+ Text.unpack <$> SQLite.columnName stmt (fromInteger i)+ return $ (columns,) <$> e++sqlQuote :: String -> String+sqlQuote xs = "`" ++ xs ++ "`"++tupled :: [String] -> String+tupled xs = "(" ++ intercalate ", " xs ++ ")"
+ src/Qhs/SQLType.hs view
@@ -0,0 +1,48 @@+module Qhs.SQLType (SQLType(..), Any, fromColumnsAndEntries) where++import Control.Applicative ((<|>))+import Data.Maybe (fromMaybe)+import Data.String (IsString(..))+import Data.Text qualified as Text+import Database.SQLite.Simple+import Database.SQLite.Simple.FromField+import Database.SQLite.Simple.Internal (Field(..))+import Database.SQLite.Simple.Ok+import Database.SQLite.Simple.ToField+import Text.Read (readMaybe)++data SQLType = SQLChar | SQLInt+ deriving Eq++instance Show SQLType where+ show SQLChar = "CHAR"+ show SQLInt = "INTEGER"++instance ToField (SQLType, String) where+ toField (SQLChar, cs) = SQLText $ Text.pack cs+ toField (SQLInt, cs) = maybe SQLNull SQLFloat $ readMaybe cs++data Any = AnyDouble Double | AnyInt Int | AnyString String | AnyNull+ deriving Eq++instance FromField Any where+ fromField (Field (SQLFloat d) _) = Ok . AnyDouble $ d+ fromField (Field (SQLInteger i) _) = Ok . AnyInt . fromIntegral $ i+ fromField (Field (SQLText s) _) = Ok . AnyString . Text.unpack $ s+ fromField (Field SQLNull _) = Ok AnyNull+ fromField f = returnError ConversionFailed f "expecting SQLText column type"++instance IsString Any where+ fromString "" = AnyNull+ fromString s = fromMaybe (AnyString s) $+ AnyInt <$> readMaybe s <|>+ AnyDouble <$> readMaybe s++instance Show Any where+ show (AnyDouble d) = show d+ show (AnyInt i) = show i+ show (AnyString s) = s+ show AnyNull = ""++fromColumnsAndEntries :: [String] -> [[String]] -> ([String], [[Any]])+fromColumnsAndEntries columns xs = (columns, map (map fromString) xs)
− src/SQL.hs
@@ -1,61 +0,0 @@-module SQL (open, close, createTable, insertRow, execute) where--import Control.Exception (try, SomeException)-import Control.Monad (forM)-import Data.List (intersperse)-import Data.String (fromString)-import qualified Data.Text as T-import qualified Database.SQLite.Simple as SQLite--import SQLType---- | Open a new database connection.-open :: String -> IO SQLite.Connection-open = SQLite.open---- | Close a database connection.-close :: SQLite.Connection -> IO ()-close = SQLite.close---- | Creates a new table.-createTable :: SQLite.Connection -> String -> [String] -> [SQLType] -> IO (Maybe String)-createTable conn name columns types = do- let stmt = "CREATE TABLE " ++ sqlQuote name ++ " "- ++ tupled (map quote (zip types columns)) ++ ";"- e <- try $ SQLite.execute_ conn (fromString stmt)- return $ either (Just . (show :: SomeException -> String)) (const Nothing) e- where quote (t, c) = sqlQuote c ++ " " ++ showType t---- | Inserts a row into a table.-insertRow :: SQLite.Connection -> String -> [String] -> [SQLType] -> [String] -> IO (Maybe String)-insertRow conn name columns types entry = do- let stmt = "INSERT INTO " ++ sqlQuote name ++ tupled (map sqlQuote columns)- ++ " VALUES " ++ tupled (map quote (zip types entry)) ++ ";"- e <- try $ SQLite.execute_ conn (fromString stmt)- return $ either (Just . (show :: SomeException -> String)) (const Nothing) e- where quote (t, "") | isDigitType t = "NULL"- quote (t, cs) | isDigitType t = cs- | otherwise = "'" ++ toSQLString cs ++ "'"- isDigitType SQLInt = True- isDigitType _ = False--toSQLString :: String -> String-toSQLString "" = ""-toSQLString ('\'':xs) = '\'':'\'':toSQLString xs-toSQLString (x:xs) = x : toSQLString xs---- | Executes a SQL statement.-execute :: SQLite.Connection -> String -> IO (Either String ([String], [[Any]]))-execute conn query = do- e <- try $ SQLite.query_ conn (fromString query)- columns <- SQLite.withStatement conn (fromString query) $ \stmt -> do- cnt <- toInteger <$> SQLite.columnCount stmt- forM [0..cnt-1] $ \i ->- T.unpack <$> SQLite.columnName stmt (fromInteger i)- return $ either (Left . (show :: SomeException -> String)) (Right . (,) columns) $ e--sqlQuote :: String -> String-sqlQuote xs = "`" ++ xs ++ "`"--tupled :: [String] -> String-tupled xs = "(" ++ concat (intersperse ", " xs) ++ ")"
− src/SQLType.hs
@@ -1,44 +0,0 @@-module SQLType (SQLType(..), showType, Any, fromColumnsAndEntries) where--import Data.String (IsString(..))-import qualified Data.Text as T-import Database.SQLite.Simple-import Database.SQLite.Simple.FromField-import Database.SQLite.Simple.Internal (Field(..))-import Database.SQLite.Simple.Ok-import Text.Read (readMaybe)--data SQLType = SQLChar | SQLInt--showType :: SQLType -> String-showType t =- case t of- SQLChar -> "CHAR"- SQLInt -> "INTEGER"--data Any = AnyDouble Double | AnyInt Int | AnyString String | AnyNull- deriving (Eq)--instance FromField Any where- fromField (Field (SQLFloat d) _) = Ok . AnyDouble $ d- fromField (Field (SQLInteger i) _) = Ok . AnyInt . fromIntegral $ i- fromField (Field (SQLText s) _) = Ok . AnyString . T.unpack $ s- fromField (Field SQLNull _) = Ok AnyNull- fromField f = returnError ConversionFailed f "expecting SQLText column type"--instance IsString Any where- fromString s =- case readMaybe s of- Just i -> AnyInt i- Nothing -> case readMaybe s of- Just d -> AnyDouble d- Nothing -> if s == "" then AnyNull else AnyString s--instance Show Any where- show (AnyDouble d) = show d- show (AnyInt i) = show i- show (AnyString s) = s- show AnyNull = ""--fromColumnsAndEntries :: [String] -> [[String]] -> ([String], [[Any]])-fromColumnsAndEntries columns xs = (columns, map (map fromString) xs)
+ stack.yaml view
@@ -0,0 +1,3 @@+resolver: lts-23.24+extra-deps:+ - simple-sql-parser-0.8.0
+ stack.yaml.lock view
@@ -0,0 +1,19 @@+# This file was autogenerated by Stack.+# You should not edit this file by hand.+# For more information, please see the documentation at:+# https://docs.haskellstack.org/en/stable/topics/lock_files++packages:+- completed:+ hackage: simple-sql-parser-0.8.0@sha256:5153612af09edda2af865e6dc1286d64ecea702541a1af824ac518268f0b5cb4,4417+ pantry-tree:+ sha256: fba7264bf9753b0c6abf40881e43b0b56e97052487c5ccbfd663892cdcfdfc13+ size: 2898+ original:+ hackage: simple-sql-parser-0.8.0+snapshots:+- completed:+ sha256: 400dfb2174640dd2f9f2ba7cbf9148699f2380ab64faa46f4be298a5d6205316+ size: 684057+ url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/23/24.yaml+ original: lts-23.24
test/FileSpec.hs view
@@ -4,8 +4,8 @@ import System.IO import Test.Hspec (Spec, describe, it, shouldBe, shouldReturn) -import File-import Option+import Qhs.File+import Qhs.Option spec :: Spec spec = do@@ -15,31 +15,33 @@ readFromFileSpec :: Spec readFromFileSpec =- describe "readFromFile" $ do+ describe "readFromFile" do let opts = Option { skipHeader = True, outputHeader = False, delimiter = Nothing, tabDelimited = False,+ pipeDelimited = False, outputDelimiter = Nothing, tabDelimitedOutput = False,+ pipeDelimitedOutput = False, keepLeadingWhiteSpace = False, gzipped = False, queryFile = Nothing, query = Nothing } - it "should read from a test file" $ do+ it "should read from a test file" do handle <- openFile "test/tests/basic.csv" ReadMode let expected = (["foo", "bar", "baz"], [["a0", "1", "a2"], ["b0", "3", "b2"], ["c0", "", "c2"]]) readFromFile opts handle `shouldReturn` expected hClose handle - it "should read from a gzipped file" $ do+ it "should read from a gzipped file" do handle <- openFile "test/tests/basic.csv.gz" ReadMode let expected = (["foo", "bar", "baz"], [["a0", "1", "a2"], ["b0", "3", "b2"], ["c0", "", "c2"]]) readFromFile (opts { gzipped = True }) handle `shouldReturn` expected hClose handle - it "should read from a test file which contains a multiline cell" $ do+ it "should read from a test file which contains a multiline cell" do handle <- openFile "test/tests/multiline.csv" ReadMode let expected = (["foo", "bar", "baz", "qux", "quux"], [["a0", "1", "a2\nb0\",3,\"b2\nc0", "", "c2"]]) readFromFile opts handle `shouldReturn` expected@@ -47,34 +49,34 @@ detectSplitterSpec :: Spec detectSplitterSpec =- describe "detectSplitter" $ do+ describe "detectSplitter" do - it "should detect the column splitter space" $ do+ it "should detect the column splitter space" do let (headLine, secondLine) = ("c0 c1 c2 c3 c4", "0 1 2 3 4") detectSplitter headLine secondLine ' ' `shouldBe` True detectSplitter headLine secondLine '\t' `shouldBe` True - it "should detect the column splitter comma" $ do+ it "should detect the column splitter comma" do let (headLine, secondLine) = ("c0,c1,c2,c3,c4", "0,1,2,3,4") detectSplitter headLine secondLine ',' `shouldBe` True - it "should detect the column splitter comma even if the column title has spaces" $ do+ it "should detect the column splitter comma even if the column title has spaces" do let (headLine, secondLine) = ("foo bar baz,qux quux,hoge huga,cmd", "100,200,300,foo bar baz qux") detectSplitter headLine secondLine ',' `shouldBe` True splitFixedSizeSpec :: Spec splitFixedSizeSpec =- describe "splitFixedSize" $ do+ describe "splitFixedSize" do - it "should split the String with isSpace" $ do+ it "should split the String with isSpace" do let (input, expected) = ("c0 c1 c2", [ "c0", "c1", "c2" ]) splitFixedSize isSpace 0 input `shouldBe` expected - it "should ignore the successive spaces when splitting with isSpace" $ do+ it "should ignore the successive spaces when splitting with isSpace" do let (input, expected) = ("c0 c1 c2 \t\t c3", [ "c0", "c1", "c2", "c3" ]) splitFixedSize isSpace 0 input `shouldBe` expected - it "should take the column size into account when splitting with isSpace" $ do+ it "should take the column size into account when splitting with isSpace" do let (input, (n1, expected1), (n2, expected2), (n3, expected3), (n4, expected4)) = ("c0 c1 c2 \t\t c3 c4 c5 ", (1, [ "c0 c1 c2 \t\t c3 c4 c5 " ]),@@ -86,15 +88,15 @@ splitFixedSize isSpace n3 input `shouldBe` expected3 splitFixedSize isSpace n4 input `shouldBe` expected4 - it "should split the String with (==',')" $ do+ it "should split the String with (==',')" do let (input, expected) = ("c0,c1,c2", [ "c0", "c1", "c2" ]) splitFixedSize (==',') 0 input `shouldBe` expected - it "should not ignore the successive commas when splitting with (==',')" $ do+ it "should not ignore the successive commas when splitting with (==',')" do let (input, expected) = ("c0,c1,c2,,c3,,,c4", [ "c0", "c1", "c2", "", "c3", "", "", "c4" ]) splitFixedSize (==',') 0 input `shouldBe` expected - it "should take the column size into account when splitting with (==',')" $ do+ it "should take the column size into account when splitting with (==',')" do let (input, (n1, expected1), (n2, expected2), (n3, expected3), (n4, expected4)) = ("c0,c1,,c2,foo bar baz,c4", (1, [ "c0,c1,,c2,foo bar baz,c4" ]),
− test/MainSpec.hs
@@ -1,38 +0,0 @@-module MainSpec (spec) where--import Control.Applicative-import Control.Monad-import System.IO-import System.Process-import Test.Hspec (Spec, describe, it, shouldReturn)--spec :: Spec-spec = qhsSpec--qhsSpec :: Spec-qhsSpec =-- describe "qhs" $ do-- let tests = [- "basic", "columns", "stdin", "header", "where", "tab", "tab2",- "count", "is_null", "not_null", "output_header", "spaces",- "output_delimiter", "tab_delimited_output", "multiline",- "query_file", "empty_query", "empty_query_file", "file_spaces",- "gzip", "gzip_stdin", "avg", "sum", "avg_sum", "seq",- "group", "group_sum", "concat", "join", "invalid"- ]-- forM_ tests $ \test -> do-- it ("should be executed correctly: " ++ test) $ do- let cp = (shell ("bash " ++ test ++ ".sh")) {- cwd = Just "test/tests",- std_out = CreatePipe,- std_err = CreatePipe- }- (_, Just out, Just err, _) <- createProcess cp- hSetBuffering out NoBuffering- hSetBuffering err NoBuffering- outExpected <- readFile $ "test/tests/" ++ test ++ ".out"- liftA2 (++) (hGetContents out) (hGetContents err) `shouldReturn` outExpected
test/ParserSpec.hs view
@@ -1,10 +1,11 @@ module ParserSpec (spec) where -import Data.Either-import qualified Data.Map as Map-import Test.Hspec (Spec, describe, it, shouldBe, shouldSatisfy)+import Data.Either (isLeft)+import Data.Either.Extra (fromRight')+import Data.Map qualified as Map+import Test.Hspec (Spec, describe, it, shouldBe) -import Parser+import Qhs.Parser spec :: Spec spec = do@@ -15,51 +16,51 @@ replaceTableNamesSpec :: Spec replaceTableNamesSpec =- describe "replaceTableNames" $ do+ describe "replaceTableNames" do - it "should replace the file names with table names" $ do+ it "should replace the file names with table names" do let query = "SELECT * FROM ./table0 WHERE c0 > 0" let expected = ("SELECT * FROM d8dc7ec0 WHERE c0 > 0", Map.fromList [("./table0", "d8dc7ec0")]) replaceTableNames query `shouldBe` expected - it "should replace multiple file names with table names" $ do+ it "should replace multiple file names with table names" do let query = "SELECT * FROM ./src/table0.csv\nJOIN /tmp/table1.csv\tON c1 = c2 WHERE c0 > 0" let expected = ("SELECT * FROM d0bf242ceb32ba2b\nJOIN bb239869bdfc764\tON c1 = c2 WHERE c0 > 0", Map.fromList [("./src/table0.csv", "d0bf242ceb32ba2b"),("/tmp/table1.csv", "bb239869bdfc764")]) replaceTableNames query `shouldBe` expected - it "should replace only the table names" $ do+ it "should replace only the table names" do let query = "SELECT * FROM ./table0 WHERE c0 LIKE '%foo ./table0 bar%'" let expected = ("SELECT * FROM d8dc7ec0 WHERE c0 LIKE '%foo ./table0 bar%'", Map.fromList [("./table0", "d8dc7ec0")]) replaceTableNames query `shouldBe` expected - it "should take care of multi-byte file names correctly" $ do+ it "should take care of multi-byte file names correctly" do let query = "SELECT * FROM ./テスト" let expected = ("SELECT * FROM e4c1e", Map.fromList [("./テスト", "e4c1e")]) replaceTableNames query `shouldBe` expected - it "should take care of file name whose sha1 hash has only numbers" $ do+ it "should take care of file name whose sha1 hash has only numbers" do let query = "SELECT * FROM vdbdc.csv" let expected = ("SELECT * FROM f8ce01ceb", Map.fromList [("vdbdc.csv", "f8ce01ceb")]) replaceTableNames query `shouldBe` expected - it "should not replace inside single quotes" $ do+ it "should not replace inside single quotes" do let query = "SELECT * FROM table0.csv WHERE c0 LIKE '%foo FROM table1.csv bar%'" let expected = ("SELECT * FROM ec0f6d989c WHERE c0 LIKE '%foo FROM table1.csv bar%'", Map.fromList [("table0.csv", "ec0f6d989c")]) replaceTableNames query `shouldBe` expected - it "should not replace inside double quotes" $ do+ it "should not replace inside double quotes" do let query = "SELECT * FROM table0.csv WHERE c0 LIKE \"%foo FROM table0.csv bar%\"" let expected = ("SELECT * FROM ec0f6d989c WHERE c0 LIKE \"%foo FROM table0.csv bar%\"", Map.fromList [("table0.csv", "ec0f6d989c")]) replaceTableNames query `shouldBe` expected - it "should replace the file name containing spaces" $ do+ it "should replace the file name containing spaces" do let query = "SELECT * FROM `foo/bar baz qux/quux.csv`" let expected = ("SELECT * FROM a3ecf94bae7d224b52e9ad3df3", Map.fromList [("`foo/bar baz qux/quux.csv`", "a3ecf94bae7d224b52e9ad3df3")])@@ -67,59 +68,59 @@ roughlyExtractTableNamesSpec :: Spec roughlyExtractTableNamesSpec =- describe "roughlyExtractTableNames" $ do+ describe "roughlyExtractTableNames" do - it "should roughly extract table name" $ do+ it "should roughly extract table name" do roughlyExtractTableNames "SELECT * FROM table0 WHERE c0 > 0" `shouldBe` [ "table0" ] roughlyExtractTableNames "select * from table0 where c0 > 0" `shouldBe` [ "table0" ] - it "should roughly extract multiple table names" $ do+ it "should roughly extract multiple table names" do roughlyExtractTableNames "SELECT * FROM table0 JOIN table1 ON c1 = c2 WHERE c0 > 0" `shouldBe` [ "table0", "table1" ] roughlyExtractTableNames "select * from table0 join table1 on c1 = c2 where c0 > 0" `shouldBe` [ "table0", "table1" ] - it "should roughly extract table names but ignore inside quotes" $ do+ it "should roughly extract table names but ignore inside quotes" do roughlyExtractTableNames "SELECT * FROM table0 JOIN table1 ON c1 = c2 WHERE c3 LIKE 'A FROM table3 '" `shouldBe` [ "table0", "table1" ] roughlyExtractTableNames "select * from table0 join table1 on c1 = c2 where c3 like 'a from table3 '" `shouldBe` [ "table0", "table1" ] - it "should roughly extract quoted table names" $ do+ it "should roughly extract quoted table names" do roughlyExtractTableNames "SELECT * FROM `src/table 0 .csv` JOIN '/tmp/table 1.csv'" `shouldBe` [ "`src/table 0 .csv`", "'/tmp/table 1.csv'" ] replaceBackTableNamesSpec :: Spec replaceBackTableNamesSpec =- describe "replaceBackTableNames" $ do+ describe "replaceBackTableNames" do - it "should replace back table names to the file names" $ do+ it "should replace back table names to the file names" do let query = "SELECT * FROM ./table0 WHERE c0 > 0" let (query', tableMap) = replaceTableNames query replaceBackTableNames tableMap query' `shouldBe` query - it "should replace back multiple table names with the file names" $ do+ it "should replace back multiple table names with the file names" do let query = "SELECT * FROM ./src/table0.csv\nJOIN /tmp/table1.csv\tON c1 = c2 WHERE c0 > 0" let (query', tableMap) = replaceTableNames query replaceBackTableNames tableMap query' `shouldBe` query - it "should replace back only the table names" $ do+ it "should replace back only the table names" do let query = "SELECT * FROM ./table0 WHERE c0 LIKE '%foo d8dc7ec0 bar%'" let (query', tableMap) = replaceTableNames query replaceBackTableNames tableMap query' `shouldBe` query - it "should replace back the table name to the file name containing spaces" $ do+ it "should replace back the table name to the file name containing spaces" do let query = "SELECT * FROM `foo/bar baz qux/quux.csv`" let (query', tableMap) = replaceTableNames query replaceBackTableNames tableMap query' `shouldBe` query extractTableNamesSpec :: Spec extractTableNamesSpec =- describe "extractTableNames" $ do+ describe "extractTableNames" do - it "should extract table name" $ do- extractTableNames "SELECT * FROM table0 WHERE c0 > 0" "" `shouldBe` Right [ "table0" ]- extractTableNames "select * from table0 where c0 > 0" "" `shouldBe` Right [ "table0" ]+ it "should extract table name" do+ fromRight' (extractTableNames "SELECT * FROM table0 WHERE c0 > 0" "") `shouldBe` [ "table0" ]+ fromRight' (extractTableNames "select * from table0 where c0 > 0" "") `shouldBe` [ "table0" ] - it "should extract multiple table names" $ do- extractTableNames "SELECT * FROM table0 JOIN table1 ON c1 = c2 WHERE c0 > 0" "" `shouldBe` Right [ "table0", "table1" ]- extractTableNames "select * from table0 join table1 on c1 = c2 where c0 > 0" "" `shouldBe` Right [ "table0", "table1" ]+ it "should extract multiple table names" do+ fromRight' (extractTableNames "SELECT * FROM table0 JOIN table1 ON c1 = c2 WHERE c0 > 0" "") `shouldBe` [ "table0", "table1" ]+ fromRight' (extractTableNames "select * from table0 join table1 on c1 = c2 where c0 > 0" "") `shouldBe` [ "table0", "table1" ] - it "should return a parse error" $ do- extractTableNames "SELECT ** FROM table0 WHERE c0 > 0" "" `shouldSatisfy` isLeft- extractTableNames "SELECT * FROM table0 WHERE > 0" "" `shouldSatisfy` isLeft+ it "should return a parse error" do+ isLeft (extractTableNames "SELECT ** FROM table0 WHERE c0 > 0" "") `shouldBe` True+ isLeft (extractTableNames "SELECT * FROM table0 WHERE > 0" "") `shouldBe` True
+ test/QhsSpec.hs view
@@ -0,0 +1,38 @@+module QhsSpec (spec) where++import Control.Monad+import System.IO+import System.Process+import Test.Hspec (Spec, describe, it, shouldReturn)++spec :: Spec+spec = qhsSpec++qhsSpec :: Spec+qhsSpec =++ describe "qhs" do++ let tests = [+ "basic", "columns", "stdin", "header", "where", "tab", "tab2",+ "count", "is_null", "not_null", "output_header", "spaces", "crlf",+ "output_delimiter", "tab_delimited_output", "multiline",+ "pipe_delimited", "pipe_delimited_output", "delimiters",+ "query_file", "empty_query", "empty_query_file", "file_spaces",+ "gzip", "gzip_stdin", "avg", "sum", "avg_sum", "seq",+ "group", "group_sum", "concat", "join", "invalid"+ ]++ forM_ tests \test -> do++ it ("should be executed correctly: " ++ test) do+ let cp = (shell ("bash " ++ test ++ ".sh")) {+ cwd = Just "test/tests",+ std_out = CreatePipe,+ std_err = CreatePipe+ }+ (_, Just out, Just err, _) <- createProcess cp+ hSetBuffering out NoBuffering+ hSetBuffering err NoBuffering+ outExpected <- readFile $ "test/tests/" ++ test ++ ".out"+ liftA2 (++) (hGetContents out) (hGetContents err) `shouldReturn` outExpected
test/SQLSpec.hs view
@@ -1,10 +1,11 @@ module SQLSpec (spec) where import Control.Monad+import Data.Either.Extra (fromRight') import Test.Hspec (Spec, describe, it, shouldBe) -import SQL-import SQLType+import Qhs.SQL as SQL+import Qhs.SQLType spec :: Spec spec = do@@ -13,17 +14,17 @@ openCloseSpec :: Spec openCloseSpec =- describe "open, close" $ do+ describe "open, close" do - it "should not throw exception" $ do+ it "should not throw exception" do conn <- SQL.open ":memory:" SQL.close conn tableSpec :: Spec tableSpec =- describe "createTable, insertRow, execute" $ do+ describe "createTable, insertRow, execute" do - it "should create a table" $ do+ it "should create a table" do conn <- SQL.open ":memory:" let columns = ["foo", "bar", "baz", "qux"] let types = repeat SQLChar@@ -33,10 +34,10 @@ ["f0", "f1", "f2", "f3"] ] forM_ entries $ SQL.insertRow conn "test_table" columns types ret <- SQL.execute conn "SELECT * FROM test_table"- ret `shouldBe` Right (fromColumnsAndEntries columns entries)+ fromRight' ret `shouldBe` fromColumnsAndEntries columns entries SQL.close conn - it "should take care of null values in a number column" $ do+ it "should take care of null values in a number column" do conn <- SQL.open ":memory:" let columns = ["foo", "num", "bar", "baz"] let types = cycle [SQLChar, SQLInt]@@ -44,19 +45,19 @@ let entries = [ ["c0", "2", "c2", "3"], ["d0", "", "d2", "2"], ["f0", "3", "f2", ""],- ["e0", "", "e2", "4"] ]+ ["e0", "", "e2", "4.3"] ] forM_ entries $ SQL.insertRow conn "test_table" columns types ret0 <- SQL.execute conn "SELECT * FROM test_table"- ret0 `shouldBe` Right (fromColumnsAndEntries columns entries)+ fromRight' ret0 `shouldBe` fromColumnsAndEntries columns entries ret1 <- SQL.execute conn "SELECT * FROM test_table WHERE num IS NOT NULL"- ret1 `shouldBe` Right (fromColumnsAndEntries columns [e | e <- entries, e !! 1 /= ""])+ fromRight' ret1 `shouldBe` fromColumnsAndEntries columns [e | e <- entries, e !! 1 /= ""] ret2 <- SQL.execute conn "SELECT avg(num) FROM test_table"- ret2 `shouldBe` Right (fromColumnsAndEntries ["avg(num)"] [["2.5"]])+ fromRight' ret2 `shouldBe` fromColumnsAndEntries ["avg(num)"] [["2.5"]] ret3 <- SQL.execute conn "SELECT avg(baz) FROM test_table"- ret3 `shouldBe` Right (fromColumnsAndEntries ["avg(baz)"] [["3.0"]])+ fromRight' ret3 `shouldBe` fromColumnsAndEntries ["avg(baz)"] [["3.1"]] SQL.close conn - it "can create a table when name and column names contain spaces" $ do+ it "can create a table when name and column names contain spaces" do conn <- SQL.open ":memory:" let columns = ["foo bar", "baz qux"] let types = repeat SQLChar@@ -66,7 +67,7 @@ ["f0 f1", "f2 f3"] ] forM_ entries $ SQL.insertRow conn "test table" columns types ret0 <- SQL.execute conn "SELECT * FROM `test table`"- ret0 `shouldBe` Right (fromColumnsAndEntries columns entries)+ fromRight' ret0 `shouldBe` fromColumnsAndEntries columns entries ret1 <- SQL.execute conn "SELECT `foo bar` FROM `test table`"- ret1 `shouldBe` Right (fromColumnsAndEntries ["foo bar"] (map (take 1) entries))+ fromRight' ret1 `shouldBe` fromColumnsAndEntries ["foo bar"] (map (take 1) entries) SQL.close conn
+ test/tests/avg.out view
@@ -0,0 +1,1 @@+2.0
+ test/tests/avg.sh view
@@ -0,0 +1,1 @@+qhs -H "SELECT avg(bar) FROM basic.csv"
+ test/tests/avg_sum.out view
@@ -0,0 +1,2 @@+avg(bar) sum(bar)+2.0 4
+ test/tests/avg_sum.sh view
@@ -0,0 +1,1 @@+qhs -H -O "SELECT avg(bar),sum(bar) FROM basic.csv"
+ test/tests/basic.csv view
@@ -0,0 +1,4 @@+foo,bar,baz+a0,1,a2+b0,3,b2+c0,,c2
+ test/tests/basic.csv.gz view
binary file changed (absent → 64 bytes)
+ test/tests/basic.out view
@@ -0,0 +1,4 @@+foo bar baz+a0 1 a2+b0 3 b2+c0 c2
+ test/tests/basic.sh view
@@ -0,0 +1,1 @@+qhs "SELECT * FROM basic.csv"
+ test/tests/big.csv view
@@ -0,0 +1,13 @@+foo,bar,baz+a0,1,a2+b1,2,b1+c1,,c1+a0,2,a1+b0,1,b2+c1,,c2+a0,3,a3+c1,,c3+c0,,c1+b1,2,b3+c0,,c2+a0,2,a3
+ test/tests/columns.out view
@@ -0,0 +1,4 @@+foo baz+a0 a2+b0 b2+c0 c2
+ test/tests/columns.sh view
@@ -0,0 +1,1 @@+qhs "SELECT c1,c3 FROM basic.csv"
+ test/tests/concat.out view
@@ -0,0 +1,8 @@+foo||baz+a0a2+b1b1+a0a1+b0b2+a0a3+b1b3+a0a3
+ test/tests/concat.sh view
@@ -0,0 +1,1 @@+qhs -H -O "SELECT foo||baz FROM big.csv WHERE bar IS NOT NULL"
+ test/tests/count.out view
@@ -0,0 +1,2 @@+COUNT(*)+2
+ test/tests/count.sh view
@@ -0,0 +1,1 @@+qhs -H -O "SELECT COUNT(*) FROM basic.csv WHERE baz <> 'b2'"
+ test/tests/crlf.csv view
@@ -0,0 +1,4 @@+foo bar baz +a0 1 a2 +b0 3 b2 +c0 c2
+ test/tests/crlf.out view
@@ -0,0 +1,4 @@+foo bar baz+a0 1 a2+b0 3 b2+c0 c2
+ test/tests/crlf.sh view
@@ -0,0 +1,1 @@+qhs -H -O "SELECT * FROM crlf.csv"
+ test/tests/delimiters.out view
@@ -0,0 +1,3 @@+a0 1 a2+b0 3 b2+c0 c2
+ test/tests/delimiters.sh view
@@ -0,0 +1,1 @@+qhs -p -T -H "SELECT * FROM pipe_delimited.csv"
+ test/tests/empty.sql view
+ test/tests/empty_query.out view
@@ -0,0 +1,2 @@+Query cannot be empty.+For basic information, try the `--help' option.
+ test/tests/empty_query.sh view
@@ -0,0 +1,1 @@+qhs ""
+ test/tests/empty_query_file.out view
@@ -0,0 +1,2 @@+Query cannot be empty.+For basic information, try the `--help' option.
+ test/tests/empty_query_file.sh view
@@ -0,0 +1,1 @@+qhs -q empty.sql
+ test/tests/file containing spaces.csv view
@@ -0,0 +1,4 @@+foo,bar,baz+a0,1,a2+b0,3,b2+c0,,c2
+ test/tests/file_spaces.out view
@@ -0,0 +1,3 @@+foo baz+a0 a2+c0 c2
+ test/tests/file_spaces.sh view
@@ -0,0 +1,1 @@+qhs -O -H "SELECT foo,baz FROM 'file containing spaces.csv' WHERE baz <> 'b2'"
+ test/tests/group.out view
@@ -0,0 +1,4 @@+foo cnt+a0 4+c1 3+c0 2
+ test/tests/group.sh view
@@ -0,0 +1,1 @@+qhs -H -O "SELECT foo,COUNT(*) cnt FROM big.csv GROUP BY foo ORDER BY cnt DESC LIMIT 3"
+ test/tests/group_sum.out view
@@ -0,0 +1,4 @@+foo sum(bar) cnt+a0 8 4+b1 4 2+b0 1 1
+ test/tests/group_sum.sh view
@@ -0,0 +1,1 @@+qhs -H -O "SELECT foo,sum(bar),COUNT(*) cnt FROM big.csv WHERE bar IS NOT NULL GROUP BY foo ORDER BY cnt DESC LIMIT 3"
+ test/tests/gzip.out view
@@ -0,0 +1,4 @@+foo bar baz+a0 1 a2+b0 3 b2+c0 c2
+ test/tests/gzip.sh view
@@ -0,0 +1,1 @@+qhs "SELECT * FROM basic.csv.gz"
+ test/tests/gzip_stdin.out view
@@ -0,0 +1,2 @@+a0 a2+b0 b2
+ test/tests/gzip_stdin.sh view
@@ -0,0 +1,1 @@+cat basic.csv.gz | qhs -H -z "SELECT foo,baz FROM - WHERE bar IS NOT NULL"
+ test/tests/header.out view
@@ -0,0 +1,3 @@+a0 a2+b0 b2+c0 c2
+ test/tests/header.sh view
@@ -0,0 +1,1 @@+qhs -H "SELECT foo,baz FROM basic.csv"
+ test/tests/invalid.out view
@@ -0,0 +1,7 @@+<<query>>:1:14:+ |+1 | SELECT * FROM+ | ^+unexpected end of input+expecting table ref+
+ test/tests/invalid.sh view
@@ -0,0 +1,1 @@+qhs "SELECT * FROM"
+ test/tests/is_null.out view
@@ -0,0 +1,1 @@+c0 c2
+ test/tests/is_null.sh view
@@ -0,0 +1,1 @@+qhs -H "SELECT * FROM basic.csv WHERE bar IS NULL"
+ test/tests/join.out view
@@ -0,0 +1,3 @@+foo baz bar+a0 a2 1+b0 b2 1
+ test/tests/join.sh view
@@ -0,0 +1,1 @@+qhs -H -O -T "SELECT basic.foo,big.baz,big.bar FROM basic.csv basic JOIN big.csv big ON basic.baz = big.baz WHERE basic.bar IS NOT NULL"
+ test/tests/multiline.csv view
@@ -0,0 +1,4 @@+foo,bar,baz,qux,quux+a0,1,"a2+b0\",3,""b2+c0",,c2
+ test/tests/multiline.out view
@@ -0,0 +1,2 @@+foo bar quux+a0 1 c2
+ test/tests/multiline.sh view
@@ -0,0 +1,1 @@+qhs -H -O "SELECT foo,bar,quux FROM multiline.csv LIMIT 1"
+ test/tests/not_null.out view
@@ -0,0 +1,2 @@+a0 1 a2+b0 3 b2
+ test/tests/not_null.sh view
@@ -0,0 +1,1 @@+qhs -H "SELECT * FROM basic.csv WHERE bar IS NOT NULL"
+ test/tests/output_delimiter.out view
@@ -0,0 +1,4 @@+foo;bar;baz+a0;1;a2+b0;3;b2+c0;;c2
+ test/tests/output_delimiter.sh view
@@ -0,0 +1,1 @@+qhs -H -O -D ';' "SELECT * FROM basic.csv"
+ test/tests/output_header.out view
@@ -0,0 +1,4 @@+foo baz+a0 a2+b0 b2+c0 c2
+ test/tests/output_header.sh view
@@ -0,0 +1,1 @@+qhs -H -O "SELECT foo,baz FROM basic.csv"
+ test/tests/pipe_delimited.csv view
@@ -0,0 +1,4 @@+foo|bar|baz+a0|1|a2+b0|3|b2+c0||c2
+ test/tests/pipe_delimited.out view
@@ -0,0 +1,3 @@+a0|1|a2+b0|3|b2+c0||c2
+ test/tests/pipe_delimited.sh view
@@ -0,0 +1,1 @@+qhs -p -H "SELECT * FROM pipe_delimited.csv"
+ test/tests/pipe_delimited_output.out view
@@ -0,0 +1,4 @@+foo|bar|baz+a0|1|a2+b0|3|b2+c0||c2
+ test/tests/pipe_delimited_output.sh view
@@ -0,0 +1,1 @@+qhs -H -O -P "SELECT * FROM basic.csv"
+ test/tests/query.sql view
@@ -0,0 +1,1 @@+SELECT foo,baz FROM - WHERE bar IS NOT NULL
+ test/tests/query_file.out view
@@ -0,0 +1,3 @@+foo baz+a0 a2+b0 b2
+ test/tests/query_file.sh view
@@ -0,0 +1,1 @@+cat basic.csv | qhs -H -O -q query.sql
+ test/tests/quote.out view
@@ -0,0 +1,3 @@+a0 sep "double quoted" bar a2+b0 sep "double quoted" bar b2+c0 sep "double quoted" bar c2
+ test/tests/quote.sh view
@@ -0,0 +1,1 @@+qhs -H "SELECT foo||' sep \"double quoted\" bar',baz FROM basic.csv"
+ test/tests/seq.out view
@@ -0,0 +1,1 @@+500.5 500500
+ test/tests/seq.sh view
@@ -0,0 +1,1 @@+seq 1 1000 | qhs "SELECT avg(c1),sum(c1) FROM -"
+ test/tests/spaces.csv view
@@ -0,0 +1,4 @@+foo bar baz+a0 1 a2+b0 3 b2+c0 c2
+ test/tests/spaces.out view
@@ -0,0 +1,4 @@+foo bar baz+a0 1 a2+b0 3 b2+c0 c2
+ test/tests/spaces.sh view
@@ -0,0 +1,1 @@+qhs -H -O "SELECT * FROM spaces.csv"
+ test/tests/stdin.out view
@@ -0,0 +1,2 @@+a0 a2+b0 b2
+ test/tests/stdin.sh view
@@ -0,0 +1,1 @@+cat basic.csv | qhs -H "SELECT foo,baz FROM - WHERE bar IS NOT NULL"
+ test/tests/sum.out view
@@ -0,0 +1,1 @@+4
+ test/tests/sum.sh view
@@ -0,0 +1,1 @@+qhs -H "SELECT sum(bar) FROM basic.csv"
+ test/tests/tab.csv view
@@ -0,0 +1,4 @@+foo bar baz+a0 1 a2+b0 3 b2+c0 c2
+ test/tests/tab.out view
@@ -0,0 +1,3 @@+a0 1 a2+b0 3 b2+c0 c2
+ test/tests/tab.sh view
@@ -0,0 +1,1 @@+qhs -t -H "SELECT * FROM tab.csv"
+ test/tests/tab2.out view
@@ -0,0 +1,3 @@+a0 1 a2+b0 3 b2+c0 c2
+ test/tests/tab2.sh view
@@ -0,0 +1,1 @@+qhs -d $'\t' -D $'\t' -H "SELECT * FROM tab.csv"
+ test/tests/tab_delimited_output.out view
@@ -0,0 +1,4 @@+foo bar baz+a0 1 a2+b0 3 b2+c0 c2
+ test/tests/tab_delimited_output.sh view
@@ -0,0 +1,1 @@+qhs -H -O -T "SELECT * FROM basic.csv"
+ test/tests/where.out view
@@ -0,0 +1,2 @@+a0 a2+c0 c2
+ test/tests/where.sh view
@@ -0,0 +1,1 @@+qhs -H "SELECT foo,baz FROM basic.csv WHERE baz <> 'b2'"