Project

General

Profile

1
# SQL code generation
2

    
3
import copy
4
import itertools
5
import operator
6
from ordereddict import OrderedDict
7
import re
8
import UserDict
9
import warnings
10

    
11
import dicts
12
import exc
13
import iters
14
import lists
15
import objects
16
import regexp
17
import strings
18
import util
19

    
20
##### Names
21

    
22
identifier_max_len = 63 # works for both PostgreSQL and MySQL
23

    
24
def concat(str_, suffix):
25
    '''Preserves version so that it won't be truncated off the string, leading
26
    to collisions.'''
27
    # Preserve version
28
    match = re.match(r'^(.*?)((?:(?:#\d+)?\)?)*(?:\.\w+)+(?:::[\w ]+)*)$', str_)
29
    if match:
30
        str_, old_suffix = match.groups()
31
        suffix = old_suffix+suffix
32
    
33
    return strings.concat(str_, suffix, identifier_max_len)
34

    
35
def truncate(str_): return concat(str_, '')
36

    
37
def is_safe_name(name):
38
    '''A name is safe *and unambiguous* if it:
39
    * contains only *lowercase* word (\w) characters
40
    * doesn't start with a digit
41
    * contains "_", so that it's not a keyword
42
    '''
43
    return re.match(r'^(?=.*_)(?!\d)[^\WA-Z]+$', name)
44

    
45
def esc_name(name, quote='"'):
46
    return quote + name.replace(quote, quote+quote) + quote
47
        # doubling an embedded quote escapes it in both PostgreSQL and MySQL
48

    
49
def unesc_name(name, quote='"'):
50
    removed_ref = [False]
51
    name = strings.remove_prefix(quote, name, removed_ref)
52
    if removed_ref[0]:
53
        name = strings.remove_suffix(quote, name, removed_ref)
54
        assert removed_ref[0]
55
        name = name.replace(quote+quote, quote)
56
    return name
57

    
58
def clean_name(name): return name.replace('"', '').replace('`', '')
59

    
60
def esc_comment(comment): return '/*'+comment.replace('*/', '* /')+'*/'
61

    
62
def lstrip(str_):
63
    '''Also removes comments.'''
64
    if str_.startswith('/*'): comment, sep, str_ = str_.partition('*/')
65
    return str_.lstrip()
66

    
67
##### General SQL code objects
68

    
69
class MockDb:
70
    def esc_value(self, value): return strings.repr_no_u(value)
71
    
72
    def esc_name(self, name): return esc_name(name)
73
    
74
    def col_info(self, col):
75
        return TypedCol(col.name, '<type>', CustomCode('<default>'), True)
76

    
77
mockDb = MockDb()
78

    
79
class BasicObject(objects.BasicObject):
80
    def __str__(self): return clean_name(strings.repr_no_u(self))
81

    
82
##### Unparameterized code objects
83

    
84
class Code(BasicObject):
85
    def __init__(self, lang='sql'):
86
        self.lang = lang
87
    
88
    def to_str(self, db): raise NotImplementedError()
89
    
90
    def __repr__(self): return self.to_str(mockDb)
91

    
92
class CustomCode(Code):
93
    def __init__(self, str_):
94
        Code.__init__(self)
95
        
96
        self.str_ = str_
97
    
98
    def to_str(self, db): return self.str_
99

    
100
def as_Code(value, db=None):
101
    '''
102
    @param db If set, runs db.std_code() on the value.
103
    '''
104
    if isinstance(value, Code): return value
105
    
106
    if util.is_str(value):
107
        if db != None: value = db.std_code(value)
108
        return CustomCode(value)
109
    else: return Literal(value)
110

    
111
class Expr(Code):
112
    def __init__(self, expr):
113
        Code.__init__(self)
114
        
115
        self.expr = expr
116
    
117
    def to_str(self, db): return '('+self.expr.to_str(db)+')'
118

    
119
##### Names
120

    
121
class Name(Code):
122
    def __init__(self, name):
123
        Code.__init__(self)
124
        
125
        name = truncate(name)
126
        
127
        self.name = name
128
    
129
    def to_str(self, db): return db.esc_name(self.name)
130

    
131
def as_Name(value):
132
    if isinstance(value, Code): return value
133
    else: return Name(value)
134

    
135
##### Literal values
136

    
137
#### Primitives
138

    
139
class Literal(Code):
140
    def __init__(self, value):
141
        Code.__init__(self)
142
        
143
        self.value = value
144
    
145
    def to_str(self, db): return db.esc_value(self.value)
146

    
147
def as_Value(value):
148
    if isinstance(value, Code): return value
149
    else: return Literal(value)
150

    
151
def is_literal(value): return isinstance(value, Literal)
152

    
153
def is_null(value): return is_literal(value) and value.value == None
154

    
155
#### Composites
156

    
157
class List(Code):
158
    def __init__(self, values):
159
        Code.__init__(self)
160
        
161
        self.values = values
162
    
163
    def to_str(self, db): return ', '.join((v.to_str(db) for v in self.values))
164

    
165
class Tuple(List):
166
    def __init__(self, *values):
167
        List.__init__(self, values)
168
    
169
    def to_str(self, db): return '('+List.to_str(self, db)+')'
