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 20KB

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