PageRenderTime 60ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 1ms

/r2/r2/lib/db/tdb_sql.py

https://github.com/stevewilber/reddit
Python | 1011 lines | 958 code | 8 blank | 45 comment | 1 complexity | 586d88630cd36e8c5429f92f2c5cbc76 MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, Apache-2.0
  1. # The contents of this file are subject to the Common Public Attribution
  2. # License Version 1.0. (the "License"); you may not use this file except in
  3. # compliance with the License. You may obtain a copy of the License at
  4. # http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
  5. # License Version 1.1, but Sections 14 and 15 have been added to cover use of
  6. # software over a computer network and provide for limited attribution for the
  7. # Original Developer. In addition, Exhibit A has been modified to be consistent
  8. # with Exhibit B.
  9. #
  10. # Software distributed under the License is distributed on an "AS IS" basis,
  11. # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
  12. # the specific language governing rights and limitations under the License.
  13. #
  14. # The Original Code is reddit.
  15. #
  16. # The Original Developer is the Initial Developer. The Initial Developer of
  17. # the Original Code is reddit Inc.
  18. #
  19. # All portions of the code written by reddit are Copyright (c) 2006-2012 reddit
  20. # Inc. All Rights Reserved.
  21. ###############################################################################
  22. from datetime import datetime
  23. import cPickle as pickle
  24. from copy import deepcopy
  25. import random
  26. import threading
  27. import sqlalchemy as sa
  28. from sqlalchemy.dialects import postgres
  29. from r2.lib.utils import storage, storify, iters, Results, tup
  30. import operators
  31. from pylons import g, c
  32. dbm = g.dbm
  33. predefined_type_ids = g.predefined_type_ids
  34. import logging
  35. log_format = logging.Formatter('sql: %(message)s')
  36. max_val_len = 1000
  37. class TransactionSet(threading.local):
  38. """A manager for SQL transactions.
  39. This implements a thread local meta-transaction which may span multiple
  40. databases. The existing tdb_sql code calls add_engine before executing
  41. writes. If thing.py calls begin then these calls will actually kick in
  42. and start a transaction that must be committed or rolled back by thing.py.
  43. Because this involves creating transactions at the connection level, this
  44. system implicitly relies on using the threadlocal strategy for the
  45. sqlalchemy engines.
  46. This system is a bit awkward, and should be replaced with something that
  47. doesn't use module-globals when doing a cleanup of tdb_sql.
  48. """
  49. def __init__(self):
  50. self.transacting_engines = set()
  51. self.transaction_begun = False
  52. def begin(self):
  53. """Indicate that a transaction has begun."""
  54. self.transaction_begun = True
  55. def add_engine(self, engine):
  56. """Add a database connection to the meta-transaction if active."""
  57. if not self.transaction_begun:
  58. return
  59. if engine not in self.transacting_engines:
  60. engine.begin()
  61. self.transacting_engines.add(engine)
  62. def commit(self):
  63. """Commit the meta-transaction."""
  64. try:
  65. for engine in self.transacting_engines:
  66. engine.commit()
  67. finally:
  68. self._clear()
  69. def rollback(self):
  70. """Roll back the meta-transaction."""
  71. try:
  72. for engine in self.transacting_engines:
  73. engine.rollback()
  74. finally:
  75. self._clear()
  76. def _clear(self):
  77. self.transacting_engines.clear()
  78. self.transaction_begun = False
  79. transactions = TransactionSet()
  80. MAX_THING_ID = 9223372036854775807 # http://www.postgresql.org/docs/8.3/static/datatype-numeric.html
  81. def make_metadata(engine):
  82. metadata = sa.MetaData(engine)
  83. metadata.bind.echo = g.sqlprinting
  84. return metadata
  85. def create_table(table, index_commands=None):
  86. t = table
  87. if g.db_create_tables:
  88. #@@hackish?
  89. if not t.bind.has_table(t.name):
  90. t.create(checkfirst = False)
  91. if index_commands:
  92. for i in index_commands:
  93. t.bind.execute(i)
  94. def index_str(table, name, on, where = None, unique = False):
  95. if unique:
  96. index_str = 'create unique index'
  97. else:
  98. index_str = 'create index'
  99. index_str += ' idx_%s_' % name
  100. index_str += table.name
  101. index_str += ' on '+ table.name + ' (%s)' % on
  102. if where:
  103. index_str += ' where %s' % where
  104. return index_str
  105. def index_commands(table, type):
  106. commands = []
  107. if type == 'thing':
  108. commands.append(index_str(table, 'id', 'thing_id'))
  109. commands.append(index_str(table, 'date', 'date'))
  110. commands.append(index_str(table, 'deleted_spam', 'deleted, spam'))
  111. commands.append(index_str(table, 'hot', 'hot(ups, downs, date), date'))
  112. commands.append(index_str(table, 'score', 'score(ups, downs), date'))
  113. commands.append(index_str(table, 'controversy', 'controversy(ups, downs), date'))
  114. elif type == 'data':
  115. commands.append(index_str(table, 'id', 'thing_id'))
  116. commands.append(index_str(table, 'thing_id', 'thing_id'))
  117. commands.append(index_str(table, 'key_value', 'key, substring(value, 1, %s)' \
  118. % max_val_len))
  119. #lower name
  120. commands.append(index_str(table, 'lower_key_value', 'key, lower(value)',
  121. where = "key = 'name'"))
  122. #ip
  123. commands.append(index_str(table, 'ip_network', 'ip_network(value)',
  124. where = "key = 'ip'"))
  125. #base_url
  126. commands.append(index_str(table, 'base_url', 'base_url(lower(value))',
  127. where = "key = 'url'"))
  128. elif type == 'rel':
  129. commands.append(index_str(table, 'thing1_name_date', 'thing1_id, name, date'))
  130. commands.append(index_str(table, 'thing2_name_date', 'thing2_id, name, date'))
  131. commands.append(index_str(table, 'thing1_id', 'thing1_id'))
  132. commands.append(index_str(table, 'thing2_id', 'thing2_id'))
  133. commands.append(index_str(table, 'name', 'name'))
  134. commands.append(index_str(table, 'date', 'date'))
  135. else:
  136. print "unknown index_commands() type %s" % type
  137. return commands
  138. def get_type_table(metadata):
  139. table = sa.Table(g.db_app_name + '_type', metadata,
  140. sa.Column('id', sa.Integer, primary_key = True),
  141. sa.Column('name', sa.String, nullable = False))
  142. return table
  143. def get_rel_type_table(metadata):
  144. table = sa.Table(g.db_app_name + '_type_rel', metadata,
  145. sa.Column('id', sa.Integer, primary_key = True),
  146. sa.Column('type1_id', sa.Integer, nullable = False),
  147. sa.Column('type2_id', sa.Integer, nullable = False),
  148. sa.Column('name', sa.String, nullable = False))
  149. return table
  150. def get_thing_table(metadata, name):
  151. table = sa.Table(g.db_app_name + '_thing_' + name, metadata,
  152. sa.Column('thing_id', sa.BigInteger, primary_key = True),
  153. sa.Column('ups', sa.Integer, default = 0, nullable = False),
  154. sa.Column('downs',
  155. sa.Integer,
  156. default = 0,
  157. nullable = False),
  158. sa.Column('deleted',
  159. sa.Boolean,
  160. default = False,
  161. nullable = False),
  162. sa.Column('spam',
  163. sa.Boolean,
  164. default = False,
  165. nullable = False),
  166. sa.Column('date',
  167. sa.DateTime(timezone = True),
  168. default = sa.func.now(),
  169. nullable = False))
  170. table.thing_name = name
  171. return table
  172. def get_data_table(metadata, name):
  173. data_table = sa.Table(g.db_app_name + '_data_' + name, metadata,
  174. sa.Column('thing_id', sa.BigInteger, nullable = False,
  175. primary_key = True),
  176. sa.Column('key', sa.String, nullable = False,
  177. primary_key = True),
  178. sa.Column('value', sa.String),
  179. sa.Column('kind', sa.String))
  180. return data_table
  181. def get_rel_table(metadata, name):
  182. rel_table = sa.Table(g.db_app_name + '_rel_' + name, metadata,
  183. sa.Column('rel_id', sa.BigInteger, primary_key = True),
  184. sa.Column('thing1_id', sa.BigInteger, nullable = False),
  185. sa.Column('thing2_id', sa.BigInteger, nullable = False),
  186. sa.Column('name', sa.String, nullable = False),
  187. sa.Column('date', sa.DateTime(timezone = True),
  188. default = sa.func.now(), nullable = False),
  189. sa.UniqueConstraint('thing1_id', 'thing2_id', 'name'))
  190. rel_table.rel_name = name
  191. return rel_table
  192. #get/create the type tables
  193. def make_type_table():
  194. metadata = make_metadata(dbm.type_db)
  195. table = get_type_table(metadata)
  196. create_table(table)
  197. return table
  198. type_table = make_type_table()
  199. def make_rel_type_table():
  200. metadata = make_metadata(dbm.relation_type_db)
  201. table = get_rel_type_table(metadata)
  202. create_table(table)
  203. return table
  204. rel_type_table = make_rel_type_table()
  205. #lookup dicts
  206. types_id = {}
  207. types_name = {}
  208. rel_types_id = {}
  209. rel_types_name = {}
  210. class ConfigurationError(Exception):
  211. pass
  212. def check_type(table, name, insert_vals):
  213. # before hitting the db, check if we can get the type id from
  214. # the ini file
  215. type_id = predefined_type_ids.get(name)
  216. if type_id:
  217. return type_id
  218. elif len(predefined_type_ids) > 0:
  219. # flip the hell out if only *some* of the type ids are defined
  220. raise ConfigurationError("Expected typeid for %s" % name)
  221. # check for type in type table, create if not existent
  222. r = table.select(table.c.name == name).execute().fetchone()
  223. if not r:
  224. r = table.insert().execute(**insert_vals)
  225. type_id = r.last_inserted_ids()[0]
  226. else:
  227. type_id = r.id
  228. return type_id
  229. #make the thing tables
  230. def build_thing_tables():
  231. for name, engines in dbm.things_iter():
  232. type_id = check_type(type_table,
  233. name,
  234. dict(name = name))
  235. tables = []
  236. for engine in engines:
  237. metadata = make_metadata(engine)
  238. #make thing table
  239. thing_table = get_thing_table(metadata, name)
  240. create_table(thing_table,
  241. index_commands(thing_table, 'thing'))
  242. #make data tables
  243. data_table = get_data_table(metadata, name)
  244. create_table(data_table,
  245. index_commands(data_table, 'data'))
  246. tables.append((thing_table, data_table))
  247. thing = storage(type_id = type_id,
  248. name = name,
  249. avoid_master_reads = dbm.avoid_master_reads.get(name),
  250. tables = tables)
  251. types_id[type_id] = thing
  252. types_name[name] = thing
  253. build_thing_tables()
  254. #make relation tables
  255. def build_rel_tables():
  256. for name, (type1_name, type2_name, engines) in dbm.rels_iter():
  257. type1_id = types_name[type1_name].type_id
  258. type2_id = types_name[type2_name].type_id
  259. type_id = check_type(rel_type_table,
  260. name,
  261. dict(name = name,
  262. type1_id = type1_id,
  263. type2_id = type2_id))
  264. tables = []
  265. for engine in engines:
  266. metadata = make_metadata(engine)
  267. #relation table
  268. rel_table = get_rel_table(metadata, name)
  269. create_table(rel_table, index_commands(rel_table, 'rel'))
  270. #make thing tables
  271. rel_t1_table = get_thing_table(metadata, type1_name)
  272. if type1_name == type2_name:
  273. rel_t2_table = rel_t1_table
  274. else:
  275. rel_t2_table = get_thing_table(metadata, type2_name)
  276. #build the data
  277. rel_data_table = get_data_table(metadata, 'rel_' + name)
  278. create_table(rel_data_table,
  279. index_commands(rel_data_table, 'data'))
  280. tables.append((rel_table,
  281. rel_t1_table,
  282. rel_t2_table,
  283. rel_data_table))
  284. rel = storage(type_id = type_id,
  285. type1_id = type1_id,
  286. type2_id = type2_id,
  287. avoid_master_reads = dbm.avoid_master_reads.get(name),
  288. name = name,
  289. tables = tables)
  290. rel_types_id[type_id] = rel
  291. rel_types_name[name] = rel
  292. build_rel_tables()
  293. def get_type_id(name):
  294. return types_name[name][0]
  295. def get_rel_type_id(name):
  296. return rel_types_name[name][0]
  297. def get_write_table(tables):
  298. if g.disallow_db_writes:
  299. raise Exception("not so fast! writes are not allowed on this app.")
  300. else:
  301. return tables[0]
  302. import re, traceback, cStringIO as StringIO
  303. _spaces = re.compile('[\s]+')
  304. def add_request_info(select):
  305. from pylons import request
  306. from r2.lib import filters
  307. def sanitize(txt):
  308. return _spaces.sub(' ', txt).replace("/", "|").replace("-", "_").replace(';', "").replace("*", "").replace(r"/", "")
  309. s = StringIO.StringIO()
  310. traceback.print_stack( file = s)
  311. tb = s.getvalue()
  312. if tb:
  313. tb = tb.split('\n')[0::2]
  314. tb = [x.split('/')[-1] for x in tb if "/r2/" in x]
  315. tb = '\n'.join(tb[-15:-2])
  316. try:
  317. if (hasattr(request, 'path') and
  318. hasattr(request, 'ip') and
  319. hasattr(request, 'user_agent')):
  320. comment = '/*\n%s\n%s\n%s\n*/' % (
  321. tb or "",
  322. filters._force_utf8(sanitize(request.fullpath)),
  323. sanitize(request.ip))
  324. return select.prefix_with(comment)
  325. except UnicodeDecodeError:
  326. pass
  327. return select
  328. def get_table(kind, action, tables, avoid_master_reads = False):
  329. if action == 'write':
  330. #if this is a write, store the kind in the c.use_write_db dict
  331. #so that all future requests use the write db
  332. if not isinstance(c.use_write_db, dict):
  333. c.use_write_db = {}
  334. c.use_write_db[kind] = True
  335. return get_write_table(tables)
  336. elif action == 'read':
  337. #check to see if we're supposed to use the write db again
  338. if c.use_write_db and c.use_write_db.has_key(kind):
  339. return get_write_table(tables)
  340. else:
  341. if avoid_master_reads and len(tables) > 1:
  342. return dbm.get_read_table(tables[1:])
  343. return dbm.get_read_table(tables)
  344. def get_thing_table(type_id, action = 'read' ):
  345. return get_table('t' + str(type_id), action,
  346. types_id[type_id].tables,
  347. avoid_master_reads = types_id[type_id].avoid_master_reads)
  348. def get_rel_table(rel_type_id, action = 'read'):
  349. return get_table('r' + str(rel_type_id), action,
  350. rel_types_id[rel_type_id].tables,
  351. avoid_master_reads = rel_types_id[rel_type_id].avoid_master_reads)
  352. #TODO does the type actually exist?
  353. def make_thing(type_id, ups, downs, date, deleted, spam, id=None):
  354. table = get_thing_table(type_id, action = 'write')[0]
  355. params = dict(ups = ups, downs = downs,
  356. date = date, deleted = deleted, spam = spam)
  357. if id:
  358. params['thing_id'] = id
  359. def do_insert(t):
  360. transactions.add_engine(t.bind)
  361. r = t.insert().execute(**params)
  362. new_id = r.last_inserted_ids()[0]
  363. new_r = r.last_inserted_params()
  364. for k, v in params.iteritems():
  365. if new_r[k] != v:
  366. raise CreationError, ("There's shit in the plumbing. " +
  367. "expected %s, got %s" % (params, new_r))
  368. return new_id
  369. try:
  370. id = do_insert(table)
  371. params['thing_id'] = id
  372. g.stats.event_count('thing.create', table.thing_name)
  373. return id
  374. except sa.exc.DBAPIError, e:
  375. if not 'IntegrityError' in e.message:
  376. raise
  377. # wrap the error to prevent db layer bleeding out
  378. raise CreationError, "Thing exists (%s)" % str(params)
  379. def set_thing_props(type_id, thing_id, **props):
  380. table = get_thing_table(type_id, action = 'write')[0]
  381. if not props:
  382. return
  383. #use real columns
  384. def do_update(t):
  385. transactions.add_engine(t.bind)
  386. new_props = dict((t.c[prop], val) for prop, val in props.iteritems())
  387. u = t.update(t.c.thing_id == thing_id, values = new_props)
  388. u.execute()
  389. do_update(table)
  390. def incr_thing_prop(type_id, thing_id, prop, amount):
  391. table = get_thing_table(type_id, action = 'write')[0]
  392. def do_update(t):
  393. transactions.add_engine(t.bind)
  394. u = t.update(t.c.thing_id == thing_id,
  395. values={t.c[prop] : t.c[prop] + amount})
  396. u.execute()
  397. do_update(table)
  398. class CreationError(Exception): pass
  399. #TODO does the type exist?
  400. #TODO do the things actually exist?
  401. def make_relation(rel_type_id, thing1_id, thing2_id, name, date=None):
  402. table = get_rel_table(rel_type_id, action = 'write')[0]
  403. transactions.add_engine(table.bind)
  404. if not date: date = datetime.now(g.tz)
  405. try:
  406. r = table.insert().execute(thing1_id = thing1_id,
  407. thing2_id = thing2_id,
  408. name = name,
  409. date = date)
  410. g.stats.event_count('rel.create', table.rel_name)
  411. return r.last_inserted_ids()[0]
  412. except sa.exc.DBAPIError, e:
  413. if not 'IntegrityError' in e.message:
  414. raise
  415. # wrap the error to prevent db layer bleeding out
  416. raise CreationError, "Relation exists (%s, %s, %s)" % (name, thing1_id, thing2_id)
  417. def set_rel_props(rel_type_id, rel_id, **props):
  418. t = get_rel_table(rel_type_id, action = 'write')[0]
  419. if not props:
  420. return
  421. #use real columns
  422. transactions.add_engine(t.bind)
  423. new_props = dict((t.c[prop], val) for prop, val in props.iteritems())
  424. u = t.update(t.c.rel_id == rel_id, values = new_props)
  425. u.execute()
  426. def py2db(val, return_kind=False):
  427. if isinstance(val, bool):
  428. val = 't' if val else 'f'
  429. kind = 'bool'
  430. elif isinstance(val, (str, unicode)):
  431. kind = 'str'
  432. elif isinstance(val, (int, float, long)):
  433. kind = 'num'
  434. elif val is None:
  435. kind = 'none'
  436. else:
  437. kind = 'pickle'
  438. val = pickle.dumps(val)
  439. if return_kind:
  440. return (val, kind)
  441. else:
  442. return val
  443. def db2py(val, kind):
  444. if kind == 'bool':
  445. val = True if val is 't' else False
  446. elif kind == 'num':
  447. try:
  448. val = int(val)
  449. except ValueError:
  450. val = float(val)
  451. elif kind == 'none':
  452. val = None
  453. elif kind == 'pickle':
  454. val = pickle.loads(val)
  455. return val
  456. def update_data(table, thing_id, **vals):
  457. transactions.add_engine(table.bind)
  458. u = table.update(sa.and_(table.c.thing_id == thing_id,
  459. table.c.key == sa.bindparam('_key')))
  460. inserts = []
  461. for key, val in vals.iteritems():
  462. val, kind = py2db(val, return_kind=True)
  463. uresult = u.execute(_key = key, value = val, kind = kind)
  464. if not uresult.rowcount:
  465. inserts.append({'key':key, 'value':val, 'kind': kind})
  466. #do one insert
  467. if inserts:
  468. i = table.insert(values = dict(thing_id = thing_id))
  469. i.execute(*inserts)
  470. def create_data(table, thing_id, **vals):
  471. transactions.add_engine(table.bind)
  472. inserts = []
  473. for key, val in vals.iteritems():
  474. val, kind = py2db(val, return_kind=True)
  475. inserts.append(dict(key=key, value=val, kind=kind))
  476. if inserts:
  477. i = table.insert(values=dict(thing_id=thing_id))
  478. i.execute(*inserts)
  479. def incr_data_prop(table, type_id, thing_id, prop, amount):
  480. t = table
  481. transactions.add_engine(t.bind)
  482. u = t.update(sa.and_(t.c.thing_id == thing_id,
  483. t.c.key == prop),
  484. values={t.c.value : sa.cast(t.c.value, sa.Float) + amount})
  485. u.execute()
  486. def fetch_query(table, id_col, thing_id):
  487. """pull the columns from the thing/data tables for a list or single
  488. thing_id"""
  489. single = False
  490. if not isinstance(thing_id, iters):
  491. single = True
  492. thing_id = (thing_id,)
  493. s = sa.select([table], id_col.in_(thing_id))
  494. try:
  495. r = add_request_info(s).execute().fetchall()
  496. except Exception, e:
  497. dbm.mark_dead(table.bind)
  498. # this thread must die so that others may live
  499. raise
  500. return (r, single)
  501. #TODO specify columns to return?
  502. def get_data(table, thing_id):
  503. r, single = fetch_query(table, table.c.thing_id, thing_id)
  504. #if single, only return one storage, otherwise make a dict
  505. res = storage() if single else {}
  506. for row in r:
  507. val = db2py(row.value, row.kind)
  508. stor = res if single else res.setdefault(row.thing_id, storage())
  509. if single and row.thing_id != thing_id:
  510. raise ValueError, ("tdb_sql.py: there's shit in the plumbing."
  511. + " got %s, wanted %s" % (row.thing_id,
  512. thing_id))
  513. stor[row.key] = val
  514. return res
  515. def set_thing_data(type_id, thing_id, brand_new_thing, **vals):
  516. table = get_thing_table(type_id, action = 'write')[1]
  517. if brand_new_thing:
  518. return create_data(table, thing_id, **vals)
  519. else:
  520. return update_data(table, thing_id, **vals)
  521. def incr_thing_data(type_id, thing_id, prop, amount):
  522. table = get_thing_table(type_id, action = 'write')[1]
  523. return incr_data_prop(table, type_id, thing_id, prop, amount)
  524. def get_thing_data(type_id, thing_id):
  525. table = get_thing_table(type_id)[1]
  526. return get_data(table, thing_id)
  527. def get_thing(type_id, thing_id):
  528. table = get_thing_table(type_id)[0]
  529. r, single = fetch_query(table, table.c.thing_id, thing_id)
  530. #if single, only return one storage, otherwise make a dict
  531. res = {} if not single else None
  532. for row in r:
  533. stor = storage(ups = row.ups,
  534. downs = row.downs,
  535. date = row.date,
  536. deleted = row.deleted,
  537. spam = row.spam)
  538. if single:
  539. res = stor
  540. # check that we got what we asked for
  541. if row.thing_id != thing_id:
  542. raise ValueError, ("tdb_sql.py: there's shit in the plumbing."
  543. + " got %s, wanted %s" % (row.thing_id,
  544. thing_id))
  545. else:
  546. res[row.thing_id] = stor
  547. return res
  548. def set_rel_data(rel_type_id, thing_id, brand_new_thing, **vals):
  549. table = get_rel_table(rel_type_id, action = 'write')[3]
  550. if brand_new_thing:
  551. return create_data(table, thing_id, **vals)
  552. else:
  553. return update_data(table, thing_id, **vals)
  554. def incr_rel_data(rel_type_id, thing_id, prop, amount):
  555. table = get_rel_table(rel_type_id, action = 'write')[3]
  556. return incr_data_prop(table, rel_type_id, thing_id, prop, amount)
  557. def get_rel_data(rel_type_id, rel_id):
  558. table = get_rel_table(rel_type_id)[3]
  559. return get_data(table, rel_id)
  560. def get_rel(rel_type_id, rel_id):
  561. r_table = get_rel_table(rel_type_id)[0]
  562. r, single = fetch_query(r_table, r_table.c.rel_id, rel_id)
  563. res = {} if not single else None
  564. for row in r:
  565. stor = storage(thing1_id = row.thing1_id,
  566. thing2_id = row.thing2_id,
  567. name = row.name,
  568. date = row.date)
  569. if single:
  570. res = stor
  571. else:
  572. res[row.rel_id] = stor
  573. return res
  574. def del_rel(rel_type_id, rel_id):
  575. tables = get_rel_table(rel_type_id, action = 'write')
  576. table = tables[0]
  577. data_table = tables[3]
  578. transactions.add_engine(table.bind)
  579. transactions.add_engine(data_table.bind)
  580. table.delete(table.c.rel_id == rel_id).execute()
  581. data_table.delete(data_table.c.thing_id == rel_id).execute()
  582. def sa_op(op):
  583. #if BooleanOp
  584. if isinstance(op, operators.or_):
  585. return sa.or_(*[sa_op(o) for o in op.ops])
  586. elif isinstance(op, operators.and_):
  587. return sa.and_(*[sa_op(o) for o in op.ops])
  588. #else, assume op is an instance of op
  589. if isinstance(op, operators.eq):
  590. fn = lambda x,y: x == y
  591. elif isinstance(op, operators.ne):
  592. fn = lambda x,y: x != y
  593. elif isinstance(op, operators.gt):
  594. fn = lambda x,y: x > y
  595. elif isinstance(op, operators.lt):
  596. fn = lambda x,y: x < y
  597. elif isinstance(op, operators.gte):
  598. fn = lambda x,y: x >= y
  599. elif isinstance(op, operators.lte):
  600. fn = lambda x,y: x <= y
  601. rval = tup(op.rval)
  602. if not rval:
  603. return '2+2=5'
  604. else:
  605. return sa.or_(*[fn(op.lval, v) for v in rval])
  606. def translate_sort(table, column_name, lval = None, rewrite_name = True):
  607. if isinstance(lval, operators.query_func):
  608. fn_name = lval.__class__.__name__
  609. sa_func = getattr(sa.func, fn_name)
  610. return sa_func(translate_sort(table,
  611. column_name,
  612. lval.lval,
  613. rewrite_name))
  614. if rewrite_name:
  615. if column_name == 'id':
  616. return table.c.thing_id
  617. elif column_name == 'hot':
  618. return sa.func.hot(table.c.ups, table.c.downs, table.c.date)
  619. elif column_name == 'score':
  620. return sa.func.score(table.c.ups, table.c.downs)
  621. elif column_name == 'controversy':
  622. return sa.func.controversy(table.c.ups, table.c.downs)
  623. #else
  624. return table.c[column_name]
  625. #TODO - only works with thing tables
  626. def add_sort(sort, t_table, select):
  627. sort = tup(sort)
  628. prefixes = t_table.keys() if isinstance(t_table, dict) else None
  629. #sort the prefixes so the longest come first
  630. prefixes.sort(key = lambda x: len(x))
  631. cols = []
  632. def make_sa_sort(s):
  633. orig_col = s.col
  634. col = orig_col
  635. if prefixes:
  636. table = None
  637. for k in prefixes:
  638. if k and orig_col.startswith(k):
  639. table = t_table[k]
  640. col = orig_col[len(k):]
  641. if table is None:
  642. table = t_table[None]
  643. else:
  644. table = t_table
  645. real_col = translate_sort(table, col)
  646. #TODO a way to avoid overlap?
  647. #add column for the sort parameter using the sorted name
  648. select.append_column(real_col.label(orig_col))
  649. #avoids overlap temporarily
  650. select.use_labels = True
  651. #keep track of which columns we added so we can add joins later
  652. cols.append((real_col, table))
  653. #default to asc
  654. return (sa.desc(real_col) if isinstance(s, operators.desc)
  655. else sa.asc(real_col))
  656. sa_sort = [make_sa_sort(s) for s in sort]
  657. s = select.order_by(*sa_sort)
  658. return s, cols
  659. def translate_thing_value(rval):
  660. if isinstance(rval, operators.timeago):
  661. return sa.text("current_timestamp - interval '%s'" % rval.interval)
  662. else:
  663. return rval
  664. #will assume parameters start with a _ for consistency
  665. def find_things(type_id, get_cols, sort, limit, constraints):
  666. table = get_thing_table(type_id)[0]
  667. constraints = deepcopy(constraints)
  668. s = sa.select([table.c.thing_id.label('thing_id')])
  669. for op in operators.op_iter(constraints):
  670. #assume key starts with _
  671. #if key.startswith('_'):
  672. key = op.lval_name
  673. op.lval = translate_sort(table, key[1:], op.lval)
  674. op.rval = translate_thing_value(op.rval)
  675. for op in constraints:
  676. s.append_whereclause(sa_op(op))
  677. if sort:
  678. s, cols = add_sort(sort, {'_': table}, s)
  679. if limit:
  680. s = s.limit(limit)
  681. try:
  682. r = add_request_info(s).execute()
  683. except Exception, e:
  684. dbm.mark_dead(table.bind)
  685. # this thread must die so that others may live
  686. raise
  687. return Results(r, lambda(row): row if get_cols else row.thing_id)
  688. def translate_data_value(alias, op):
  689. lval = op.lval
  690. need_substr = False if isinstance(lval, operators.query_func) else True
  691. lval = translate_sort(alias, 'value', lval, False)
  692. #add the substring func
  693. if need_substr:
  694. lval = sa.func.substring(lval, 1, max_val_len)
  695. op.lval = lval
  696. #convert the rval to db types
  697. #convert everything to strings for pg8.3
  698. op.rval = tuple(str(py2db(v)) for v in tup(op.rval))
  699. #TODO sort by data fields
  700. #TODO sort by id wants thing_id
  701. def find_data(type_id, get_cols, sort, limit, constraints):
  702. t_table, d_table = get_thing_table(type_id)
  703. constraints = deepcopy(constraints)
  704. used_first = False
  705. s = None
  706. need_join = False
  707. have_data_rule = False
  708. first_alias = d_table.alias()
  709. s = sa.select([first_alias.c.thing_id.label('thing_id')])#, distinct=True)
  710. for op in operators.op_iter(constraints):
  711. key = op.lval_name
  712. vals = tup(op.rval)
  713. if key == '_id':
  714. op.lval = first_alias.c.thing_id
  715. elif key.startswith('_'):
  716. need_join = True
  717. op.lval = translate_sort(t_table, key[1:], op.lval)
  718. op.rval = translate_thing_value(op.rval)
  719. else:
  720. have_data_rule = True
  721. id_col = None
  722. if not used_first:
  723. alias = first_alias
  724. used_first = True
  725. else:
  726. alias = d_table.alias()
  727. id_col = first_alias.c.thing_id
  728. if id_col is not None:
  729. s.append_whereclause(id_col == alias.c.thing_id)
  730. s.append_column(alias.c.value.label(key))
  731. s.append_whereclause(alias.c.key == key)
  732. #add the substring constraint if no other functions are there
  733. translate_data_value(alias, op)
  734. for op in constraints:
  735. s.append_whereclause(sa_op(op))
  736. if not have_data_rule:
  737. raise Exception('Data queries must have at least one data rule.')
  738. #TODO in order to sort by data columns, this is going to need to be smarter
  739. if sort:
  740. need_join = True
  741. s, cols = add_sort(sort, {'_':t_table}, s)
  742. if need_join:
  743. s.append_whereclause(first_alias.c.thing_id == t_table.c.thing_id)
  744. if limit:
  745. s = s.limit(limit)
  746. try:
  747. r = add_request_info(s).execute()
  748. except Exception, e:
  749. dbm.mark_dead(t_table.bind)
  750. # this thread must die so that others may live
  751. raise
  752. return Results(r, lambda(row): row if get_cols else row.thing_id)
  753. def find_rels(rel_type_id, get_cols, sort, limit, constraints):
  754. tables = get_rel_table(rel_type_id)
  755. r_table, t1_table, t2_table, d_table = tables
  756. constraints = deepcopy(constraints)
  757. t1_table, t2_table = t1_table.alias(), t2_table.alias()
  758. s = sa.select([r_table.c.rel_id.label('rel_id')])
  759. need_join1 = ('thing1_id', t1_table)
  760. need_join2 = ('thing2_id', t2_table)
  761. joins_needed = set()
  762. for op in operators.op_iter(constraints):
  763. #vals = con.rval
  764. key = op.lval_name
  765. prefix = key[:4]
  766. if prefix in ('_t1_', '_t2_'):
  767. #not a thing attribute
  768. key = key[4:]
  769. if prefix == '_t1_':
  770. join = need_join1
  771. joins_needed.add(join)
  772. elif prefix == '_t2_':
  773. join = need_join2
  774. joins_needed.add(join)
  775. table = join[1]
  776. op.lval = translate_sort(table, key, op.lval)
  777. op.rval = translate_thing_value(op.rval)
  778. #ors = [sa_op(con, key, v) for v in vals]
  779. #s.append_whereclause(sa.or_(*ors))
  780. elif prefix.startswith('_'):
  781. op.lval = r_table.c[key[1:]]
  782. else:
  783. alias = d_table.alias()
  784. s.append_whereclause(r_table.c.rel_id == alias.c.thing_id)
  785. s.append_column(alias.c.value.label(key))
  786. s.append_whereclause(alias.c.key == key)
  787. translate_data_value(alias, op)
  788. for op in constraints:
  789. s.append_whereclause(sa_op(op))
  790. if sort:
  791. s, cols = add_sort(sort,
  792. {'_':r_table, '_t1_':t1_table, '_t2_':t2_table},
  793. s)
  794. #do we need more joins?
  795. for (col, table) in cols:
  796. if table == need_join1[1]:
  797. joins_needed.add(need_join1)
  798. elif table == need_join2[1]:
  799. joins_needed.add(need_join2)
  800. for j in joins_needed:
  801. col, table = j
  802. s.append_whereclause(r_table.c[col] == table.c.thing_id)
  803. if limit:
  804. s = s.limit(limit)
  805. try:
  806. r = add_request_info(s).execute()
  807. except Exception, e:
  808. dbm.mark_dead(r_table.bind)
  809. # this thread must die so that others may live
  810. raise
  811. return Results(r, lambda (row): (row if get_cols else row.rel_id))
  812. if logging.getLogger('sqlalchemy').handlers:
  813. logging.getLogger('sqlalchemy').handlers[0].formatter = log_format
  814. #inconsitencies:
  815. #relationships assume their thing and data tables are in the same
  816. #database. things don't make that assumption. in practice thing/data
  817. #tables always go together.
  818. #
  819. #we create thing tables for a relationship's things that aren't on the
  820. #same database as the relationship, although they're never used in
  821. #practice. we could remove a healthy chunk of code if we removed that.