170

    
171
class Row(Tuple):
172
    def to_str(self, db): return 'ROW'+Tuple.to_str(self, db)
173

    
174
### Arrays
175

    
176
class Array(List):
177
    def __init__(self, values):
178
        values = map(remove_col_rename, values)
179
        
180
        List.__init__(self, values)
181
    
182
    def to_str(self, db): return 'ARRAY['+List.to_str(self, db)+']'
183

    
184
def to_Array(value):
185
    if isinstance(value, Array): return value
186
    return Array(lists.mk_seq(value))
187

    
188
##### Derived elements
189

    
190
src_self = object() # tells Col that it is its own source column
191

    
192
class Derived(Code):
193
    def __init__(self, srcs):
194
        '''An element which was derived from some other element(s).
195
        @param srcs See self.set_srcs()
196
        '''
197
        Code.__init__(self)
198
        
199
        self.set_srcs(srcs)
200
    
201
    def set_srcs(self, srcs, overwrite=True):
202
        '''
203
        @param srcs (self_type...)|src_self The element(s) this is derived from
204
        '''
205
        if not overwrite and self.srcs != (): return # already set
206
        
207
        if srcs == src_self: srcs = (self,)
208
        srcs = tuple(srcs) # make Col hashable
209
        self.srcs = srcs
210
    
211
    def _compare_on(self):
212
        compare_on = self.__dict__.copy()
213
        del compare_on['srcs'] # ignore
214
        return compare_on
215

    
216
def cols_srcs(cols): return lists.uniqify(iters.flatten((v.srcs for v in cols)))
217

    
218
##### Tables
219

    
220
class Table(Derived):
221
    def __init__(self, name, schema=None, srcs=(), is_temp=False):
222
        '''
223
        @param schema str|None (for no schema)
224
        @param srcs (Table...)|src_self See Derived.set_srcs()
225
        '''
226
        Derived.__init__(self, srcs)
227
        
228
        if util.is_str(name): name = truncate(name)
229
        
230
        self.name = name
231
        self.schema = schema
232
        self.is_temp = is_temp
233
        self.order_by = None
234
        self.index_cols = {}
235
    
236
    def to_str(self, db):
237
        str_ = ''
238
        if self.schema != None: str_ += as_Name(self.schema).to_str(db)+'.'
239
        str_ += as_Name(self.name).to_str(db)
240
        return str_
241
    
242
    def to_Table(self): return self
243
    
244
    def _compare_on(self):
245
        compare_on = Derived._compare_on(self)
246
        del compare_on['order_by'] # ignore
247
        del compare_on['index_cols'] # ignore
248
        return compare_on
249

    
250
def is_underlying_table(table):
251
    return isinstance(table, Table) and table.to_Table() is table
252

    
253
class NoUnderlyingTableException(Exception):
254
    def __init__(self, ref):
255
        Exception.__init__(self, 'for: '+strings.as_tt(strings.urepr(ref)))
256
        self.ref = ref
257

    
258
def underlying_table(table):
259
    table = remove_table_rename(table)
260
    if table != None and table.srcs:
261
        table, = table.srcs # for derived tables or row vars
262
    if not is_underlying_table(table): raise NoUnderlyingTableException(table)
263
    return table
264

    
265
def as_Table(table, schema=None):
266
    if table == None or isinstance(table, Code): return table
267
    else: return Table(table, schema)
268

    
269
def suffixed_table(table, suffix):
270
    table = copy.copy(table) # don't modify input!
271
    table.name = concat(table.name, suffix)
272
    return table
273

    
274
class NamedTable(Table):
275
    def __init__(self, name, code, cols=None):
276
        Table.__init__(self, name)
277
        
278
        code = as_Table(code)
279
        if not isinstance(code, (Table, FunctionCall, Expr)): code = Expr(code)
280
        if cols != None: cols = [to_name_only_col(c).to_Col() for c in cols]
281
        
282
        self.code = code
283
        self.cols = cols
284
    
285
    def to_str(self, db):
286
        str_ = self.code.to_str(db)
287
        if str_.find('\n') >= 0: whitespace = '\n'
288
        else: whitespace = ' '
289
        str_ += whitespace+'AS '+Table.to_str(self, db)
290
        if self.cols != None:
291
            str_ += ' ('+(', '.join((c.to_str(db) for c in self.cols)))+')'
292
        return str_
293
    
294
    def to_Table(self): return Table(self.name)
295

    
296
def remove_table_rename(table):
297
    if isinstance(table, NamedTable): table = table.code
298
    return table
299

    
300
##### Columns
301

    
302
class Col(Derived):
303
    def __init__(self, name, table=None, srcs=()):
304
        '''
305
        @param table Table|None (for no table)
306
        @param srcs (Col...)|src_self See Derived.set_srcs()
307
        '''
308
        Derived.__init__(self, srcs)
309
        
310
        if util.is_str(name): name = truncate(name)
311
        if util.is_str(table): table = Table(table)
312
        assert table == None or isinstance(table, Table)
313
        
314
        self.name = name
315
        self.table = table
316
    
317
    def to_str(self, db, for_str=False):
318
        str_ = as_Name(self.name).to_str(db)
319
        if for_str: str_ = clean_name(str_)
320
        if self.table != None:
321
            table = self.table.to_Table()
