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