10

私の仕事では、多くの厄介なSQLに出くわし、SQLを解析してきれいに出力するプログラムを作成するという素晴らしいアイデアを思いつきました。私はそれをかなり早く作りましたが、解決方法がわからない問題に遭遇しました。

それでは、SQLが「1のバーからfooを選択」であるとしましょう。私の考えでは、キーワードの後に​​データが続くので、キーワードを解析し、次のキーワードの前にすべてのジブリッシュをキャプチャして、価値がある場合は後でクリーンアップするために保存するだけです。コードは次のとおりです。

import Text.Parsec
import Text.Parsec.Combinator
import Text.Parsec.Char
import Data.Text (strip)

newtype Statement = Statement [Atom]
data Atom = Branch String [Atom] | Leaf String deriving Show

trim str = reverse $ trim' (reverse $ trim' str)
  where
    trim' (' ':xs) = trim' xs
    trim' str = str

printStatement atoms = mapM_ printAtom atoms
printAtom atom = loop 0 atom 
  where
    loop depth (Leaf str) = putStrLn $ (replicate depth ' ') ++ str
    loop depth (Branch str atoms) = do 
      putStrLn $ (replicate depth ' ') ++ str
      mapM_ (loop (depth + 2)) atoms

keywords :: [String]
keywords = [
  "select",
  "update",
  "delete",
  "from",
  "where"]

keywordparser :: Parsec String u String
keywordparser = try ((choice $ map string keywords) <?> "keywordparser")

stuffparser :: Parsec String u String
stuffparser = manyTill anyChar (eof <|> (lookAhead keywordparser >> return ()))

statementparser = do
  key <- keywordparser
  stuff <- stuffparser
  return $ Branch key [Leaf (trim stuff)]
  <?> "statementparser"

tp = parse (many statementparser) ""

ここで重要なのはスタッフパーサーです。これは、キーワードの間にあるものであり、列リストからwhere基準まで何でもかまいません。この関数は、キーワードに至るまでのすべての文字をキャッチします。しかし、それが完了する前に何か他のものが必要です。副選択がある場合はどうなりますか?「selectid、(select product from products)frombar」。その場合、そのキーワードにヒットすると、すべてが台無しになり、解析が間違って、インデントが台無しになります。また、句に括弧を付けることもできます。

したがって、anyCharを別のコンビネータに変更する必要があります。このコンビネータは、文字を1つずつ丸呑みするだけでなく、括弧を探します。見つかった場合は、すべてをトラバースしてキャプチャします。さらに括弧がある場合は、それを実行します。括弧を完全に閉じてから、すべてを連結して返します。これが私が試したことですが、うまく機能させることができません。

stuffparser :: Parsec String u String
stuffparser = fmap concat $ manyTill somechars (eof <|> (lookAhead keywordparser >> return ()))
  where
    somechars = parens <|> fmap (\c -> [c]) anyChar
    parens= between (char '(') (char ')') somechars

これは次のようにエラーになります:

> tp "select asdf(qwerty) from foo where 1"
Left (line 1, column 14):
unexpected "w"
expecting ")"

しかし、これを機能するように書き直す方法は考えられません。括弧の部分でmanyTillを使用しようとしましたが、代わりに文字列生成括弧と単一文字の両方がある場合、タイプチェックを取得するのに問題が発生します。誰かがこれについて行く方法について何か提案がありますか?

4

1 に答える 1

6

ええ、betweenあなたが探しているものにはうまくいかないかもしれません。もちろん、あなたのユースケースでは、hammarの提案に従い、既成のSQLパーサーを入手します。(個人的な意見:または、本当に必要な場合を除いてSQLを使用しないようにしてください。データベースクエリに文字列を使用するという考えは、歴史的な間違いでした)。

注:<++>文字列か文字かに関係なく、2つのパーサーの結果を連結するという演算子を追加します。(下部のコード。)

まず、括弧を解析するタスクの場合:トップレベルは、関連する文字間のいくつかのものを解析します。これは、コードが正確に言っていることです。

parseParen = char '(' <++> inner <++> char ')'

次に、inner関数は他のものを解析する必要があります。非括弧(おそらく別の括弧のセットを含む)、およびそれに続く非括弧のジャンクです。

