root/livinglogic.python.xist/src/ll/toxicc.py @ 4042:374af534512f

Revision 4042:374af534512f, 10.3 KB (checked in by Walter Doerwald <walter@…>, 9 years ago)

Strip trailing whitespace.

Line 
1#! /usr/bin/env python
2# -*- coding: utf-8 -*-
3
4## Copyright 2004-2010 by LivingLogic AG, Bayreuth/Germany.
5## Copyright 2004-2010 by Walter Dörwald
6##
7## All Rights Reserved
8##
9## See ll/__init__.py for the license
10
11
12'''
13It can be used for generating Oracle or SQL
14server functions that return XML strings. This is done by embedding processing
15instructions containing SQL code into XML files and transforming those files
16with XIST.
17
18An Oracle example that generates an HTML table containing the result of a search
19for names in a ``person`` table might look like this::
20
21    from ll.xist import xsc
22    from ll.xist.ns import html, htmlspecials
23    from ll import toxic
24
25    class search(xsc.Element):
26        def convert(self, converter):
27            e = xsc.Frag(
28                toxic.args("search varchar2"),
29                toxic.vars("i integer;"),
30                toxic.type("varchar2(32000);"),
31                htmlspecials.plaintable(
32                    toxic.code("""
33                        i := 1;
34                        for row in (select name from person where name like search) loop
35                            """),
36                            html.tr(
37                                html.th(toxic.expr("i"), align="right"),
38                                html.td(toxic.expr("xmlescape(row.name)"))
39                            ),
40                            toxic.code("""
41                            i := i+1;
42                        end loop;
43                    """)
44                )
45            )
46            return e.convert(converter)
47
48    print toxic.compile(search().conv().string(encoding"us-ascii")).encode("us-ascii")
49
50The same example for SQL Server might look like this::
51
52    from ll.xist import xsc
53    from ll.xist.ns import html, htmlspecials
54    from ll import toxic
55
56    class search(xsc.Element):
57        def convert(self, converter):
58            e = xsc.Frag(
59                toxic.args("@search varchar(100)"),
60                toxic.vars("declare @i integer;"),
61                toxic.type("varchar(max)"),
62                htmlspecials.plaintable(
63                    toxic.code("""
64                        set @i = 1;
65
66                        declare @row_name varchar(100);
67                        declare person_cursor cursor for
68                            select name from person where name like @search
69
70                        open person_cursor
71
72                        while 1 = 1
73                        begin
74                            fetch next from person_cursor into @row_name;
75                            if (@@fetch_status != 0)
76                                break
77
78                            """),
79                            html.tr(
80                                html.th(toxic.expr("@i"), align="right"),
81                                html.td(toxic.expr("schema.xmlescape(@row_name)"))
82                            ),
83                            toxic.code("""
84                            set @i = @i+1;
85                        end
86
87                        close person_cursor
88                        deallocate person_cursor
89                    """)
90                )
91            )
92            return e.convert(converter)
93
94    print toxic.xml2sqs(search().conv().string(encoding"us-ascii")).encode("us-ascii")
95
96
97Running the Oracle script will give the following output (the indentation will
98be different though)::
99
100    (
101        search varchar2
102    )
103    return varchar2
104    as
105        c_out varchar2(32000);
106        i integer;
107    begin
108        c_out := c_out || '<table cellpadding="0" border="0" cellspacing="0">';
109        i := 1;
110        for row in (select name from person where name like search) loop
111            c_out := c_out || '<tr><th align="right">';
112            c_out := c_out || i;
113            c_out := c_out || '</th><td>';
114            c_out := c_out || xmlescape(row.name);
115            c_out := c_out || '</td></tr>';
116            i := i+1;
117        end loop;
118        c_out := c_out || '</table>';
119        return c_out;
120    end;
121
122Instead of generating the XML from a single XIST element, it's of course also
123possible to use an XML file. One that generates the same function as the
124one above looks like this::
125
126    <?args
127        search varchar2
128    ?>
129    <?vars
130        i integer;
131    ?>
132    <plaintable class="search">
133        <?code
134            i := 1;
135            for row in (select name from person where name like search) loop
136                ?>
137                <tr>
138                    <th align="right"><?expr i?></th>
139                    <td><?expr xmlescape(row.name)?></td>
140                </tr>
141                <?code
142                i := i + 1;
143            end loop;
144        ?>
145    </plaintable>
146
147When the file is saved as :file:`search.sqlxsc` then parsing the file,
148transforming it and printing the function body works like this::
149
150    from ll.xist import parsers
151    from ll.xist.ns import html, htmlspecials
152    from ll import toxic
153
154    node = parsers.parsefile("search.sqlxsc")
155    node = node.conv()
156    print toxic.xml2ora(node.string(encoding="us-ascii")).encode("us-ascii")
157'''
158
159
160import cStringIO
161
162from ll import misc
163from ll.xist import xsc, publishers
164
165
166__docformat__ = "reStructuredText"
167
168
169__all__ = ["compile", "prettify"]
170
171
172def _stringifyoracle(string, nchar=False):
173    """
174    Format :var:`string` as multiple PL/SQL string constants or expressions.
175    :var:`nchar` specifies if a ``NVARCHAR`` constant should be generated or a
176    ``VARCHAR``. This is a generator.
177    """
178    current = []
179
180    for c in string:
181        if ord(c) < 32:
182            if current:
183                if nchar:
184                    yield u"N'{0}'".format(u"".join(current))
185                else:
186                    yield u"'{0}'".format(u"".join(current))
187                current = []
188            yield u"chr({0})".format(ord(c))
189        else:
190            if c == u"'":
191                c = u"''"
192            current.append(c)
193            if len(current) > 1000:
194                if nchar:
195                    yield u"N'{0}'".format(u"".join(current))
196                else:
197                    yield u"'{0}'".format(u"".join(current))
198                current = []
199    if current:
200        if nchar:
201            yield u"N'{0}'".format(u"".join(current))
202        else:
203            yield u"'{0}'".format(u"".join(current))
204
205
206def _compile_oracle(string):
207    """
208    TOXIC compile function for Oracle
209    """
210    foundproc = False
211    foundargs = []
212    foundvars = []
213    foundsql = []
214    foundtype = u"clob"
215
216    for (t, s) in misc.tokenizepi(string):
217        if t is None:
218            foundsql.append((-1, s))
219        elif t == "code":
220            foundsql.append((0, s))
221        elif t == "expr":
222            foundsql.append((1, s))
223        elif t == "args":
224            foundargs.append(s)
225        elif t == "vars":
226            foundvars.append(s)
227        elif t == "type":
228            foundtype = s
229        elif t == "proc":
230            foundproc = True
231        else:
232            # Treat unknown PIs as text
233            foundsql.append((-1, u"<?{0} {1}?>".format(t, s)))
234
235    result = []
236    if foundargs:
237        result.append(u"(\n\t{0}\n)\n".format(u",\n\t".join(foundargs)))
238    plaintype = foundtype
239    if u"(" in plaintype:
240        plaintype = plaintype[:plaintype.find(u"(")]
241    isclob = plaintype.lower() in ("clob", "nclob")
242    if not foundproc:
243        result.append(u"return {0}\n".format(plaintype))
244    result.append(u"as\n")
245    if not foundproc:
246        result.append(u"\tc_out {0};\n".format(foundtype))
247    if foundvars:
248        result.append(u"\t{0}\n".format(u"".join(foundvars)))
249    nchar = foundtype.lower().startswith(u"n")
250    if isclob:
251        for arg in (u"clob", u"varchar2"):
252            result.append(u"\tprocedure write(p_text in {0}{1})\n".format(plaintype.rstrip(u"clob"), arg))
253            result.append(u"\tas\n")
254            result.append(u"\t\tbegin\n")
255            if arg == u"clob":
256                result.append(u"\t\t\tif p_text is not null and length(p_text) != 0 then\n")
257                result.append(u"\t\t\t\tdbms_lob.append(c_out, p_text);\n")
258            else:
259                result.append(u"\t\t\tif p_text is not null then\n")
260                result.append(u"\t\t\t\tdbms_lob.writeappend(c_out, length(p_text), p_text);\n")
261            result.append(u"\t\tend if;\n")
262            result.append(u"\tend;\n")
263    result.append(u"begin\n")
264    if isclob:
265        result.append(u"\tdbms_lob.createtemporary(c_out, true);\n")
266    for (mode, string) in foundsql:
267        if mode == -1:
268            for s in _stringifyoracle(string, nchar):
269                if isclob:
270                    result.append(u"\twrite({0});\n".format(s))
271                else:
272                    result.append(u"\tc_out := c_out || {0};\n".format(s))
273        elif mode == 0:
274            result.append(string)
275            result.append(u"\n")
276        else: # mode == 1
277            if isclob:
278                result.append(u"\twrite({0});\n".format(string))
279            else:
280                result.append(u"\tc_out := c_out || {0};\n".format(string))
281    if not foundproc:
282        result.append(u"\treturn c_out;\n")
283    result.append(u"end;\n")
284    return u"".join(result)
285
286
287def _compile_sqlserver(string):
288    """
289    TOXIC compile function for SQL Server
290    """
291    foundproc = False
292    foundargs = []
293    foundvars = []
294    foundsql = []
295    foundtype = "varchar(max)"
296
297    for (t, s) in misc.tokenizepi(string):
298        if t is None:
299            foundsql.append((-1, s))
300        elif t == "code":
301            foundsql.append((0, s))
302        elif t == "expr":
303            foundsql.append((1, s))
304        elif t == "args":
305            foundargs.append(s)
306        elif t == "vars":
307            foundvars.append(s)
308        elif t == "type":
309            foundtype = s
310        elif t == "proc":
311            foundproc = True
312        else:
313            # Treat unknown PIs as text
314            foundsql.append((-1, "<?{0} {1}?>".format(t, s)))
315
316    result = []
317    if foundargs:
318        result.append("(\n\t{0}\n)\n".format(u",\n\t".join(foundargs)))
319    if not foundproc:
320        result.append("returns {0}\n".format(foundtype))
321    result.append("as\n")
322    result.append("begin\n")
323    if not foundproc:
324        result.append("\tdeclare @c_out {0};\n".format(foundtype))
325        result.append("\tset @c_out = '';\n")
326    if foundvars:
327        result.append("\t{0}\n".format(u"".join(foundvars)))
328    for (mode, string) in foundsql:
329        if mode == -1:
330            string = "'{0}'" .format(string.replace("'", "''"))
331            result.append("\tset @c_out = @c_out + {0};\n".format(string))
332        elif mode == 0:
333            result.append(string)
334            result.append("\n")
335        else: # mode == 1
336            result.append("\tset @c_out = @c_out + set @c_out = @c_out + convert(varchar, isnull({0}, ''));\n".format(string))
337    if not foundproc:
338        result.append("\treturn @c_out;\n")
339    result.append("end;\n")
340    return "".join(result)
341
342
343def compile(string, mode="oracle"):
344    """
345    The :class:`unicode` object :var:`string` must be a string containing
346    embedded TOXIC processing instructions. :func:`compile` creates the body of
347    an Oracle or SQL Server function or procedure from it. :var:`mode` can be
348    either ``"oracle"`` or ``"sqlserver".
349    """
350    if mode == "oracle":
351        return  _compile_oracle(string)
352    elif mode == "sqlserver":
353        return _compile_sqlserver(string)
354    raise ValueError("unknown mode {0!r}".format(mode))
355
356
357def prettify(string, mode="oracle"):
358    """
359    Try to fix the indentation of the PL/SQL snippet passed in. :var:`mode` can
360    be either ``"oracle"`` or ``"sqlserver"``.
361    """
362    lines = [line.lstrip() for line in string.splitlines()]
363    newlines = []
364    if mode == "oracle":
365        indents = {
366            "(": (0, 1),
367            ");": (-1, 0),
368            ")": (-1, 0),
369            "as": (0, 1),
370            "begin": (0, 1),
371            "loop": (0, 1),
372            "end;": (-1, 0),
373            "end": (-1, 0),
374            "exception": (-1, 1),
375            "if": (0, 1),
376            "for": (0, 1),
377            "while": (0, 1),
378            "elsif": (-1, 1),
379            "else": (-1, 1),
380        }
381    elif mode == "sqlserver":
382        indents = {
383            "(": (0, 1),
384            ");": (-1, 0),
385            ")": (-1, 0),
386            "as": (0, 1),
387            "begin": (0, 1),
388            "end;": (-1, 0),
389            "end": (-1, 0),
390        }
391    else:
392        raise ValueError("unknown mode {0!r}".format(mode))
393    indent = 0
394    firstafteras = False
395    for line in lines:
396        if not line:
397            newlines.append("")
398        else:
399            prefix = line.split(None, 1)[0]
400            (pre, post) = indents.get(prefix, (0, 0))
401            if line.endswith("("):
402                post = 1
403            elif firstafteras and prefix == "begin":
404                # as followed by begin has same indentation
405                pre = -1
406            indent = max(0, indent+pre)
407            newlines.append("{0}{1}".format("\t"*indent, line))
408            indent = max(0, indent+post)
409            if prefix == "as":
410                firstafteras = True
411    return "\n".join(newlines)
Note: See TracBrowser for help on using the browser.