Skip to content

Commit dacce4f

Browse files
author
James William Pye
committed
Add Reference Symbols.
Implements a means to generate the SQL of a symbol using SQL [on the server].
1 parent 000b77c commit dacce4f

4 files changed

Lines changed: 148 additions & 53 deletions

File tree

postgresql/documentation/changes.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,6 @@ Changes
3131
connections, whereas the db.wait() method is modified for single targets.
3232
* Implement an ALock class for managing advisory locks using the
3333
threading.Lock APIs. [Feedback from Valentine Gogichashvili]
34+
* Implement reference symbols. Allow libraries to define symbols that
35+
are used to create queries that inherit the original symbol's type and
36+
execution method. ``db.prepare(db.prepare(...).first())``

postgresql/documentation/lib.txt

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,36 @@ effect it will have when invoked:
336336
... FROM STDIN``, the iterable must produce COPY lines.
337337

338338

339+
Reference Symbols
340+
=================
341+
342+
Reference Symbols provide a way to construct a Bound Symbol using the Symbol's
343+
query. When invoked, A Reference Symbol's query is executed in order to produce
344+
an SQL statement to be used as a Bound Symbol. In ILF files, a reference is
345+
identified by its symbol name being prefixed with an ampersand::
346+
347+
[&refsym::first]
348+
SELECT 'SELECT 1::int4'::text
349+
350+
Then executed::
351+
352+
>>> # Runs the 'refsym' SQL, and creates a Bound Symbol using the results.
353+
>>> sym = lib.refsym()
354+
>>> assert sym() == 1
355+
356+
The Reference Symbol's type and execution method are inherited by the created
357+
Bound Symbol. With one exception, ``const`` reference symbols are
358+
special in that they immediately resolved into the target Bound Symbol.
359+
360+
A Reference Symbol's source query *must* produce rows of text columns. Multiple
361+
columns and multiple rows may be produced by the query, but they must be
362+
character types as the results are promptly joined together with whitespace so
363+
that the target statement may be prepared.
364+
365+
Reference Symbols are most likely to be used in dynamic DDL and DML situations,
366+
or, somewhat more specifically, any query whose definition depends on a
367+
generated column list.
368+
339369
Distributing and Usage
340370
======================
341371

@@ -347,7 +377,7 @@ connection is made using the category.
347377
For mere Python extensions, however, ``distutils`` has a feature that can
348378
aid in ILF distribution. The ``package_data`` setup keyword can be used to
349379
include ILF files alongside the Python modules that make up a project. See
350-
http://docs.python.org/3.0/distutils/setupscript.html#installing-package-data
380+
http://docs.python.org/3.1/distutils/setupscript.html#installing-package-data
351381
for more detailed information on the keyword parameter.
352382

353383
The recommended way to manage libraries for extending projects is to
@@ -476,6 +506,10 @@ The following terms are used throughout this chapter:
476506
An interface to an individual Symbol ready for execution against the subject
477507
database.
478508

509+
Bound Reference
510+
An interface to an individual Reference Symbol that will produce a Bound
511+
Symbol when executed.
512+
479513
ILF
480514
INI-style Library Format. "lib{NAME}.sql" files.
481515

@@ -493,6 +527,9 @@ The following terms are used throughout this chapter:
493527
An named database operation provided by a Library. Usually, an SQL statement
494528
with Annotations.
495529

530+
Reference Symbol
531+
A Symbol whose SQL statement *produces* the source for a Bound Symbol.
532+
496533
Category
497534
An object supporting a classification for connectors that provides database
498535
initialization facilities for produced connections. For query libraries,

postgresql/lib/__init__.py

Lines changed: 85 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,27 @@
11
##
2-
# copyright 2009, James William Pye
3-
# http://python.projects.postgresql.org
2+
# .lib - query libraries; manage SQL outside of Python.
43
##
54
"""
65
PostgreSQL query libraries.
76
87
The purpose of query libraries is provide a means to manage a mapping of symbols
9-
to database operations. These operations can be simple statements, procedures,
10-
or something more complex.
8+
to database operations or objects. These operations can be simple statements,
9+
procedures, or something more complex.
1110
1211
Libraries are intended to allow the programmer to isolate and manage SQL outside
1312
of a system's code-flow. It provides a means to construct the basic Python
1413
interfaces to a PostgreSQL-based application.
1514
"""
1615
import io
1716
import os.path
18-
import operator
19-
get0 = operator.itemgetter(0)
2017
from types import ModuleType
2118
from abc import abstractmethod, abstractproperty
2219
from ..python.element import Element, ElementSet
2320
from .. import api as pg_api
2421
from .. import sys as pg_sys
2522
from .. import exceptions as pg_exc
2623
from ..python.itertools import find
24+
from itertools import chain
2725