322
            if for_str: str_ = concat(strings.ustr(table), '.'+str_)
323
            else: str_ = table.to_str(db)+'.'+str_
324
        return str_
325
    
326
    def __str__(self): return self.to_str(mockDb, for_str=True)
327
    
328
    def to_Col(self): return self
329

    
330
def is_col(col): return isinstance(col, Col)
331

    
332
def is_table_col(col): return is_col(col) and col.table != None
333

    
334
def index_col(col):
335
    if not is_table_col(col): return None
336
    
337
    table = col.table
338
    try: name = table.index_cols[col.name]
339
    except KeyError: return None
340
    else: return Col(name, table, col.srcs)
341

    
342
def is_temp_col(col): return is_table_col(col) and col.table.is_temp
343

    
344
def as_Col(col, table=None, name=None):
345
    '''
346
    @param name If not None, any non-Col input will be renamed using NamedCol.
347
    '''
348
    if name != None:
349
        col = as_Value(col)
350
        if not isinstance(col, Col): col = NamedCol(name, col)
351
    
352
    if isinstance(col, Code): return col
353
    elif util.is_str(col): return Col(col, table)
354
    else: return Literal(col)
355

    
356
def with_table(col, table):
357
    if isinstance(col, NamedCol): pass # doesn't take a table
358
    elif isinstance(col, FunctionCall):
359
        col = copy.deepcopy(col) # don't modify input!
360
        col.args[0].table = table
361
    elif isinstance(col, Col):
362
        col = copy.copy(col) # don't modify input!
363
        col.table = table
364
    return col
365

    
366
def with_default_table(col, table):
367
    col = as_Col(col)
368
    if col.table == None: col = with_table(col, table)
369
    return col
370

    
371
def set_cols_table(table, cols):
372
    table = as_Table(table)
373
    
374
    for i, col in enumerate(cols):
375
        col = cols[i] = as_Col(col)
376
        col.table = table
377

    
378
def to_name_only_col(col, check_table=None):
379
    col = as_Col(col)
380
    if not is_table_col(col): return col
381
    
382
    if check_table != None:
383
        table = col.table
384
        assert table == None or table == check_table
385
    return Col(col.name)
386

    
387
def suffixed_col(col, suffix):
388
    return Col(concat(col.name, suffix), col.table, col.srcs)
389

    
390
def has_srcs(col): return is_col(col) and col.srcs
391

    
392
def cross_join_srcs(cols):
393
    cols = filter(has_srcs, cols) # empty srcs will mess up the cross join
394
    srcs = [[s.name for s in c.srcs] for c in cols]
395
    if not srcs: return [] # itertools.product() returns [()] for empty input
396
    return [Col(','.join(s)) for s in itertools.product(*srcs)]
397

    
398
class NamedCol(Col):
399
    def __init__(self, name, code):
400
        Col.__init__(self, name)
401
        
402
        code = as_Value(code)
403
        
404
        self.code = code
405
    
406
    def to_str(self, db):
407
        return self.code.to_str(db)+' AS '+Col.to_str(self, db)
408
    
409
    def to_Col(self): return Col(self.name)
410

    
411
def remove_col_rename(col):
412
    if isinstance(col, NamedCol): col = col.code
413
    return col
414

    
415
def underlying_col(col):
416
    col = remove_col_rename(col)
417
    if not isinstance(col, Col): raise NoUnderlyingTableException(col)
418
    
419
    return Col(col.name, underlying_table(col.table), col.srcs)
420

    
421
def wrap(wrap_func, value):
422
    '''Wraps a value, propagating any column renaming to the returned value.'''
423
    if isinstance(value, NamedCol):
424
        return NamedCol(value.name, wrap_func(value.code))
425
    else: return wrap_func(value)
426

    
427
class ColDict(dicts.DictProxy):
428
    '''A dict that automatically makes inserted entries Col objects.
429
    Anything that isn't a column is wrapped in a NamedCol with the key's column
430
    name by `as_Col(value, name=key.name)`.
431
    '''
432
    
433
    def __init__(self, db, keys_table, dict_={}):
434
        dicts.DictProxy.__init__(self, OrderedDict())
435
        
436
        keys_table = as_Table(keys_table)
437
        
438
        self.db = db
439
        self.table = keys_table
440
        self.update(dict_) # after setting vars because __setitem__() needs them
441
    
442
    def copy(self): return ColDict(self.db, self.table, self.inner.copy())
443
    
444
    def __getitem__(self, key):
445
        return dicts.DictProxy.__getitem__(self, self._key(key))
446
    
447
    def __setitem__(self, key, value):
448
        key = self._key(key)
449
        if value == None:
450
            try: value = self.db.col_info(key).default
451
            except NoUnderlyingTableException: pass # not a table column
452
        dicts.DictProxy.__setitem__(self, key, as_Col(value, name=key.name))
453
    
454
    def _key(self, key): return as_Col(key, self.table)
455

    
456
##### Definitions
457

    
458
class TypedCol(Col):
459
    def __init__(self, name, type_, default=None, nullable=True,
460
        constraints=None):
461
        assert default == None or isinstance(default, Code)
462
        
463
        Col.__init__(self, name)
464
        
465
        self.type = type_
466
        self.default = default
467
        self.nullable = nullable
468
        self.constraints = constraints
