github.com/jincm/wesharechain@v0.0.0-20210122032815-1537409ce26a/server/block/db/update_match.py (about) 1 # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 # not use this file except in compliance with the License. You may obtain 3 # a copy of the License at 4 # 5 # http://www.apache.org/licenses/LICENSE-2.0 6 # 7 # Unless required by applicable law or agreed to in writing, software 8 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 # License for the specific language governing permissions and limitations 11 # under the License. 12 13 import copy 14 15 from sqlalchemy import inspect 16 from sqlalchemy import orm 17 from sqlalchemy import sql 18 from sqlalchemy import types as sqltypes 19 20 from util.convert import to_list 21 22 def update_on_match( 23 query, 24 specimen, 25 surrogate_key, 26 values=None, 27 attempts=3, 28 include_only=None, 29 process_query=None, 30 handle_failure=None 31 ): 32 """Emit an UPDATE statement matching the given specimen. 33 34 E.g.:: 35 36 with enginefacade.writer() as session: 37 specimen = MyInstance( 38 uuid='ccea54f', 39 interface_id='ad33fea', 40 vm_state='SOME_VM_STATE', 41 ) 42 43 values = { 44 'vm_state': 'SOME_NEW_VM_STATE' 45 } 46 47 base_query = model_query( 48 context, models.Instance, 49 project_only=True, session=session) 50 51 hostname_query = model_query( 52 context, models.Instance, session=session, 53 read_deleted='no'). 54 filter(func.lower(models.Instance.hostname) == 'SOMEHOSTNAME') 55 56 surrogate_key = ('uuid', ) 57 58 def process_query(query): 59 return query.where(~exists(hostname_query)) 60 61 def handle_failure(query): 62 try: 63 instance = base_query.one() 64 except NoResultFound: 65 raise exception.InstanceNotFound(instance_id=instance_uuid) 66 67 if session.query(hostname_query.exists()).scalar(): 68 raise exception.InstanceExists( 69 name=values['hostname'].lower()) 70 71 # try again 72 return False 73 74 persistent_instance = base_query.update_on_match( 75 specimen, 76 surrogate_key, 77 values=values, 78 process_query=process_query, 79 handle_failure=handle_failure 80 ) 81 82 The UPDATE statement is constructed against the given specimen 83 using those values which are present to construct a WHERE clause. 84 If the specimen contains additional values to be ignored, the 85 ``include_only`` parameter may be passed which indicates a sequence 86 of attributes to use when constructing the WHERE. 87 88 The UPDATE is performed against an ORM Query, which is created from 89 the given ``Session``, or alternatively by passing the ```query`` 90 parameter referring to an existing query. 91 92 Before the query is invoked, it is also passed through the callable 93 sent as ``process_query``, if present. This hook allows additional 94 criteria to be added to the query after it is created but before 95 invocation. 96 97 The function will then invoke the UPDATE statement and check for 98 "success" one or more times, up to a maximum of that passed as 99 ``attempts``. 100 101 The initial check for "success" from the UPDATE statement is that the 102 number of rows returned matches 1. If zero rows are matched, then 103 the UPDATE statement is assumed to have "failed", and the failure handling 104 phase begins. 105 106 The failure handling phase involves invoking the given ``handle_failure`` 107 function, if any. This handler can perform additional queries to attempt 108 to figure out why the UPDATE didn't match any rows. The handler, 109 upon detection of the exact failure condition, should throw an exception 110 to exit; if it doesn't, it has the option of returning True or False, 111 where False means the error was not handled, and True means that there 112 was not in fact an error, and the function should return successfully. 113 114 If the failure handler is not present, or returns False after ``attempts`` 115 number of attempts, then the function overall raises CantUpdateException. 116 If the handler returns True, then the function returns with no error. 117 118 The return value of the function is a persistent version of the given 119 specimen; this may be the specimen itself, if no matching object were 120 already present in the session; otherwise, the existing object is 121 returned, with the state of the specimen merged into it. The returned 122 persistent object will have the given values populated into the object. 123 124 The object is is returned as "persistent", meaning that it is 125 associated with the given 126 Session and has an identity key (that is, a real primary key 127 value). 128 129 In order to produce this identity key, a strategy must be used to 130 determine it as efficiently and safely as possible: 131 132 1. If the given specimen already contained its primary key attributes 133 fully populated, then these attributes were used as criteria in the 134 UPDATE, so we have the primary key value; it is populated directly. 135 136 2. If the target backend supports RETURNING, then when the update() query 137 is performed with a RETURNING clause so that the matching primary key 138 is returned atomically. This currently includes Postgresql, Oracle 139 and others (notably not MySQL or SQLite). 140 141 3. If the target backend is MySQL, and the given model uses a 142 single-column, AUTO_INCREMENT integer primary key value (as is 143 the case for Nova), MySQL's recommended approach of making use 144 of ``LAST_INSERT_ID(expr)`` is used to atomically acquire the 145 matching primary key value within the scope of the UPDATE 146 statement, then it fetched immediately following by using 147 ``SELECT LAST_INSERT_ID()``. 148 http://dev.mysql.com/doc/refman/5.0/en/information-\ 149 functions.html#function_last-insert-id 150 151 4. Otherwise, for composite keys on MySQL or other backends such 152 as SQLite, the row as UPDATED must be re-fetched in order to 153 acquire the primary key value. The ``surrogate_key`` 154 parameter is used for this in order to re-fetch the row; this 155 is a column name with a known, unique value where 156 the object can be fetched. 157 158 159 """ 160 161 if values is None: 162 values = {} 163 164 entity = inspect(specimen) 165 mapper = entity.mapper 166 if [desc['type'] for desc in query.column_descriptions] != \ 167 [mapper.class_]: 168 raise AssertionError("Query does not match given specimen") 169 170 criteria = manufacture_entity_criteria( 171 specimen, include_only=include_only, exclude=[surrogate_key]) 172 173 query = query.filter(criteria) 174 175 if process_query: 176 query = process_query(query) 177 178 surrogate_key_arg = ( 179 surrogate_key, entity.attrs[surrogate_key].loaded_value) 180 pk_value = None 181 182 for attempt in range(attempts): 183 try: 184 pk_value = query.update_returning_pk(values, surrogate_key_arg) 185 except MultiRowsMatched: 186 raise 187 except NoRowsMatched: 188 if handle_failure and handle_failure(query): 189 break 190 else: 191 break 192 else: 193 raise NoRowsMatched("Zero rows matched for %d attempts" % attempts) 194 195 if pk_value is None: 196 pk_value = entity.mapper.primary_key_from_instance(specimen) 197 198 # NOTE(mdbooth): Can't pass the original specimen object here as it might 199 # have lists of multiple potential values rather than actual values. 200 values = copy.copy(values) 201 values[surrogate_key] = surrogate_key_arg[1] 202 persistent_obj = manufacture_persistent_object( 203 query.session, specimen.__class__(), values, pk_value) 204 205 return persistent_obj 206 207 208 def manufacture_persistent_object( 209 session, specimen, values=None, primary_key=None): 210 """Make an ORM-mapped object persistent in a Session without SQL. 211 212 The persistent object is returned. 213 214 If a matching object is already present in the given session, the specimen 215 is merged into it and the persistent object returned. Otherwise, the 216 specimen itself is made persistent and is returned. 217 218 The object must contain a full primary key, or provide it via the values or 219 primary_key parameters. The object is peristed to the Session in a "clean" 220 state with no pending changes. 221 222 :param session: A Session object. 223 224 :param specimen: a mapped object which is typically transient. 225 226 :param values: a dictionary of values to be applied to the specimen, 227 in addition to the state that's already on it. The attributes will be 228 set such that no history is created; the object remains clean. 229 230 :param primary_key: optional tuple-based primary key. This will also 231 be applied to the instance if present. 232 233 234 """ 235 state = inspect(specimen) 236 mapper = state.mapper 237 238 for k, v in values.items(): 239 orm.attributes.set_committed_value(specimen, k, v) 240 241 pk_attrs = [ 242 mapper.get_property_by_column(col).key 243 for col in mapper.primary_key 244 ] 245 246 if primary_key is not None: 247 for key, value in zip(pk_attrs, primary_key): 248 orm.attributes.set_committed_value( 249 specimen, 250 key, 251 value 252 ) 253 254 for key in pk_attrs: 255 if state.attrs[key].loaded_value is orm.attributes.NO_VALUE: 256 raise ValueError("full primary key must be present") 257 258 orm.make_transient_to_detached(specimen) 259 260 if state.key not in session.identity_map: 261 session.add(specimen) 262 return specimen 263 else: 264 return session.merge(specimen, load=False) 265 266 267 def manufacture_entity_criteria(entity, include_only=None, exclude=None): 268 """Given a mapped instance, produce a WHERE clause. 269 270 The attributes set upon the instance will be combined to produce 271 a SQL expression using the mapped SQL expressions as the base 272 of comparison. 273 274 Values on the instance may be set as tuples in which case the 275 criteria will produce an IN clause. None is also acceptable as a 276 scalar or tuple entry, which will produce IS NULL that is properly 277 joined with an OR against an IN expression if appropriate. 278 279 :param entity: a mapped entity. 280 281 :param include_only: optional sequence of keys to limit which 282 keys are included. 283 284 :param exclude: sequence of keys to exclude 285 286 """ 287 288 state = inspect(entity) 289 exclude = set(exclude) if exclude is not None else set() 290 291 existing = dict( 292 (attr.key, attr.loaded_value) 293 for attr in state.attrs 294 if attr.loaded_value is not orm.attributes.NO_VALUE 295 and attr.key not in exclude 296 ) 297 if include_only: 298 existing = dict( 299 (k, existing[k]) 300 for k in set(existing).intersection(include_only) 301 ) 302 303 return manufacture_criteria(state.mapper, existing) 304 305 306 def manufacture_criteria(mapped, values): 307 """Given a mapper/class and a namespace of values, produce a WHERE clause. 308 309 The class should be a mapped class and the entries in the dictionary 310 correspond to mapped attribute names on the class. 311 312 A value may also be a tuple in which case that particular attribute 313 will be compared to a tuple using IN. The scalar value or 314 tuple can also contain None which translates to an IS NULL, that is 315 properly joined with OR against an IN expression if appropriate. 316 317 :param cls: a mapped class, or actual :class:`.Mapper` object. 318 319 :param values: dictionary of values. 320 321 """ 322 323 mapper = inspect(mapped) 324 325 # organize keys using mapped attribute ordering, which is deterministic 326 value_keys = set(values) 327 keys = [k for k in mapper.column_attrs.keys() if k in value_keys] 328 return sql.and_(*[ 329 _sql_crit(mapper.column_attrs[key].expression, values[key]) 330 for key in keys 331 ]) 332 333 334 def _sql_crit(expression, value): 335 """Produce an equality expression against the given value. 336 337 This takes into account a value that is actually a collection 338 of values, as well as a value of None or collection that contains 339 None. 340 341 """ 342 343 values = to_list(value, default=(None, )) 344 if len(values) == 1: 345 if values[0] is None: 346 return expression == sql.null() 347 else: 348 return expression == values[0] 349 elif _none_set.intersection(values): 350 return sql.or_( 351 expression == sql.null(), 352 _sql_crit(expression, set(values).difference(_none_set)) 353 ) 354 else: 355 return expression.in_(values) 356 357 358 def update_returning_pk(query, values, surrogate_key): 359 """Perform an UPDATE, returning the primary key of the matched row. 360 361 The primary key is returned using a selection of strategies: 362 363 * if the database supports RETURNING, RETURNING is used to retrieve 364 the primary key values inline. 365 366 * If the database is MySQL and the entity is mapped to a single integer 367 primary key column, MySQL's last_insert_id() function is used 368 inline within the UPDATE and then upon a second SELECT to get the 369 value. 370 371 * Otherwise, a "refetch" strategy is used, where a given "surrogate" 372 key value (typically a UUID column on the entity) is used to run 373 a new SELECT against that UUID. This UUID is also placed into 374 the UPDATE query to ensure the row matches. 375 376 :param query: a Query object with existing criterion, against a single 377 entity. 378 379 :param values: a dictionary of values to be updated on the row. 380 381 :param surrogate_key: a tuple of (attrname, value), referring to a 382 UNIQUE attribute that will also match the row. This attribute is used 383 to retrieve the row via a SELECT when no optimized strategy exists. 384 385 :return: the primary key, returned as a tuple. 386 Is only returned if rows matched is one. Otherwise, CantUpdateException 387 is raised. 388 389 """ 390 391 entity = query.column_descriptions[0]['type'] 392 mapper = inspect(entity).mapper 393 session = query.session 394 395 bind = session.connection(mapper=mapper) 396 if bind.dialect.implicit_returning: 397 pk_strategy = _pk_strategy_returning 398 elif bind.dialect.name == 'mysql' and \ 399 len(mapper.primary_key) == 1 and \ 400 isinstance( 401 mapper.primary_key[0].type, sqltypes.Integer): 402 pk_strategy = _pk_strategy_mysql_last_insert_id 403 else: 404 pk_strategy = _pk_strategy_refetch 405 406 return pk_strategy(query, mapper, values, surrogate_key) 407 408 409 def _assert_single_row(rows_updated): 410 if rows_updated == 1: 411 return rows_updated 412 elif rows_updated > 1: 413 raise MultiRowsMatched("%d rows matched; expected one" % rows_updated) 414 else: 415 raise NoRowsMatched("No rows matched the UPDATE") 416 417 418 def _pk_strategy_refetch(query, mapper, values, surrogate_key): 419 420 surrogate_key_name, surrogate_key_value = surrogate_key 421 surrogate_key_col = mapper.attrs[surrogate_key_name].expression 422 423 rowcount = query.\ 424 filter(surrogate_key_col == surrogate_key_value).\ 425 update(values, synchronize_session=False) 426 427 _assert_single_row(rowcount) 428 # SELECT my_table.id AS my_table_id FROM my_table 429 # WHERE my_table.y = ? AND my_table.z = ? 430 # LIMIT ? OFFSET ? 431 fetch_query = query.session.query( 432 *mapper.primary_key).filter( 433 surrogate_key_col == surrogate_key_value) 434 435 primary_key = fetch_query.one() 436 437 return primary_key 438 439 440 def _pk_strategy_returning(query, mapper, values, surrogate_key): 441 surrogate_key_name, surrogate_key_value = surrogate_key 442 surrogate_key_col = mapper.attrs[surrogate_key_name].expression 443 444 update_stmt = _update_stmt_from_query(mapper, query, values) 445 update_stmt = update_stmt.where(surrogate_key_col == surrogate_key_value) 446 update_stmt = update_stmt.returning(*mapper.primary_key) 447 448 # UPDATE my_table SET x=%(x)s, z=%(z)s WHERE my_table.y = %(y_1)s 449 # AND my_table.z = %(z_1)s RETURNING my_table.id 450 result = query.session.execute(update_stmt) 451 rowcount = result.rowcount 452 _assert_single_row(rowcount) 453 primary_key = tuple(result.first()) 454 455 return primary_key 456 457 458 def _pk_strategy_mysql_last_insert_id(query, mapper, values, surrogate_key): 459 460 surrogate_key_name, surrogate_key_value = surrogate_key 461 surrogate_key_col = mapper.attrs[surrogate_key_name].expression 462 463 surrogate_pk_col = mapper.primary_key[0] 464 update_stmt = _update_stmt_from_query(mapper, query, values) 465 update_stmt = update_stmt.where(surrogate_key_col == surrogate_key_value) 466 update_stmt = update_stmt.values( 467 {surrogate_pk_col: sql.func.last_insert_id(surrogate_pk_col)}) 468 469 # UPDATE my_table SET id=last_insert_id(my_table.id), 470 # x=%s, z=%s WHERE my_table.y = %s AND my_table.z = %s 471 result = query.session.execute(update_stmt) 472 rowcount = result.rowcount 473 _assert_single_row(rowcount) 474 # SELECT last_insert_id() AS last_insert_id_1 475 primary_key = query.session.scalar(sql.func.last_insert_id()), 476 477 return primary_key 478 479 480 def _update_stmt_from_query(mapper, query, values): 481 upd_values = dict( 482 ( 483 mapper.column_attrs[key], value 484 ) for key, value in values.items() 485 ) 486 query = query.enable_eagerloads(False) 487 context = query._compile_context() 488 primary_table = context.statement.froms[0] 489 update_stmt = sql.update(primary_table, 490 context.whereclause, 491 upd_values) 492 return update_stmt 493 494 495 _none_set = frozenset([None]) 496 497 498 class CantUpdateException(Exception): 499 pass 500 501 502 class NoRowsMatched(CantUpdateException): 503 pass 504 505 506 class MultiRowsMatched(CantUpdateException): 507 pass