Não pode escolher mais do que 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 

833 linhas
27 KiB

  1. """
  2. An extention to DBAPI-2.0 for more easily building SQL statements.
  3. This extension allows you to call a DBAPI Cursor's execute method with a string
  4. that contains format specifiers for escaped and/or unescaped arguments. Escaped
  5. arguments are specified using `` %X `` or `` %S `` (capital X or capital S).
  6. You can also mix positional and keyword arguments in the call, and this takes
  7. advantage of the Python call syntax niceties. Also, lists passed in as
  8. parameters to be formatted are automatically detected and joined by commas (this
  9. works for both unescaped and escaped parameters-- lists to be escaped have their
  10. elements escaped individually). In addition, if you pass in a dictionary
  11. corresponding to an escaped formatting specifier, the dictionary is rendered as
  12. a list of comma-separated <key> = <value> pairs, such as are suitable for an
  13. INSERT statement.
  14. For performance, the results of analysing and preparing the query is kept in a
  15. cache and reused on subsequence calls, similarly to the re or struct library.
  16. (This is intended to become a reference implementation for a proposal for an
  17. extension to tbe DBAPI-2.0.)
  18. .. note:: for now the transformation only works with DBAPIs that supports
  19. parametric arguments in the form of Python's syntax for now
  20. (e.g. psycopg2). It could easily be extended to support other DBAPI
  21. syntaxes.
  22. For more details and motivation, see the accompanying explanation document at
  23. http://furius.ca/pubcode/pub/conf/lib/python/dbapiext.html
  24. 5-minute usage instructions:
  25. Run execute_f() with a cursor object and appropriate arguments::
  26. execute_f(cursor, ' SELECT %s FROM %(t)s WHERE id = %S ', cols, id, t=table)
  27. Ideally, we should be able to monkey-patch this method onto the cursor class
  28. of the DBAPI library (this may not be possible if it is an extension module).
  29. By default, the result of analyzing each query is cached automatically and
  30. reused on further invocations, to minimize the amount of analysis to be
  31. performed at runtime. If you want to do this explicitly, first compile your
  32. query, and execute it later with the resulting object, e.g.::
  33. analq = qcompile(' SELECT %s FROM %(t)s WHERE id = %S ')
  34. ...
  35. analq.execute(cursor, cols, id, t=table)
  36. **Note to developers: this module contains tests, if you make any changes,
  37. please make sure to run and fix the tests.**
  38. Also, a formatting specifier is provided for where clauses: ``%A``, which joins
  39. its contained entries with ``AND``. The only accepted data types are list of
  40. pairs or a dictionary. Maybe we could provide an OR version (``%A`` and
  41. ``%O``).
  42. Future Work
  43. ===========
  44. - We could provide a reduce() method on the QueryAnalyzer, that will apply the
  45. given parameters and save the calculated arguments for later use; This would
  46. allow us to apply queries using multiple calls, to fill in only certain
  47. parameters at a time. This method would return a new QueryAnalyzer, albeit
  48. one that would contain some pre-cooked apply_kwds and delay_kwds to be
  49. accumulated to in the apply call.
  50. - Provide a simple test function that would allow people to test their queries
  51. without having to create a TestCursor.
  52. """
  53. # stdlib imports
  54. import re
  55. from datetime import date, datetime
  56. from itertools import starmap
  57. from itertools import count
  58. from pprint import pprint
  59. # These imports only work in Python 2.x, but the built-ins are fine in 3.x.
  60. try:
  61. from itertools import imap
  62. from itertools import izip
  63. except ImportError:
  64. imap = map
  65. izip = zip
  66. # The first module is Python 2.x, the second is 3.x.
  67. try:
  68. from StringIO import StringIO
  69. except ImportError:
  70. from io import StringIO
  71. __all__ = ('execute_f', 'qcompile', 'set_paramstyle', 'execute_obj')
  72. # Create aliases for Python 3.x compatibility
  73. try:
  74. xrange
  75. except NameError:
  76. xrange = range
  77. try:
  78. unicode
  79. except NameError:
  80. unicode = str
  81. # Convenince function since Python 3.x has new syntax for next
  82. def _next(i):
  83. try:
  84. return i.next()
  85. except AttributeError:
  86. return next(i)
  87. # Ditto for dictionary iterators
  88. def _iteritems(d):
  89. try:
  90. return d.iteritems()
  91. except AttributeError:
  92. return d.items()
  93. def _iterkeys(d):
  94. try:
  95. return d.iterkeys()
  96. except AttributeError:
  97. return d.keys()
  98. class QueryAnalyzer(object):
  99. """
  100. Analyze and contain a query string in a way that we can quickly put it back
  101. together when given the actual arguments. This object contains knowledge of
  102. which arguments are positional and keyword, and is able to conditionally
  103. apply escaping when necessary, and expand lists as well.
  104. This is meant to be kept around or cached for efficiency.
  105. """
  106. # Note: the last few formatting characters are extra, from us.
  107. re_fmt = '[#0 +-]?([0-9]+|\\*)?(\\.[0-9]*)?[hlL]?[diouxXeEfFgGcrsSAO]'
  108. regexp = re.compile('%%(\\(([a-zA-Z0-9_]+)\\))?(%s)' % re_fmt)
  109. def __init__(self, query, paramstyle=None):
  110. self.orig_query = query
  111. self.positional = []
  112. """List of positional arguments to be consumed later. The list consists
  113. of keynames."""
  114. self.components = None
  115. "A sequence of strings or match objects."
  116. if paramstyle is None:
  117. paramstyle = _def_paramstyle
  118. self.paramstyle = paramstyle
  119. self.init_style(paramstyle)
  120. "The parameter style supported by the underlying DBAPI."
  121. self.analyze() # Initialize.
  122. def init_style(self, paramstyle):
  123. "Pre-calculate style-specific constants."
  124. if paramstyle == 'pyformat':
  125. self.style_fmt = '%%%%(%(name)s)s'
  126. self.style_argstype = dict
  127. elif paramstyle == 'named':
  128. self.style_fmt = ':%(name)s'
  129. self.style_argstype = dict
  130. elif paramstyle == 'qmark':
  131. self.style_fmt = '?'
  132. self.style_argstype = list
  133. elif paramstyle == 'format':
  134. self.style_fmt = '%%%%s'
  135. self.style_argstype = list
  136. elif paramstyle == 'numeric':
  137. self.style_fmt = ':%(no)d'
  138. self.style_argstype = list
  139. # Non-standard. For our modified Sybase (from 0.37).
  140. elif paramstyle == 'atnamed':
  141. self.style_fmt = '@%(name)s'
  142. self.style_argstype = dict
  143. else:
  144. raise ValueError(
  145. "Parameter style '%s' is not supported." % paramstyle)
  146. def analyze(self):
  147. query = self.orig_query
  148. poscount = count(1)
  149. comps = self.components = []
  150. for x in gensplit(self.regexp, query):
  151. if isinstance(x, (str, unicode)):
  152. comps.append(x)
  153. else:
  154. keyname, fmt = x.group(2, 3)
  155. if keyname is None:
  156. keyname = '__p%d' % _next(poscount)
  157. self.positional.append(keyname)
  158. sep = ', '
  159. if fmt in 'XS':
  160. fmt = 's'
  161. escaped = True
  162. elif fmt in 'A':
  163. fmt = 's'
  164. escaped = True
  165. sep = ' AND '
  166. elif fmt in 'O':
  167. fmt = 's'
  168. escaped = True
  169. sep = ' OR '
  170. else:
  171. escaped = False
  172. comps.append( (keyname, escaped, sep, fmt) )
  173. def __str__(self):
  174. """
  175. Return the string that would be used before application of the
  176. positional and keyword arguments.
  177. """
  178. style_fmt = self.style_fmt
  179. oss = StringIO()
  180. no = count(1)
  181. for x in self.components:
  182. if isinstance(x, (str, unicode)):
  183. oss.write(x)
  184. else:
  185. keyname, escaped, sep, fmt = x
  186. if escaped:
  187. oss.write(style_fmt % {'name': keyname,
  188. 'no': _next(no)})
  189. else:
  190. oss.write('%%(%s)%s' % (keyname, fmt))
  191. return oss.getvalue()
  192. def apply(self, *args, **kwds):
  193. if len(args) != len(self.positional):
  194. raise TypeError('not enough arguments for format string')
  195. # Merge the positional arguments in the keywords dict.
  196. for name, value in izip(self.positional, args):
  197. assert name not in kwds
  198. kwds[name] = value
  199. # Patch up the components into a string.
  200. listexpans = {} # cached list expansions.
  201. apply_kwds, delay_kwds = {}, self.style_argstype()
  202. no = count(1)
  203. style_fmt = self.style_fmt
  204. dict_fmt = '%%(key)s = %s' % style_fmt
  205. output = []
  206. for x in self.components:
  207. if isinstance(x, (str, unicode)):
  208. out = x
  209. else:
  210. keyname, escaped, sep, fmt = x
  211. # Split keyword lists.
  212. # Expand into lists of words.
  213. value = kwds[keyname]
  214. if isinstance(value, (tuple, list, set)):
  215. try:
  216. words = listexpans[keyname] # Try cache.
  217. except KeyError:
  218. # Compute list expansion.
  219. words = ['%s_l%d__' % (keyname, x)
  220. for x in xrange(len(value))]
  221. listexpans[keyname] = words
  222. if escaped:
  223. outfmt = [style_fmt %
  224. {'name': x, 'no': _next(no)} for x in words]
  225. else:
  226. outfmt = ['%%(%s)%s' % (x, fmt) for x in words]
  227. elif isinstance(value, dict):
  228. # If a dict is passed in, the format specified *must* be for
  229. # escape; we detect this and raise an appropriate error.
  230. if not escaped:
  231. raise ValueError("Attempting to format a dict in "
  232. "an SQL statement without escaping.")
  233. # Convert dict in a list of comma-separated 'name=value' pairs.
  234. items = list(value.items())
  235. words = ['%s_key_%s__' % (keyname, x[0]) for x in items]
  236. value = [x[1] for x in items]
  237. outfmt = [dict_fmt % {'key': k, 'name': word}
  238. for word, (k, v) in izip(words, items)]
  239. else:
  240. words, value = (keyname,), (value,)
  241. if escaped:
  242. outfmt = [style_fmt % {'name': keyname, 'no': _next(no)}]
  243. else:
  244. outfmt = ['%%(%s)%s' % (keyname, fmt)]
  245. if escaped:
  246. okwds = delay_kwds
  247. else:
  248. okwds = apply_kwds
  249. # Dispatch values on the appropriate output dictionary.
  250. assert len(words) == len(value)
  251. if isinstance(okwds, dict):
  252. okwds.update(izip(words, value))
  253. else:
  254. okwds.extend(value)
  255. # Create formatting string.
  256. out = sep.join(outfmt)
  257. output.append(out)
  258. # Apply positional arguments, here, now.
  259. newquery = ''.join(output)
  260. # Return the string with the delayed arguments as formatting specifiers,
  261. # to be formatted by DBAPI, and the delayed arguments.
  262. return newquery % apply_kwds, delay_kwds
  263. def execute(self, cursor_, *args, **kwds):
  264. """
  265. Execute the analyzed query on the given cursor, with the given arguments
  266. and keywords.
  267. """
  268. # Translate this call into a compatible call to execute().
  269. cquery, ckwds = self.apply(*args, **kwds)
  270. # Execute the transformed query.
  271. return cursor_.execute(cquery, ckwds)
  272. def gensplit(regexp, s):
  273. """
  274. Regexp-splitter generator. Generates strings and match objects.
  275. """
  276. c = 0
  277. for mo in regexp.finditer(s):
  278. yield s[c:mo.start()]
  279. yield mo
  280. c = mo.end()
  281. yield s[c:]
  282. _def_paramstyle = 'pyformat'
  283. def set_paramstyle(style_or_dbapi):
  284. """
  285. Sets the default paramstyle to be used by the underlying DBAPI.
  286. You can pass in a DBAPI module object or a string. See PEP249 for details.
  287. """
  288. global _def_paramstyle
  289. if isinstance(style_or_dbapi, str):
  290. _def_paramstyle = style_or_dbapi
  291. else:
  292. _def_paramstyle = style_or_dbapi.paramstyle
  293. assert _def_paramstyle in ('qmark', 'numeric',
  294. 'named', 'format', 'pyformat')
  295. qcompile = QueryAnalyzer
  296. """
  297. Compile a query in a compatible query analyzer.
  298. """
  299. # Query cache used to avoid having to analyze the same queries multiple times.
  300. # Hashed on the query string.
  301. _query_cache = {}
  302. # Note: we use cursor_ and query_ because we often call this function with
  303. # vars() which include those names on the caller side.
  304. def execute_f(cursor_, query_, *args, **kwds):
  305. """
  306. Fancy execute method for a cursor. (Note: this is implemented as a function
  307. but is really meant to be a method to replace or complement the standard
  308. method Cursor.execute() from DBAPI-2.0.)
  309. Convert fancy query arguments into a DBAPI-compatible set of arguments and
  310. execute.
  311. This method supports a different syntax than the DBAPI execute() method:
  312. - By default, %s placeholders are not escaped.
  313. - Use the %S or %(name)S placeholder to specify escaped strings.
  314. - You can specify positional arguments without having to place them in an
  315. extra tuple.
  316. - Keyword arguments are used as expected to fill in missing values.
  317. Positional arguments are used to fill non-keyword placeholders.
  318. - Arguments that are tuples, lists or sets will be automatically joined by colons.
  319. If the corresponding formatting is %S or %(name)S, the members of the
  320. sequence will be escaped individually.
  321. See qcompile() for details.
  322. Note that this function accepts a '_paramstyle' optional argument, to set
  323. which parameter style to use.
  324. """
  325. debug = debug_convert or kwds.pop('__debug__', None)
  326. if debug:
  327. print('\n' + '=' * 80)
  328. print('\noriginal =')
  329. print(query_)
  330. print('\nargs =')
  331. pprint(args)
  332. print('\nkwds =')
  333. pprint(kwds)
  334. # Get the cached query analyzer or create one.
  335. try:
  336. q = _query_cache[query_]
  337. except KeyError:
  338. _query_cache[query_] = q = qcompile(
  339. query_,
  340. paramstyle=kwds.pop('paramstyle', None))
  341. if debug:
  342. print('\nquery analyzer =', str(q))
  343. # Translate this call into a compatible call to execute().
  344. cquery, ckwds = q.apply(*args, **kwds)
  345. if debug:
  346. print('\ntransformed =')
  347. print(cquery)
  348. print('\nnewkwds =')
  349. pprint(ckwds)
  350. # Execute the transformed query.
  351. return cursor_.execute(cquery, ckwds)
  352. # Add support for ntuple wrapping (std in 2.6).
  353. try:
  354. from collections import namedtuple
  355. # Patch from Catherine Devlin <catherine dot devlin at gmail dot com>:
  356. #
  357. # "Column names with ``$`` and ``#`` are legal in SQL, but not in
  358. # namedtuple field names. This throws exceptions when you try to
  359. # execute_obj on queries with such column names. For the apps I write
  360. # (rooting around in Oracle data dictionary views), there's no avoiding
  361. # the ``$`` and ``#`` characters. Therefore, I added code to munge column
  362. # names until they are namedtuple-legal. Another alternative would be to
  363. # simply change the error message raised into something that would suggest
  364. # that the user use column aliases in the SQL statement to change column
  365. # names into something namedtuple-legal." (2010-05-25)
  366. from collections import _iskeyword
  367. not_alphanumeric = re.compile('[^a-zA-Z0-9]')
  368. def rename_duplicates(lst, append_char = '_'):
  369. newlist = []
  370. for itm in lst:
  371. while itm in newlist:
  372. itm += append_char
  373. newlist.append(itm)
  374. return newlist
  375. def _fix_fieldname(fieldname):
  376. "Ensure that a field name will pass collection.namedtuple's criteria."
  377. fieldname = not_alphanumeric.sub('_', fieldname)
  378. while _iskeyword(fieldname):
  379. fieldname = fieldname + '_'
  380. return fieldname
  381. def ntuple(typename, field_names, verbose=False):
  382. field_names = [_fix_fieldname(fn) for fn in field_names.split()]
  383. field_names = rename_duplicates(field_names)
  384. return namedtuple(typename, ' '.join(field_names), verbose)
  385. except ImportError:
  386. ntuple = None
  387. if ntuple:
  388. from operator import itemgetter
  389. def execute_obj(conn, *args, **kwds):
  390. """
  391. Run a query on the given connection or cursor and yield ntuples of the
  392. results. 'curs' can be either a Connection or a Cursor object.
  393. """
  394. # Convert to a cursor if necessary.
  395. if re.search('Cursor', conn.__class__.__name__, re.I):
  396. curs = conn
  397. else:
  398. curs = conn.cursor()
  399. # Execute the query.
  400. execute_f(curs, *args, **kwds)
  401. # Yield all the results wrapped up in an ntuple.
  402. names = list(map(itemgetter(0), curs.description))
  403. TupleCls = ntuple('Row', ' '.join(names))
  404. return starmap(TupleCls, imap(tuple, curs))
  405. else:
  406. execute_obj = None
  407. #-------------------------------------------------------------------------------
  408. class _TestCursor(object):
  409. """
  410. Fake cursor that fakes the escaped replacments like a real DBAPI cursor, but
  411. simply returns the final string.
  412. """
  413. execute_f = execute_f
  414. def execute(self, query, args):
  415. return self.render_fake(query, args).strip()
  416. @staticmethod
  417. def render_fake(query, kwds):
  418. """
  419. Take arguments as the DBAPI of execute() accepts and fake escaping the
  420. arguments as the DBAPI implementation would and return the resulting
  421. string. This is used only for testing, to make testing easier and more
  422. intuitive, to view the completed queries without the replacement
  423. variables.
  424. """
  425. for key, value in list(kwds.items()):
  426. if isinstance(value, type(None)):
  427. kwds[key] = 'NULL'
  428. elif isinstance(value, str):
  429. kwds[key] = repr(value)
  430. elif isinstance(value, unicode):
  431. kwds[key] = repr(value.encode('utf-8'))
  432. elif isinstance(value, (date, datetime)):
  433. kwds[key] = repr(value.isoformat())
  434. result = query % kwds
  435. if debug_convert:
  436. print('\n--- 5. after full replacement (fake dbapi application)')
  437. print(result)
  438. return result
  439. def _multi2one(s):
  440. "Join a multi-line string in a single line."
  441. s = re.sub('[ \n]+', ' ', s).strip()
  442. return re.sub(', ', ',', s)
  443. import unittest
  444. class TestExtension(unittest.TestCase):
  445. """
  446. Tests for the extention functions.
  447. """
  448. def compare_nows(self, s1, s2):
  449. """
  450. Compare two strings without considering the whitespace.
  451. """
  452. s1 = _multi2one(s1)
  453. s2 = _multi2one(s2)
  454. self.assertEquals(s1, s2)
  455. def test_basic(self):
  456. "Basic replacement tests."
  457. cursor = _TestCursor()
  458. simple, isimple, seq = 'SIMPLE', 42, ('L1', 'L2', 'L3')
  459. for query, args, kwds, expect in (
  460. # With simple arguments.
  461. (' %s ', (simple,), dict(), " SIMPLE "),
  462. (' %S ', (simple,), dict(), " 'SIMPLE' "),
  463. (' %X ', (simple,), dict(), " 'SIMPLE' "),
  464. (' %d ', (isimple,), dict(), " 42 "),
  465. (' %(k)s ', (), dict(k=simple), " SIMPLE "),
  466. (' %(k)d ', (), dict(k=isimple), " 42 "),
  467. (' %(k)S ', (), dict(k=simple), " 'SIMPLE' "),
  468. (' %(k)X ', (), dict(k=simple), " 'SIMPLE' "),
  469. # Same but with lists.
  470. (' %s ', (seq,), dict(), " L1,L2,L3 "),
  471. (' %S ', (seq,), dict(), " 'L1','L2','L3' "),
  472. (' %X ', (seq,), dict(), " 'L1','L2','L3' "),
  473. (' %(k)s ', (), dict(k=seq), " L1,L2,L3 "),
  474. (' %(k)S ', (), dict(k=seq), " 'L1','L2','L3' "),
  475. (' %(k)X ', (), dict(k=seq), " 'L1','L2','L3' "),
  476. ):
  477. # Normal invocation.
  478. self.compare_nows(
  479. cursor.execute_f(query, *args, **kwds),
  480. expect)
  481. # Repeated destination formatting string.
  482. self.compare_nows(
  483. cursor.execute_f(query + query, *(args + args) , **kwds),
  484. expect + expect)
  485. def test_misc(self):
  486. d = date(2006, 7, 28)
  487. cursor = _TestCursor()
  488. self.compare_nows(
  489. cursor.execute_f('''
  490. INSERT INTO %(table)s (%s)
  491. SET VALUES (%S)
  492. WHERE id = %(id)S
  493. AND name IN (%(name)S)
  494. AND name NOT IN (%(name)S)
  495. ''',
  496. ('col1', 'col2'),
  497. (42, "bli"),
  498. id="02351440-7b7e-4260",
  499. name=[45, 56, 67, 78],
  500. table='table'),
  501. """
  502. INSERT INTO table (col1, col2)
  503. SET VALUES (42, 'bli')
  504. WHERE id = '02351440-7b7e-4260'
  505. AND name IN (45, 56, 67, 78)
  506. AND name NOT IN (45, 56, 67, 78)
  507. """)
  508. # Note: this should fail in the old text.
  509. self.compare_nows(
  510. cursor.execute_f(''' %(id)s AND %(id)S ''',
  511. id=['fulano', 'mengano']),
  512. """ fulano,mengano AND 'fulano','mengano' """)
  513. self.compare_nows(
  514. cursor.execute_f('''
  515. SELECT %s FROM %s WHERE id = %S
  516. ''',
  517. ('id', 'name', 'title'), 'books',
  518. '02351440-7b7e-4260'),
  519. """SELECT id,name,title FROM books
  520. WHERE id = '02351440-7b7e-4260'""")
  521. self.compare_nows(
  522. cursor.execute_f('''
  523. SELECT %s FROM %s WHERE id = %(id)S %(id)S
  524. ''', ('id', 'name', 'title'), 'books', id=d),
  525. """SELECT id,name,title FROM books
  526. WHERE id = '2006-07-28' '2006-07-28'""")
  527. self.compare_nows(
  528. cursor.execute_f(''' %(id)S %(id)S ''', id='02351440-7b7e-4260'),
  529. " '02351440-7b7e-4260' '02351440-7b7e-4260' ")
  530. self.compare_nows(
  531. cursor.execute_f(''' %s %(id)S %(id)s ''',
  532. 'books',
  533. id='02351440-7b7e-4260'),
  534. " books '02351440-7b7e-4260' 02351440-7b7e-4260 ")
  535. self.compare_nows(
  536. cursor.execute_f('''
  537. SELECT %s FROM %(table)s WHERE col1 = %S AND col2 < %(val)S
  538. ''', ('col1', 'col2', 'col3'), 'value1', table='my-table', val=42),
  539. """ SELECT col1,col2,col3 FROM my-table
  540. WHERE col1 = 'value1' AND col2 < 42 """)
  541. self.compare_nows(
  542. cursor.execute_f("""
  543. INSERT INTO thumbnails
  544. (basename, photo1, photo2, photo3)
  545. VALUES (%S, %S)
  546. """, 'PHOTONAME', ('BIN1', 'BIN2', 'BIN3')),
  547. """
  548. INSERT INTO thumbnails
  549. (basename, photo1, photo2, photo3)
  550. VALUES ('PHOTONAME', 'BIN1', 'BIN2', 'BIN3')
  551. """)
  552. def test_null(self):
  553. cursor = _TestCursor()
  554. self.compare_nows(
  555. cursor.execute_f('''
  556. INSERT INTO poodle (hair)
  557. SET VALUES (%S)
  558. ''', None),
  559. """
  560. INSERT INTO poodle (hair)
  561. SET VALUES (NULL)
  562. """)
  563. def test_paramstyles(self):
  564. d = date(2006, 7, 28)
  565. cursor = _TestCursor()
  566. query = '''
  567. Simple: %s Escaped: %S
  568. Kwd: %(bli)s KwdEscaped: %(bli)S
  569. '''
  570. args = ('hansel', 'gretel')
  571. kwds = dict(bli='bethel')
  572. test_data = {
  573. 'pyformat': ("""
  574. Simple: hansel Escaped: %(__p2)s
  575. Kwd: bethel KwdEscaped: %(bli)s
  576. """, {'__p2': 'gretel', 'bli': 'bethel'}),
  577. 'named': ("""
  578. Simple: hansel Escaped: :__p2
  579. Kwd: bethel KwdEscaped: :bli
  580. """, {'__p2': 'gretel', 'bli': 'bethel'}),
  581. 'qmark': ("""
  582. Simple: hansel Escaped: ?
  583. Kwd: bethel KwdEscaped: ?
  584. """, ['gretel', 'bethel']),
  585. 'format': ("""
  586. Simple: hansel Escaped: %s
  587. Kwd: bethel KwdEscaped: %s
  588. """, ['gretel', 'bethel']),
  589. 'numeric': ("""
  590. Simple: hansel Escaped: :1
  591. Kwd: bethel KwdEscaped: :2
  592. """, ['gretel', 'bethel']),
  593. }
  594. for style, (estr, eargs) in _iteritems(test_data):
  595. qstr, qargs = qcompile(query, paramstyle=style).apply(
  596. *args, **kwds)
  597. self.compare_nows(qstr, estr)
  598. self.assertEquals(qargs, eargs)
  599. # Visual debugging.
  600. print_it = 0
  601. for style in _iterkeys(test_data):
  602. qanal = qcompile("""
  603. %S %(c1)S %S %S %(c2)S
  604. """, paramstyle=style)
  605. qstr, qargs = qanal.apply(1, 2, 3, c1='CC1', c2='CC2')
  606. if print_it:
  607. print(qstr)
  608. print(qargs)
  609. def test_dict(self):
  610. "Tests for passing in a dictionary argument."
  611. cursor = _TestCursor()
  612. data = {'brazil': 'portuguese',
  613. 'peru': 'spanish',
  614. 'japan': 'japanese',
  615. 'philipines': 'tagalog'}
  616. self.assertRaises(ValueError, execute_f,
  617. cursor, ' unescaped: %s ', data)
  618. res = execute_f(cursor, ' UPDATE %s SET %S; ', 'mytable', data)
  619. self.compare_nows(res, """
  620. UPDATE mytable
  621. SET brazil = 'portuguese',
  622. japan = 'japanese',
  623. philipines = 'tagalog',
  624. peru = 'spanish'; """)
  625. def test_and(self):
  626. "Tests for passing in a dictionary argument."
  627. cursor = _TestCursor()
  628. keydata = {'udid': '11111111111111111111',
  629. 'imgid': 17}
  630. valuedata = {'rating': 9}
  631. self.assertRaises(ValueError, execute_f,
  632. cursor, ' unescaped: %s ', keydata)
  633. res = execute_f(cursor, ' UPDATE %s SET %S WHERE %A; ', 'mytable',
  634. valuedata, keydata)
  635. self.compare_nows(res, """
  636. UPDATE mytable
  637. SET rating = 9
  638. WHERE udid = '11111111111111111111' AND imgid = 17;
  639. """)
  640. res = execute_f(cursor, ' UPDATE %s SET %S WHERE %O; ', 'mytable',
  641. valuedata, keydata)
  642. self.compare_nows(res, """
  643. UPDATE mytable
  644. SET rating = 9
  645. WHERE udid = '11111111111111111111' OR imgid = 17;
  646. """)
  647. def test_sqlite3(self):
  648. import sqlite3 as dbapi
  649. set_paramstyle(dbapi)
  650. conn = dbapi.connect(':memory:')
  651. curs = conn.cursor()
  652. execute_f(curs, """
  653. CREATE TABLE books (
  654. author TEXT,
  655. title TEXT,
  656. PRIMARY KEY (title)
  657. );
  658. """)
  659. execute_f(curs, """
  660. INSERT INTO books VALUES (%S);
  661. """, ("Tolstoy", "War and Peace"))
  662. execute_f(curs, """
  663. INSERT INTO books (author) VALUES (%S);
  664. """, "Dostoyesvki")
  665. debug_convert = 0
  666. if __name__ == '__main__':
  667. unittest.main() # or use nosetests