469
    
470
    def to_str(self, db):
471
        str_ = Col.to_str(self, db)+' '+as_Code(self.type).to_str(db)
472
        if not self.nullable: str_ += ' NOT NULL'
473
        if self.default != None: str_ += ' DEFAULT '+self.default.to_str(db)
474
        if self.constraints != None: str_ += ' '+self.constraints
475
        return str_
476
    
477
    def to_Col(self): return Col(self.name)
478

    
479
class SetOf(Code):
480
    def __init__(self, type_):
481
        Code.__init__(self)
482
        
483
        self.type = type_
484
    
485
    def to_str(self, db):
486
        return 'SETOF '+self.type.to_str(db)
487

    
488
class RowType(Code):
489
    def __init__(self, table):
490
        Code.__init__(self)
491
        
492
        self.table = table
493
    
494
    def to_str(self, db):
495
        return self.table.to_str(db)+'%ROWTYPE'
496

    
497
class ColType(Code):
498
    def __init__(self, col):
499
        Code.__init__(self)
500
        
501
        self.col = col
502
    
503
    def to_str(self, db):
504
        return self.col.to_str(db)+'%TYPE'
505

    
506
class ArrayType(Code):
507
    def __init__(self, elem_type):
508
        Code.__init__(self)
509
        elem_type = as_Code(elem_type)
510
        
511
        self.elem_type = elem_type
512
    
513
    def to_str(self, db):
514
        return self.elem_type.to_str(db)+'[]'
515

    
516
##### Functions
517

    
518
Function = Table
519
as_Function = as_Table
520

    
521
class InternalFunction(CustomCode): pass
522

    
523
#### Calls
524

    
525
class NamedArg(NamedCol):
526
    def __init__(self, name, value):
527
        NamedCol.__init__(self, name, value)
528
    
529
    def to_str(self, db):
530
        return Col.to_str(self, db)+' := '+self.code.to_str(db)
531

    
532
class FunctionCall(Code):
533
    def __init__(self, function, *args, **kw_args):
534
        '''
535
        @param args [Code|literal-value...] The function's arguments
536
        '''
537
        Code.__init__(self)
538
        
539
        function = as_Function(function)
540
        def filter_(arg): return remove_col_rename(as_Value(arg))
541
        args = map(filter_, args)
542
        args += [NamedArg(k, filter_(v)) for k, v in kw_args.iteritems()]
543
        
544
        self.function = function
545
        self.args = args
546
    
547
    def to_str(self, db):
548
        args_str = ', '.join((v.to_str(db) for v in self.args))
549
        return self.function.to_str(db)+'('+args_str+')'
550

    
551
def wrap_in_func(function, value):
552
    '''Wraps a value inside a function call.
553
    Propagates any column renaming to the returned value.
554
    '''
555
    return wrap(lambda v: FunctionCall(function, v), value)
556

    
557
def unwrap_func_call(func_call, check_name=None):
558
    '''Unwraps any function call to its first argument.
559
    Also removes any column renaming.
560
    '''
561
    func_call = remove_col_rename(func_call)
562
    if not isinstance(func_call, FunctionCall): return func_call
563
    
564
    if check_name != None:
565
        name = func_call.function.name
566
        assert name == None or name == check_name
567
    return func_call.args[0]
568

    
569
#### Definitions
570

    
571
class FunctionDef(Code):
572
    def __init__(self, function, return_type, body, params=[], modifiers=None):
573
        Code.__init__(self)
574
        
575
        return_type = as_Code(return_type)
576
        body = as_Code(body)
577
        
578
        self.function = function
579
        self.return_type = return_type
580
        self.body = body
581
        self.params = params
582
        self.modifiers = modifiers
583
    
584
    def to_str(self, db):
585
        params_str = (', '.join((p.to_str(db) for p in self.params)))
586
        str_ = '''\
587
CREATE FUNCTION '''+self.function.to_str(db)+'''('''+params_str+''')
588
RETURNS '''+self.return_type.to_str(db)+'''
589
LANGUAGE '''+self.body.lang+'''
590
'''
591
        if self.modifiers != None: str_ += self.modifiers+'\n'
592
        str_ += '''\
593
AS $$
594
'''+self.body.to_str(db)+'''
595
$$;
596
'''
597
        return str_
598

    
599
class FunctionParam(TypedCol):
600
    def __init__(self, name, type_, default=None, out=False):
601
        TypedCol.__init__(self, name, type_, default)
602
        
603
        self.out = out
604
    
605
    def to_str(self, db):
606
        str_ = TypedCol.to_str(self, db)
607
        if self.out: str_ = 'OUT '+str_
608
        return str_
609
    
610
    def to_Col(self): return Col(self.name)
611

    
612
### PL/pgSQL
613

    
614
class ReturnQuery(Code):
615
    def __init__(self, query):
616
        Code.__init__(self)
617
        
618
        query = as_Code(query)
619
        
620
        self.query = query
621
    
622
    def to_str(self, db):
623
        return 'RETURN QUERY\n'+strings.indent(self.query.to_str(db))+';\n'
624

    
625
## Exceptions
626

    
627
class BaseExcHandler(BasicObject):
628
    def to_str(self, db, body): raise NotImplementedError()
629
    
