PageRenderTime 27ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/plugins/holland.lib.mysql/holland/lib/mysql/schema/base.py

https://github.com/holland-backup/holland
Python | 394 lines | 299 code | 18 blank | 77 comment | 11 complexity | a8498f851b2d6c3875b6f8b39658b9c0 MD5 | raw file
Possible License(s): BSD-3-Clause
  1. """Summarize a MySQL Schema"""
  2. import logging
  3. import re
  4. import time
  5. from holland.lib.mysql.client import MySQLError
  6. LOG = logging.getLogger(__name__)
  7. #: engines we consider 'transactional'
  8. #: transactional in this context means '--single-transaction'
  9. #: is probably a reasonable option for mysqldump
  10. TRANSACTIONAL_ENGINES = "innodb", "federated", "myisam_mrg", "memory", "view", "blackhole"
  11. class MySQLSchema(object):
  12. """A catalog summary of a MySQL Instance"""
  13. def __init__(self):
  14. self.databases = []
  15. self._database_filters = []
  16. self._table_filters = []
  17. self._engine_filters = []
  18. self.timestamp = None
  19. def excluded_tables(self):
  20. """Iterate over tables excluded in this schema"""
  21. for database in self.databases:
  22. for table in database:
  23. if table.excluded:
  24. yield table
  25. excluded_tables = property(excluded_tables)
  26. def excluded_databases(self):
  27. """Iterate over databases excluded in this schema"""
  28. for database in self.databases:
  29. if database.excluded:
  30. yield database
  31. excluded_databases = property(excluded_databases)
  32. def add_database_filter(self, filterobj):
  33. """Add a database filter to this summary
  34. :param filterobj: a callable that returns True if a database
  35. should be filtered by name
  36. :type filterobj: callable, such as `IncludeFilter` or `ExcludeFilter`
  37. """
  38. self._database_filters.append(filterobj)
  39. def add_table_filter(self, filterobj):
  40. """Add a table filter to this summary
  41. :param filterobj: a callable that returns True if a table
  42. should be filtered by name
  43. :type filterobj: callable, such as `IncludeFilter` or `ExcludeFilter`
  44. """
  45. self._table_filters.append(filterobj)
  46. def add_engine_filter(self, filterobj):
  47. """Add an engine filter to this summary
  48. :param filterobj: a callable that returns True if a table
  49. should be filtered by name
  50. :type filterobj: callable, such as `IncludeFilter` or `ExcludeFilter`
  51. """
  52. self._engine_filters.append(filterobj)
  53. def is_db_filtered(self, name):
  54. """Check if the database name is filtered by any database filters
  55. :param name: database name that should be checked against the list of
  56. registered database filters.
  57. :type name: str
  58. :returns: True if the database named by `name` should be filtered
  59. """
  60. for _filter in self._database_filters:
  61. if _filter(name):
  62. return True
  63. return None
  64. def is_table_filtered(self, name):
  65. """Check if the table name is filtered by any table filters
  66. :param name: table name that should be checked against the list of
  67. registered table filters.
  68. :type name: str
  69. :returns: True if the database named by `name` should be filtered
  70. """
  71. for _filter in self._table_filters:
  72. if _filter(name):
  73. return True
  74. return None
  75. def is_engine_filtered(self, name):
  76. """Check if the engine name is filtered by any engine filters
  77. :param name: engine name that should be checked against the list of
  78. registered engine filters.
  79. :type name: str
  80. :returns: True if the table with the storage engine named by `name`
  81. should be filtered
  82. """
  83. for _filter in self._engine_filters:
  84. if _filter(name):
  85. return True
  86. return None
  87. def refresh(self, db_iter, tbl_iter, fast_iterate=False):
  88. """Summarize the schema by walking over the given database and table
  89. iterators
  90. :param db_iter: Required. A `DatabaseIterator` instance that will
  91. provide an iterator instance when called with no
  92. arguments. This iterator must yield `Database`
  93. instances.
  94. :param tbl_iter: Required. A `TableIterator` instance that will return
  95. provide an iterator instance when called with a
  96. database name. This iterator must yield `Table`
  97. instances from the requested database.
  98. :param fast_iterate: Optional. Skips table iteration when there are no
  99. useful filters - include pattern = *,
  100. exclude pattern = ''
  101. """
  102. for database in db_iter():
  103. self.databases.append(database)
  104. if self.is_db_filtered(database.name):
  105. database.excluded = True
  106. continue
  107. # skip iterating over tables when:
  108. # 1) we are matching all tables (using default pattern)
  109. # 2) we are matching all engines (using default pattern)
  110. # 3) caller does not require table iteration
  111. # pylint: disable=too-many-boolean-expressions
  112. if (
  113. fast_iterate
  114. and (
  115. len(self._table_filters) == 2
  116. and self._table_filters[0].patterns == [".*\\..*$"]
  117. and self._table_filters[1].patterns == []
  118. )
  119. and (
  120. len(self._engine_filters) == 2
  121. and self._engine_filters[0].patterns == [".*$"]
  122. and self._engine_filters[1].patterns == []
  123. )
  124. ):
  125. # optimize case where we have no table level filters
  126. continue
  127. try:
  128. for table in tbl_iter(database.name):
  129. if self.is_table_filtered(table.database + "." + table.name):
  130. table.excluded = True
  131. if self.is_engine_filtered(table.engine):
  132. table.excluded = True
  133. database.add_table(table)
  134. except MySQLError as exc:
  135. # mimic mysqldump behavior here and skip any databases that
  136. # are not readable
  137. if exc.args[0] == 1018:
  138. continue
  139. raise
  140. self.timestamp = time.time()
  141. class Database(object):
  142. """Representation of a MySQL Database
  143. Only the name an whether this database is
  144. excluded is recorded"""
  145. __slots__ = ("name", "excluded", "tables")
  146. def __init__(self, name):
  147. self.name = name
  148. self.tables = []
  149. self.excluded = False
  150. def add_table(self, tableobj):
  151. """Add the table object to this database
  152. :param tableobj: `Table` instance that should be added to this
  153. `Database` instance
  154. """
  155. self.tables.append(tableobj)
  156. def excluded_tables(self):
  157. """List tables associated with this database that are flagged as
  158. excluded"""
  159. for tableobj in self.tables:
  160. if tableobj.excluded:
  161. yield tableobj
  162. def is_transactional(self):
  163. """Check if this database is safe to dump in --single-transaction
  164. mode
  165. """
  166. for tableobj in self.tables:
  167. if not tableobj.is_transactional:
  168. return False
  169. return None
  170. def size(self):
  171. """Size of all non-excluded objects in this database
  172. :returns: int. sum of all data and indexes of tables that are not
  173. excluded from this database
  174. """
  175. return sum(
  176. [
  177. table.size
  178. for table in self.tables
  179. if not table.excluded and table.engine not in ("mrg_myisam", "federated")
  180. ]
  181. )
  182. size = property(size)
  183. def __str__(self):
  184. return "Database(name=%r, table_count=%d, excluded=%r)" % (
  185. self.name,
  186. len(self.tables),
  187. self.excluded,
  188. )
  189. __repr__ = __str__
  190. class Table(object):
  191. """Representation of a MySQL Table"""
  192. __slots__ = ("database", "name", "data_size", "index_size", "engine", "excluded")
  193. def __init__(self, database, name, data_size, index_size, engine):
  194. """Init Table"""
  195. self.database = database
  196. self.name = name
  197. self.data_size = int(data_size)
  198. self.index_size = int(index_size)
  199. self.engine = engine
  200. self.excluded = False
  201. def size(self):
  202. """Return size of table"""
  203. return self.data_size + self.index_size
  204. size = property(size)
  205. def is_transactional(self):
  206. """Return if the table is transactional"""
  207. return self.engine in TRANSACTIONAL_ENGINES
  208. is_transactional = property(is_transactional)
  209. def __str__(self):
  210. data_size = "%.2fMB" % (self.data_size / 1024.0 ** 2)
  211. index_size = "%.2fMB" % (self.index_size / 1024.0 ** 2)
  212. return "%sTable(name=%r, data_size=%s, \
  213. index_size=%s, engine=%s, txn=%s)" % (
  214. self.excluded and "[EXCL]" or "",
  215. self.name,
  216. data_size,
  217. index_size,
  218. self.engine,
  219. str(self.is_transactional),
  220. )
  221. class DatabaseIterator(object):
  222. """Iterate over databases returns by a MySQLClient instance
  223. client must have a show_databases() method
  224. """
  225. STD_EXCLUSIONS = ("information_schema", "performance_schema", "lost+found")
  226. def __init__(self, client):
  227. """Construct a new iterator to produce `Database` instances for the
  228. database requested by the __call__ method.
  229. :param client: `MySQLClient` instance to use to iterate over objects in
  230. the specified databasea
  231. """
  232. self.client = client
  233. def __call__(self):
  234. for name in self.client.show_databases():
  235. if name not in self.STD_EXCLUSIONS:
  236. yield Database(name)
  237. class TableIterator(object):
  238. """Iterate over tables returned by the client instance
  239. client must have a show_table_metadata(database_name) method
  240. """
  241. def __init__(self, client):
  242. """Construct a new iterator to produce `Table` instances for the
  243. database requested by the __call__ method.
  244. :param client: `MySQLClient` instance to use to iterate over objects in
  245. the specified database
  246. """
  247. self.client = client
  248. def __call__(self, database):
  249. raise NotImplementedError()
  250. class MetadataTableIterator(TableIterator):
  251. """Iterate over SHOW TABLE STATUS in the requested database
  252. and yield Table instances
  253. """
  254. def __call__(self, database):
  255. for metadata in self.client.show_table_metadata(database):
  256. yield Table(**metadata)
  257. class SimpleTableIterator(MetadataTableIterator):
  258. """Iterator over tables returns by the client instance
  259. Unlike a MetadataTableIterator, this will not lookup the table size
  260. but rather just uses SHOW DATABASES/SHOW TABLES/SHOW CREATE TABLE
  261. SHOW CREATE TABLE is only used for engine lookup in MySQL 5.0.
  262. """
  263. ENGINE_PCRE = re.compile(r"^[)].*ENGINE=(\S+)", re.M)
  264. def __init__(self, client, record_engines=False):
  265. """Construct a new iterator to produce `Table` instances for the
  266. database requested by the __call__ method.
  267. :param client: `MySQLClient` instance to use to iterate over objects in
  268. the specified database
  269. """
  270. super().__init__(client)
  271. self.client = client
  272. self.record_engines = record_engines
  273. def _faster_mysql51_metadata(self, database):
  274. sql = (
  275. "SELECT TABLE_SCHEMA AS `database`, "
  276. " TABLE_NAME AS `name`, "
  277. " 0 AS `data_size`, "
  278. " 0 AS `index_size`, "
  279. " LOWER(COALESCE(ENGINE, 'view')) AS `engine` "
  280. "FROM INFORMATION_SCHEMA.TABLES "
  281. "WHERE TABLE_SCHEMA = %s"
  282. )
  283. cursor = self.client.cursor()
  284. try:
  285. cursor.execute(sql, (database,))
  286. return cursor.fetchall()
  287. finally:
  288. cursor.close()
  289. def _lookup_engine(self, database, table):
  290. ddl = self.client.show_create_table(database, table)
  291. match = self.ENGINE_PCRE.search(ddl)
  292. if match:
  293. return match.group(1)
  294. raise ValueError("Failed to lookup storage engine")
  295. def __call__(self, database):
  296. if self.client.server_version() >= (5, 1):
  297. for metadata in self._faster_mysql51_metadata(database):
  298. yield Table(*metadata)
  299. else:
  300. for table, kind in self.client.show_tables(database, full=True):
  301. LOG.debug("Table: %s, Kind: %s", table, kind)
  302. metadata = [
  303. ("database", database),
  304. ("name", table),
  305. ("data_size", 0),
  306. ("index_size", 0),
  307. ]
  308. if kind == "VIEW":
  309. metadata.append(("engine", "view"))
  310. else:
  311. if self.record_engines:
  312. engine = self._lookup_engine(database, table).lower()
  313. metadata.append(("engine", engine))
  314. else:
  315. metadata.append(("engine", ""))
  316. yield Table(**dict(metadata))