diff --git a/Database/TxtSushi/CommandLineArgument.hs b/Database/TxtSushi/CommandLineArgument.hs
--- a/Database/TxtSushi/CommandLineArgument.hs
+++ b/Database/TxtSushi/CommandLineArgument.hs
@@ -27,7 +27,6 @@
     argumentCountIsFixed) where
 
 import Data.List
-import Data.Map (Map)
 import qualified Data.Map as Map
 
 data CommandLineDescription = CommandLineDescription {
diff --git a/Database/TxtSushi/EvaluatedExpression.hs b/Database/TxtSushi/EvaluatedExpression.hs
--- a/Database/TxtSushi/EvaluatedExpression.hs
+++ b/Database/TxtSushi/EvaluatedExpression.hs
@@ -27,7 +27,6 @@
     coerceBool) where
 
 import Data.Char
-import Data.List
 import Data.Ord
 
 import Database.TxtSushi.ParseUtil
diff --git a/Database/TxtSushi/ExternalSort.hs b/Database/TxtSushi/ExternalSort.hs
--- a/Database/TxtSushi/ExternalSort.hs
+++ b/Database/TxtSushi/ExternalSort.hs
@@ -17,7 +17,6 @@
     defaultByteQuota,
     defaultMaxOpenFiles) where
 
-import Control.Monad
 import Data.Binary
 import Data.Binary.Get
 import Data.Int
diff --git a/Database/TxtSushi/FlatFile.hs b/Database/TxtSushi/FlatFile.hs
--- a/Database/TxtSushi/FlatFile.hs
+++ b/Database/TxtSushi/FlatFile.hs
@@ -11,10 +11,6 @@
 --
 -----------------------------------------------------------------------------
 
-{- |
-The 'FlatFile' module is for reading misc. 'FlatFile' formats like CSV or
-tab delimited
--}
 module Database.TxtSushi.FlatFile (
     formatTableWithWidths,
     maxTableColumnWidths,
@@ -25,7 +21,6 @@
     tabDelimitedFormat,
     doubleQuote) where
 
-import Data.Function
 import Data.List
 import Data.Ord
 
diff --git a/Database/TxtSushi/IOUtil.hs b/Database/TxtSushi/IOUtil.hs
--- a/Database/TxtSushi/IOUtil.hs
+++ b/Database/TxtSushi/IOUtil.hs
@@ -13,15 +13,15 @@
 module Database.TxtSushi.IOUtil (
     bufferStdioToTempFile,
     getContentsFromFileOrStdin,
-    printSingleFileUsage) where
+    printSingleFileUsage,
+    versionStr) where
 
-import Data.List
-import Data.Version (Version(..))
 import System.Directory
 import System.Environment
 import System.IO
 
-import Paths_txt_sushi
+versionStr :: String
+versionStr = "0.6.0"
 
 -- | buffers standard input to a temp file and returns a path to that file
 bufferStdioToTempFile :: IO FilePath
@@ -48,6 +48,4 @@
     progName <- getProgName
     putStrLn $ progName ++ " (" ++ versionStr ++ ")"
     putStrLn $ "Usage: " ++ progName ++ " file_name_or_dash"
-    
-    where
-        versionStr = intercalate "." (map show . versionBranch $ version)
+
diff --git a/Database/TxtSushi/ParseUtil.hs b/Database/TxtSushi/ParseUtil.hs
--- a/Database/TxtSushi/ParseUtil.hs
+++ b/Database/TxtSushi/ParseUtil.hs
@@ -40,7 +40,7 @@
         anyParseTxt = signedParseTxt <|> unsignedParseTxt <?> "integer"
         unsignedParseTxt = many1 digit
         signedParseTxt = do
-            char '-'
+            _ <- char '-'
             unsignedDigitTxt <- unsignedParseTxt
             return $ '-' : unsignedDigitTxt
 
@@ -75,11 +75,11 @@
         txtWithoutExponent = signedTxt <|> unsignedTxt <?> "real"
         unsignedTxt = do
             intTxt <- many1 digit
-            char '.'
+            _ <- char '.'
             fracTxt <- many1 digit
             return $ intTxt ++ "." ++ fracTxt
         signedTxt = do