630
    def __repr__(self): return self.to_str(mockDb, '<body>')
631

    
632
suppress_exc = 'NULL;\n';
633

    
634
reraise_exc = 'RAISE USING ERRCODE = SQLSTATE, MESSAGE = SQLERRM;\n';
635

    
636
class ExcHandler(BaseExcHandler):
637
    def __init__(self, exc, handler=None):
638
        if handler != None: handler = as_Code(handler)
639
        
640
        self.exc = exc
641
        self.handler = handler
642
    
643
    def to_str(self, db, body):
644
        body = as_Code(body)
645
        
646
        if self.handler != None:
647
            handler_str = '\n'+strings.indent(self.handler.to_str(db), 2)
648
        else: handler_str = ' '+suppress_exc
649
        
650
        str_ = '''\
651
BEGIN
652
'''+strings.indent(body.to_str(db))+'''\
653
EXCEPTION
654
    WHEN '''+self.exc+''' THEN'''+handler_str+'''\
655
END;\
656
'''
657
        return str_
658

    
659
class NestedExcHandler(BaseExcHandler):
660
    def __init__(self, *handlers):
661
        '''
662
        @param handlers Sorted from outermost to innermost
663
        '''
664
        self.handlers = handlers
665
    
666
    def to_str(self, db, body):
667
        for handler in reversed(self.handlers): body = handler.to_str(db, body)
668
        return body
669

    
670
class ExcToWarning(Code):
671
    def __init__(self, return_):
672
        '''
673
        @param return_ Statement to return a default value in case of error
674
        '''
675
        Code.__init__(self)
676
        
677
        return_ = as_Code(return_)
678
        
679
        self.return_ = return_
680
    
681
    def to_str(self, db):
682
        return "RAISE WARNING '%', SQLERRM;\n"+self.return_.to_str(db)
683

    
684
unique_violation_handler = ExcHandler('unique_violation')
685

    
686
# Note doubled "\"s because inside Python string
687
plpythonu_error_handler = ExcHandler('internal_error', '''\
688
-- Handle PL/Python exceptions
689
DECLARE
690
    matches text[] := regexp_matches(SQLERRM,
691
        E'^(?:PL/Python: )?(\\\\w+): (.*)$'); -- .* also matches \\n
692
    exc_name text := matches[1];
693
    msg text := matches[2];
694
BEGIN
695
    /* Re-raise PL/Python exceptions with the PL/Python prefix removed.
696
    This allows the exception to be parsed like a native exception.
697
    Always raise as data_exception so it goes in the errors table. */
698
    IF exc_name IS NOT NULL THEN
699
        RAISE data_exception USING MESSAGE = msg;
700
    -- Re-raise non-PL/Python exceptions
701
    ELSE
702
        '''+reraise_exc+'''\
703
    END IF;
704
END;
705
''')
706

    
707
def data_exception_handler(handler):
708
    return ExcHandler('data_exception', handler)
709

    
710
row_var = Table('row')
711

    
712
class RowExcIgnore(Code):
713
    def __init__(self, row_type, select_query, with_row, cols=None,
714
        exc_handler=unique_violation_handler, row_var=row_var):
715
        '''
716
        @param row_type Ignored if a custom row_var is used.
717
        @pre If a custom row_var is used, it must already be defined.
718
        '''
719
        Code.__init__(self, lang='plpgsql')
720
        
721
        row_type = as_Code(row_type)
722
        select_query = as_Code(select_query)
723
        with_row = as_Code(with_row)
724
        row_var = as_Table(row_var)
725
        
726
        self.row_type = row_type
727
        self.select_query = select_query
728
        self.with_row = with_row
729
        self.cols = cols
730
        self.exc_handler = exc_handler
731
        self.row_var = row_var
732
    
733
    def to_str(self, db):
734
        if self.cols == None: row_vars = [self.row_var]
735
        else: row_vars = [Col(c.name, self.row_var) for c in self.cols]
736
        
737
        # Need an EXCEPTION block for each individual row because "When an error
738
        # is caught by an EXCEPTION clause, [...] all changes to persistent
739
        # database state within the block are rolled back."
740
        # This is unfortunate because "A block containing an EXCEPTION clause is
741
        # significantly more expensive to enter and exit than a block without
742
        # one."
743
        # (http://www.postgresql.org/docs/8.3/static/\
744
        # plpgsql-control-structures.html#PLPGSQL-ERROR-TRAPPING)
745
        str_ = '''\
746
FOR '''+(', '.join((v.to_str(db) for v in row_vars)))+''' IN
747
'''+strings.indent(self.select_query.to_str(db))+'''\
748
LOOP
749
'''+strings.indent(self.exc_handler.to_str(db, self.with_row))+'''\
750
END LOOP;
751
'''
752
        if self.row_var == row_var:
753
            str_ = '''\
754
DECLARE
755
    '''+self.row_var.to_str(db)+''' '''+self.row_type.to_str(db)+''';
756
BEGIN
757
'''+strings.indent(str_)+'''\
758
END;
759
'''
760
        return str_
761

    
762
##### Casts
763

    
764
class Cast(FunctionCall):
765
    def __init__(self, type_, value):
766
        type_ = as_Code(type_)
767
        value = as_Value(value)
768
        
769
        self.type_ = type_