parseParen = char '(' <++> inner <++> char ')' where
    inner = many (noneOf "()") <++> option "" (parseParen <++> inner)

ソリューションの残りの部分では、最上位のSQLキーワードで物事を分割することに類似していると仮定します。(つまり、括弧内のものを無視します)。つまり、そのように動作するパーサーがあります。

Main> parseTest parseSqlToplevel "select asdf(select m( 2) fr(o)m w where n) from b where delete 4"
[(Select," asdf(select m( 2) fr(o)m w where n) "),(From," b "),(Where," "),(Delete," 4")]

などを取得するparseKwパーサーがあるとしますselect。キーワードを消費した後、次の[トップレベル]キーワードまで読み取る必要があります。私の解決策の最後の秘訣は、lookAheadコンビネータを使用して次の単語がキーワードであるかどうかを判断し、キーワードである場合はそれを元に戻すことです。そうでない場合は、括弧またはその他の文字を使用して、残りの部分を繰り返します。

-- consume spaces, then eat a word or parenthesis
parseOther = many space <++>
    (("" <$ lookAhead (try parseKw)) <|> -- if there's a keyword, put it back!
     option "" ((parseParen <|> many1 (noneOf "() \t")) <++> parseOther))

私の全体的な解決策は次のとおりです

-- overloaded operator to concatenate string results from parsers
class CharOrStr a where toStr :: a -> String
instance CharOrStr Char where toStr x = [x]
instance CharOrStr String where toStr = id
infixl 4 <++>
f <++> g = (\x y -> toStr x ++ toStr y) <$> f <*> g

data Keyword = Select | Update | Delete | From | Where deriving (Eq, Show)

parseKw =
    (Select <$ string "select") <|>
    (Update <$ string "update") <|>
    (Delete <$ string "delete") <|>
    (From <$ string "from") <|>
    (Where <$ string "where") <?>
    "keyword (select, update, delete, from, where)"

-- consume spaces, then eat a word or parenthesis
parseOther = many space <++>
    (("" <$ lookAhead (try parseKw)) <|> -- if there's a keyword, put it back!
     option "" ((parseParen <|> many1 (noneOf "() \t")) <++> parseOther))

parseSqlToplevel = many ((,) <$> parseKw <*> (space <++> parseOther)) <* eof

parseParen = char '(' <++> inner <++> char ')' where
    inner = many (noneOf "()") <++> option "" (parseParen <++> inner)

編集-見積もりをサポートするバージョン

見積もりをサポートするために、parensと同じことを行うことができます。

import Control.Applicative hiding (many, (<|>))
import Text.Parsec
import Text.Parsec.Combinator

-- overloaded operator to concatenate string results from parsers
class CharOrStr a where toStr :: a -> String
instance CharOrStr Char where toStr x = [x]
instance CharOrStr String where toStr = id
infixl 4 <++>
f <++> g = (\x y -> toStr x ++ toStr y) <$> f <*> g

data Keyword = Select | Update | Delete | From | Where deriving (Eq, Show)

parseKw =
    (Select <$ string "select") <|>
    (Update <$ string "update") <|>
    (Delete <$ string "delete") <|>
    (From <$ string "from") <|>
    (Where <$ string "where") <?>
    "keyword (select, update, delete, from, where)"

-- consume spaces, then eat a word or parenthesis
parseOther = many space <++>
    (("" <$ lookAhead (try parseKw)) <|> -- if there's a keyword, put it back!
     option "" ((parseParen <|> parseQuote <|> many1 (noneOf "'() \t")) <++> parseOther))

parseSqlToplevel = many ((,) <$> parseKw <*> (space <++> parseOther)) <* eof

parseQuote = char '\'' <++> inner <++> char '\'' where
    inner = many (noneOf "'\\") <++>
        option "" (char '\\' <++> anyChar <++> inner)

parseParen = char '(' <++> inner <++> char ')' where
    inner = many (noneOf "'()") <++>
        (parseQuote <++> inner <|> option "" (parseParen <++> inner))

で試してみましたparseTest parseSqlToplevel "select ('a(sdf'())b"。乾杯

于 2011-07-19T04:35:24.747 に答える