.. _conditions: Conditions ^^^^^^^^^^ Conditions are used for: * Optimistic concurrency when :ref:`saving <user-engine-save>` or :ref:`deleting <user-engine-delete>` objects * To specify a Query's :ref:`key condition <user-query-key>` * To :ref:`filter results <user-query-filter>` from a Query or Scan ===================== Built-In Conditions ===================== There is no DynamoDB type that supports all of the conditions. For example, ``contains`` does not work with a numeric type ``"N"`` such as Number or Integer. DynamoDB's `ConditionExpression Reference`__ has the full specification. .. code-block:: python class Model(BaseModel): column = Column(SomeType) # Comparisons Model.column < value Model.column <= value Model.column == value Model.column >= value Model.column > value Model.column != value Model.column.begins_with(value) Model.column.between(low, high) Model.column.contains(value) Model.column.in_([foo, bar, baz]) Model.column.is_(None) Model.column.is_not(False) # bitwise operators combine conditions not_none = Model.column.is_not(None) in_the_future = Model.column > now in_the_past = ~in_the_future either = not_none | in_the_future both = not_none & in_the_future __ http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.SpecifyingConditions.html#ConditionExpressionReference ================ Document Paths ================ You can construct conditions against individual elements of List and Map types with the usual indexing notation. .. code-block:: python Item = Map( name=String, price=Number, quantity=Integer) Metrics = Map(**{ "payment-duration": Number, "coupons.used"=Integer, "coupons.available"=Integer }) class Receipt(BaseModel): transaction_id = Column(UUID, column=True) total = Column(Integer) items = Column(List(Item)) metrics = Column(Metrics) Here are some basic conditions using paths: .. code-block:: python Receipt.metrics["payment-duration"] > 30000 Receipt.items[0]["name"].begins_with("deli:salami:") .. _user-conditions-atomic: =================== Atomic Conditions =================== When you specify ``atomic=True`` during :func:`Engine.save <bloop.engine.Engine.save>` or :func:`Engine.delete <bloop.engine.Engine.delete>`, Bloop will insert a pre-constructed condition on each object to be modified. The condition depends on how the local version of your object was last synchronized with the corresponding row in DynamoDB. Here are the rules: .. _atomic-rules: 1. If the object was created locally and hasn't been saved or deleted, expect **all** of the object's columns to be None in DynamoDB. 2. If the object came from DynamoDB (load, query, stream), only include columns that should have been in the response. 1. If a column is missing and **was** expected, include it in the atomic condition and expect the value to be None in DynamoDB. 2. If a column is missing and **wasn't** expected (query on a projected Index), don't include it. 3. Recompute the atomic condition whenever the local state is synchronized with the DynamoDB value. The following examples use this model: .. code-block:: python class Document(BaseModel): id = Column(Integer, hash_key=True) folder = Column(String) name = Column(String) size = Column(Integer) data = Column(Binary) by_name = GlobalSecondaryIndex( projection=["size"], hash_key="name") --------------------- Example: New Object --------------------- This demonstrates :ref:`Rule 1 <atomic-rules>`. For a new instance created locally but not yet saved: .. code-block:: python document = Document(id=10, folder="~", name=".bashrc") The following atomic condition would be generated: .. code-block:: python atomic = ( Document.id.is_(None) & Document.folder.is_(None) & Document.name.is_(None) & Document.size.is_(None) & Document.data.is_(None) ) In this case, atomic means "only save if this object didn't exist before". -------------------------------- Example: Load a Partial Object -------------------------------- This demonstrates :ref:`Rule 2.1 <atomic-rules>`. :func:`Engine.load <bloop.engine.Engine.load>` will return all columns for an object; if a column's value is missing, it hasn't been set. An atomic save or delete would expect those missing columns to still not have values. First, save an object and then load it into a new instance: .. code-block:: python original_document = Document(id=10, folder="~", name=".bashrc") engine.save(original_document) document = Document(id=10) engine.load(document) The document has the following attributes: .. code-block:: python document.id = 10 document.folder = "~" document.name = ".bashrc" document.size = None document.data = None Now, modify the object locally: .. code-block:: python document.data = b"# ... for non-login shells." document.size = len(document.data) If you try to save this with an atomic condition, it will expect all of the values to be the same as were last loaded from DynamoDB -- **not** the values you just set. The atomic condition is: .. code-block:: python atomic = ( (Document.id == 10) & (Document.folder == "~") & (Document.name == ".bashrc") & Document.size.is_(None) & Document.data.is_(None) ) If another call changed folder or name, or set a value for size or data, the atomic save will fail. -------------------------- Example: Scan on a Table -------------------------- This demonstrates :ref:`Rule 2.2 <atomic-rules>`. Here, the scan uses ``select`` to only return a few columns (and the hash key column). .. code-block:: python scan = engine.scan(Document, projection=[Document.name]) results = list(scan) Each result will have values for ``id`` and ``name``, but the scan did not try to load the other columns. Those columns won't be set to ``None`` - they won't even be loaded by the Column's typedef. Here's a document the scan above found: .. code-block:: python scan_doc = Document(id=343, name="john") If you set the size on this file and then perform an atomic save: .. code-block:: python scan_doc.size = 117 engine.save(scan_doc, atomic=True) The following condition is used: .. code-block:: python atomic = ( (Document.id == 10) & (Document.name == ".bashrc") ) There's no way to know if the previous value for eg. ``folder`` had a value, since the scan told DynamoDB not to include that column when it performed the scan. There's no save assumption for the state of that column in DynamoDB, so it's not part of the generated atomic condition. -------------------------------- Example: Query on a Projection -------------------------------- This demonstrates :ref:`Rule 2.1 <atomic-rules>`. The scan above expected a subset of available columns, and finds a value for each. This query will also expect a subset of all columns (using the index's projection) but the value will be missing. .. code-block:: python query = engine.query( Document.by_name, key=Document.name == ".profile") result = query.first() This index projects the ``size`` column, which means it's expected to populate the ``id``, ``name``, and ``size`` columns. If the result looks like this: .. code-block:: python result = Document(id=747, name="tps-reports.xls", size=None) Then this document didn't have a value for size. Take a minute to compare this to the result from the previous example. Most importantly, this object has a value (None) for the size column, while the scan doesn't. This all comes down to whether the operation expects a value to be present or not. The atomic condition used for this object will be: .. code-block:: python atomic = ( (Document.id == 747) & (Document.name == "tps-reports.xls") & Document.size.is_(None) ) If the value in DynamoDB has a value for ``size``, the operation will fail. If the document's ``data`` column has changed since the query executed, this atomic condition won't care. ------------------------- Example: Save then Save ------------------------- This demonstrates :ref:`Rule 3 <atomic-rules>`. Whenever you save or delete and the operation succeeds, the atomic condition is recomputed to match the current state of the object. Again, the condition will only expect values for any columns that have values. To compare, here are two different Documents: .. code-block:: python data_is_none = Document(id=5, data=None) no_data = Document(id=6) engine.save(data_is_none, no_data) By setting a value for ``data``, the first object's atomic condition must expect the value to still be None. The second object didn't indicate an expectation about the value of ``data``, so there's nothing to expect for the next operation. Here are the two atomic conditions after the save: .. code-block:: python # Atomic for data_is_none atomic = ( (Document.id == 5) & Document.data.is_(None) ) # Atomic for no_data atomic = ( (Document.id == 6) ) You can also hit this case by querying an index with a small projection, and only making changes to the projected columns. When you save, the next atomic condition will still only be on the projected columns.