770
        self.value = value
771
    
772
    def to_str(self, db):
773
        return 'CAST('+self.value.to_str(db)+' AS '+self.type_.to_str(db)+')'
774

    
775
def cast_literal(value):
776
    if not is_literal(value): return value
777
    
778
    if util.is_str(value.value): value = Cast('text', value)
779
    return value
780

    
781
##### Conditions
782

    
783
class NotCond(Code):
784
    def __init__(self, cond):
785
        Code.__init__(self)
786
        
787
        self.cond = cond
788
    
789
    def to_str(self, db): return 'NOT '+self.cond.to_str(db)
790

    
791
class ColValueCond(Code):
792
    def __init__(self, col, value):
793
        Code.__init__(self)
794
        
795
        value = as_ValueCond(value)
796
        
797
        self.col = col
798
        self.value = value
799
    
800
    def to_str(self, db): return self.value.to_str(db, self.col)
801

    
802
def combine_conds(conds, keyword=None):
803
    '''
804
    @param keyword The keyword to add before the conditions, if any
805
    '''
806
    str_ = ''
807
    if keyword != None:
808
        if conds == []: whitespace = ''
809
        elif len(conds) == 1: whitespace = ' '
810
        else: whitespace = '\n'
811
        str_ += keyword+whitespace
812
    
813
    str_ += '\nAND '.join(conds)
814
    return str_
815

    
816
##### Condition column comparisons
817

    
818
class ValueCond(BasicObject):
819
    def __init__(self, value):
820
        value = remove_col_rename(as_Value(value))
821
        
822
        self.value = value
823
    
824
    def to_str(self, db, left_value):
825
        '''
826
        @param left_value The Code object that the condition is being applied on
827
        '''
828
        raise NotImplemented()
829
    
830
    def __repr__(self): return self.to_str(mockDb, '<left_value>')
831

    
832
class CompareCond(ValueCond):
833
    def __init__(self, value, operator='='):
834
        '''
835
        @param operator By default, compares NULL values literally. Use '~=' or
836
            '~!=' to pass NULLs through.
837
        '''
838
        ValueCond.__init__(self, value)
839
        self.operator = operator
840
    
841
    def to_str(self, db, left_value):
842
        left_value = remove_col_rename(as_Col(left_value))
843
        
844
        right_value = self.value
845
        
846
        # Parse operator
847
        operator = self.operator
848
        passthru_null_ref = [False]
849
        operator = strings.remove_prefix('~', operator, passthru_null_ref)
850
        neg_ref = [False]
851
        operator = strings.remove_prefix('!', operator, neg_ref)
852
        equals = operator.endswith('=') # also includes <=, >=
853
        
854
        # Handle nullable columns
855
        check_null = False
856
        if not passthru_null_ref[0]: # NULLs compare equal
857
            try: left_value = ensure_not_null(db, left_value)
858
            except ensure_not_null_excs: # fall back to alternate method
859
                check_null = equals and isinstance(right_value, Col)
860
            else:
861
                if isinstance(left_value, EnsureNotNull):
862
                    right_value = ensure_not_null(db, right_value,
863
                        left_value.type) # apply same function to both sides
864
        
865
        if equals and is_null(right_value): operator = 'IS'
866
        
867
        left = left_value.to_str(db)
868
        right = right_value.to_str(db)
869
        
870
        # Create str
871
        str_ = left+' '+operator+' '+right
872
        if check_null:
873
            str_ = '('+str_+' OR ('+left+' IS NULL AND '+right+' IS NULL))'
874
        if neg_ref[0]: str_ = 'NOT '+str_
875
        return str_
876

    
877
# Tells as_ValueCond() to assume a non-ValueCond is a literal value
878
assume_literal = object()
879

    
880
def as_ValueCond(value, default_table=assume_literal):
881
    if not isinstance(value, ValueCond):
882
        if default_table is not assume_literal:
883
            value = with_default_table(value, default_table)
884
        return CompareCond(value)
885
    else: return value
886

    
887
##### Joins
888

    
889
join_same = object() # tells Join the left and right columns have the same name
890

    
891
# Tells Join the left and right columns have the same name and are never NULL
892
join_same_not_null = object()
893

    
894
filter_out = object() # tells Join to filter out rows that match the join
895

    
896
class Join(BasicObject):
897
    def __init__(self, table, mapping={}, type_=None):
898
        '''
899
        @param mapping dict(right_table_col=left_table_col, ...)
900
            * if left_table_col is join_same: left_table_col = right_table_col
901
              * Note that right_table_col must be a string
902
            * if left_table_col is join_same_not_null:
903
              left_table_col = right_table_col and both have NOT NULL constraint
904
              * Note that right_table_col must be a string
905
        @param type_ None (for plain join)|str (e.g. 'LEFT')|filter_out
906
            * filter_out: equivalent to 'LEFT' with the query filtered by
907
              `table_pkey IS NULL` (indicating no match)
908
        '''
909
        if util.is_str(table): table = Table(table)
910
        assert type_ == None or util.is_str(type_) or type_ is filter_out
911
        
912
        self.table = table
913
        self.mapping = mapping
914
        self.type_ = type_
915
    
916
    def to_str(self, db, left_table_):
917
        def join(entry):
918
            '''Parses non-USING joins'''