-            char '-'
+            _ <- char '-'
             unsignedDigitTxt <- unsignedTxt
             return ('-':unsignedDigitTxt)
 
@@ -101,10 +101,10 @@
     let quote = char quoteChar
         manyFunc = if allowEmpty then many else many1
     
-    quote
+    _ <- quote
     textValue <- manyFunc $ (anyChar `genExcept` quote) <|>
                             try (escapedQuote quoteChar)
-    quote
+    _ <- quote
     spaces
     
     return textValue
@@ -178,7 +178,7 @@
         -- for lists greater than 1 we do need to care about the separator
         parseEach (headParser:parserTail) = do
             resultHead <- headParser
-            sepParser
+            _ <- sepParser
             resultTail <- parseEach parserTail
             
             return $ resultHead:resultTail
diff --git a/Database/TxtSushi/Relational.hs b/Database/TxtSushi/Relational.hs
--- a/Database/TxtSushi/Relational.hs
+++ b/Database/TxtSushi/Relational.hs
@@ -11,9 +11,6 @@
 --
 -----------------------------------------------------------------------------
 
-{- |
-Simple table transformations
--}
 module Database.TxtSushi.Relational (
     joinTables,
     crossJoinTables,
@@ -34,21 +31,23 @@
         sortedTable1 = sortBy (compare `on` joinOrdFunc1) table1
         sortedTable2 = sortBy (compare `on` joinOrdFunc2) table2
     in
-        joinPresortedTables joinOrdFunc1 sortedTable1 joinOrdFunc2 sortedTable2
+        joinPresortedTables joinOrdFunc1 sortedTable1 Nothing joinOrdFunc2 sortedTable2 Nothing
 
 -- | join together two tables that are presorted on the given column index pairs
 joinPresortedTables :: (Ord o) =>
     (a -> o)
     -> [a]
+    -> Maybe a
     -> (b -> o)
     -> [b]
+    -> Maybe b
     -> [(a, b)]
-joinPresortedTables joinOrdFunc1 sortedTable1 joinOrdFunc2 sortedTable2 =
+joinPresortedTables joinOrdFunc1 sortedTable1 maybeNull1 joinOrdFunc2 sortedTable2 maybeNull2 =
     let
         tableGroups1 = groupBy rowEq1 sortedTable1
         tableGroups2 = groupBy rowEq2 sortedTable2
     in
-        joinGroupedTables joinOrdFunc1 tableGroups1 joinOrdFunc2 tableGroups2
+        joinGroupedTables joinOrdFunc1 tableGroups1 maybeNull1 joinOrdFunc2 tableGroups2 maybeNull2
     where
         rowEq1 x y = (compare `on` joinOrdFunc1) x y == EQ
         rowEq2 x y = (compare `on` joinOrdFunc2) x y == EQ
@@ -62,28 +61,54 @@
 joinGroupedTables :: (Ord o) =>
     (a -> o)
     -> [[a]]
+    -> Maybe a
     -> (b -> o)
     -> [[b]]
+    -> Maybe b
     -> [(a, b)]
-joinGroupedTables _ [] _ _  = []
-joinGroupedTables _ _  _ [] = []
+joinGroupedTables _ [] Nothing      _ _  _       = []
+joinGroupedTables _ _  _            _ [] Nothing = []
+
+joinGroupedTables _ [] (Just null1) _ tableGroups2 _ = zip (repeat null1) (concat tableGroups2)
+joinGroupedTables _ tableGroups1 _ _ [] (Just null2) = zip (concat tableGroups1) (repeat null2)
+
 joinGroupedTables
     joinOrdFunc1
     tableGroups1@(headTableGroup1:tableGroupsTail1)
+    maybeNull1
+    
     joinOrdFunc2
-    tableGroups2@(headTableGroup2:tableGroupsTail2) =
+    tableGroups2@(headTableGroup2:tableGroupsTail2)
+    maybeNull2 =
     let
         headRow1 = head headTableGroup1
         headRow2 = head headTableGroup2
     in
         case joinOrdFunc1 headRow1 `compare` joinOrdFunc2 headRow2 of
             -- drop the 1st group if its smaller
-            LT -> joinGroupedTables joinOrdFunc1 tableGroupsTail1 joinOrdFunc2 tableGroups2
+            LT ->
+                let joinRemainder =
+                        joinGroupedTables
+                            joinOrdFunc1 tableGroupsTail1 maybeNull1
+                            joinOrdFunc2 tableGroups2 maybeNull2 in
+                case maybeNull2 of
+                    Nothing    -> joinRemainder
+                    Just null2 -> zip headTableGroup1 (repeat null2) ++ joinRemainder
             
             -- drop the 2nd group if its smaller
-            GT -> joinGroupedTables joinOrdFunc1 tableGroups1 joinOrdFunc2 tableGroupsTail2
+            GT ->
+                let joinRemainder =
+                        joinGroupedTables
+                            joinOrdFunc1 tableGroups1 maybeNull1
+                            joinOrdFunc2 tableGroupsTail2 maybeNull2 in
+                case maybeNull1 of
+                    Nothing    -> joinRemainder
+                    Just null1 -> zip (repeat null1) headTableGroup2 ++ joinRemainder
             
             -- the two groups are equal so permute
             _  ->
-                (crossJoinTables headTableGroup1 headTableGroup2) ++
-                (joinGroupedTables joinOrdFunc1 tableGroupsTail1 joinOrdFunc2 tableGroupsTail2)
+                let joinRemainder =
+                        joinGroupedTables
+                            joinOrdFunc1 tableGroupsTail1 maybeNull1
+                            joinOrdFunc2 tableGroupsTail2 maybeNull2 in
+                crossJoinTables headTableGroup1 headTableGroup2 ++ joinRemainder
diff --git a/Database/TxtSushi/SQLExecution.hs b/Database/TxtSushi/SQLExecution.hs
--- a/Database/TxtSushi/SQLExecution.hs
+++ b/Database/TxtSushi/SQLExecution.hs
@@ -20,13 +20,11 @@
 
 import Control.Applicative
 import Data.Binary
-import Data.Char
 import Data.Function
 import Data.List
 import qualified Data.Map as M
 
 import Database.TxtSushi.SQLExpression
-import Database.TxtSushi.SQLFunctionDefinitions
 import Database.TxtSushi.EvaluatedExpression
 import Database.TxtSushi.ExternalSort
 import Database.TxtSushi.Relational
@@ -50,7 +48,8 @@
         qualifiedColumnsWithContext = M.empty,
         evaluationContext = evalCtxt,
         tableData = tblRows,
-        isInScope = idInHeader}
+        isInScope = idInHeader,
+        nullRow = replicate (length headerNames) ""}
     where
         makeColExpr colName = ColumnExpression (ColumnIdentifier Nothing colName) colName
         
