No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ledatasourcesql.py 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  1. #-*- coding: utf-8 -*-
  2. import pymysql
  3. import leobject
  4. from leobject.datasources.dummy import DummyDatasource
  5. from leobject.leobject import REL_SUB, REL_SUP
  6. from leobject.letype import LeType
  7. from mosql.db import Database, all_to_dicts, one_to_dict
  8. from mosql.query import select, insert, update, delete, join
  9. from mosql.util import raw, or_
  10. import mosql.mysql
  11. from DataSource.MySQL.MySQL import MySQL
  12. from EditorialModel.classtypes import EmNature
  13. ## MySQL DataSource for LeObject
  14. class LeDataSourceSQL(DummyDatasource):
  15. RELATIONS_POSITIONS_FIELDS = {REL_SUP: 'superior_id', REL_SUB: 'subordinate_id'}
  16. def __init__(self, module=pymysql, conn_args=None):
  17. super(LeDataSourceSQL, self).__init__()
  18. self.module = module
  19. self.datasource_utils = MySQL
  20. if conn_args is None:
  21. conn_args = self.datasource_utils.connections['default']
  22. self.connection = Database(self.module, host=conn_args['host'], user=conn_args['user'], passwd=conn_args['passwd'], db=conn_args['db'])
  23. ## @brief inserts a new object
  24. # @param letype LeType
  25. # @param leclass LeClass
  26. # @param datas dict : dictionnary of field:value pairs to save
  27. # @return int : lodel_id of the created object
  28. # @todo add the returning clause and the insertion in "object"
  29. def insert(self, letype, leclass, datas):
  30. if isinstance(datas, list):
  31. res = list()
  32. for data in datas:
  33. res.append(self.insert(letype, leclass, data))
  34. return res
  35. elif isinstance(datas, dict):
  36. with self.connection as cur:
  37. object_datas = {'class_id': leclass._class_id, 'type_id': letype._type_id}
  38. if cur.execute(insert(self.datasource_utils.objects_table_name, object_datas)) != 1:
  39. raise RuntimeError('SQL error')
  40. if cur.execute('SELECT last_insert_id() as lodel_id') != 1:
  41. raise RuntimeError('SQL error')
  42. lodel_id, = cur.fetchone()
  43. datas[self.datasource_utils.field_lodel_id] = lodel_id
  44. query_table_name = self.datasource_utils.get_table_name_from_class(leclass.__name__)
  45. query = insert(query_table_name, datas)
  46. if cur.execute(query) != 1:
  47. raise RuntimeError('SQL error')
  48. return lodel_id
  49. ## @brief search for a collection of objects
  50. # @param leclass LeClass
  51. # @param letype LeType
  52. # @field_list list
  53. # @param filters list : list of tuples formatted as (FIELD, OPERATOR, VALUE)
  54. # @param relation_filters list : list of tuples formatted as (('superior'|'subordinate', FIELD), OPERATOR, VALUE)
  55. # @return list
  56. def get(self, leclass, letype, field_list, filters, relational_filters=None):
  57. query_table_name = self.datasource_utils.get_table_name_from_class(leclass.__name__)
  58. where_filters = self._prepare_filters(filters, query_table_name)
  59. join_fields = {}
  60. if relational_filters is not None and len(relational_filters) > 0:
  61. rel_filters = self._prepare_rel_filters(relational_filters)
  62. for rel_filter in rel_filters:
  63. # join condition
  64. relation_table_join_field = "%s.%s" % (self.datasource_utils.relations_table_name, self.RELATIONS_POSITIONS_FIELDS[rel_filter['position']])
  65. query_table_join_field = "%s.%s" % (query_table_name, self.datasource_utils.field_lodel_id)
  66. join_fields[query_table_join_field] = relation_table_join_field
  67. # Adding "where" filters
  68. where_filters['%s.%s' % (self.datasource_utils.relations_table_name, self.datasource_utils.relations_field_nature)] = rel_filter['nature']
  69. where_filters[rel_filter['condition_key']] = rel_filter['condition_value']
  70. # building the query
  71. query = select(query_table_name, where=where_filters, select=field_list, joins=join(self.datasource_utils.relations_table_name, join_fields))
  72. else:
  73. query = select(query_table_name, where=where_filters, select=field_list)
  74. # Executing the query
  75. with self.connection as cur:
  76. results = all_to_dicts(cur.execute(query))
  77. return results
  78. ## @brief delete an existing object
  79. # @param letype LeType
  80. # @param leclass LeClass
  81. # @param filters list : list of tuples formatted as (FIELD, OPERATOR, VALUE)
  82. # @param relational_filters list : list of tuples formatted as (('superior'|'subordinate', FIELD), OPERATOR, VALUE)
  83. # @return bool : True on success
  84. def delete(self, letype, leclass, filters, relational_filters):
  85. query_table_name = self.datasource_utils.get_table_name_from_class(leclass.__name__)
  86. prep_filters = self._prepare_filters(filters, query_table_name)
  87. prep_rel_filters = self._prepare_rel_filters(relational_filters)
  88. if len(prep_rel_filters) > 0:
  89. query = "DELETE %s FROM " % query_table_name
  90. for prep_rel_filter in prep_rel_filters:
  91. query += "%s INNER JOIN %s ON (%s.%s = %s.%s)" % (
  92. self.datasource_utils.relations_table_name,
  93. query_table_name,
  94. self.datasource_utils.relations_table_name,
  95. prep_rel_filter['position'],
  96. query_table_name,
  97. self.datasource_utils.field_lodel_id
  98. )
  99. if prep_rel_filter['condition_key'][0] is not None:
  100. prep_filters[("%s.%s" % (self.datasource_utils.relations_table_name, prep_rel_filter['condition_key'][0]), prep_rel_filter['condition_key'][1])] = prep_rel_filter['condition_value']
  101. if prep_filters is not None and len(prep_filters) > 0:
  102. query += " WHERE "
  103. filter_counter = 0
  104. for filter_item in prep_filters:
  105. if filter_counter > 1:
  106. query += " AND "
  107. query += "%s %s %s" % (filter_item[0][0], filter_item[0][1], filter_item[1])
  108. else:
  109. query = delete(query_table_name, filters)
  110. query_delete_from_object = delete(self.datasource_utils.objects_table_name, {'lodel_id': filters['lodel_id']})
  111. with self.connection as cur:
  112. cur.execute(query)
  113. cur.execute(query_delete_from_object)
  114. return True
  115. ## @brief update an existing object's data
  116. # @param letype LeType
  117. # @param leclass LeClass
  118. # @param filters list : list of tuples formatted as (FIELD, OPERATOR, VALUE)
  119. # @param rel_filters list : list of tuples formatted as (('superior'|'subordinate', FIELD), OPERATOR, VALUE)
  120. # @param data dict
  121. # @return bool
  122. # @todo prendre en compte les rel_filters
  123. def update(self, letype, leclass, filters, rel_filters, data):
  124. query_table_name = self.datasource_utils.get_table_name_from_class(leclass.__name__)
  125. where_filters = filters
  126. set_data = data
  127. prepared_rel_filters = self._prepare_rel_filters(rel_filters)
  128. # Building the query
  129. query = update(table=query_table_name, where=where_filters, set=set_data)
  130. # Executing the query
  131. with self.connection as cur:
  132. cur.execute(query)
  133. return True
  134. ## @brief prepares the relational filters
  135. # @params rel_filters : (("superior"|"subordinate"), operator, value)
  136. # @return list
  137. def _prepare_rel_filters(self, rel_filters):
  138. prepared_rel_filters = []
  139. if rel_filters is not None and len(rel_filters) > 0:
  140. for rel_filter in rel_filters:
  141. rel_filter_dict = {
  142. 'position': REL_SUB if rel_filter[0][0] == REL_SUP else REL_SUB,
  143. 'nature': rel_filter[0][1],
  144. 'condition_key': (self.RELATIONS_POSITIONS_FIELDS[rel_filter[0][0]], rel_filter[1]),
  145. 'condition_value': rel_filter[2]
  146. }
  147. prepared_rel_filters.append(rel_filter_dict)
  148. return prepared_rel_filters
  149. ## @brief prepares the filters to be used by the mosql library's functions
  150. # @params filters : (FIELD, OPERATOR, VALUE) tuples
  151. # @return dict : Dictionnary with (FIELD, OPERATOR):VALUE style elements
  152. def _prepare_filters(self, filters, tablename=None):
  153. prepared_filters = {}
  154. if filters is not None and len(filters) > 0:
  155. for filter_item in filters:
  156. if '.' in filter_item[0]:
  157. prepared_filter_key = (filter_item[0], filter_item[1])
  158. else:
  159. prepared_filter_key = ("%s.%s" % (tablename, filter_item[0]), filter_item[1])
  160. prepared_filter_value = filter_item[2]
  161. prepared_filters[prepared_filter_key] = prepared_filter_value
  162. return prepared_filters
  163. ## @brief Make a relation between 2 LeType
  164. # @note rel2type relations. Superior is the LeType from the EmClass and subordinate the LeType for the EmType
  165. # @param lesup LeType : LeType child class instance that is from the EmClass containing the rel2type field
  166. # @param lesub LeType : LeType child class instance that is from the EmType linked by the rel2type field ( @ref EditorialModel.fieldtypes.rel2type.EmFieldType.rel_to_type_id )
  167. # @return The relation_id if success else return False
  168. def add_related(self, lesup, lesub, rank, **rel_attr):
  169. with self.connection as cur:
  170. #First step : relation table insert
  171. sql = insert(MySQL.relations_table_name,{
  172. 'id_sup': lesup.lodel_id,
  173. 'id_sub': lesub.lodel_id,
  174. 'rank': 0, #default value that will be set latter
  175. })
  176. cur.execute(sql)
  177. relation_id = cur.lastrowid
  178. if len(rel_attr) > 0:
  179. #There is some relation attribute to add in another table
  180. attr_table = get_r2t2table_name(lesup._leclass.__name__, lesub.__class__.__name__)
  181. rel_attr['id_relation'] = relation_id
  182. sql = insert(attr_table, rel_attr)
  183. cur.execute(sql)
  184. self._set_relation_rank(id_relation, rank)
  185. return relation_id
  186. ## @brief Deletes the relation between 2 LeType
  187. # @param lesup LeType
  188. # @param lesub LeType
  189. # @return True if success else False
  190. def del_related(self, lesup, lesub):
  191. with self.connection as cur:
  192. sql = delete(
  193. self.datasource_utils.relations_table_name,
  194. {
  195. 'id_sup': lesup.lodel_id,
  196. 'id_sub': lesub.lodel_id
  197. }
  198. )
  199. if cur.execute(sql) != 1:
  200. return False
  201. return True
  202. ## @brief Set the rank of a relation identified by its ID
  203. # @param id_relation int : relation ID
  204. # @param rank int|str : 'first', 'last', or an integer value
  205. # @throw ValueError if rank is not valid
  206. # @throw leobject.leobject.LeObjectQueryError if id_relation don't exists
  207. def set_relation_rank(self, id_relation, rank):
  208. self._check_rank(rank)
  209. self._set_relation_rank(id_relation, rank)
  210. ## @brief Set the rank of a relation identified by its ID
  211. #
  212. # @note this solution is not the more efficient solution but it
  213. # garantee that ranks are continuous and starts at 1
  214. # @warning there is no way to fail on rank parameters even giving very bad parameters, if you want a method that may fail on rank use set_relation_rank() instead
  215. # @param id_relation int : relation ID
  216. # @param rank int|str : 'first', 'last', or an integer value
  217. # @throw leobject.leobject.LeObjectQueryError if id_relation don't exists
  218. def _set_relation_rank(self, id_relation, rank):
  219. ret = self.get_relation(id_relation, no_attr = True)
  220. if not ret:
  221. raise leobject.leobject.LeObjectQueryError("No relation with id_relation = %d"%id_relation)
  222. lesup = ret['lesup']
  223. lesub = ret['lesup']
  224. cur_rank = ret['rank']
  225. rank = 1 if rank == 'first' or rank < 1 else rank
  226. if cur_rank == rank:
  227. return True
  228. relations = self.get_related(lesup, lesub.__class__, get_sub=True)
  229. if not isinstance(rank, int) or rank > len(relations):
  230. rank = len(relations)
  231. if cur_rank == rank:
  232. return True
  233. #insert the relation at the good position
  234. our_relation = relations.pop(cur_rank)
  235. relations.insert(our_relation, rank)
  236. #gathering (relation_id, new_rank)
  237. rdatas = [ (attrs['relation_id'], new_rank+1) for new_rank,(sup, sub, attrs) in enumerate(relations) ]
  238. sql = insert(MySQL.relations_table_name, columns=(MySQL.relations_pkname, 'rank'), values = rdatas, on_duplicate_key_update={'rank',mosql.util.raw('VALUES(`rank`)')})
  239. ## @brief Check a rank value
  240. # @param rank int | str : Can be an integer >= 1 , 'first' or 'last'
  241. # @throw ValueError if the rank is not valid
  242. def _check_rank(self, rank):
  243. if isinstance(rank, str) and rank != 'first' and rank != 'last':
  244. raise ValueError("Invalid rank value : %s"%rank)
  245. elif isinstance(rank, int) and rank < 1:
  246. raise ValueError("Invalid rank value : %d"%rank)
  247. else:
  248. raise ValueError("Invalid rank type : %s"%type(rank))
  249. ## @brief Link two object given a relation nature, depth and rank
  250. # @param lesup LeObject : a LeObject
  251. # @param lesub LeObject : a LeObject
  252. # @param nature str|None : The relation nature or None if rel2type
  253. # @param rank int : a rank
  254. def add_relation(self, lesup, lesub, nature=None, depth=None, rank=None, **rel_attr):
  255. if len(rel_attr) > 0 and nature is not None:
  256. #not a rel2type but have some relation attribute
  257. raise AttributeError("No relation attributes allowed for non rel2type relations")
  258. with self.connection as cur:
  259. sql = insert(self.datasource_utils.relations_table_name, {'id_sup': lesup.lodel_id, 'id_sub': lesub.lodel_id, 'nature': nature, 'rank': rank, 'depth': depth})
  260. if cur.execute(sql) != 1:
  261. raise RuntimeError("Unknow SQL error")
  262. if len(rel_attr) > 0:
  263. #a relation table exists
  264. cur.execute('SELECT last_insert_id()')
  265. relation_id, = cur.fetchone()
  266. raise NotImplementedError()
  267. return True
  268. ## @brief Delete a rel2type relation
  269. # @warning this method may not be efficient
  270. # @param id_relation int : The relation identifier
  271. # @return bool
  272. def del_relation(self, id_relation):
  273. with self.connection as cur:
  274. pk_where = {MySQL.relations_pkname:id_relation}
  275. if not MySQL.fk_on_delete_cascade and len(lesup._linked_types[lesub.__class__]) > 0:
  276. #Delete the row in the relation attribute table
  277. ret = self.get_relation(id_relation, no_attr = False)
  278. lesup = ret['lesup']
  279. lesub = ret['lesub']
  280. sql = delete(MySQL.relations_table_name, pk_where)
  281. if cur.execute(sql) != 1:
  282. raise RuntimeError("Unknown SQL Error")
  283. sql = delete(MySQL.relations_table_name, pk_where)
  284. if cur.execute(sql) != 1:
  285. raise RuntimeError("Unknown SQL Error")
  286. return True
  287. ## @brief Fetch a relation
  288. # @param id_relation int : The relation identifier
  289. # @param no_attr bool : If true dont fetch rel_attr
  290. # @return a dict{'id_relation':.., 'lesup':.., 'lesub':..,'rank':.., 'depth':.., #if not none#'nature':.., #if exists#'dict_attr':..>}
  291. #
  292. # @todo TESTS
  293. def get_relation(self, id_relation, no_attr = False):
  294. relation = dict()
  295. with self.connection as cur:
  296. sql = select(MySQL.relation_table_name, {MySQL.relations_pkname: id_relation})
  297. if cur.execute(sql) != 1:
  298. raise RuntimeError("Unknow SQL error")
  299. res = all_to_dicts(cur)
  300. if len(res) == 0:
  301. return False
  302. if len(res) > 1:
  303. raise RuntimeError("When selecting on primary key, get more than one result. Bailout")
  304. if res['nature'] != None:
  305. raise ValueError("The relation with id %d is not a rel2type relation"%id_relation)
  306. leobj = leobject.lefactory.LeFactory.leobj_from_name('LeObject')
  307. lesup = leobj.uid2leobj(res['id_sup'])
  308. lesub = leobj.uid2leobj(res['id_sub'])
  309. relation['id_relation'] = res['id_relation']
  310. relation['lesup'] = lesup
  311. relation['lesub'] = lesub
  312. relation['rank'] = rank
  313. relation['depth'] = depth
  314. if not (res['nature'] is None):
  315. relation['nature'] = res['nature']
  316. if not no_attr and res['nature'] is None and len(lesup._linked_types[lesub.__class__]) != 0:
  317. #Fetch relation attributes
  318. rel_attr_table = MySQL.get_r2t2table_name(lesup.__class__.__name__, lesub.__class__.__name__)
  319. sql = select(MySQL.rel_attr_table, {MySQL.relations_pkname: id_relation})
  320. if cur.execute(sql) != 1:
  321. raise RuntimeError("Unknow SQL error")
  322. res = all_to_dicts(cur)
  323. if len(res) == 0:
  324. #Here raising a warning and adding empty (or default) attributes will be better
  325. raise RuntimeError("This relation should have attributes but none found !!!")
  326. if len(res) > 1:
  327. raise RuntimeError("When selecting on primary key, get more than one result. Bailout")
  328. attrs = res[0]
  329. relation['rel_attr'] = attrs
  330. return relation
  331. ## @brief Fetch all relations concerning an object (rel2type relations)
  332. # @param leo LeType : LeType child instance
  333. # @return a list of tuple (lesup, lesub, dict_attr)
  334. def get_relations(self, leo):
  335. sql = select(self.datasource_utils.relations_table_name, where=or_(({'id_sub':leo.lodel_id},{'id_sup':leo.lodel_id})))
  336. with self.connection as cur:
  337. results = all_to_dicts(cur.execute(sql))
  338. relations = []
  339. for result in results:
  340. id_sup = result['id_sup']
  341. id_sub = result['id_sub']
  342. del result['id_sup']
  343. del result['id_sub']
  344. rel_attr = result
  345. relations.append((id_sup, id_sub, rel_attr))
  346. return relations
  347. ## @brief Add a superior to a LeObject
  348. # @note in the MySQL version the method will have a depth=None argument to allow reccursive calls to add all the path to the root with corresponding depth
  349. # @param lesup LeType : superior LeType child class instance
  350. # @param lesub LeType : subordinate LeType child class instance
  351. # @param nature str : A relation nature @ref EditorialModel.classtypesa
  352. # @param rank int : The rank of this relation
  353. # @param depth None|int : The depth of the relation (used to make reccursive calls in order to link with all superiors)
  354. # @return The relation ID or False if fails
  355. def add_superior(self, lesup, lesub, nature, rank, depth=None):
  356. params = {'id_sup': lesup.lodel_id,'id_sub': lesub.lodel_id,'nature': nature,'rank': rank}
  357. if depth is not None:
  358. params['depth'] = depth
  359. sql_insert = insert(self.datasource_utils.relations_table_name, params)
  360. with self.connection as cur:
  361. if cur.execute(sql_insert) != 1:
  362. return False
  363. cur.execute('SELECT last_insert_id()')
  364. relation_id, = cur.fetchone()
  365. if nature in EmNature.getall():
  366. parent_superiors = lesup.superiors()
  367. for superior in parent_superiors:
  368. depth = depth - 1 if depth is not None else 1
  369. self.add_relation(lesup=superior.lodel_id, lesub=lesub.lodel_id, nature=nature, depth=depth, rank=rank)
  370. return relation_id
  371. ## @brief Fetch a superiors list ordered by depth for a LeType
  372. # @param lesub LeType : subordinate LeType child class instance
  373. # @param nature str : A relation nature @ref EditorialModel.classtypes
  374. # @return A list of LeType ordered by depth (the first is the direct superior)
  375. def get_superiors(self, lesub, nature):
  376. sql = select(
  377. self.datasource_utils.relations_table_name,
  378. columns=('id_sup',),
  379. where={'id_sub': lesub.lodel_id, 'nature': nature},
  380. order_by=('depth desc',)
  381. )
  382. result = []
  383. with self.connection as cur:
  384. results = all_to_dicts(cur.execute(sql))
  385. superiors = [LeType(result['id_sup']) for result in results]
  386. return superiors
  387. ## @brief Fetch the list of the subordinates given a nature
  388. # @param lesup LeType : superior LeType child class instance
  389. # @param nature str : A relation nature @ref EditorialModel.classtypes
  390. # @return A list of LeType ordered by rank that are subordinates of lesup in a "nature" relation
  391. def get_subordinates(self, lesup, nature):
  392. with self.connection as cur:
  393. id_sup = lesup.lodel_id if isinstance(lesup, leobject.letype.LeType) else MySQL.leroot_lodel_id
  394. sql = select(
  395. MySQL.relations_table_name,
  396. columns=('id_sup',),
  397. where={'id_sup': id_sup, 'nature': nature},
  398. order_by=('rank',)
  399. )
  400. cur.execut(sql)
  401. res = all_to_dicts(cur)
  402. return [LeType(r['id_sup']) for r in res]