919
            right_table_col, left_table_col = entry
920
            
921
            # Switch order (right_table_col is on the left in the comparison)
922
            left = right_table_col
923
            right = left_table_col
924
            left_table = self.table
925
            right_table = left_table_
926
            
927
            # Parse left side
928
            left = with_default_table(left, left_table)
929
            
930
            # Parse special values
931
            left_on_right = Col(left.name, right_table)
932
            if right is join_same: right = left_on_right
933
            elif right is join_same_not_null:
934
                right = CompareCond(left_on_right, '~=')
935
            
936
            # Parse right side
937
            right = as_ValueCond(right, right_table)
938
            
939
            return right.to_str(db, left)
940
        
941
        # Create join condition
942
        type_ = self.type_
943
        joins = self.mapping
944
        if joins == {}: join_cond = None
945
        elif type_ is not filter_out and reduce(operator.and_,
946
            (v is join_same_not_null for v in joins.itervalues())):
947
            # all cols w/ USING, so can use simpler USING syntax
948
            cols = map(to_name_only_col, joins.iterkeys())
949
            join_cond = 'USING ('+(', '.join((c.to_str(db) for c in cols)))+')'
950
        else: join_cond = combine_conds(map(join, joins.iteritems()), 'ON')
951
        
952
        if isinstance(self.table, NamedTable): whitespace = '\n'
953
        else: whitespace = ' '
954
        
955
        # Create join
956
        if type_ is filter_out: type_ = 'LEFT'
957
        str_ = ''
958
        if type_ != None: str_ += type_+' '
959
        str_ += 'JOIN'+whitespace+self.table.to_str(db)
960
        if join_cond != None: str_ += whitespace+join_cond
961
        return str_
962
    
963
    def __repr__(self): return self.to_str(mockDb, '<left_table>')
964

    
965
##### Value exprs
966

    
967
all_cols = CustomCode('*')
968

    
969
default = CustomCode('DEFAULT')
970

    
971
row_count = FunctionCall(InternalFunction('COUNT'), all_cols)
972

    
973
class Coalesce(FunctionCall):
974
    def __init__(self, *args):
975
        FunctionCall.__init__(self, InternalFunction('COALESCE'), *args)
976

    
977
class Nullif(FunctionCall):
978
    def __init__(self, *args):
979
        FunctionCall.__init__(self, InternalFunction('NULLIF'), *args)
980

    
981
null_as_str = Cast('text', 'NULL')
982

    
983
def to_text(value): return Coalesce(Cast('text', value), null_as_str)
984

    
985
# See <http://www.postgresql.org/docs/8.3/static/datatype-numeric.html>
986
null_sentinels = {
987
    'character varying': r'\N',
988
    'double precision': 'NaN',
989
    'integer': 2147483647,
990
    'text': r'\N',
991
    'date': 'infinity',
992
    'timestamp with time zone': 'infinity',
993
    'taxonrank': 'unknown',
994
}
995

    
996
class EnsureNotNull(Coalesce):
997
    def __init__(self, value, type_):
998
        if isinstance(type_, ArrayType): null = []
999
        else: null = null_sentinels[type_]
1000
        Coalesce.__init__(self, as_Col(value), Cast(type_, null))
1001
        
1002
        self.type = type_
1003
    
1004
    def to_str(self, db):
1005
        col = self.args[0]
1006
        index_col_ = index_col(col)
1007
        if index_col_ != None: return index_col_.to_str(db)
1008
        return Coalesce.to_str(self, db)
1009

    
1010
#### Arrays
1011

    
1012
class ArrayMerge(FunctionCall):
1013
    def __init__(self, sep, array):
1014
        array = to_Array(array)
1015
        FunctionCall.__init__(self, InternalFunction('array_to_string'), array,
1016
            sep)
1017

    
1018
def merge_not_null(db, sep, values):
1019
    return ArrayMerge(sep, map(to_text, values))
1020

    
1021
##### Table exprs
1022

    
1023
class Values(Code):
1024
    def __init__(self, values):
1025
        '''
1026
        @param values [...]|[[...], ...] Can be one or multiple rows.
1027
        '''
1028
        Code.__init__(self)
1029
        
1030
        rows = values
1031
        if len(values) >= 1 and not lists.is_seq(values[0]): # only one row
1032
            rows = [values]
1033
        for i, row in enumerate(rows):
1034
            rows[i] = map(remove_col_rename, map(as_Value, row))
1035
        
1036
        self.rows = rows
1037
    
1038
    def to_str(self, db):
1039
        return 'VALUES '+(', '.join((Tuple(*r).to_str(db) for r in self.rows)))
1040

    
1041
def NamedValues(name, cols, values):
1042
    '''
1043
    @param cols None|[...]
1044
    @post `cols` will be changed to Col objects with the table set to `name`.
1045
    '''
1046
    table = NamedTable(name, Values(values), cols)
1047
    if cols != None: set_cols_table(table, cols)
1048
    return table
1049

    
1050
##### Database structure
1051

    
1052
def is_nullable(db, value):
1053
    if not is_table_col(value): return is_null(value)
1054
    try: return db.col_info(value).nullable
1055
    except NoUnderlyingTableException: return True # not a table column