@@ -86,7 +85,8 @@
         qualifiedColumnsWithContext = M.empty,
         evaluationContext = eval,
         tableData = [shouldNeverOccurError] :: [String],
-        isInScope = const False}
+        isInScope = const False,
+        nullRow = []}
     where
         eval (ColumnExpression _ colStr)    = columnNotInScopeError colStr
         eval expr                           = evalWithContext eval expr
@@ -157,16 +157,24 @@
             let table = M.findWithDefault (tableNotInScopeError tblName) tblName tableMap
             in  maybeRename maybeTblAlias table
         
-        -- TODO inner join should allow joining on expressions too!!
         InnerJoin leftJoinTblExpr rightJoinTblExpr onConditionExpr maybeTblAlias ->
             let
                 leftJoinTbl = evalTableExpression sortCfg leftJoinTblExpr tableMap
                 rightJoinTbl = evalTableExpression sortCfg rightJoinTblExpr tableMap
                 joinExprs = extractJoinExprs leftJoinTbl rightJoinTbl onConditionExpr
-                joinedTbl = innerJoinDbTables sortCfg joinExprs leftJoinTbl rightJoinTbl
+                joinedTbl = sortAndJoinDbTables False sortCfg joinExprs leftJoinTbl rightJoinTbl
             in
                 maybeRename maybeTblAlias joinedTbl
         
