Features

..Associations ..————

..Belongs-to and has-many relationships can be declared as such:

..And then used as such:

..Or:

Query watermarking

By defining a watermark in your jardin_conf.py file:

WATERMARK = 'MyGreatApp'

Queries will show up as such in your SQL logs:

/* MyGreatApp | path/to/file.py:function_name:line_number */ SELECT * FROM ....;

Scopes

Query scopes can be defined inside your model as such:

class User(jardin.Model):

  scopes = {
    'active': {'active': True},
    'recent': ["last_sign_up_at > %(week_ago)s", {'week_ago': datetime.utcnow() - timedelta(weeks=1)}]
  }

Then used as such:

User.select(scopes = ['active', 'recent'])

Which will issue this statement

SELECT * FROM users u WHERE u.active IS TRUE AND u.last_sign_up_at > ...;

Soft deletes

If you don’t want to actually remove rows from the database when deleting a record, you can activate soft-deletes:

class User(jardin.Model):

  soft_delete = True

When the destroy method is called on a model instance, the deleted_at database field on the corresponding table will be set to the current UTC time.

Then, when calling select, count, delete or update, rows with a non-NULL deleted_at value will be ignored. This can be overridden by passing the skip_soft_delete=True argument.

The find method is not affected.

To force delete a single record, call destroy(force=True).

To customize the database column used to store the deletion timestamp, do:

class User(jardin.Model):

  soft_delete = 'my_custom_db_column'

Multiple databases and master/replica split

Multiple databases can be declared in jardin_conf.py:

DATABASES = {
  'my_first_db': 'postgres://...',
  'my_first_db_replica': 'postgres://...',
  'my_second_db': 'postgres://...',
  'my_second_db_replical': 'postgres://...'
}

And then in your model declarations:

class Db1Model(jardin.Model):
  db_name = {'master': 'my_first_db', 'replica': 'my_first_db_replica'}

class Db2Model(jardin.Model):
  db_name = {'master': 'my_second_db', 'replica': 'my_second_db_replica'}

class User(Db1Model): pass

class Project(Db2Model): pass

Replica lag measurement

You can measure the current replica lag in seconds using any class inheriting from jardin.Model:

jardin.Model.replica_lag()
# 0.001

MyModel.replica_lag()
# 0.001

Connection drops recovery

The exceptions psycopg2.InterfaceError and psycopg2.OperationalError are rescued and a new connection is initiated. Three attempts with exponential decay are made before bubbling up the exception.