1056

    
1057
text_types = set(['character varying', 'text'])
1058

    
1059
def is_text_type(type_): return type_ in text_types
1060

    
1061
def is_text_col(db, col): return is_text_type(db.col_info(col).type)
1062

    
1063
def canon_type(type_):
1064
    if type_ in text_types: return 'text'
1065
    else: return type_
1066

    
1067
ensure_not_null_excs = (NoUnderlyingTableException, KeyError)
1068

    
1069
def ensure_not_null(db, col, type_=None):
1070
    '''
1071
    @param col If type_ is not set, must have an underlying column.
1072
    @param type_ If set, overrides the underlying column's type and casts the
1073
        column to it if needed.
1074
    @return EnsureNotNull|Col
1075
    @throws ensure_not_null_excs
1076
    '''
1077
    col = remove_col_rename(col)
1078
    
1079
    try: col_type = db.col_info(underlying_col(col)).type
1080
    except NoUnderlyingTableException:
1081
        if type_ == None and is_null(col): raise # NULL has no type
1082
    else:
1083
        if type_ == None: type_ = col_type
1084
        elif type_ != col_type: col = Cast(type_, col)
1085
    
1086
    if is_nullable(db, col):
1087
        try: col = EnsureNotNull(col, type_)
1088
        except KeyError, e:
1089
            # Warn of no null sentinel for type, even if caller catches error
1090
            warnings.warn(UserWarning(exc.str_(e)))
1091
            raise
1092
    
1093
    return col
1094

    
1095
def try_mk_not_null(db, value):
1096
    '''
1097
    Warning: This function does not guarantee that its result is NOT NULL.
1098
    '''
1099
    try: return ensure_not_null(db, value)
1100
    except ensure_not_null_excs: return value
1101

    
1102
##### Expression transforming
1103

    
1104
true_expr = 'true'
1105
false_expr = 'false'
1106

    
1107
true_re = true_expr
1108
false_re = false_expr
1109
bool_re = r'(?:'+true_re+r'|'+false_re+r')'
1110
atom_re = r'(?:'+bool_re+r'|\([^()]*\)'+r')'
1111

    
1112
def logic_op_re(op, value_re, expr_re=''):
1113
    op_re = ' '+op+' '
1114
    return '(?:'+expr_re+op_re+value_re+'|'+value_re+op_re+expr_re+')'
1115

    
1116
not_re = r'\bNOT '
1117
not_false_re = not_re+false_re+r'\b'
1118
not_true_re = not_re+true_re+r'\b'
1119
and_false_re = logic_op_re('AND', false_re, atom_re)
1120
and_false_not_true_re = '(?:'+not_true_re+'|'+and_false_re+')'
1121
and_true_re = logic_op_re('AND', true_re)
1122
or_re = logic_op_re('OR', bool_re)
1123
or_and_true_re = '(?:'+and_true_re+'|'+or_re+')'
1124

    
1125
def simplify_parens(expr):
1126
    return regexp.sub_nested(r'\(('+atom_re+')\)', r'\1', expr)
1127

    
1128
def simplify_recursive(sub_func, expr):
1129
    '''
1130
    @param sub_func See regexp.sub_recursive() sub_func param
1131
    '''
1132
    return regexp.sub_recursive(lambda s: sub_func(simplify_parens(s)), expr)
1133
        # simplify_parens() is also done at end in final iteration
1134

    
1135
def simplify_expr(expr):
1136
    def simplify_logic_ops(expr):
1137
        total_n = 0
1138
        expr, n = re.subn(not_false_re, true_re, expr)
1139
        total_n += n
1140
        expr, n = re.subn(and_false_not_true_re, false_expr, expr)
1141
        total_n += n
1142
        expr, n = re.subn(or_and_true_re, r'', expr)
1143
        total_n += n
1144
        return expr, total_n
1145
    
1146
    expr = expr.replace('NULL IS NULL', true_expr)
1147
    expr = expr.replace('NULL IS NOT NULL', false_expr)
1148
    expr = simplify_recursive(simplify_logic_ops, expr)
1149
    return expr
1150

    
1151
name_re = r'(?:\w+|(?:"[^"]*")+)'
1152

    
1153
def parse_expr_col(str_):
1154
    match = re.match(r'^\('+name_re+r'\(('+name_re+r').*\)\)$', str_)
1155
    if match: str_ = match.group(1)
1156
    return unesc_name(str_)
1157

    
1158
def map_expr(db, expr, mapping, in_cols_found=None):
1159
    '''Replaces output columns with input columns in an expression.
1160
    @param in_cols_found If set, will be filled in with the expr's (input) cols
1161
    '''
1162
    for out, in_ in mapping.iteritems():
1163
        orig_expr = expr
1164
        out = to_name_only_col(out)
1165
        in_str = to_name_only_col(remove_col_rename(in_)).to_str(db)
1166
        
1167
        # Replace out both with and without quotes
1168
        expr = expr.replace(out.to_str(db), in_str)
1169
        expr = re.sub(r'(?<!["\'\.=\[])\b'+out.name+r'\b(?!["\',\.=\]])',
1170
            in_str, expr)
1171
        
1172
        if in_cols_found != None and expr != orig_expr: # replaced something
1173
            in_cols_found.append(in_)
1174
    
1175
    return simplify_expr(expr)
(29-29/42)