sql-metadata icon indicating copy to clipboard operation
sql-metadata copied to clipboard

`Parser(sql).tables` hangs for some cases

Open callofdutyops opened this issue 8 months ago • 1 comments

version: 2.15.0

This is a demo which can reproduce this bug.

def test_hang(self):
    from sql_metadata import Parser
    s = """WITH a AS (SELECT  MAX(b) AS c
        FROM    d
        WHERE   domain =e''$.f') AS g
    FROM    h;"""
    Parser(s).tables

With some debug, I find that it seems stuck in the while self._is_in_with_block and token.next_token: loop.

Image

callofdutyops avatar May 08 '25 10:05 callofdutyops

A quick "fix" for someone who also meets this bug. (Actually, it's not a fix, it just prevents the infinite while loop.)

class StuckFixedParser(Parser):
    @property
    def with_names(self) -> List[str]:
        """
        Returns with statements aliases list from a given query

        E.g. WITH database1.tableFromWith AS (SELECT * FROM table3)
             SELECT "xxxxx" FROM database1.tableFromWith alias
             LEFT JOIN database2.table2 ON ("tt"."ttt"."fff" = "xx"."xxx")
        will return ["database1.tableFromWith"]
        """
        if self._with_names is not None:
            return self._with_names
        with_names = UniqueList()
        for token in self._not_parsed_tokens:
            if token.previous_token.normalized == "WITH":
                self._is_in_with_block = True
                while self._is_in_with_block and token.next_token:
                    if token.next_token.is_as_keyword:
                        self._handle_with_name_save(token=token, with_names=with_names)
                        while token.next_token and not token.is_with_query_end:
                            token = token.next_token
                        is_end_of_with_block = (
                            token.next_token_not_comment is None
                            or token.next_token_not_comment.normalized in WITH_ENDING_KEYWORDS
                        )
                        if is_end_of_with_block:
                            self._is_in_with_block = False
                        elif token.next_token and token.next_token.is_as_keyword:
                            break
                    else:
                        token = token.next_token

        self._with_names = with_names
        return self._with_names

Notice the else branch after the if is_end_of_with_block: condition.

callofdutyops avatar May 12 '25 03:05 callofdutyops