2826
try:
2927
libdir = os.path.abspath(os.path.dirname(__file__))
@@ -83,6 +81,7 @@ def __init__(self,
8381
method = None,
8482
type = None,
8583
parameters = None,
84+
reference = False,
8685
):
8786
self.library = library
8887
self.source = source
@@ -94,6 +93,7 @@ def __init__(self,
9493
self.method = method
9594
self.type = type
9695
self.parameters = parameters
96+
self.reference = reference
9797

9898
def __str__(self):
9999
"""
@@ -163,15 +163,22 @@ def __init__(self, symbols, preface = None):
163163
self._name = None
164164
s = self.symbolsd = {}
165165
self.preload = set()
166-
for name, (typ, exe, doc, query) in symbols:
166+
for name, (isref, typ, exe, doc, query) in symbols:
167167
if typ and typ not in self.symtypes:
168-
raise ValueError("symbol %r has an invalid type: %r" %(name, typ))
168+
raise ValueError(
169+
"symbol %r has an invalid type: %r" %(name, typ)
170+
)
169171
if typ == 'preload':
170172
self.preload.add(name)
171173
typ = None
172174
elif typ == 'proc':
173175
pass
174-
SYM = Symbol(self, query, name = name, method = exe, type = typ)
176+
SYM = Symbol(self, query,
177+
name = name,
178+
method = exe,
179+
type = typ,
180+
reference = isref
181+
)
175182
s[name] = SYM
176183

177184
class ILF(SymbolCollection):
@@ -196,27 +203,6 @@ def get_symbol(self, name):
196203
def symbols(self):
197204
return self.symbolsd.keys()
198205

199-
def __init__(self, symbols, preface = None):
200-
"""
201-
Given an iterable of (symtype, symexe, doc, sql) tuples, create an
202-
anonymous ILF library.
203-
"""
204-
self.preface = preface
205-
self._address = None
206-
self._name = None
207-
s = self.symbolsd = {}
208-
self.preload = set()
209-
for name, (typ, exe, doc, query) in symbols:
210-
if typ and typ not in self.symtypes:
211-
raise ValueError("symbol %r has an invalid type: %r" %(name, typ))
212-
if typ == 'preload':
213-
self.preload.add(name)
214-
typ = None
215-
elif typ == 'proc':
216-
pass
217-
SYM = Symbol(self, query, name = name, method = exe, type = typ)
218-
s[name] = SYM
219-
220206
@classmethod
221207
def from_lines(typ, lines):
222208
"""
@@ -247,7 +233,9 @@ def from_lines(typ, lines):
247233
# symbol name
248234
# symbol type
249235
# how to execute symbol
250-
name, styp, exe, *_ = (tuple(symdesc.strip().strip('[]').split(':')) + (None, None))
236+
name, styp, exe, *_ = (tuple(
237+
symdesc.strip().strip('[]').split(':')
238+
) + (None, None))
251239
doc = ''
252240
endofcomment = 0
253241
# resolve any symbol references; only one per line.
@@ -266,7 +254,12 @@ def from_lines(typ, lines):
266254
query = ''.join(block[endofcomment:])
267255
if styp == 'proc':
268256
query = query.strip()
269-
syms.append((name, (styp, exe, doc, query)))
257+
if name.startswith('&'):
258+
name = name[1:]
259+
isref = True
260+
else:
261+
isref = False
262+
syms.append((name, (isref, styp, exe, doc, query)))
270263
return typ(syms, preface = preface)
271264

272265
@classmethod
@@ -303,9 +296,38 @@ def __init__(self, symbol, database):
303296
def __call__(self, *args, **kw):
304297
return self.method(*args, **kw)
305298

299+
class BoundReference(object):
300+
"""
301+
A symbol bound to a database whose results make up the source of a symbol
302+
that will be created upon the execution of this symbol.
303+
304+
A reference to a symbol.
305+
"""
306+
307+
def __init__(self, symbol, database):
308+
self.symbol = symbol
309+
self.database = database
310+
self.method = database.prepare(symbol).chunks
311+
312+
def __call__(self, *args, **kw):
313+
chunks = chain.from_iterable(self.method(*args, **kw))
314+
# Join columns with a space, and rows with a newline.
315+
src = '\n'.join([' '.join(row) for row in chunks])
316+
return BoundSymbol(
317+
Symbol(
318+
self.symbol.library, src,
319+
name = self.symbol.name,
320+
method = self.symbol.method,
321+
type = self.symbol.type,
322+
parameters = self.symbol.parameters,
323+
reference = False,
324+
),
325+
self.database,
326+
)
327+
306328
class Binding(object):
307329
"""
308-
Interface to a library bound to a database(connection).
330+
Library bound to a database(connection).
309331
"""
310332
def __init__(self, database, library):
311333
self.__dict__.update({
@@ -328,8 +350,8 @@ def __dir__(self):
328350

329351
def __getattr__(self, name):
330352
"""
331-
Return a BoundSymbol against the Binding's database with the symbol named
332-
``name`` in the Binding's library.
353+
Return a BoundSymbol against the Binding's database with the
354+
symbol named ``name`` in the Binding's library.
333355
"""
334356
d = self.__dict__
335357
s = d['__symbol_cache__']
@@ -339,32 +361,43 @@ def __getattr__(self, name):
339361
bs = s.get(name)
340362
if bs is None:
341363
# No symbol cached with that name.
364+
# Everything is crammed in here because
365+
# we do *not* want methods on this object.
366+
# The namespace is primarily reserved for symbols.
342367
sym = lib.get_symbol(name)
343368
if sym is None:
344369
raise AttributeError(
345370
"symbol %r does not exist in library %r" %(
346371
name, lib.address
347372
)
348373
)
349-
if not isinstance(sym, Symbol):
350-
# subjective symbol...
351-
sym = sym(db)
352-
if not isinstance(sym, Symbol):
353-
raise TypeError(
354-
"callable symbol, %r, did not produce Symbol instance" %(
355-
name,
356-
)
357-
)
358-
if sym.type == 'const':
359-
r = BoundSymbol(sym, db)()
360-
if sym.method in ('chunks', 'rows', 'column'):
361-
# resolve the iterator
362-
r = list(r)
363-
bs = s[name] = r
364-
else:
365-
bs = BoundSymbol(sym, db)
374+
if sym.reference:
375+
# Reference.
376+
bs = BoundReference(sym, db)
377+
if sym.type == 'const':
378+
# Constant Reference means a BoundSymbol.
379+
bs = bs()
366380
if sym.type != 'transient':
367381
s[name] = bs
382+
else:
383+
if not isinstance(sym, Symbol):
384+
# subjective symbol...
385+
sym = sym(db)
386+
if not isinstance(sym, Symbol):
387+
raise TypeError(
388+
"callable symbol, %r, did not produce " \
389+
"Symbol instance" %(name,)
390+
)
391+
if sym.type == 'const':
392+
r = BoundSymbol(sym, db)()
393+
if sym.method in ('chunks', 'rows', 'column'):
394+
# resolve the iterator
395+
r = list(r)
396+
bs = s[name] = r
397+
else:
398+
bs = BoundSymbol(sym, db)
399+
if sym.type != 'transient':
400+
s[name] = bs
368401
return bs
369402

370403
class Category(pg_api.Category):

postgresql/test/test_lib.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,21 @@
5353
5454
[sym_srf_proc:proc]
5555
test_ilf_srf_proc(int)
56+
57+
[&sym_reference]
58+
SELECT 'SELECT 1';
59+
60+
[&sym_reference_params]
61+
SELECT 'SELECT ' || $1::text;
62+
63+
[&sym_reference_first::first]
64+
SELECT 'SELECT 1::int4';
65+
66+
[&sym_reference_const:const:first]
67+
SELECT 'SELECT 1::int4';
68+
69+
[&sym_reference_proc:proc]
70+
SELECT 'test_ilf_proc(int)'::text
5671
"""
5772

5873
class test_lib(pg_unittest.TestCaseWithCluster):
@@ -84,6 +99,13 @@ def _testILF(self, lib):
8499
self.failUnlessEqual(b.sym_proc(2,), 2)
85100
self.failUnlessEqual(list(b.sym_srf_proc(2,)), [2])
86101
self.failUnlessRaises(AttributeError, getattr, b, 'LIES')
102+
# reference symbols
103+
self.failUnlessEqual(b.sym_reference()(), [(1,)])
104+
self.failUnlessEqual(b.sym_reference_params('1::int')(), [(1,)])
105+
self.failUnlessEqual(b.sym_reference_params("'foo'::text")(), [('foo',)])
106+
self.failUnlessEqual(b.sym_reference_first()(), 1)
107+
self.failUnlessEqual(b.sym_reference_const(), 1)
108+
self.failUnlessEqual(b.sym_reference_proc()(2,), 2)
87109

88110
def testILF_from_lines(self):
89111
lib = pg_lib.ILF.from_lines([l + '\n' for l in ilf.splitlines()])

0 commit comments

Comments
 (0)