packages feed

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 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+[![CI Status](https://github.com/itchyny/qhs/actions/workflows/ci.yaml/badge.svg?branch=main)](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'"