A semantic search engine for source code https://bitshift.benkurtovic.com/
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 
 
 

217 строки
9.3 KiB

  1. """
  2. Subpackage with classes and functions to handle communication with the MySQL
  3. database backend, which manages the search index.
  4. """
  5. import os
  6. import mmh3
  7. import oursql
  8. from .migration import VERSION, MIGRATIONS
  9. from ..query.nodes import (String, Regex, Text, Language, Author, Date, Symbol,
  10. BinaryOp, UnaryOp)
  11. __all__ = ["Database"]
  12. class Database(object):
  13. """Represents the MySQL database."""
  14. def __init__(self, migrate=False):
  15. self._conn = self._connect()
  16. self._check_version(migrate)
  17. def _connect(self):
  18. """Establish a connection to the database."""
  19. root = os.path.dirname(os.path.abspath(__file__))
  20. default_file = os.path.join(root, ".my.cnf")
  21. return oursql.connect(db="bitshift", read_default_file=default_file,
  22. autoping=True, autoreconnect=True)
  23. def _migrate(self, cursor, current):
  24. """Migrate the database to the latest schema version."""
  25. for version in xrange(current, VERSION):
  26. print "Migrating to %d..." % version + 1
  27. for query in MIGRATIONS[version - 1]:
  28. cursor.execute(query)
  29. cursor.execute("UPDATE version SET version = ?", (version + 1,))
  30. def _check_version(self, migrate):
  31. """Check the database schema version and respond accordingly.
  32. If the schema is out of date, migrate if *migrate* is True, else raise
  33. an exception.
  34. """
  35. with self._conn.cursor() as cursor:
  36. cursor.execute("SELECT version FROM version")
  37. version = cursor.fetchone()[0]
  38. if version < VERSION:
  39. if migrate:
  40. self._migrate(cursor, version)
  41. else:
  42. err = "Database schema out of date. " \
  43. "Run `python -m bitshift.database.migration`."
  44. raise RuntimeError(err)
  45. def _explode_query_tree(self, tree):
  46. """Convert a query tree into components of an SQL SELECT statement."""
  47. def _parse_node(node, tables):
  48. if isinstance(node, Text):
  49. tables |= {"code", "symbols"}
  50. # (FTS: codelet_name, =: symbol_name, FTS: code_code) vs. node.text (_Literal)
  51. pass
  52. elif isinstance(node, Language):
  53. tables |= {"code"}
  54. return "(code_lang = ?)", tables, [node.lang]
  55. elif isinstance(node, Author):
  56. tables |= {"authors"}
  57. if isinstance(node.name, Regex):
  58. return "(author_name REGEXP ?)", [node.name.regex]
  59. cond = "(MATCH(author_name) AGAINST (? IN BOOLEAN MODE))"
  60. return cond, tables, [node.name.string]
  61. elif isinstance(node, Date):
  62. column = {node.CREATE: "codelet_date_created",
  63. node.MODIFY: "codelet_date_modified"}[node.type]
  64. op = {node.BEFORE: "<=", node.AFTER: ">="}[node.relation]
  65. return "(" + column + " " + op + " ?)", tables, [node.date]
  66. elif isinstance(node, Symbol):
  67. tables |= {"symbols"}
  68. cond_base = "(symbol_type = ? AND symbol_name = ?)"
  69. if node.type != node.ALL:
  70. return cond_base, tables, [node.type, node.name]
  71. cond = "(" + " OR ".join([cond_base] * len(node.TYPES)) + ")"
  72. args = zip(node.TYPES.keys(), [node.name] * len(node.TYPES))
  73. return cond, tables, [arg for tup in args for arg in tup]
  74. elif isinstance(node, BinaryOp):
  75. left_cond, tbls, left_args = _parse_node(node.left, tables)
  76. right_cond, tables, right_args = _parse_node(node.right, tbls)
  77. op = node.OPS[node.op]
  78. cond = "(" + left_cond + " " + op + " " + right_cond + ")"
  79. return cond, tables, left_args + right_args
  80. elif isinstance(node, UnaryOp):
  81. cond, tables, args = _parse_node(node.node, tables)
  82. return "(" + node.OPS[node.op] + " " + cond + ")", tables, args
  83. conditional, tables, arglist = _parse_node(tree.root, set())
  84. # joins = " ".join(tables)
  85. return conditional, joins, tuple(arglist)
  86. def _search_with_query(self, cursor, query, page):
  87. """Execute an SQL query based on a query tree, and return results.
  88. The returned data is a 2-tuple of (list of codelet IDs, estimated
  89. number of total results).
  90. """
  91. conditional, joins, args = self._explode_query_tree(query)
  92. base = """SELECT codelet_id
  93. FROM codelets %s
  94. WHERE %s
  95. ORDER BY codelet_rank LIMIT 10"""
  96. qstring = base % (joins, conditional)
  97. if page > 1:
  98. qstring += " OFFSET %d" % ((page - 1) * 10)
  99. cursor.execute(qstring, args)
  100. ids = [id for id, in cursor.fetchall()]
  101. num_results = 0 # TODO: NotImplemented
  102. return ids, num_results
  103. def _get_codelets_from_ids(self, cursor, ids):
  104. """Return a list of Codelet objects given a list of codelet IDs."""
  105. raise NotImplementedError() # TODO
  106. def _decompose_url(self, cursor, url):
  107. """Break up a URL into an origin (with a URL base) and a suffix."""
  108. query = """SELECT origin_id, SUBSTR(?, LENGTH(origin_url_base))
  109. FROM origins
  110. WHERE origin_url_base IS NOT NULL
  111. AND ? LIKE CONCAT(origin_url_base, "%")"""
  112. cursor.execute(query, (url, url))
  113. result = cursor.fetchone()
  114. return result if result else (1, url)
  115. def _insert_symbols(self, cursor, code_id, sym_type, symbols):
  116. """Insert a list of symbols of a given type into the database."""
  117. sym_types = ["functions", "classes", "variables"]
  118. query1 = "INSERT INTO symbols VALUES (DEFAULT, ?, ?, ?)"
  119. query2 = """INSERT INTO symbol_locations VALUES
  120. (DEFAULT, ?, ?, ?, ?, ?, ?)"""
  121. for (name, decls, uses) in symbols:
  122. cursor.execute(query1, (code_id, sym_types.index(sym_type), name))
  123. sym_id = cursor.lastrowid
  124. params = ([tuple([sym_id, 0] + list(loc)) for loc in decls] +
  125. [tuple([sym_id, 1] + list(loc)) for loc in uses])
  126. cursor.executemany(query2, params)
  127. def close(self):
  128. """Disconnect from the database."""
  129. self._conn.close()
  130. def search(self, query, page=1):
  131. """
  132. Search the database for a query and return the *n*\ th page of results.
  133. :param query: The query to search for.
  134. :type query: :py:class:`~.query.tree.Tree`
  135. :param page: The result page to display.
  136. :type page: int
  137. :return: The total number of results, and the *n*\ th page of results.
  138. :rtype: 2-tuple of (long, list of :py:class:`.Codelet`\ s)
  139. """
  140. query1 = """SELECT cdata_codelet, cache_count_mnt, cache_count_exp
  141. FROM cache
  142. INNER JOIN cache_data ON cache_id = cdata_cache
  143. WHERE cache_id = ?"""
  144. query2 = "INSERT INTO cache VALUES (?, ?, ?, DEFAULT)"
  145. query3 = "INSERT INTO cache_data VALUES (?, ?)"
  146. cache_id = mmh3.hash64(str(page) + ":" + query.serialize())[0]
  147. with self._conn.cursor() as cursor:
  148. cursor.execute(query1, (cache_id,))
  149. results = cursor.fetchall()
  150. if results: # Cache hit
  151. num_results = results[0][1] * (10 ** results[0][2])
  152. ids = [res[0] for res in results]
  153. else: # Cache miss
  154. ids, num_results = self._search_with_query(cursor, query, page)
  155. num_exp = max(len(str(num_results)) - 3, 0)
  156. num_results = int(round(num_results, -num_exp))
  157. num_mnt = num_results / (10 ** num_exp)
  158. cursor.execute(query2, (cache_id, num_mnt, num_exp))
  159. cursor.executemany(query3, [(cache_id, c_id) for c_id in ids])
  160. return (num_results, self._get_codelets_from_ids(cursor, ids))
  161. def insert(self, codelet):
  162. """
  163. Insert a codelet into the database.
  164. :param codelet: The codelet to insert.
  165. :type codelet: :py:class:`.Codelet`
  166. """
  167. query1 = """INSERT INTO code VALUES (?, ?, ?)
  168. ON DUPLICATE KEY UPDATE code_id=code_id"""
  169. query2 = """INSERT INTO codelets VALUES
  170. (DEFAULT, ?, ?, ?, ?, ?, ?, ?)"""
  171. query3 = "INSERT INTO authors VALUES (DEFAULT, ?, ?, ?)"
  172. hash_key = str(codelet.language) + ":" + codelet.code.encode("utf8")
  173. code_id = mmh3.hash64(hash_key)[0]
  174. with self._conn.cursor() as cursor:
  175. cursor.execute(query1, (code_id, codelet.language, codelet.code))
  176. if cursor.rowcount == 1:
  177. for sym_type, symbols in codelet.symbols.iteritems():
  178. self._insert_symbols(cursor, code_id, sym_type, symbols)
  179. origin, url = self._decompose_url(cursor, codelet.url)
  180. cursor.execute(query2, (codelet.name, code_id, origin, url,
  181. codelet.rank, codelet.date_created,
  182. codelet.date_modified))
  183. codelet_id = cursor.lastrowid
  184. authors = [(codelet_id, a[0], a[1]) for a in codelet.authors]
  185. cursor.executemany(query3, authors)