+        OuterJoin leftJoinTblExpr rightJoinTblExpr onConditionExpr maybeTblAlias ->
+            let
+                leftJoinTbl = evalTableExpression sortCfg leftJoinTblExpr tableMap
+                rightJoinTbl = evalTableExpression sortCfg rightJoinTblExpr tableMap
+                joinExprs = extractJoinExprs leftJoinTbl rightJoinTbl onConditionExpr
+                joinedTbl = sortAndJoinDbTables True sortCfg joinExprs leftJoinTbl rightJoinTbl
+            in
+                maybeRename maybeTblAlias joinedTbl
+        
         SelectExpression selectStmt maybeTblAlias ->
             maybeRename maybeTblAlias (select sortCfg selectStmt tableMap)
         
@@ -330,7 +338,10 @@
     tableData :: [a],
     
     -- | is the given identifier in scope for this table
-    isInScope :: ColumnIdentifier -> Bool}
+    isInScope :: ColumnIdentifier -> Bool,
+    
+    -- | we can only do an outer join if the concept of a NULL row exists
+    nullRow :: a}
 
 allIdentifiers :: Expression -> [ColumnIdentifier]
 allIdentifiers (FunctionExpression _ args _) = concatMap allIdentifiers args
@@ -433,7 +444,8 @@
         columnsWithContext = mapSnd toGroupContext (columnsWithContext tbl),
         qualifiedColumnsWithContext = M.map (mapSnd toGroupContext) (qualifiedColumnsWithContext tbl),
         evaluationContext = toGroupContext $ evaluationContext tbl,
-        tableData = groupedData}
+        tableData = groupedData,
+        nullRow = [nullRow tbl]}
     where
         eval = evaluationContext tbl
         rowOrd row = [eval expr row | expr <- grpExprs]
@@ -448,7 +460,8 @@
         columnsWithContext = mapSnd toGroupContext (columnsWithContext tbl),
         qualifiedColumnsWithContext = M.map (mapSnd toGroupContext) (qualifiedColumnsWithContext tbl),
         evaluationContext = toGroupContext $ evaluationContext tbl,
-        tableData = [tableData tbl]}
+        tableData = [tableData tbl],
+        nullRow = [nullRow tbl]}
 
 compareWithDirection :: (Ord a) => [Bool] -> [a] -> [a] -> Ordering
 compareWithDirection (asc:ascTail) (x:xt) (y:yt) = case x `compare` y of
@@ -458,13 +471,14 @@
 compareWithDirection [] [] [] = EQ
 compareWithDirection _ _ _ = error "Internal Error: List sizes should match"
 
-innerJoinDbTables ::
-    SortConfiguration
+sortAndJoinDbTables ::
+    Bool
+    -> SortConfiguration
     -> [(Expression, Expression)]
     -> BoxedTable
     -> BoxedTable
     -> BoxedTable
-innerJoinDbTables sortCfg joinExprs (BoxedTable fstTable) (BoxedTable sndTable) =
+sortAndJoinDbTables outerJoin sortCfg joinExprs (BoxedTable fstTable) (BoxedTable sndTable) =
     BoxedTable $ zipDbTables joinedData fstTable sndTable
     where
         fstEval = evaluationContext fstTable
@@ -476,7 +490,10 @@
         sortedFstData = sortByCfg sortCfg (compare `on` fstRowOrd) (tableData fstTable)
         sortedSndData = sortByCfg sortCfg (compare `on` sndRowOrd) (tableData sndTable)
         
-        joinedData = joinPresortedTables fstRowOrd sortedFstData sndRowOrd sortedSndData
+        fstNull = if outerJoin then Just (nullRow fstTable) else Nothing
+        sndNull = if outerJoin then Just (nullRow sndTable) else Nothing
+        
+        joinedData = joinPresortedTables fstRowOrd sortedFstData fstNull sndRowOrd sortedSndData sndNull
 
 crossJoinDbTables ::
     BoxedTable
@@ -493,7 +510,8 @@
     qualifiedColumnsWithContext = M.unionWithKey ambiguousTableError fstQualCols sndQualCols,
     evaluationContext = evalCtxt,
     tableData = zippedData,
-    isInScope = isInFstOrSndScope}
+    isInScope = isInFstOrSndScope,
+    nullRow = (nullRow fstTable, nullRow sndTable)}
     
     where
         isInFstScope = isInScope fstTable
