Indexable

Define attributes on ORM-mapped classes that have “index” attributes forcolumns with Indexable types.

“index” means the attribute is associated with an element of anIndexable column with the predefined index to access it.The Indexable types include types such asARRAY, JSON andHSTORE.

The indexable extension providesColumn-like interface for any element of anIndexable typed column. In simple cases, it can betreated as a Column - mapped attribute.

New in version 1.1.

Synopsis

Given Person as a model with a primary key and JSON data field.While this field may have any number of elements encoded within it,we would like to refer to the element called name individuallyas a dedicated attribute which behaves like a standalone column:

  1. from sqlalchemy import Column, JSON, Integer
  2. from sqlalchemy.ext.declarative import declarative_base
  3. from sqlalchemy.ext.indexable import index_property
  4.  
  5. Base = declarative_base()
  6.  
  7. class Person(Base):
  8. __tablename__ = 'person'
  9.  
  10. id = Column(Integer, primary_key=True)
  11. data = Column(JSON)
  12.  
  13. name = index_property('data', 'name')

Above, the name attribute now behaves like a mapped column. Wecan compose a new Person and set the value of name:

  1. >>> person = Person(name='Alchemist')

The value is now accessible:

  1. >>> person.name
  2. 'Alchemist'

Behind the scenes, the JSON field was initialized to a new blank dictionaryand the field was set:

  1. >>> person.data
  2. {"name": "Alchemist'}

The field is mutable in place:

  1. >>> person.name = 'Renamed'
  2. >>> person.name
  3. 'Renamed'
  4. >>> person.data
  5. {'name': 'Renamed'}

When using index_property, the change that we make to the indexablestructure is also automatically tracked as history; we no longer needto use MutableDict in order to track this changefor the unit of work.

Deletions work normally as well:

  1. >>> del person.name
  2. >>> person.data
  3. {}

Above, deletion of person.name deletes the value from the dictionary,but not the dictionary itself.

A missing key will produce AttributeError:

  1. >>> person = Person()
  2. >>> person.name
  3. ...
  4. AttributeError: 'name'

Unless you set a default value:

  1. >>> class Person(Base):
  2. >>> __tablename__ = 'person'
  3. >>>
  4. >>> id = Column(Integer, primary_key=True)
  5. >>> data = Column(JSON)
  6. >>>
  7. >>> name = index_property('data', 'name', default=None) # See default
  8.  
  9. >>> person = Person()
  10. >>> print(person.name)
  11. None

The attributes are also accessible at the class level.Below, we illustrate Person.name used to generatean indexed SQL criteria:

  1. >>> from sqlalchemy.orm import Session
  2. >>> session = Session()
  3. >>> query = session.query(Person).filter(Person.name == 'Alchemist')

The above query is equivalent to:

  1. >>> query = session.query(Person).filter(Person.data['name'] == 'Alchemist')

Multiple index_property objects can be chained to producemultiple levels of indexing:

  1. from sqlalchemy import Column, JSON, Integer
  2. from sqlalchemy.ext.declarative import declarative_base
  3. from sqlalchemy.ext.indexable import index_property
  4.  
  5. Base = declarative_base()
  6.  
  7. class Person(Base):
  8. __tablename__ = 'person'
  9.  
  10. id = Column(Integer, primary_key=True)
  11. data = Column(JSON)
  12.  
  13. birthday = index_property('data', 'birthday')
  14. year = index_property('birthday', 'year')
  15. month = index_property('birthday', 'month')
  16. day = index_property('birthday', 'day')

Above, a query such as:

  1. q = session.query(Person).filter(Person.year == '1980')

On a PostgreSQL backend, the above query will render as:

  1. SELECT person.id, person.data
  2. FROM person
  3. WHERE person.data -> %(data_1)s -> %(param_1)s = %(param_2)s

Default Values

index_property includes special behaviors for when the indexeddata structure does not exist, and a set operation is called:

  • For an index_property that is given an integer index value,the default data structure will be a Python list of None values,at least as long as the index value; the value is then set at itsplace in the list. This means for an index value of zero, the listwill be initialized to [None] before setting the given value,and for an index value of five, the list will be initialized to[None, None, None, None, None] before setting the fifth elementto the given value. Note that an existing list is not extendedin place to receive a value.

  • for an index_property that is given any other kind of indexvalue (e.g. strings usually), a Python dictionary is used as thedefault data structure.

  • The default data structure can be set to any Python callable using theindex_property.datatype parameter, overriding the previousrules.

Subclassing

index_property can be subclassed, in particular for the commonuse case of providing coercion of values or SQL expressions as they areaccessed. Below is a common recipe for use with a PostgreSQL JSON type,where we want to also include automatic casting plus astext():

  1. class pg_json_property(index_property):
  2. def __init__(self, attr_name, index, cast_type):
  3. super(pg_json_property, self).__init__(attr_name, index)
  4. self.cast_type = cast_type
  5.  
  6. def expr(self, model):
  7. expr = super(pg_json_property, self).expr(model)
  8. return expr.astext.cast(self.cast_type)

The above subclass can be used with the PostgreSQL-specificversion of postgresql.JSON:

  1. from sqlalchemy import Column, Integer
  2. from sqlalchemy.ext.declarative import declarative_base
  3. from sqlalchemy.dialects.postgresql import JSON
  4.  
  5. Base = declarative_base()
  6.  
  7. class Person(Base):
  8. __tablename__ = 'person'
  9.  
  10. id = Column(Integer, primary_key=True)
  11. data = Column(JSON)
  12.  
  13. age = pg_json_property('data', 'age', Integer)

The age attribute at the instance level works as before; howeverwhen rendering SQL, PostgreSQL’s ->> operator will be usedfor indexed access, instead of the usual index operator of ->:

  1. >>> query = session.query(Person).filter(Person.age < 20)

The above query will render:

  1. SELECT person.id, person.data
  2. FROM person
  3. WHERE CAST(person.data ->> %(data_1)s AS INTEGER) < %(param_1)s

API Reference

  • class sqlalchemy.ext.indexable.indexproperty(_attr_name, index, default=, datatype=None, mutable=True, onebased=True)
  • Bases: sqlalchemy.ext.hybrid.hybrid_property
  • A property generator. The generated property describes an objectattribute that corresponds to an Indexablecolumn.

    New in version 1.1.

    See also

    sqlalchemy.ext.indexable

    • init(attr_name, index, default=, datatype=None, mutable=True, onebased=True)
    • Create a new index_property.

      • Parameters
        • attr_name – An attribute name of an Indexable typed column, or otherattribute that returns an indexable structure.

        • index – The index to be used for getting and setting this value. Thisshould be the Python-side index value for integers.

        • default – A value which will be returned instead of _AttributeError_when there is not a value at given index.

        • datatype – default datatype to use when the field is empty.By default, this is derived from the type of index used; aPython list for an integer index, or a Python dictionary forany other style of index. For a list, the list will beinitialized to a list of None values that is at leastindex elements long.

        • mutable – if False, writes and deletes to the attribute willbe disallowed.

        • onebased – assume the SQL representation of this value isone-based; that is, the first index in SQL is 1, not zero.