1#
2# Copyright (C) 2009-2020 the sqlparse authors and contributors
3# <see AUTHORS file>
4#
5# This module is part of python-sqlparse and is released under
6# the BSD License: https://opensource.org/licenses/BSD-3-Clause
7
8from sqlparse import sql, tokens as T
9
10
11class StatementSplitter:
12 """Filter that split stream at individual statements"""
13
14 def __init__(self):
15 self._reset()
16
17 def _reset(self):
18 """Set the filter attributes to its default values"""
19 self._in_declare = False
20 self._in_case = False
21 self._is_create = False
22 self._begin_depth = 0
23
24 self.consume_ws = False
25 self.tokens = []
26 self.level = 0
27
28 def _change_splitlevel(self, ttype, value):
29 """Get the new split level (increase, decrease or remain equal)"""
30
31 # parenthesis increase/decrease a level
32 if ttype is T.Punctuation and value == '(':
33 return 1
34 elif ttype is T.Punctuation and value == ')':
35 return -1
36 elif ttype not in T.Keyword: # if normal token return
37 return 0
38
39 # Everything after here is ttype = T.Keyword
40 # Also to note, once entered an If statement you are done and basically
41 # returning
42 unified = value.upper()
43
44 # three keywords begin with CREATE, but only one of them is DDL
45 # DDL Create though can contain more words such as "or replace"
46 if ttype is T.Keyword.DDL and unified.startswith('CREATE'):
47 self._is_create = True
48 return 0
49
50 # can have nested declare inside of being...
51 if unified == 'DECLARE' and self._is_create and self._begin_depth == 0:
52 self._in_declare = True
53 return 1
54
55 if unified == 'BEGIN':
56 self._begin_depth += 1
57 if self._is_create:
58 # FIXME(andi): This makes no sense. ## this comment neither
59 return 1
60 return 0
61
62 # BEGIN and CASE/WHEN both end with END
63 if unified == 'END':
64 if not self._in_case:
65 self._begin_depth = max(0, self._begin_depth - 1)
66 else:
67 self._in_case = False
68 return -1
69
70 if (unified in ('IF', 'FOR', 'WHILE', 'CASE')
71 and self._is_create and self._begin_depth > 0):
72 if unified == 'CASE':
73 self._in_case = True
74 return 1
75
76 if unified in ('END IF', 'END FOR', 'END WHILE'):
77 return -1
78
79 # Default
80 return 0
81
82 def process(self, stream):
83 """Process the stream"""
84 EOS_TTYPE = T.Whitespace, T.Comment.Single
85
86 # Run over all stream tokens
87 for ttype, value in stream:
88 # Yield token if we finished a statement and there's no whitespaces
89 # It will count newline token as a non whitespace. In this context
90 # whitespace ignores newlines.
91 # why don't multi line comments also count?
92 if self.consume_ws and ttype not in EOS_TTYPE:
93 yield sql.Statement(self.tokens)
94
95 # Reset filter and prepare to process next statement
96 self._reset()
97
98 # Change current split level (increase, decrease or remain equal)
99 self.level += self._change_splitlevel(ttype, value)
100
101 # Append the token to the current statement
102 self.tokens.append(sql.Token(ttype, value))
103
104 # Check if we get the end of a statement
105 # Issue762: Allow GO (or "GO 2") as statement splitter.
106 # When implementing a language toggle, it's not only to add
107 # keywords it's also to change some rules, like this splitting
108 # rule.
109 if (self.level <= 0 and ttype is T.Punctuation and value == ';') \
110 or (ttype is T.Keyword and value.split()[0] == 'GO'):
111 self.consume_ws = True
112
113 # Yield pending statement (if any)
114 if self.tokens and not all(t.is_whitespace for t in self.tokens):
115 yield sql.Statement(self.tokens)