diff --git a/Database/TxtSushi/SQLExpression.hs b/Database/TxtSushi/SQLExpression.hs
--- a/Database/TxtSushi/SQLExpression.hs
+++ b/Database/TxtSushi/SQLExpression.hs
@@ -26,9 +26,6 @@
     expressionToString,
     columnToString) where
 
-import Data.Char
-import Data.List
-
 import Database.TxtSushi.EvaluatedExpression
 
 --------------------------------------------------------------------------------
@@ -53,6 +50,11 @@
         rightJoinTable :: TableExpression,
         onCondition :: Expression,
         maybeTableAlias :: Maybe String} |
+    OuterJoin {
+        leftJoinTable :: TableExpression,
+        rightJoinTable :: TableExpression,
+        onCondition :: Expression,
+        maybeTableAlias :: Maybe String} |
     CrossJoin {
         leftJoinTable :: TableExpression,
         rightJoinTable :: TableExpression,
@@ -70,6 +72,8 @@
 allTableNames :: TableExpression -> [String]
 allTableNames (TableIdentifier tblName _) = [tblName]
 allTableNames (InnerJoin lftTbl rtTbl _ _) =
+    (allTableNames lftTbl) ++ (allTableNames rtTbl)
+allTableNames (OuterJoin lftTbl rtTbl _ _) =
     (allTableNames lftTbl) ++ (allTableNames rtTbl)
 allTableNames (CrossJoin lftTbl rtTbl _) =
     (allTableNames lftTbl) ++ (allTableNames rtTbl)
diff --git a/Database/TxtSushi/SQLFunctionDefinitions.hs b/Database/TxtSushi/SQLFunctionDefinitions.hs
--- a/Database/TxtSushi/SQLFunctionDefinitions.hs
+++ b/Database/TxtSushi/SQLFunctionDefinitions.hs
@@ -36,7 +36,7 @@
 -- Functions with "normal" syntax --
 normalSyntaxFunctions :: [SQLFunction]
 normalSyntaxFunctions =
-    [absFunction, upperFunction, lowerFunction, trimFunction, ifThenElseFunction,
+    [absFunction, upperFunction, lowerFunction, trimFunction, lengthFunction, ifThenElseFunction,
      asIntFunction, asRealFunction, isNumericFunction,
      -- all aggregates except count which accepts a (*)
      avgFunction, firstFunction, lastFunction, maxFunction,
@@ -110,6 +110,15 @@
     applyFunction       = applyUnaryString trimFunction trimSpace,
     functionGrammar     = normalGrammar trimFunction,
     functionDescription = "trims whitespace from the beginning and end of the given text"}
+
+lengthFunction :: SQLFunction
+lengthFunction = SQLFunction {
+    functionName        = "LEN",
+    minArgCount         = 1,
+    argCountIsFixed     = True,
+    applyFunction       = IntExpression . length . coerceString . head . checkArgCount lengthFunction,
+    functionGrammar     = normalGrammar lengthFunction,
+    functionDescription = "the number of characters in the given text"}
 
 ifThenElseFunction :: SQLFunction
 ifThenElseFunction = SQLFunction {
diff --git a/Database/TxtSushi/SQLParser.hs b/Database/TxtSushi/SQLParser.hs
--- a/Database/TxtSushi/SQLParser.hs
+++ b/Database/TxtSushi/SQLParser.hs
@@ -129,11 +129,11 @@
 parseRangeColumns = parseRangeInner
     where
         parseRangeInner = do
-            parseToken "FOR"
+            _ <- parseToken "FOR"
             bindingId <- parseColumnId
-            parseToken "IN"
+            _ <- parseToken "IN"
             colRange <- parseColRange
-            parseToken "YIELD"
+            _ <- parseToken "YIELD"
             expr <- parseExpression
             
             return $ ExpressionColumnRange bindingId colRange expr
@@ -141,7 +141,7 @@
             where
                 parseColRange = brace $ do
                     maybeStartCol <- maybeParse parseColumnId
-                    parseToken ".."
+                    _ <- parseToken ".."
                     maybeEndCol <- maybeParse parseColumnId
                     
                     return $ ColumnRange maybeStartCol maybeEndCol
@@ -152,7 +152,7 @@
 parseAllColsFromTbl :: GenParser Char st ColumnSelection
 parseAllColsFromTbl = do
     tableVal <- parseIdentifier
-    string "." >> spaces >> parseToken "*"
+    _ <- string "." >> spaces >> parseToken "*"
     
     return $ AllColumnsFrom tableVal
 
@@ -197,6 +197,14 @@
     nextTblId <- parseTableIdentifier
     
     let
+        ifJoinParse = ifParseThenElse
+            -- if
+            outerJoinSep -- TODO commit to join
+            -- then
+            (parseOuterJoinRemainder nextTblId)
+            --else
+            ifCrossOrInnerJoinParse
+        
         ifCrossOrInnerJoinParse = ifParseThenElse
             -- if
             crossJoinSep -- TODO commit to join
@@ -213,9 +221,10 @@
             -- else
             (return nextTblId)
         
-    ifCrossOrInnerJoinParse
+    ifJoinParse
     
     where
+        outerJoinSep = parseToken "OUTER" >> parseToken "JOIN"
         crossJoinSep = (commaSeparator >> return "") <|> (parseToken "CROSS" >> parseToken "JOIN")
         innerJoinSep = ((maybeParse $ parseToken "INNER") >> parseToken "JOIN")
 
@@ -223,7 +232,7 @@
 parseInnerJoinRemainder leftTblExpr = do
     rightTblExpr <- parseTableExpression
     
-    parseToken "ON"
+    _ <- parseToken "ON"
     onPart <- parseExpression
     
     maybeAlias <- maybeParseAlias
@@ -234,6 +243,21 @@
             onCondition=onPart,
             maybeTableAlias=maybeAlias}
 
+parseOuterJoinRemainder :: TableExpression -> GenParser Char a TableExpression
+parseOuterJoinRemainder leftTblExpr = do
+    rightTblExpr <- parseTableExpression
+    
+    _ <- parseToken "ON"
+    onPart <- parseExpression
+    
+    maybeAlias <- maybeParseAlias
+    
+    return OuterJoin {
+            leftJoinTable=leftTblExpr,
+            rightJoinTable=rightTblExpr,
+            onCondition=onPart,
+            maybeTableAlias=maybeAlias}
+
 parseCrossJoinRemainder :: TableExpression -> GenParser Char a TableExpression
 parseCrossJoinRemainder leftTblExpr = do
     rightTblExpr <- parseTableExpression
@@ -335,12 +359,12 @@
 parseSubstringFunction :: GenParser Char st Expression
 parseSubstringFunction = do
     funcStr <- parseToken $ functionName substringFromFunction
-    eatSpacesAfter $ char '('
+    _ <- eatSpacesAfter $ char '('
     strExpr <- parseExpression
     fromStr <- parseToken "FROM"
     startExpr <- parseExpression
     maybeForStrAndLength <- preservingIfParseThen (parseToken "FOR") parseExpression
-    eatSpacesAfter $ char ')'
+    _ <- eatSpacesAfter $ char ')'
     
     let funcStrStart =
             funcStr ++ "(" ++ expressionToString strExpr ++ " " ++
@@ -373,7 +397,7 @@
 parseCountStar :: GenParser Char st Expression
 parseCountStar = do
     funcStr <- try (parseToken $ functionName countFunction)
-    parenthesize (parseToken "*")
+    _ <- parenthesize (parseToken "*")
     
     return $ FunctionExpression countFunction [IntConstantExpression 0 "*"] (funcStr ++ "(*)")
 
@@ -417,17 +441,17 @@
 -- | Wraps braces parsers around the given inner parser
 brace :: GenParser Char st a -> GenParser Char st a
 brace innerParser = do
-    eatSpacesAfter $ char '['
+    _ <- eatSpacesAfter $ char '['
     innerParseResults <- innerParser
-    eatSpacesAfter $ char ']'
+    _ <- eatSpacesAfter $ char ']'
     return innerParseResults
 
 -- | Wraps parentheses parsers around the given inner parser
 parenthesize :: GenParser Char st a -> GenParser Char st a
 parenthesize innerParser = do
-    eatSpacesAfter $ char '('
+    _ <- eatSpacesAfter $ char '('
     innerParseResults <- innerParser
-    eatSpacesAfter $ char ')'
+    _ <- eatSpacesAfter $ char ')'
     return innerParseResults
 
 parseReservedWord :: GenParser Char st String
@@ -441,8 +465,8 @@
     map functionName normalSyntaxFunctions ++
     map functionName (concat infixFunctions) ++
     map functionName specialFunctions ++
-    ["BY","CROSS", "FROM", "FOR", "GROUP", "HAVING", "IN", "INNER", "JOIN", "ON",
-     "ORDER", "SELECT", "WHERE", "TRUE", "FALSE", "YIELD"]
+    ["BY", "CROSS", "FROM", "FOR", "GROUP", "HAVING", "IN", "INNER", "JOIN", "ON",
+     "ORDER", "OUTER", "SELECT", "WHERE", "TRUE", "FALSE", "YIELD"]
 
 -- | tries parsing both the upper and lower case versions of the given string
 upperOrLower :: String -> GenParser Char st String
diff --git a/Setup.hs b/Setup.hs
--- a/Setup.hs
+++ b/Setup.hs
@@ -1,2 +1,7 @@
+module Main (main) where
+
 import Distribution.Simple
+
+main :: IO ()
 main = defaultMain
+
diff --git a/csvtotab.hs b/csvtotab.hs
--- a/csvtotab.hs
+++ b/csvtotab.hs
@@ -11,7 +11,6 @@
 --
 -----------------------------------------------------------------------------
 import System.Environment
-import System.IO
 
 import Database.TxtSushi.FlatFile
 import Database.TxtSushi.IOUtil
diff --git a/csvzip.hs b/csvzip.hs
new file mode 100644
--- /dev/null
+++ b/csvzip.hs
@@ -0,0 +1,54 @@
+-----------------------------------------------------------------------------
+-- |
+-- Module      :  csvzip
+-- Copyright   :  (c) Keith Sheppard 2009-2010
+-- License     :  BSD3
+-- Maintainer  :  keithshep@gmail.com
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Joins CSV files by pasting the columns together. Analogous to cbind for
+-- those familliar with the R programming language. This utility streams
+-- data so it can work on very large files. If the table row lengths don't
+-- match then the shorter tables will be padded with empty cells.
+--
+-----------------------------------------------------------------------------
+import System.Environment (getArgs, getProgName)
+
+import Database.TxtSushi.FlatFile (csvFormat, formatTable, parseTable)
+import Database.TxtSushi.IOUtil (getContentsFromFileOrStdin, versionStr)
+
+main :: IO ()
+main = do
+    fileNames <- getArgs
+    
+    case fileNames of
+        -- parse all CSV files giving us a list of tables, then zip and print them
+        (_ : _ : _) -> do
+            tables <- mapM getAndParseTable fileNames
+            putStr $ formatTable csvFormat (zipAllColumns tables)
+        
+        _ -> printUsage
+
+-- | read the contents of the given files name and parse it as a CSV file
+getAndParseTable :: String -> IO [[String]]
+getAndParseTable = fmap (parseTable csvFormat) . getContentsFromFileOrStdin
+
+-- | zips together the columns of a non-empty list of tables
+zipAllColumns :: [[[String]]] -> [[String]]
+zipAllColumns = foldl1 (zipCols [] [])
+    where
+        -- if row counts don't match we pad the table that fell short with empty cells
+        zipCols _     _     (x:xt) (y:yt) = (x ++ y) : zipCols x y xt yt
+        zipCols _     _     []     []     = []
+        zipCols _     prevY xs     []     = zipWith (++) xs (padCols prevY)
+        zipCols prevX _     []     ys     = zipWith (++) (padCols prevX) ys
+        
+        padCols lastRow = repeat (replicate (length lastRow) "")
+
+printUsage :: IO ()
+printUsage = do
+    progName <- getProgName
+    putStrLn $ progName ++ " (" ++ versionStr ++ ")"
+    putStrLn $ "Usage: " ++ progName ++ " csvfile_or_dash csvfile_or_dash ..."
+
diff --git a/namecolumns.hs b/namecolumns.hs
--- a/namecolumns.hs
+++ b/namecolumns.hs
@@ -11,7 +11,6 @@
 --
 -----------------------------------------------------------------------------
 import System.Environment
-import System.IO
 
 import Database.TxtSushi.FlatFile
 import Database.TxtSushi.IOUtil
diff --git a/tabtocsv.hs b/tabtocsv.hs
--- a/tabtocsv.hs
+++ b/tabtocsv.hs
@@ -11,7 +11,6 @@
 --
 -----------------------------------------------------------------------------
 import System.Environment
-import System.IO
 
 import Database.TxtSushi.FlatFile
 import Database.TxtSushi.IOUtil
diff --git a/transposecsv.hs b/transposecsv.hs
--- a/transposecsv.hs
+++ b/transposecsv.hs
@@ -12,7 +12,6 @@
 -----------------------------------------------------------------------------
 import Data.List
 import System.Environment
-import System.IO
 
 import Database.TxtSushi.FlatFile
 import Database.TxtSushi.IOUtil
diff --git a/transposetab.hs b/transposetab.hs
--- a/transposetab.hs
+++ b/transposetab.hs
@@ -12,7 +12,6 @@
 -----------------------------------------------------------------------------
 import Data.List
 import System.Environment
-import System.IO
 
 import Database.TxtSushi.FlatFile
 import Database.TxtSushi.IOUtil
diff --git a/tssql.hs b/tssql.hs
--- a/tssql.hs
+++ b/tssql.hs
@@ -11,11 +11,9 @@
 -----------------------------------------------------------------------------
 import Data.Char
 import Data.List
-import Data.Version (Version(..))
 import qualified Data.Map as M
 import System.Environment
 import System.Exit
-import System.IO
 
 import Text.ParserCombinators.Parsec
 
@@ -27,8 +25,6 @@
 import Database.TxtSushi.SQLFunctionDefinitions
 import Database.TxtSushi.SQLParser
 
-import Paths_txt_sushi
-
 helpOption :: OptionDescription
 helpOption = OptionDescription {
     isRequired              = False,
@@ -93,8 +89,6 @@
 printUsage progName = do
     putStrLn $ progName ++ " (" ++ versionStr ++ ")"
     putStrLn $ "Usage: " ++ progName ++ " " ++ formatCommandLine sqlCmdLine
-    where
-        versionStr = intercalate "." (map show $ versionBranch version)
 
 argsToSortConfig :: M.Map OptionDescription a -> SortConfiguration
 argsToSortConfig argMap =
diff --git a/txt-sushi.cabal b/txt-sushi.cabal
--- a/txt-sushi.cabal
+++ b/txt-sushi.cabal
@@ -1,8 +1,8 @@
 Name:                txt-sushi
-Version:             0.5.1
+Version:             0.6.0
 Synopsis:            The SQL link in your *NIX chain
 Description:
-    TxtSushi is a collection of command line utilities for processing
+    txt-sushi is a collection of command line utilities for processing
     comma-separated and tab-delimited files (AKA flat files, spreadsheets).
     The most important utility (tssql) lets you perform SQL selects on CSV files.
     By focusing exclusively on processing text files with a tabular structure,
@@ -14,19 +14,19 @@
 Author:              Keith Sheppard
 Maintainer:          keithshep@gmail.com
 Homepage:            http://keithsheppard.name/txt-sushi
-Bug-Reports:         http://code.google.com/p/txt-sushi/issues/list
+Bug-Reports:         https://github.com/keithshep/txt-sushi/issues
 Build-Type:          Simple
 Category:            Database, Console
 Cabal-Version:       >= 1.6
 
 Source-Repository head
-  type:     darcs
-  location: http://patch-tag.com/r/keithshep/txt-sushi/pullrepo
+  type:     git
+  location: git://github.com/keithshep/txt-sushi.git
 
 Source-Repository this
-  type:     darcs
-  location: http://patch-tag.com/r/keithshep/txt-sushi/pullrepo
-  tag:      0.5.1
+  type:     git
+  location: git://github.com/keithshep/txt-sushi.git
+  tag:      0.6.0
 
 Executable tssql
   Main-Is:          tssql.hs
@@ -61,6 +61,10 @@
 
 Executable transposetab
   Main-Is:          transposetab.hs
+  GHC-Options:      -O2 -Wall
+
+Executable csvzip
+  Main-Is:          csvzip.hs
   GHC-Options:      -O2 -Wall
 
 Library
