Merge remote-tracking branch 'cqlengine/master' into cqlengine-integration
Conflicts: .gitignore .travis.yml LICENSE MANIFEST.in docs/Makefile docs/conf.py docs/index.rst requirements.txt setup.py tox.ini
This commit is contained in:
commit
778ef61c52
19
.gitignore
vendored
19
.gitignore
vendored
@ -1,11 +1,10 @@
|
||||
*.pyc
|
||||
*.py[co]
|
||||
*.swp
|
||||
*.swo
|
||||
*.so
|
||||
*.egg
|
||||
*.egg-info
|
||||
.tox
|
||||
.idea/
|
||||
.python-version
|
||||
build
|
||||
MANIFEST
|
||||
@ -20,3 +19,19 @@ setuptools*.egg
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
|
||||
# IDE
|
||||
.project
|
||||
.pydevproject
|
||||
.settings/
|
||||
.idea/
|
||||
*.iml
|
||||
|
||||
.DS_Store
|
||||
|
||||
# Unit test / coverage reports
|
||||
.coverage
|
||||
.tox
|
||||
|
||||
#iPython
|
||||
*.ipynb
|
||||
|
32
.travis.yml
32
.travis.yml
@ -17,3 +17,35 @@ install:
|
||||
|
||||
script:
|
||||
- tox -e $TOX_ENV
|
||||
|
||||
# TODO: merge cqlengine tests
|
||||
#env:
|
||||
# - CASSANDRA_VERSION=12
|
||||
# - CASSANDRA_VERSION=20
|
||||
# - CASSANDRA_VERSION=21
|
||||
#
|
||||
#python:
|
||||
# - "2.7"
|
||||
# - "3.4"
|
||||
#
|
||||
#before_install:
|
||||
# - sudo echo "deb http://www.apache.org/dist/cassandra/debian ${CASSANDRA_VERSION}x main" | sudo tee -a /etc/apt/sources.list
|
||||
# - sudo echo "deb-src http://www.apache.org/dist/cassandra/debian ${CASSANDRA_VERSION}x main" | sudo tee -a /etc/apt/sources.list
|
||||
# - sudo rm -rf ~/.gnupg
|
||||
# - sudo gpg --keyserver pgp.mit.edu --recv-keys F758CE318D77295D
|
||||
# - sudo gpg --export --armor F758CE318D77295D | sudo apt-key add -
|
||||
# - sudo gpg --keyserver pgp.mit.edu --recv-keys 2B5C1B00
|
||||
# - sudo gpg --export --armor 2B5C1B00 | sudo apt-key add -
|
||||
# - sudo gpg --keyserver pgp.mit.edu --recv-keys 0353B12C
|
||||
# - sudo gpg --export --armor 0353B12C | sudo apt-key add -
|
||||
# - sudo apt-get update
|
||||
# - sudo apt-get -o Dpkg::Options::="--force-confnew" install -y cassandra
|
||||
# - sudo rm -rf /var/lib/cassandra/*
|
||||
# - sudo sh -c "echo 'JVM_OPTS=\"\${JVM_OPTS} -Djava.net.preferIPv4Stack=false\"' >> /etc/cassandra/cassandra-env.sh"
|
||||
# - sudo service cassandra start
|
||||
#
|
||||
#install:
|
||||
# - "pip install -r requirements.txt --use-mirrors"
|
||||
#
|
||||
#script:
|
||||
# - "nosetests cqlengine/tests --no-skip"
|
||||
|
17
AUTHORS
Normal file
17
AUTHORS
Normal file
@ -0,0 +1,17 @@
|
||||
PRIMARY AUTHORS
|
||||
|
||||
Blake Eggleston <bdeggleston@gmail.com>
|
||||
Jon Haddad <jon@jonhaddad.com>
|
||||
|
||||
CONTRIBUTORS (let us know if we missed you)
|
||||
|
||||
Eric Scrivner - test environment, connection pooling
|
||||
Kevin Deldycke
|
||||
Roey Berman
|
||||
Danny Cosson
|
||||
Michael Hall
|
||||
Netanel Cohen-Tzemach
|
||||
Mariusz Kryński
|
||||
Greg Doermann
|
||||
@pandu-rao
|
||||
Amy Hanlon
|
12
CONTRIBUTING.md
Normal file
12
CONTRIBUTING.md
Normal file
@ -0,0 +1,12 @@
|
||||
### Contributing code to cqlengine
|
||||
|
||||
Before submitting a pull request, please make sure that it follows these guidelines:
|
||||
|
||||
* Limit yourself to one feature or bug fix per pull request.
|
||||
* Include unittests that thoroughly test the feature/bug fix
|
||||
* Write clear, descriptive commit messages.
|
||||
* Many granular commits are preferred over large monolithic commits
|
||||
* If you're adding or modifying features, please update the documentation
|
||||
|
||||
If you're working on a big ticket item, please check in on [cqlengine-users](https://groups.google.com/forum/?fromgroups#!forum/cqlengine-users).
|
||||
We'd hate to have to steer you in a different direction after you've already put in a lot of hard work.
|
14
Makefile
Normal file
14
Makefile
Normal file
@ -0,0 +1,14 @@
|
||||
clean:
|
||||
find . -name *.pyc -delete
|
||||
rm -rf cqlengine/__pycache__
|
||||
|
||||
|
||||
build: clean
|
||||
python setup.py build
|
||||
|
||||
release: clean
|
||||
python setup.py sdist upload
|
||||
|
||||
|
||||
.PHONY: build
|
||||
|
96
README.md
Normal file
96
README.md
Normal file
@ -0,0 +1,96 @@
|
||||
cqlengine
|
||||
===============
|
||||
|
||||
![cqlengine build status](https://travis-ci.org/cqlengine/cqlengine.svg?branch=master)
|
||||
![cqlengine pypi version](http://img.shields.io/pypi/v/cqlengine.svg)
|
||||
![cqlengine monthly downloads](http://img.shields.io/pypi/dm/cqlengine.svg)
|
||||
|
||||
cqlengine is a Cassandra CQL 3 Object Mapper for Python
|
||||
|
||||
**Users of versions < 0.16, the default keyspace 'cqlengine' has been removed. Please read this before upgrading:** [Breaking Changes](https://cqlengine.readthedocs.org/en/latest/topics/models.html#keyspace-change)
|
||||
|
||||
[Documentation](https://cqlengine.readthedocs.org/en/latest/)
|
||||
|
||||
[Report a Bug](https://github.com/cqlengine/cqlengine/issues)
|
||||
|
||||
[Users Mailing List](https://groups.google.com/forum/?fromgroups#!forum/cqlengine-users)
|
||||
|
||||
## Installation
|
||||
```
|
||||
pip install cqlengine
|
||||
```
|
||||
|
||||
## Getting Started on your local machine
|
||||
|
||||
```python
|
||||
#first, define a model
|
||||
import uuid
|
||||
from cqlengine import columns
|
||||
from cqlengine.models import Model
|
||||
|
||||
class ExampleModel(Model):
|
||||
read_repair_chance = 0.05 # optional - defaults to 0.1
|
||||
example_id = columns.UUID(primary_key=True, default=uuid.uuid4)
|
||||
example_type = columns.Integer(index=True)
|
||||
created_at = columns.DateTime()
|
||||
description = columns.Text(required=False)
|
||||
|
||||
#next, setup the connection to your cassandra server(s) and the default keyspace...
|
||||
>>> from cqlengine import connection
|
||||
>>> connection.setup(['127.0.0.1'], "cqlengine")
|
||||
|
||||
# or if you're still on cassandra 1.2
|
||||
>>> connection.setup(['127.0.0.1'], "cqlengine", protocol_version=1)
|
||||
|
||||
# create your keyspace. This is, in general, not what you want in production
|
||||
# see https://cassandra.apache.org/doc/cql3/CQL.html#createKeyspaceStmt for options
|
||||
>>> create_keyspace("cqlengine", "SimpleStrategy", 1)
|
||||
|
||||
#...and create your CQL table
|
||||
>>> from cqlengine.management import sync_table
|
||||
>>> sync_table(ExampleModel)
|
||||
|
||||
#now we can create some rows:
|
||||
>>> em1 = ExampleModel.create(example_type=0, description="example1", created_at=datetime.now())
|
||||
>>> em2 = ExampleModel.create(example_type=0, description="example2", created_at=datetime.now())
|
||||
>>> em3 = ExampleModel.create(example_type=0, description="example3", created_at=datetime.now())
|
||||
>>> em4 = ExampleModel.create(example_type=0, description="example4", created_at=datetime.now())
|
||||
>>> em5 = ExampleModel.create(example_type=1, description="example5", created_at=datetime.now())
|
||||
>>> em6 = ExampleModel.create(example_type=1, description="example6", created_at=datetime.now())
|
||||
>>> em7 = ExampleModel.create(example_type=1, description="example7", created_at=datetime.now())
|
||||
>>> em8 = ExampleModel.create(example_type=1, description="example8", created_at=datetime.now())
|
||||
|
||||
#and now we can run some queries against our table
|
||||
>>> ExampleModel.objects.count()
|
||||
8
|
||||
>>> q = ExampleModel.objects(example_type=1)
|
||||
>>> q.count()
|
||||
4
|
||||
>>> for instance in q:
|
||||
>>> print instance.description
|
||||
example5
|
||||
example6
|
||||
example7
|
||||
example8
|
||||
|
||||
#here we are applying additional filtering to an existing query
|
||||
#query objects are immutable, so calling filter returns a new
|
||||
#query object
|
||||
>>> q2 = q.filter(example_id=em5.example_id)
|
||||
|
||||
>>> q2.count()
|
||||
1
|
||||
>>> for instance in q2:
|
||||
>>> print instance.description
|
||||
example5
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
If you'd like to contribute to cqlengine, please read the [contributor guidelines](https://github.com/bdeggleston/cqlengine/blob/master/CONTRIBUTING.md)
|
||||
|
||||
|
||||
## Authors
|
||||
|
||||
cqlengine was developed primarily by (Blake Eggleston)[blakeeggleston](https://twitter.com/blakeeggleston) and [Jon Haddad](https://twitter.com/rustyrazorblade), with contributions from several others in the community.
|
||||
|
7
RELEASE.txt
Normal file
7
RELEASE.txt
Normal file
@ -0,0 +1,7 @@
|
||||
Check changelog
|
||||
Ensure docs are updated
|
||||
Tests pass
|
||||
Update VERSION
|
||||
Push tag to github
|
||||
Push release to pypi
|
||||
|
46
Vagrantfile
vendored
Normal file
46
Vagrantfile
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
# -*- mode: ruby -*-
|
||||
# vi: set ft=ruby :
|
||||
|
||||
Vagrant::Config.run do |config|
|
||||
# All Vagrant configuration is done here. The most common configuration
|
||||
# options are documented and commented below. For a complete reference,
|
||||
# please see the online documentation at vagrantup.com.
|
||||
|
||||
# Every Vagrant virtual environment requires a box to build off of.
|
||||
config.vm.box = "precise"
|
||||
|
||||
# The url from where the 'config.vm.box' box will be fetched if it
|
||||
# doesn't already exist on the user's system.
|
||||
config.vm.box_url = "http://files.vagrantup.com/precise64.box"
|
||||
|
||||
# Boot with a GUI so you can see the screen. (Default is headless)
|
||||
# config.vm.boot_mode = :gui
|
||||
|
||||
# Assign this VM to a host-only network IP, allowing you to access it
|
||||
# via the IP. Host-only networks can talk to the host machine as well as
|
||||
# any other machines on the same network, but cannot be accessed (through this
|
||||
# network interface) by any external networks.
|
||||
config.vm.network :hostonly, "192.168.33.10"
|
||||
|
||||
config.vm.customize ["modifyvm", :id, "--memory", "2048"]
|
||||
|
||||
# Forward a port from the guest to the host, which allows for outside
|
||||
# computers to access the VM, whereas host only networking does not.
|
||||
config.vm.forward_port 80, 8080
|
||||
|
||||
# Share an additional folder to the guest VM. The first argument is
|
||||
# an identifier, the second is the path on the guest to mount the
|
||||
# folder, and the third is the path on the host to the actual folder.
|
||||
# config.vm.share_folder "v-data", "/vagrant_data", "../data"
|
||||
#config.vm.share_folder "v-root" "/vagrant", ".", :nfs => true
|
||||
|
||||
# Provision with puppet
|
||||
config.vm.provision :shell, :inline => "apt-get update"
|
||||
|
||||
config.vm.provision :puppet, :options => ['--verbose', '--debug'] do |puppet|
|
||||
puppet.facter = {'hostname' => 'cassandraengine'}
|
||||
# puppet.manifests_path = "puppet/manifests"
|
||||
# puppet.manifest_file = "site.pp"
|
||||
puppet.module_path = "modules"
|
||||
end
|
||||
end
|
18
bin/get_changelog.py
Normal file
18
bin/get_changelog.py
Normal file
@ -0,0 +1,18 @@
|
||||
from github import Github
|
||||
import sys, os
|
||||
|
||||
g = Github(os.environ["GITHUB_TOKEN"])
|
||||
|
||||
milestone = sys.argv[1]
|
||||
print "Fetching all issues for milestone %s" % milestone
|
||||
repo = g.get_repo("cqlengine/cqlengine")
|
||||
|
||||
milestones = repo.get_milestones()
|
||||
milestone = [x for x in milestones if x.title == milestone][0]
|
||||
issues = repo.get_issues(milestone=milestone, state="closed")
|
||||
|
||||
for issue in issues:
|
||||
print "[%d] %s" % (issue.number, issue.title)
|
||||
|
||||
print("Done")
|
||||
|
6
bin/test.py
Executable file
6
bin/test.py
Executable file
@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import nose
|
||||
|
||||
|
||||
nose.main()
|
279
changelog
Normal file
279
changelog
Normal file
@ -0,0 +1,279 @@
|
||||
CHANGELOG
|
||||
|
||||
|
||||
0.21.1
|
||||
|
||||
removed references to create_missing_keyspace
|
||||
|
||||
|
||||
0.21.0
|
||||
|
||||
Create keyspace no longer defaults to SimpleStrategy with 3 replicas. It must be manually specified.
|
||||
sync_table() no longer has an option to create keyspaces automatically.
|
||||
cqlengine_test is now the default keyspace used for tests and is explicitly created
|
||||
all references hardcoded to the "test" keyspace have been removed and use the default instead
|
||||
instead of testing with bin/nose, use setup_package
|
||||
improved validation error messages to include column name
|
||||
fixed updating empty sets
|
||||
improvements to puppet download script
|
||||
per query timeouts now supported, see https://cqlengine.readthedocs.org/en/latest/topics/queryset.html?highlight=timeout#per-query-timeouts
|
||||
|
||||
0.19.0
|
||||
|
||||
Fixed tests with Cassandra version 1.2 and 2.1
|
||||
Fixed broken static columns
|
||||
support for IF NOT EXISTS via Model.if_not_exists().create()
|
||||
|
||||
0.18.1
|
||||
|
||||
[264] fixed support for bytearrays in blob columns
|
||||
|
||||
|
||||
0.18.0
|
||||
|
||||
[260] support for static columns
|
||||
[259] docs update - examples for the queryset method references
|
||||
[258] improve docs around blind updates
|
||||
[256] write docs about developing cqlengine itself
|
||||
[254] use CASSANDRA_VERSION in tests
|
||||
[252] memtable_flush_period_in_ms not in C 1.2 - guard against test failure
|
||||
[251] test fails occasionally despite lists looking the same
|
||||
[246] document Batches default behaviour
|
||||
[239] add sphinx contrib module to docs
|
||||
[237] update to cassandra-driver 2.1
|
||||
[232] update docs w/ links to external tutorials
|
||||
[229] get travis to test different cassandra versions
|
||||
[211] inet datatype support
|
||||
[209] Python 3 support
|
||||
|
||||
|
||||
0.17.0
|
||||
|
||||
* retry_connect on setup()
|
||||
* optional default TTL on model
|
||||
* fixed caching documentation
|
||||
|
||||
|
||||
0.16.0
|
||||
|
||||
225: No handling of PagedResult from execute
|
||||
222: figure out how to make travis not totally fail when a test is skipped
|
||||
220: delayed connect. use setup(delayed_connect=True)
|
||||
218: throw exception on create_table and delete_table
|
||||
212: Unchanged primary key trigger error on update
|
||||
206: FAQ - why we dont' do #189
|
||||
191: Add support for simple table properties.
|
||||
172: raise exception when None is passed in as query param
|
||||
170: trying to perform queries before connection is established should raise useful exception
|
||||
162: Not Possible to Make Non-Equality Filtering Queries
|
||||
161: Filtering on DateTime column
|
||||
154: Blob(bytes) column type issue
|
||||
128: remove default read_repair_chance & ensure changes are saved when using sync_table
|
||||
106: specify caching on model
|
||||
99: specify caching options table management
|
||||
94: type checking on sync_table (currently allows anything then fails miserably)
|
||||
73: remove default 'cqlengine' keyspace table management
|
||||
71: add named table and query expression usage to docs
|
||||
|
||||
|
||||
|
||||
0.15.0
|
||||
* native driver integration
|
||||
|
||||
0.14.0
|
||||
|
||||
* fix for setting map to empty (Lifto)
|
||||
* report when creating models with attributes that conflict with cqlengine (maedhroz)
|
||||
* use stable version of sure package (maedhroz)
|
||||
* performance improvements
|
||||
|
||||
|
||||
0.13.0
|
||||
* adding support for post batch callbacks (thanks Daniel Dotsenko github.com/dvdotsenko)
|
||||
* fixing sync table for tables with multiple keys (thanks Daniel Dotsenko github.com/dvdotsenko)
|
||||
* fixing bug in Bytes column (thanks Netanel Cohen-Tzemach github.com/natict)
|
||||
* fixing bug with timestamps and DST (thanks Netanel Cohen-Tzemach github.com/natict)
|
||||
|
||||
0.12.0
|
||||
|
||||
* Normalize and unquote boolean values. (Thanks Kevin Deldycke github.com/kdeldycke)
|
||||
* Fix race condition in connection manager (Thanks Roey Berman github.com/bergundy)
|
||||
* allow access to instance columns as if it is a dict (Thanks Kevin Deldycke github.com/kdeldycke)
|
||||
* Added support for blind partial updates to queryset (Thanks Danny Cosson github.com/dcosson)
|
||||
* Model instance equality check bugfix (Thanks to Michael Hall, github.com/mahall)
|
||||
* Fixed bug syncing tables with camel cased names (Thanks to Netanel Cohen-Tzemach, github.com/natict)
|
||||
* Fixed bug dealing with eggs (Thanks Kevin Deldycke github.com/kdeldycke)
|
||||
|
||||
0.11.0
|
||||
|
||||
* support for USING TIMESTAMP <microseconds from epoch> via a .timestamp(timedelta(seconds=30)) syntax
|
||||
- allows for long, timedelta, and datetime
|
||||
* fixed use of USING TIMESTAMP in batches
|
||||
* clear TTL and timestamp off models after persisting to DB
|
||||
* allows UUID without dashes - (Thanks to Michael Hall, github.com/mahall)
|
||||
* fixes regarding syncing schema settings (thanks Kai Lautaportti github.com/dokai)
|
||||
|
||||
0.10.0
|
||||
|
||||
* support for execute_on_exception within batches
|
||||
|
||||
0.9.2
|
||||
* fixing create keyspace with network topology strategy
|
||||
* fixing regression with query expressions
|
||||
|
||||
0.9
|
||||
* adding update method
|
||||
* adding support for ttls
|
||||
* adding support for per-query consistency
|
||||
* adding BigInt column (thanks @Lifto)
|
||||
* adding support for timezone aware time uuid functions (thanks @dokai)
|
||||
* only saving collection fields on insert if they've been modified
|
||||
* adding model method that returns a list of modified columns
|
||||
|
||||
0.8.5
|
||||
* adding support for timeouts
|
||||
|
||||
0.8.4
|
||||
* changing value manager previous value copying to deepcopy
|
||||
|
||||
0.8.3
|
||||
* better logging for operational errors
|
||||
|
||||
0.8.2
|
||||
* fix for connection failover
|
||||
|
||||
0.8.1
|
||||
* fix for models not exactly matching schema
|
||||
|
||||
0.8.0
|
||||
* support for table polymorphism
|
||||
* var int type
|
||||
|
||||
0.7.1
|
||||
* refactoring query class to make defining custom model instantiation logic easier
|
||||
|
||||
0.7.0
|
||||
* added counter columns
|
||||
* added support for compaction settings at the model level
|
||||
* deprecated delete_table in favor of drop_table
|
||||
* deprecated create_table in favor of sync_table
|
||||
* added support for custom QuerySets
|
||||
|
||||
0.6.0
|
||||
* added table sync
|
||||
|
||||
0.5.2
|
||||
* adding hex conversion to Bytes column
|
||||
|
||||
0.5.1
|
||||
* improving connection pooling
|
||||
* fixing bug with clustering order columns not being quoted
|
||||
|
||||
0.5
|
||||
* eagerly loading results into the query result cache, the cql driver does this anyway,
|
||||
and pulling them from the cursor was causing some problems with gevented queries,
|
||||
this will cause some breaking changes for users calling execute directly
|
||||
|
||||
0.4.10
|
||||
* changing query parameter placeholders from uuid1 to uuid4
|
||||
|
||||
0.4.7
|
||||
* adding support for passing None into query batch methods to clear any batch objects
|
||||
|
||||
0.4.6
|
||||
* fixing the way models are compared
|
||||
|
||||
0.4.5
|
||||
* fixed bug where container columns would not call their child to_python method, this only really affected columns with special to_python logic
|
||||
|
||||
0.4.4
|
||||
* adding query logging back
|
||||
* fixed bug updating an empty list column
|
||||
|
||||
0.4.3
|
||||
* fixed bug with Text column validation
|
||||
|
||||
0.4.2
|
||||
* added support for instantiating container columns with column instances
|
||||
|
||||
0.4.1
|
||||
* fixed bug in TimeUUID from datetime method
|
||||
|
||||
0.4.0
|
||||
* removed default values from all column types
|
||||
* explicit primary key is required (automatic id removed)
|
||||
* added validation on keyname types on .create()
|
||||
* changed table_name to __table_name__, read_repair_chance to __read_repair_chance__, keyspace to __keyspace__
|
||||
* modified table name auto generator to ignore module name
|
||||
* changed internal implementation of model value get/set
|
||||
* added TimeUUID.from_datetime(), used for generating UUID1's for a specific
|
||||
time
|
||||
|
||||
0.3.3
|
||||
* added abstract base class models
|
||||
|
||||
0.3.2
|
||||
* comprehesive rewrite of connection management (thanks @rustyrazorblade)
|
||||
|
||||
0.3
|
||||
* added support for Token function (thanks @mrk-its)
|
||||
* added support for compound partition key (thanks @mrk-its)s
|
||||
* added support for defining clustering key ordering (thanks @mrk-its)
|
||||
* added values_list to Query class, bypassing object creation if desired (thanks @mrk-its)
|
||||
* fixed bug with Model.objects caching values (thanks @mrk-its)
|
||||
* fixed Cassandra 1.2.5 compatibility bug
|
||||
* updated model exception inheritance
|
||||
|
||||
0.2.1
|
||||
* adding support for datetimes with tzinfo (thanks @gdoermann)
|
||||
* fixing bug in saving map updates (thanks @pandu-rao)
|
||||
|
||||
0.2
|
||||
* expanding internal save function to use update where appropriate
|
||||
* adding set, list, and map collection types
|
||||
* adding support for allow filtering flag
|
||||
* updating management functions to work with cassandra 1.2
|
||||
* fixed a bug querying datetimes
|
||||
* modifying datetime serialization to preserve millisecond accuracy
|
||||
* adding cql function call generators MaxTimeUUID and MinTimeUUID
|
||||
|
||||
0.1.1
|
||||
* fixed a bug occurring when creating tables from the REPL
|
||||
|
||||
0.1
|
||||
* added min_length and max_length validators to the Text column
|
||||
* added == and != equality operators to model class
|
||||
* fixed bug with default values that would evaluate to False (ie: 0, '')
|
||||
|
||||
0.0.9
|
||||
* fixed column inheritance bug
|
||||
* manually defined table names are no longer inherited by subclasses
|
||||
|
||||
0.0.8
|
||||
* added configurable read repair chance to model definitions
|
||||
* added configurable keyspace strategy class and replication factor to keyspace creator
|
||||
|
||||
0.0.7
|
||||
* fixed manual table name bug
|
||||
* changed model level db_name field to table_name
|
||||
|
||||
0.0.6
|
||||
* added TimeUUID column
|
||||
|
||||
0.0.5
|
||||
* added connection pooling
|
||||
* adding a few convenience query classmethods to the model class
|
||||
|
||||
0.0.4-ALPHA
|
||||
* added Sphinx docs
|
||||
* changing create_column_family management function to create_table
|
||||
* changing delete_column_family management function to delete_table
|
||||
* added partition key validation to QuerySet delete method
|
||||
* added .get() method to QuerySet
|
||||
* added create method shortcut to the model class
|
||||
|
||||
0.0.3-ALPHA
|
||||
* added queryset result caching
|
||||
* added queryset array index access and slicing
|
||||
* updating table name generation (more readable)
|
||||
|
1
cqlengine/VERSION
Normal file
1
cqlengine/VERSION
Normal file
@ -0,0 +1 @@
|
||||
0.21.0
|
33
cqlengine/__init__.py
Normal file
33
cqlengine/__init__.py
Normal file
@ -0,0 +1,33 @@
|
||||
import pkg_resources
|
||||
|
||||
from cassandra import ConsistencyLevel
|
||||
|
||||
from cqlengine.columns import *
|
||||
from cqlengine.functions import *
|
||||
from cqlengine.models import Model
|
||||
from cqlengine.query import BatchQuery
|
||||
|
||||
|
||||
__cqlengine_version_path__ = pkg_resources.resource_filename('cqlengine',
|
||||
'VERSION')
|
||||
__version__ = open(__cqlengine_version_path__, 'r').readline().strip()
|
||||
|
||||
# compaction
|
||||
SizeTieredCompactionStrategy = "SizeTieredCompactionStrategy"
|
||||
LeveledCompactionStrategy = "LeveledCompactionStrategy"
|
||||
|
||||
# Caching constants.
|
||||
CACHING_ALL = "ALL"
|
||||
CACHING_KEYS_ONLY = "KEYS_ONLY"
|
||||
CACHING_ROWS_ONLY = "ROWS_ONLY"
|
||||
CACHING_NONE = "NONE"
|
||||
|
||||
ANY = ConsistencyLevel.ANY
|
||||
ONE = ConsistencyLevel.ONE
|
||||
TWO = ConsistencyLevel.TWO
|
||||
THREE = ConsistencyLevel.THREE
|
||||
QUORUM = ConsistencyLevel.QUORUM
|
||||
LOCAL_ONE = ConsistencyLevel.LOCAL_ONE
|
||||
LOCAL_QUORUM = ConsistencyLevel.LOCAL_QUORUM
|
||||
EACH_QUORUM = ConsistencyLevel.EACH_QUORUM
|
||||
ALL = ConsistencyLevel.ALL
|
754
cqlengine/columns.py
Normal file
754
cqlengine/columns.py
Normal file
@ -0,0 +1,754 @@
|
||||
#column field types
|
||||
from copy import deepcopy, copy
|
||||
from datetime import datetime
|
||||
from datetime import date
|
||||
import re
|
||||
from cassandra.cqltypes import DateType
|
||||
|
||||
from cqlengine.exceptions import ValidationError
|
||||
from cassandra.encoder import cql_quote
|
||||
import six
|
||||
|
||||
import sys
|
||||
|
||||
# move to central spot
|
||||
class UnicodeMixin(object):
|
||||
if sys.version_info > (3, 0):
|
||||
__str__ = lambda x: x.__unicode__()
|
||||
else:
|
||||
__str__ = lambda x: six.text_type(x).encode('utf-8')
|
||||
|
||||
|
||||
class BaseValueManager(object):
|
||||
|
||||
def __init__(self, instance, column, value):
|
||||
self.instance = instance
|
||||
self.column = column
|
||||
self.previous_value = deepcopy(value)
|
||||
self.value = value
|
||||
self.explicit = False
|
||||
|
||||
@property
|
||||
def deleted(self):
|
||||
return self.value is None and self.previous_value is not None
|
||||
|
||||
@property
|
||||
def changed(self):
|
||||
"""
|
||||
Indicates whether or not this value has changed.
|
||||
|
||||
:rtype: boolean
|
||||
|
||||
"""
|
||||
return self.value != self.previous_value
|
||||
|
||||
def reset_previous_value(self):
|
||||
self.previous_value = copy(self.value)
|
||||
|
||||
def getval(self):
|
||||
return self.value
|
||||
|
||||
def setval(self, val):
|
||||
self.value = val
|
||||
|
||||
def delval(self):
|
||||
self.value = None
|
||||
|
||||
def get_property(self):
|
||||
_get = lambda slf: self.getval()
|
||||
_set = lambda slf, val: self.setval(val)
|
||||
_del = lambda slf: self.delval()
|
||||
|
||||
if self.column.can_delete:
|
||||
return property(_get, _set, _del)
|
||||
else:
|
||||
return property(_get, _set)
|
||||
|
||||
|
||||
class ValueQuoter(object):
|
||||
"""
|
||||
contains a single value, which will quote itself for CQL insertion statements
|
||||
"""
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
|
||||
def __str__(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, self.__class__):
|
||||
return self.value == other.value
|
||||
return False
|
||||
|
||||
|
||||
class Column(object):
|
||||
|
||||
#the cassandra type this column maps to
|
||||
db_type = None
|
||||
value_manager = BaseValueManager
|
||||
|
||||
instance_counter = 0
|
||||
|
||||
def __init__(self,
|
||||
primary_key=False,
|
||||
partition_key=False,
|
||||
index=False,
|
||||
db_field=None,
|
||||
default=None,
|
||||
required=False,
|
||||
clustering_order=None,
|
||||
polymorphic_key=False,
|
||||
static=False):
|
||||
"""
|
||||
:param primary_key: bool flag, indicates this column is a primary key. The first primary key defined
|
||||
on a model is the partition key (unless partition keys are set), all others are cluster keys
|
||||
:param partition_key: indicates that this column should be the partition key, defining
|
||||
more than one partition key column creates a compound partition key
|
||||
:param index: bool flag, indicates an index should be created for this column
|
||||
:param db_field: the fieldname this field will map to in the database
|
||||
:param default: the default value, can be a value or a callable (no args)
|
||||
:param required: boolean, is the field required? Model validation will raise and
|
||||
exception if required is set to True and there is a None value assigned
|
||||
:param clustering_order: only applicable on clustering keys (primary keys that are not partition keys)
|
||||
determines the order that the clustering keys are sorted on disk
|
||||
:param polymorphic_key: boolean, if set to True, this column will be used for saving and loading instances
|
||||
of polymorphic tables
|
||||
"""
|
||||
self.partition_key = partition_key
|
||||
self.primary_key = partition_key or primary_key
|
||||
self.index = index
|
||||
self.db_field = db_field
|
||||
self.default = default
|
||||
self.required = required
|
||||
self.clustering_order = clustering_order
|
||||
self.polymorphic_key = polymorphic_key
|
||||
#the column name in the model definition
|
||||
self.column_name = None
|
||||
self.static = static
|
||||
|
||||
self.value = None
|
||||
|
||||
#keep track of instantiation order
|
||||
self.position = Column.instance_counter
|
||||
Column.instance_counter += 1
|
||||
|
||||
def validate(self, value):
|
||||
"""
|
||||
Returns a cleaned and validated value. Raises a ValidationError
|
||||
if there's a problem
|
||||
"""
|
||||
if value is None:
|
||||
if self.required:
|
||||
raise ValidationError('{} - None values are not allowed'.format(self.column_name or self.db_field))
|
||||
return value
|
||||
|
||||
def to_python(self, value):
|
||||
"""
|
||||
Converts data from the database into python values
|
||||
raises a ValidationError if the value can't be converted
|
||||
"""
|
||||
return value
|
||||
|
||||
def to_database(self, value):
|
||||
"""
|
||||
Converts python value into database value
|
||||
"""
|
||||
if value is None and self.has_default:
|
||||
return self.get_default()
|
||||
return value
|
||||
|
||||
@property
|
||||
def has_default(self):
|
||||
return self.default is not None
|
||||
|
||||
@property
|
||||
def is_primary_key(self):
|
||||
return self.primary_key
|
||||
|
||||
@property
|
||||
def can_delete(self):
|
||||
return not self.primary_key
|
||||
|
||||
def get_default(self):
|
||||
if self.has_default:
|
||||
if callable(self.default):
|
||||
return self.default()
|
||||
else:
|
||||
return self.default
|
||||
|
||||
def get_column_def(self):
|
||||
"""
|
||||
Returns a column definition for CQL table definition
|
||||
"""
|
||||
static = "static" if self.static else ""
|
||||
return '{} {} {}'.format(self.cql, self.db_type, static)
|
||||
|
||||
def set_column_name(self, name):
|
||||
"""
|
||||
Sets the column name during document class construction
|
||||
This value will be ignored if db_field is set in __init__
|
||||
"""
|
||||
self.column_name = name
|
||||
|
||||
@property
|
||||
def db_field_name(self):
|
||||
""" Returns the name of the cql name of this column """
|
||||
return self.db_field or self.column_name
|
||||
|
||||
@property
|
||||
def db_index_name(self):
|
||||
""" Returns the name of the cql index """
|
||||
return 'index_{}'.format(self.db_field_name)
|
||||
|
||||
@property
|
||||
def cql(self):
|
||||
return self.get_cql()
|
||||
|
||||
def get_cql(self):
|
||||
return '"{}"'.format(self.db_field_name)
|
||||
|
||||
def _val_is_null(self, val):
|
||||
""" determines if the given value equates to a null value for the given column type """
|
||||
return val is None
|
||||
|
||||
|
||||
class Blob(Column):
|
||||
db_type = 'blob'
|
||||
|
||||
def to_database(self, value):
|
||||
|
||||
if not isinstance(value, (six.binary_type, bytearray)):
|
||||
raise Exception("expecting a binary, got a %s" % type(value))
|
||||
|
||||
val = super(Bytes, self).to_database(value)
|
||||
return bytearray(val)
|
||||
|
||||
def to_python(self, value):
|
||||
#return value[2:].decode('hex')
|
||||
return value
|
||||
|
||||
Bytes = Blob
|
||||
|
||||
class Ascii(Column):
|
||||
db_type = 'ascii'
|
||||
|
||||
class Inet(Column):
|
||||
db_type = 'inet'
|
||||
|
||||
|
||||
class Text(Column):
|
||||
db_type = 'text'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.min_length = kwargs.pop('min_length', 1 if kwargs.get('required', False) else None)
|
||||
self.max_length = kwargs.pop('max_length', None)
|
||||
super(Text, self).__init__(*args, **kwargs)
|
||||
|
||||
def validate(self, value):
|
||||
value = super(Text, self).validate(value)
|
||||
if value is None: return
|
||||
if not isinstance(value, (six.string_types, bytearray)) and value is not None:
|
||||
raise ValidationError('{} {} is not a string'.format(self.column_name, type(value)))
|
||||
if self.max_length:
|
||||
if len(value) > self.max_length:
|
||||
raise ValidationError('{} is longer than {} characters'.format(self.column_name, self.max_length))
|
||||
if self.min_length:
|
||||
if len(value) < self.min_length:
|
||||
raise ValidationError('{} is shorter than {} characters'.format(self.column_name, self.min_length))
|
||||
return value
|
||||
|
||||
|
||||
class Integer(Column):
|
||||
db_type = 'int'
|
||||
|
||||
def validate(self, value):
|
||||
val = super(Integer, self).validate(value)
|
||||
if val is None: return
|
||||
try:
|
||||
return int(val)
|
||||
except (TypeError, ValueError):
|
||||
raise ValidationError("{} {} can't be converted to integral value".format(self.column_name, value))
|
||||
|
||||
def to_python(self, value):
|
||||
return self.validate(value)
|
||||
|
||||
def to_database(self, value):
|
||||
return self.validate(value)
|
||||
|
||||
|
||||
class BigInt(Integer):
|
||||
db_type = 'bigint'
|
||||
|
||||
|
||||
class VarInt(Column):
|
||||
db_type = 'varint'
|
||||
|
||||
def validate(self, value):
|
||||
val = super(VarInt, self).validate(value)
|
||||
if val is None:
|
||||
return
|
||||
try:
|
||||
return int(val)
|
||||
except (TypeError, ValueError):
|
||||
raise ValidationError(
|
||||
"{} {} can't be converted to integral value".format(self.column_name, value))
|
||||
|
||||
def to_python(self, value):
|
||||
return self.validate(value)
|
||||
|
||||
def to_database(self, value):
|
||||
return self.validate(value)
|
||||
|
||||
|
||||
class CounterValueManager(BaseValueManager):
|
||||
def __init__(self, instance, column, value):
|
||||
super(CounterValueManager, self).__init__(instance, column, value)
|
||||
self.value = self.value or 0
|
||||
self.previous_value = self.previous_value or 0
|
||||
|
||||
|
||||
class Counter(Integer):
|
||||
db_type = 'counter'
|
||||
|
||||
value_manager = CounterValueManager
|
||||
|
||||
def __init__(self,
|
||||
index=False,
|
||||
db_field=None,
|
||||
required=False):
|
||||
super(Counter, self).__init__(
|
||||
primary_key=False,
|
||||
partition_key=False,
|
||||
index=index,
|
||||
db_field=db_field,
|
||||
default=0,
|
||||
required=required,
|
||||
)
|
||||
|
||||
|
||||
class DateTime(Column):
|
||||
db_type = 'timestamp'
|
||||
|
||||
def to_python(self, value):
|
||||
if value is None: return
|
||||
if isinstance(value, datetime):
|
||||
return value
|
||||
elif isinstance(value, date):
|
||||
return datetime(*(value.timetuple()[:6]))
|
||||
try:
|
||||
return datetime.utcfromtimestamp(value)
|
||||
except TypeError:
|
||||
return datetime.utcfromtimestamp(DateType.deserialize(value))
|
||||
|
||||
def to_database(self, value):
|
||||
value = super(DateTime, self).to_database(value)
|
||||
if value is None: return
|
||||
if not isinstance(value, datetime):
|
||||
if isinstance(value, date):
|
||||
value = datetime(value.year, value.month, value.day)
|
||||
else:
|
||||
raise ValidationError("{} '{}' is not a datetime object".format(self.column_name, value))
|
||||
epoch = datetime(1970, 1, 1, tzinfo=value.tzinfo)
|
||||
offset = epoch.tzinfo.utcoffset(epoch).total_seconds() if epoch.tzinfo else 0
|
||||
|
||||
return int(((value - epoch).total_seconds() - offset) * 1000)
|
||||
|
||||
|
||||
class Date(Column):
|
||||
db_type = 'timestamp'
|
||||
|
||||
def to_python(self, value):
|
||||
if value is None: return
|
||||
if isinstance(value, datetime):
|
||||
return value.date()
|
||||
elif isinstance(value, date):
|
||||
return value
|
||||
try:
|
||||
return datetime.utcfromtimestamp(value).date()
|
||||
except TypeError:
|
||||
return datetime.utcfromtimestamp(DateType.deserialize(value)).date()
|
||||
|
||||
def to_database(self, value):
|
||||
value = super(Date, self).to_database(value)
|
||||
if value is None: return
|
||||
if isinstance(value, datetime):
|
||||
value = value.date()
|
||||
if not isinstance(value, date):
|
||||
raise ValidationError("{} '{}' is not a date object".format(self.column_name, repr(value)))
|
||||
|
||||
return int((value - date(1970, 1, 1)).total_seconds() * 1000)
|
||||
|
||||
|
||||
class UUID(Column):
|
||||
"""
|
||||
Type 1 or 4 UUID
|
||||
"""
|
||||
db_type = 'uuid'
|
||||
|
||||
re_uuid = re.compile(r'[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}')
|
||||
|
||||
def validate(self, value):
|
||||
val = super(UUID, self).validate(value)
|
||||
if val is None: return
|
||||
from uuid import UUID as _UUID
|
||||
if isinstance(val, _UUID): return val
|
||||
if isinstance(val, six.string_types) and self.re_uuid.match(val):
|
||||
return _UUID(val)
|
||||
raise ValidationError("{} {} is not a valid uuid".format(self.column_name, value))
|
||||
|
||||
def to_python(self, value):
|
||||
return self.validate(value)
|
||||
|
||||
def to_database(self, value):
|
||||
return self.validate(value)
|
||||
|
||||
from uuid import UUID as pyUUID, getnode
|
||||
|
||||
|
||||
class TimeUUID(UUID):
|
||||
"""
|
||||
UUID containing timestamp
|
||||
"""
|
||||
|
||||
db_type = 'timeuuid'
|
||||
|
||||
@classmethod
|
||||
def from_datetime(self, dt):
|
||||
"""
|
||||
generates a UUID for a given datetime
|
||||
|
||||
:param dt: datetime
|
||||
:type dt: datetime
|
||||
:return:
|
||||
"""
|
||||
global _last_timestamp
|
||||
|
||||
epoch = datetime(1970, 1, 1, tzinfo=dt.tzinfo)
|
||||
offset = epoch.tzinfo.utcoffset(epoch).total_seconds() if epoch.tzinfo else 0
|
||||
timestamp = (dt - epoch).total_seconds() - offset
|
||||
|
||||
node = None
|
||||
clock_seq = None
|
||||
|
||||
nanoseconds = int(timestamp * 1e9)
|
||||
timestamp = int(nanoseconds // 100) + 0x01b21dd213814000
|
||||
|
||||
if clock_seq is None:
|
||||
import random
|
||||
clock_seq = random.randrange(1 << 14) # instead of stable storage
|
||||
time_low = timestamp & 0xffffffff
|
||||
time_mid = (timestamp >> 32) & 0xffff
|
||||
time_hi_version = (timestamp >> 48) & 0x0fff
|
||||
clock_seq_low = clock_seq & 0xff
|
||||
clock_seq_hi_variant = (clock_seq >> 8) & 0x3f
|
||||
if node is None:
|
||||
node = getnode()
|
||||
return pyUUID(fields=(time_low, time_mid, time_hi_version,
|
||||
clock_seq_hi_variant, clock_seq_low, node), version=1)
|
||||
|
||||
|
||||
class Boolean(Column):
|
||||
db_type = 'boolean'
|
||||
|
||||
def validate(self, value):
|
||||
""" Always returns a Python boolean. """
|
||||
value = super(Boolean, self).validate(value)
|
||||
|
||||
if value is not None:
|
||||
value = bool(value)
|
||||
|
||||
return value
|
||||
|
||||
def to_python(self, value):
|
||||
return self.validate(value)
|
||||
|
||||
|
||||
class Float(Column):
|
||||
db_type = 'double'
|
||||
|
||||
def __init__(self, double_precision=True, **kwargs):
|
||||
self.db_type = 'double' if double_precision else 'float'
|
||||
super(Float, self).__init__(**kwargs)
|
||||
|
||||
def validate(self, value):
|
||||
value = super(Float, self).validate(value)
|
||||
if value is None: return
|
||||
try:
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
raise ValidationError("{} {} is not a valid float".format(self.column_name, value))
|
||||
|
||||
def to_python(self, value):
|
||||
return self.validate(value)
|
||||
|
||||
def to_database(self, value):
|
||||
return self.validate(value)
|
||||
|
||||
|
||||
class Decimal(Column):
|
||||
db_type = 'decimal'
|
||||
|
||||
def validate(self, value):
|
||||
from decimal import Decimal as _Decimal
|
||||
from decimal import InvalidOperation
|
||||
val = super(Decimal, self).validate(value)
|
||||
if val is None: return
|
||||
try:
|
||||
return _Decimal(val)
|
||||
except InvalidOperation:
|
||||
raise ValidationError("{} '{}' can't be coerced to decimal".format(self.column_name, val))
|
||||
|
||||
def to_python(self, value):
|
||||
return self.validate(value)
|
||||
|
||||
def to_database(self, value):
|
||||
return self.validate(value)
|
||||
|
||||
|
||||
class BaseContainerColumn(Column):
|
||||
"""
|
||||
Base Container type for collection-like columns.
|
||||
|
||||
https://cassandra.apache.org/doc/cql3/CQL.html#collections
|
||||
"""
|
||||
|
||||
def __init__(self, value_type, **kwargs):
|
||||
"""
|
||||
:param value_type: a column class indicating the types of the value
|
||||
"""
|
||||
inheritance_comparator = issubclass if isinstance(value_type, type) else isinstance
|
||||
if not inheritance_comparator(value_type, Column):
|
||||
raise ValidationError('value_type must be a column class')
|
||||
if inheritance_comparator(value_type, BaseContainerColumn):
|
||||
raise ValidationError('container types cannot be nested')
|
||||
if value_type.db_type is None:
|
||||
raise ValidationError('value_type cannot be an abstract column type')
|
||||
|
||||
if isinstance(value_type, type):
|
||||
self.value_type = value_type
|
||||
self.value_col = self.value_type()
|
||||
else:
|
||||
self.value_col = value_type
|
||||
self.value_type = self.value_col.__class__
|
||||
|
||||
super(BaseContainerColumn, self).__init__(**kwargs)
|
||||
|
||||
def validate(self, value):
|
||||
value = super(BaseContainerColumn, self).validate(value)
|
||||
# It is dangerous to let collections have more than 65535.
|
||||
# See: https://issues.apache.org/jira/browse/CASSANDRA-5428
|
||||
if value is not None and len(value) > 65535:
|
||||
raise ValidationError("{} Collection can't have more than 65535 elements.".format(self.column_name))
|
||||
return value
|
||||
|
||||
def get_column_def(self):
|
||||
"""
|
||||
Returns a column definition for CQL table definition
|
||||
"""
|
||||
static = "static" if self.static else ""
|
||||
db_type = self.db_type.format(self.value_type.db_type)
|
||||
return '{} {} {}'.format(self.cql, db_type, static)
|
||||
|
||||
def _val_is_null(self, val):
|
||||
return not val
|
||||
|
||||
|
||||
class BaseContainerQuoter(ValueQuoter):
|
||||
|
||||
def __nonzero__(self):
|
||||
return bool(self.value)
|
||||
|
||||
|
||||
class Set(BaseContainerColumn):
|
||||
"""
|
||||
Stores a set of unordered, unique values
|
||||
|
||||
http://www.datastax.com/docs/1.2/cql_cli/using/collections
|
||||
"""
|
||||
db_type = 'set<{}>'
|
||||
|
||||
class Quoter(BaseContainerQuoter):
|
||||
|
||||
def __str__(self):
|
||||
cq = cql_quote
|
||||
return '{' + ', '.join([cq(v) for v in self.value]) + '}'
|
||||
|
||||
def __init__(self, value_type, strict=True, default=set, **kwargs):
|
||||
"""
|
||||
:param value_type: a column class indicating the types of the value
|
||||
:param strict: sets whether non set values will be coerced to set
|
||||
type on validation, or raise a validation error, defaults to True
|
||||
"""
|
||||
self.strict = strict
|
||||
|
||||
super(Set, self).__init__(value_type, default=default, **kwargs)
|
||||
|
||||
def validate(self, value):
|
||||
val = super(Set, self).validate(value)
|
||||
if val is None: return
|
||||
types = (set,) if self.strict else (set, list, tuple)
|
||||
if not isinstance(val, types):
|
||||
if self.strict:
|
||||
raise ValidationError('{} {} is not a set object'.format(self.column_name, val))
|
||||
else:
|
||||
raise ValidationError('{} {} cannot be coerced to a set object'.format(self.column_name, val))
|
||||
|
||||
if None in val:
|
||||
raise ValidationError("{} None not allowed in a set".format(self.column_name))
|
||||
|
||||
return {self.value_col.validate(v) for v in val}
|
||||
|
||||
def to_python(self, value):
|
||||
if value is None: return set()
|
||||
return {self.value_col.to_python(v) for v in value}
|
||||
|
||||
def to_database(self, value):
|
||||
if value is None: return None
|
||||
|
||||
if isinstance(value, self.Quoter): return value
|
||||
return self.Quoter({self.value_col.to_database(v) for v in value})
|
||||
|
||||
|
||||
|
||||
|
||||
class List(BaseContainerColumn):
|
||||
"""
|
||||
Stores a list of ordered values
|
||||
|
||||
http://www.datastax.com/docs/1.2/cql_cli/using/collections_list
|
||||
"""
|
||||
db_type = 'list<{}>'
|
||||
|
||||
class Quoter(BaseContainerQuoter):
|
||||
|
||||
def __str__(self):
|
||||
cq = cql_quote
|
||||
return '[' + ', '.join([cq(v) for v in self.value]) + ']'
|
||||
|
||||
def __nonzero__(self):
|
||||
return bool(self.value)
|
||||
|
||||
def __init__(self, value_type, default=list, **kwargs):
|
||||
return super(List, self).__init__(value_type=value_type, default=default, **kwargs)
|
||||
|
||||
def validate(self, value):
|
||||
val = super(List, self).validate(value)
|
||||
if val is None: return
|
||||
if not isinstance(val, (set, list, tuple)):
|
||||
raise ValidationError('{} {} is not a list object'.format(self.column_name, val))
|
||||
if None in val:
|
||||
raise ValidationError("{} None is not allowed in a list".format(self.column_name))
|
||||
return [self.value_col.validate(v) for v in val]
|
||||
|
||||
def to_python(self, value):
|
||||
if value is None: return []
|
||||
return [self.value_col.to_python(v) for v in value]
|
||||
|
||||
def to_database(self, value):
|
||||
if value is None: return None
|
||||
if isinstance(value, self.Quoter): return value
|
||||
return self.Quoter([self.value_col.to_database(v) for v in value])
|
||||
|
||||
|
||||
class Map(BaseContainerColumn):
|
||||
"""
|
||||
Stores a key -> value map (dictionary)
|
||||
|
||||
http://www.datastax.com/docs/1.2/cql_cli/using/collections_map
|
||||
"""
|
||||
|
||||
db_type = 'map<{}, {}>'
|
||||
|
||||
class Quoter(BaseContainerQuoter):
|
||||
|
||||
def __str__(self):
|
||||
cq = cql_quote
|
||||
return '{' + ', '.join([cq(k) + ':' + cq(v) for k,v in self.value.items()]) + '}'
|
||||
|
||||
def get(self, key):
|
||||
return self.value.get(key)
|
||||
|
||||
def keys(self):
|
||||
return self.value.keys()
|
||||
|
||||
def items(self):
|
||||
return self.value.items()
|
||||
|
||||
def __init__(self, key_type, value_type, default=dict, **kwargs):
|
||||
"""
|
||||
:param key_type: a column class indicating the types of the key
|
||||
:param value_type: a column class indicating the types of the value
|
||||
"""
|
||||
inheritance_comparator = issubclass if isinstance(key_type, type) else isinstance
|
||||
if not inheritance_comparator(key_type, Column):
|
||||
raise ValidationError('key_type must be a column class')
|
||||
if inheritance_comparator(key_type, BaseContainerColumn):
|
||||
raise ValidationError('container types cannot be nested')
|
||||
if key_type.db_type is None:
|
||||
raise ValidationError('key_type cannot be an abstract column type')
|
||||
|
||||
if isinstance(key_type, type):
|
||||
self.key_type = key_type
|
||||
self.key_col = self.key_type()
|
||||
else:
|
||||
self.key_col = key_type
|
||||
self.key_type = self.key_col.__class__
|
||||
super(Map, self).__init__(value_type, default=default, **kwargs)
|
||||
|
||||
def get_column_def(self):
|
||||
"""
|
||||
Returns a column definition for CQL table definition
|
||||
"""
|
||||
db_type = self.db_type.format(
|
||||
self.key_type.db_type,
|
||||
self.value_type.db_type
|
||||
)
|
||||
static = "static" if self.static else ""
|
||||
return '{} {} {}'.format(self.cql, db_type, static)
|
||||
|
||||
def validate(self, value):
|
||||
val = super(Map, self).validate(value)
|
||||
if val is None: return
|
||||
if not isinstance(val, dict):
|
||||
raise ValidationError('{} {} is not a dict object'.format(self.column_name, val))
|
||||
return {self.key_col.validate(k):self.value_col.validate(v) for k,v in val.items()}
|
||||
|
||||
def to_python(self, value):
|
||||
if value is None:
|
||||
return {}
|
||||
if value is not None:
|
||||
return {self.key_col.to_python(k): self.value_col.to_python(v) for k,v in value.items()}
|
||||
|
||||
def to_database(self, value):
|
||||
if value is None: return None
|
||||
if isinstance(value, self.Quoter): return value
|
||||
return self.Quoter({self.key_col.to_database(k):self.value_col.to_database(v) for k,v in value.items()})
|
||||
|
||||
|
||||
|
||||
class _PartitionKeysToken(Column):
|
||||
"""
|
||||
virtual column representing token of partition columns.
|
||||
Used by filter(pk__token=Token(...)) filters
|
||||
"""
|
||||
|
||||
def __init__(self, model):
|
||||
self.partition_columns = model._partition_keys.values()
|
||||
super(_PartitionKeysToken, self).__init__(partition_key=True)
|
||||
|
||||
@property
|
||||
def db_field_name(self):
|
||||
return 'token({})'.format(', '.join(['"{}"'.format(c.db_field_name) for c in self.partition_columns]))
|
||||
|
||||
def to_database(self, value):
|
||||
from cqlengine.functions import Token
|
||||
assert isinstance(value, Token)
|
||||
value.set_columns(self.partition_columns)
|
||||
return value
|
||||
|
||||
def get_cql(self):
|
||||
return "token({})".format(", ".join(c.cql for c in self.partition_columns))
|
||||
|
125
cqlengine/connection.py
Normal file
125
cqlengine/connection.py
Normal file
@ -0,0 +1,125 @@
|
||||
#http://pypi.python.org/pypi/cql/1.0.4
|
||||
#http://code.google.com/a/apache-extras.org/p/cassandra-dbapi2 /
|
||||
#http://cassandra.apache.org/doc/cql/CQL.html
|
||||
from __future__ import absolute_import
|
||||
from collections import namedtuple
|
||||
from cassandra.cluster import Cluster, _NOT_SET, NoHostAvailable
|
||||
from cassandra.query import SimpleStatement, Statement, dict_factory
|
||||
from cqlengine.statements import BaseCQLStatement
|
||||
from cqlengine.exceptions import CQLEngineException, UndefinedKeyspaceException
|
||||
from cassandra import ConsistencyLevel
|
||||
|
||||
import six
|
||||
import logging
|
||||
|
||||
|
||||
LOG = logging.getLogger('cqlengine.cql')
|
||||
NOT_SET = _NOT_SET # required for passing timeout to Session.execute
|
||||
|
||||
|
||||
class CQLConnectionError(CQLEngineException): pass
|
||||
|
||||
Host = namedtuple('Host', ['name', 'port'])
|
||||
|
||||
cluster = None
|
||||
session = None
|
||||
lazy_connect_args = None
|
||||
default_consistency_level = None
|
||||
|
||||
def setup(
|
||||
hosts,
|
||||
default_keyspace,
|
||||
consistency=ConsistencyLevel.ONE,
|
||||
lazy_connect=False,
|
||||
retry_connect=False,
|
||||
**kwargs):
|
||||
"""
|
||||
Records the hosts and connects to one of them
|
||||
|
||||
:param hosts: list of hosts, see http://datastax.github.io/python-driver/api/cassandra/cluster.html
|
||||
:type hosts: list
|
||||
:param default_keyspace: The default keyspace to use
|
||||
:type default_keyspace: str
|
||||
:param consistency: The global consistency level
|
||||
:type consistency: int
|
||||
:param lazy_connect: True if should not connect until first use
|
||||
:type lazy_connect: bool
|
||||
:param retry_connect: bool
|
||||
:param retry_connect: True if we should retry to connect even if there was a connection failure initially
|
||||
"""
|
||||
global cluster, session, default_consistency_level, lazy_connect_args
|
||||
|
||||
if 'username' in kwargs or 'password' in kwargs:
|
||||
raise CQLEngineException("Username & Password are now handled by using the native driver's auth_provider")
|
||||
|
||||
if not default_keyspace:
|
||||
raise UndefinedKeyspaceException()
|
||||
|
||||
from cqlengine import models
|
||||
models.DEFAULT_KEYSPACE = default_keyspace
|
||||
|
||||
default_consistency_level = consistency
|
||||
if lazy_connect:
|
||||
kwargs['default_keyspace'] = default_keyspace
|
||||
kwargs['consistency'] = consistency
|
||||
kwargs['lazy_connect'] = False
|
||||
kwargs['retry_connect'] = retry_connect
|
||||
lazy_connect_args = (hosts, kwargs)
|
||||
return
|
||||
|
||||
cluster = Cluster(hosts, **kwargs)
|
||||
try:
|
||||
session = cluster.connect()
|
||||
except NoHostAvailable:
|
||||
if retry_connect:
|
||||
kwargs['default_keyspace'] = default_keyspace
|
||||
kwargs['consistency'] = consistency
|
||||
kwargs['lazy_connect'] = False
|
||||
kwargs['retry_connect'] = retry_connect
|
||||
lazy_connect_args = (hosts, kwargs)
|
||||
raise
|
||||
session.row_factory = dict_factory
|
||||
|
||||
|
||||
def execute(query, params=None, consistency_level=None, timeout=NOT_SET):
|
||||
|
||||
handle_lazy_connect()
|
||||
|
||||
if not session:
|
||||
raise CQLEngineException("It is required to setup() cqlengine before executing queries")
|
||||
|
||||
if consistency_level is None:
|
||||
consistency_level = default_consistency_level
|
||||
|
||||
if isinstance(query, Statement):
|
||||
pass
|
||||
|
||||
elif isinstance(query, BaseCQLStatement):
|
||||
params = query.get_context()
|
||||
query = str(query)
|
||||
query = SimpleStatement(query, consistency_level=consistency_level)
|
||||
|
||||
elif isinstance(query, six.string_types):
|
||||
query = SimpleStatement(query, consistency_level=consistency_level)
|
||||
|
||||
LOG.info(query.query_string)
|
||||
|
||||
params = params or {}
|
||||
result = session.execute(query, params, timeout=timeout)
|
||||
|
||||
return result
|
||||
|
||||
def get_session():
|
||||
handle_lazy_connect()
|
||||
return session
|
||||
|
||||
def get_cluster():
|
||||
handle_lazy_connect()
|
||||
return cluster
|
||||
|
||||
def handle_lazy_connect():
|
||||
global lazy_connect_args
|
||||
if lazy_connect_args:
|
||||
hosts, kwargs = lazy_connect_args
|
||||
lazy_connect_args = None
|
||||
setup(hosts, **kwargs)
|
8
cqlengine/exceptions.py
Normal file
8
cqlengine/exceptions.py
Normal file
@ -0,0 +1,8 @@
|
||||
#cqlengine exceptions
|
||||
class CQLEngineException(Exception): pass
|
||||
class ModelException(CQLEngineException): pass
|
||||
class ValidationError(CQLEngineException): pass
|
||||
|
||||
class UndefinedKeyspaceException(CQLEngineException): pass
|
||||
class LWTException(CQLEngineException): pass
|
||||
class IfNotExistsWithCounterColumn(CQLEngineException): pass
|
126
cqlengine/functions.py
Normal file
126
cqlengine/functions.py
Normal file
@ -0,0 +1,126 @@
|
||||
from datetime import datetime
|
||||
from uuid import uuid1
|
||||
import sys
|
||||
import six
|
||||
from cqlengine.exceptions import ValidationError
|
||||
# move to central spot
|
||||
|
||||
class UnicodeMixin(object):
|
||||
if sys.version_info > (3, 0):
|
||||
__str__ = lambda x: x.__unicode__()
|
||||
else:
|
||||
__str__ = lambda x: six.text_type(x).encode('utf-8')
|
||||
|
||||
class QueryValue(UnicodeMixin):
|
||||
"""
|
||||
Base class for query filter values. Subclasses of these classes can
|
||||
be passed into .filter() keyword args
|
||||
"""
|
||||
|
||||
format_string = '%({})s'
|
||||
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
self.context_id = None
|
||||
|
||||
def __unicode__(self):
|
||||
return self.format_string.format(self.context_id)
|
||||
|
||||
def set_context_id(self, ctx_id):
|
||||
self.context_id = ctx_id
|
||||
|
||||
def get_context_size(self):
|
||||
return 1
|
||||
|
||||
def update_context(self, ctx):
|
||||
ctx[str(self.context_id)] = self.value
|
||||
|
||||
|
||||
class BaseQueryFunction(QueryValue):
|
||||
"""
|
||||
Base class for filtering functions. Subclasses of these classes can
|
||||
be passed into .filter() and will be translated into CQL functions in
|
||||
the resulting query
|
||||
"""
|
||||
|
||||
class MinTimeUUID(BaseQueryFunction):
|
||||
"""
|
||||
return a fake timeuuid corresponding to the smallest possible timeuuid for the given timestamp
|
||||
|
||||
http://cassandra.apache.org/doc/cql3/CQL.html#timeuuidFun
|
||||
"""
|
||||
|
||||
format_string = 'MinTimeUUID(%({})s)'
|
||||
|
||||
def __init__(self, value):
|
||||
"""
|
||||
:param value: the time to create a maximum time uuid from
|
||||
:type value: datetime
|
||||
"""
|
||||
if not isinstance(value, datetime):
|
||||
raise ValidationError('datetime instance is required')
|
||||
super(MinTimeUUID, self).__init__(value)
|
||||
|
||||
def to_database(self, val):
|
||||
epoch = datetime(1970, 1, 1, tzinfo=val.tzinfo)
|
||||
offset = epoch.tzinfo.utcoffset(epoch).total_seconds() if epoch.tzinfo else 0
|
||||
return int(((val - epoch).total_seconds() - offset) * 1000)
|
||||
|
||||
def update_context(self, ctx):
|
||||
ctx[str(self.context_id)] = self.to_database(self.value)
|
||||
|
||||
|
||||
class MaxTimeUUID(BaseQueryFunction):
|
||||
"""
|
||||
return a fake timeuuid corresponding to the largest possible timeuuid for the given timestamp
|
||||
|
||||
http://cassandra.apache.org/doc/cql3/CQL.html#timeuuidFun
|
||||
"""
|
||||
|
||||
format_string = 'MaxTimeUUID(%({})s)'
|
||||
|
||||
def __init__(self, value):
|
||||
"""
|
||||
:param value: the time to create a minimum time uuid from
|
||||
:type value: datetime
|
||||
"""
|
||||
if not isinstance(value, datetime):
|
||||
raise ValidationError('datetime instance is required')
|
||||
super(MaxTimeUUID, self).__init__(value)
|
||||
|
||||
def to_database(self, val):
|
||||
epoch = datetime(1970, 1, 1, tzinfo=val.tzinfo)
|
||||
offset = epoch.tzinfo.utcoffset(epoch).total_seconds() if epoch.tzinfo else 0
|
||||
return int(((val - epoch).total_seconds() - offset) * 1000)
|
||||
|
||||
def update_context(self, ctx):
|
||||
ctx[str(self.context_id)] = self.to_database(self.value)
|
||||
|
||||
|
||||
class Token(BaseQueryFunction):
|
||||
"""
|
||||
compute the token for a given partition key
|
||||
|
||||
http://cassandra.apache.org/doc/cql3/CQL.html#tokenFun
|
||||
"""
|
||||
|
||||
def __init__(self, *values):
|
||||
if len(values) == 1 and isinstance(values[0], (list, tuple)):
|
||||
values = values[0]
|
||||
super(Token, self).__init__(values)
|
||||
self._columns = None
|
||||
|
||||
def set_columns(self, columns):
|
||||
self._columns = columns
|
||||
|
||||
def get_context_size(self):
|
||||
return len(self.value)
|
||||
|
||||
def __unicode__(self):
|
||||
token_args = ', '.join('%({})s'.format(self.context_id + i) for i in range(self.get_context_size()))
|
||||
return "token({})".format(token_args)
|
||||
|
||||
def update_context(self, ctx):
|
||||
for i, (col, val) in enumerate(zip(self._columns, self.value)):
|
||||
ctx[str(self.context_id + i)] = col.to_database(val)
|
||||
|
321
cqlengine/management.py
Normal file
321
cqlengine/management.py
Normal file
@ -0,0 +1,321 @@
|
||||
import json
|
||||
import warnings
|
||||
import six
|
||||
from cqlengine import SizeTieredCompactionStrategy, LeveledCompactionStrategy
|
||||
from cqlengine import ONE
|
||||
from cqlengine.named import NamedTable
|
||||
|
||||
from cqlengine.connection import execute, get_cluster
|
||||
from cqlengine.exceptions import CQLEngineException
|
||||
|
||||
import logging
|
||||
from collections import namedtuple
|
||||
Field = namedtuple('Field', ['name', 'type'])
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from cqlengine.models import Model
|
||||
|
||||
# system keyspaces
|
||||
schema_columnfamilies = NamedTable('system', 'schema_columnfamilies')
|
||||
|
||||
def create_keyspace(name, strategy_class, replication_factor, durable_writes=True, **replication_values):
|
||||
"""
|
||||
creates a keyspace
|
||||
|
||||
:param name: name of keyspace to create
|
||||
:param strategy_class: keyspace replication strategy class
|
||||
:param replication_factor: keyspace replication factor
|
||||
:param durable_writes: 1.2 only, write log is bypassed if set to False
|
||||
:param **replication_values: 1.2 only, additional values to ad to the replication data map
|
||||
"""
|
||||
cluster = get_cluster()
|
||||
|
||||
if name not in cluster.metadata.keyspaces:
|
||||
#try the 1.2 method
|
||||
replication_map = {
|
||||
'class': strategy_class,
|
||||
'replication_factor':replication_factor
|
||||
}
|
||||
replication_map.update(replication_values)
|
||||
if strategy_class.lower() != 'simplestrategy':
|
||||
# Although the Cassandra documentation states for `replication_factor`
|
||||
# that it is "Required if class is SimpleStrategy; otherwise,
|
||||
# not used." we get an error if it is present.
|
||||
replication_map.pop('replication_factor', None)
|
||||
|
||||
query = """
|
||||
CREATE KEYSPACE {}
|
||||
WITH REPLICATION = {}
|
||||
""".format(name, json.dumps(replication_map).replace('"', "'"))
|
||||
|
||||
if strategy_class != 'SimpleStrategy':
|
||||
query += " AND DURABLE_WRITES = {}".format('true' if durable_writes else 'false')
|
||||
|
||||
execute(query)
|
||||
|
||||
|
||||
def delete_keyspace(name):
|
||||
cluster = get_cluster()
|
||||
if name in cluster.metadata.keyspaces:
|
||||
execute("DROP KEYSPACE {}".format(name))
|
||||
|
||||
def create_table(model):
|
||||
raise CQLEngineException("create_table is deprecated, please use sync_table")
|
||||
|
||||
def sync_table(model):
|
||||
"""
|
||||
Inspects the model and creates / updates the corresponding table and columns.
|
||||
|
||||
Note that the attributes removed from the model are not deleted on the database.
|
||||
They become effectively ignored by (will not show up on) the model.
|
||||
"""
|
||||
|
||||
if not issubclass(model, Model):
|
||||
raise CQLEngineException("Models must be derived from base Model.")
|
||||
|
||||
if model.__abstract__:
|
||||
raise CQLEngineException("cannot create table from abstract model")
|
||||
|
||||
|
||||
#construct query string
|
||||
cf_name = model.column_family_name()
|
||||
raw_cf_name = model.column_family_name(include_keyspace=False)
|
||||
|
||||
ks_name = model._get_keyspace()
|
||||
|
||||
cluster = get_cluster()
|
||||
|
||||
keyspace = cluster.metadata.keyspaces[ks_name]
|
||||
tables = keyspace.tables
|
||||
|
||||
#check for an existing column family
|
||||
if raw_cf_name not in tables:
|
||||
qs = get_create_table(model)
|
||||
|
||||
try:
|
||||
execute(qs)
|
||||
except CQLEngineException as ex:
|
||||
# 1.2 doesn't return cf names, so we have to examine the exception
|
||||
# and ignore if it says the column family already exists
|
||||
if "Cannot add already existing column family" not in unicode(ex):
|
||||
raise
|
||||
else:
|
||||
# see if we're missing any columns
|
||||
fields = get_fields(model)
|
||||
field_names = [x.name for x in fields]
|
||||
for name, col in model._columns.items():
|
||||
if col.primary_key or col.partition_key: continue # we can't mess with the PK
|
||||
if col.db_field_name in field_names: continue # skip columns already defined
|
||||
|
||||
# add missing column using the column def
|
||||
query = "ALTER TABLE {} add {}".format(cf_name, col.get_column_def())
|
||||
logger.debug(query)
|
||||
execute(query)
|
||||
|
||||
update_compaction(model)
|
||||
|
||||
|
||||
table = cluster.metadata.keyspaces[ks_name].tables[raw_cf_name]
|
||||
|
||||
indexes = [c for n,c in model._columns.items() if c.index]
|
||||
|
||||
for column in indexes:
|
||||
if table.columns[column.db_field_name].index:
|
||||
continue
|
||||
|
||||
qs = ['CREATE INDEX index_{}_{}'.format(raw_cf_name, column.db_field_name)]
|
||||
qs += ['ON {}'.format(cf_name)]
|
||||
qs += ['("{}")'.format(column.db_field_name)]
|
||||
qs = ' '.join(qs)
|
||||
execute(qs)
|
||||
|
||||
def get_create_table(model):
|
||||
cf_name = model.column_family_name()
|
||||
qs = ['CREATE TABLE {}'.format(cf_name)]
|
||||
|
||||
#add column types
|
||||
pkeys = [] # primary keys
|
||||
ckeys = [] # clustering keys
|
||||
qtypes = [] # field types
|
||||
def add_column(col):
|
||||
s = col.get_column_def()
|
||||
if col.primary_key:
|
||||
keys = (pkeys if col.partition_key else ckeys)
|
||||
keys.append('"{}"'.format(col.db_field_name))
|
||||
qtypes.append(s)
|
||||
for name, col in model._columns.items():
|
||||
add_column(col)
|
||||
|
||||
qtypes.append('PRIMARY KEY (({}){})'.format(', '.join(pkeys), ckeys and ', ' + ', '.join(ckeys) or ''))
|
||||
|
||||
qs += ['({})'.format(', '.join(qtypes))]
|
||||
|
||||
with_qs = []
|
||||
|
||||
table_properties = ['bloom_filter_fp_chance', 'caching', 'comment',
|
||||
'dclocal_read_repair_chance', 'default_time_to_live', 'gc_grace_seconds',
|
||||
'index_interval', 'memtable_flush_period_in_ms', 'populate_io_cache_on_flush',
|
||||
'read_repair_chance', 'replicate_on_write']
|
||||
for prop_name in table_properties:
|
||||
prop_value = getattr(model, '__{}__'.format(prop_name), None)
|
||||
if prop_value is not None:
|
||||
# Strings needs to be single quoted
|
||||
if isinstance(prop_value, six.string_types):
|
||||
prop_value = "'{}'".format(prop_value)
|
||||
with_qs.append("{} = {}".format(prop_name, prop_value))
|
||||
|
||||
_order = ['"{}" {}'.format(c.db_field_name, c.clustering_order or 'ASC') for c in model._clustering_keys.values()]
|
||||
if _order:
|
||||
with_qs.append('clustering order by ({})'.format(', '.join(_order)))
|
||||
|
||||
compaction_options = get_compaction_options(model)
|
||||
if compaction_options:
|
||||
compaction_options = json.dumps(compaction_options).replace('"', "'")
|
||||
with_qs.append("compaction = {}".format(compaction_options))
|
||||
|
||||
# Add table properties.
|
||||
if with_qs:
|
||||
qs += ['WITH {}'.format(' AND '.join(with_qs))]
|
||||
|
||||
qs = ' '.join(qs)
|
||||
return qs
|
||||
|
||||
|
||||
def get_compaction_options(model):
|
||||
"""
|
||||
Generates dictionary (later converted to a string) for creating and altering
|
||||
tables with compaction strategy
|
||||
|
||||
:param model:
|
||||
:return:
|
||||
"""
|
||||
if not model.__compaction__:
|
||||
return {}
|
||||
|
||||
result = {'class':model.__compaction__}
|
||||
|
||||
def setter(key, limited_to_strategy = None):
|
||||
"""
|
||||
sets key in result, checking if the key is limited to either SizeTiered or Leveled
|
||||
:param key: one of the compaction options, like "bucket_high"
|
||||
:param limited_to_strategy: SizeTieredCompactionStrategy, LeveledCompactionStrategy
|
||||
:return:
|
||||
"""
|
||||
mkey = "__compaction_{}__".format(key)
|
||||
tmp = getattr(model, mkey)
|
||||
if tmp and limited_to_strategy and limited_to_strategy != model.__compaction__:
|
||||
raise CQLEngineException("{} is limited to {}".format(key, limited_to_strategy))
|
||||
|
||||
if tmp:
|
||||
# Explicitly cast the values to strings to be able to compare the
|
||||
# values against introspected values from Cassandra.
|
||||
result[key] = str(tmp)
|
||||
|
||||
setter('tombstone_compaction_interval')
|
||||
setter('tombstone_threshold')
|
||||
|
||||
setter('bucket_high', SizeTieredCompactionStrategy)
|
||||
setter('bucket_low', SizeTieredCompactionStrategy)
|
||||
setter('max_threshold', SizeTieredCompactionStrategy)
|
||||
setter('min_threshold', SizeTieredCompactionStrategy)
|
||||
setter('min_sstable_size', SizeTieredCompactionStrategy)
|
||||
|
||||
setter('sstable_size_in_mb', LeveledCompactionStrategy)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_fields(model):
|
||||
# returns all fields that aren't part of the PK
|
||||
ks_name = model._get_keyspace()
|
||||
col_family = model.column_family_name(include_keyspace=False)
|
||||
field_types = ['regular', 'static']
|
||||
query = "select * from system.schema_columns where keyspace_name = %s and columnfamily_name = %s"
|
||||
tmp = execute(query, [ks_name, col_family])
|
||||
|
||||
# Tables containing only primary keys do not appear to create
|
||||
# any entries in system.schema_columns, as only non-primary-key attributes
|
||||
# appear to be inserted into the schema_columns table
|
||||
try:
|
||||
return [Field(x['column_name'], x['validator']) for x in tmp if x['type'] in field_types]
|
||||
except KeyError:
|
||||
return [Field(x['column_name'], x['validator']) for x in tmp]
|
||||
# convert to Field named tuples
|
||||
|
||||
|
||||
def get_table_settings(model):
|
||||
# returns the table as provided by the native driver for a given model
|
||||
cluster = get_cluster()
|
||||
ks = model._get_keyspace()
|
||||
table = model.column_family_name(include_keyspace=False)
|
||||
table = cluster.metadata.keyspaces[ks].tables[table]
|
||||
return table
|
||||
|
||||
def update_compaction(model):
|
||||
"""Updates the compaction options for the given model if necessary.
|
||||
|
||||
:param model: The model to update.
|
||||
|
||||
:return: `True`, if the compaction options were modified in Cassandra,
|
||||
`False` otherwise.
|
||||
:rtype: bool
|
||||
"""
|
||||
logger.debug("Checking %s for compaction differences", model)
|
||||
table = get_table_settings(model)
|
||||
|
||||
existing_options = table.options.copy()
|
||||
|
||||
existing_compaction_strategy = existing_options['compaction_strategy_class']
|
||||
|
||||
existing_options = json.loads(existing_options['compaction_strategy_options'])
|
||||
|
||||
desired_options = get_compaction_options(model)
|
||||
|
||||
desired_compact_strategy = desired_options.get('class', SizeTieredCompactionStrategy)
|
||||
|
||||
desired_options.pop('class', None)
|
||||
|
||||
do_update = False
|
||||
|
||||
if desired_compact_strategy not in existing_compaction_strategy:
|
||||
do_update = True
|
||||
|
||||
for k, v in desired_options.items():
|
||||
val = existing_options.pop(k, None)
|
||||
if val != v:
|
||||
do_update = True
|
||||
|
||||
# check compaction_strategy_options
|
||||
if do_update:
|
||||
options = get_compaction_options(model)
|
||||
# jsonify
|
||||
options = json.dumps(options).replace('"', "'")
|
||||
cf_name = model.column_family_name()
|
||||
query = "ALTER TABLE {} with compaction = {}".format(cf_name, options)
|
||||
logger.debug(query)
|
||||
execute(query)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def delete_table(model):
|
||||
raise CQLEngineException("delete_table has been deprecated in favor of drop_table()")
|
||||
|
||||
|
||||
def drop_table(model):
|
||||
|
||||
# don't try to delete non existant tables
|
||||
meta = get_cluster().metadata
|
||||
|
||||
ks_name = model._get_keyspace()
|
||||
raw_cf_name = model.column_family_name(include_keyspace=False)
|
||||
|
||||
try:
|
||||
table = meta.keyspaces[ks_name].tables[raw_cf_name]
|
||||
execute('drop table {};'.format(model.column_family_name(include_keyspace=True)))
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
||||
|
850
cqlengine/models.py
Normal file
850
cqlengine/models.py
Normal file
@ -0,0 +1,850 @@
|
||||
from collections import OrderedDict
|
||||
import re
|
||||
import warnings
|
||||
|
||||
from cqlengine import columns
|
||||
from cqlengine.exceptions import ModelException, CQLEngineException, ValidationError
|
||||
from cqlengine.query import ModelQuerySet, DMLQuery, AbstractQueryableColumn, NOT_SET
|
||||
from cqlengine.query import DoesNotExist as _DoesNotExist
|
||||
from cqlengine.query import MultipleObjectsReturned as _MultipleObjectsReturned
|
||||
|
||||
class ModelDefinitionException(ModelException): pass
|
||||
|
||||
|
||||
class PolyMorphicModelException(ModelException): pass
|
||||
|
||||
DEFAULT_KEYSPACE = None
|
||||
|
||||
class UndefinedKeyspaceWarning(Warning):
|
||||
pass
|
||||
|
||||
|
||||
class hybrid_classmethod(object):
|
||||
"""
|
||||
Allows a method to behave as both a class method and
|
||||
normal instance method depending on how it's called
|
||||
"""
|
||||
|
||||
def __init__(self, clsmethod, instmethod):
|
||||
self.clsmethod = clsmethod
|
||||
self.instmethod = instmethod
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
if instance is None:
|
||||
return self.clsmethod.__get__(owner, owner)
|
||||
else:
|
||||
return self.instmethod.__get__(instance, owner)
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""
|
||||
Just a hint to IDEs that it's ok to call this
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class QuerySetDescriptor(object):
|
||||
"""
|
||||
returns a fresh queryset for the given model
|
||||
it's declared on everytime it's accessed
|
||||
"""
|
||||
|
||||
def __get__(self, obj, model):
|
||||
""" :rtype: ModelQuerySet """
|
||||
if model.__abstract__:
|
||||
raise CQLEngineException('cannot execute queries against abstract models')
|
||||
queryset = model.__queryset__(model)
|
||||
|
||||
# if this is a concrete polymorphic model, and the polymorphic
|
||||
# key is an indexed column, add a filter clause to only return
|
||||
# logical rows of the proper type
|
||||
if model._is_polymorphic and not model._is_polymorphic_base:
|
||||
name, column = model._polymorphic_column_name, model._polymorphic_column
|
||||
if column.partition_key or column.index:
|
||||
# look for existing poly types
|
||||
return queryset.filter(**{name: model.__polymorphic_key__})
|
||||
|
||||
return queryset
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""
|
||||
Just a hint to IDEs that it's ok to call this
|
||||
|
||||
:rtype: ModelQuerySet
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class TransactionDescriptor(object):
|
||||
"""
|
||||
returns a query set descriptor
|
||||
"""
|
||||
def __get__(self, instance, model):
|
||||
if instance:
|
||||
def transaction_setter(*prepared_transaction, **unprepared_transactions):
|
||||
if len(prepared_transaction) > 0:
|
||||
transactions = prepared_transaction[0]
|
||||
else:
|
||||
transactions = instance.objects.iff(**unprepared_transactions)._transaction
|
||||
instance._transaction = transactions
|
||||
return instance
|
||||
|
||||
return transaction_setter
|
||||
qs = model.__queryset__(model)
|
||||
|
||||
def transaction_setter(**unprepared_transactions):
|
||||
transactions = model.objects.iff(**unprepared_transactions)._transaction
|
||||
qs._transaction = transactions
|
||||
return qs
|
||||
return transaction_setter
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class TTLDescriptor(object):
|
||||
"""
|
||||
returns a query set descriptor
|
||||
"""
|
||||
def __get__(self, instance, model):
|
||||
if instance:
|
||||
#instance = copy.deepcopy(instance)
|
||||
# instance method
|
||||
def ttl_setter(ts):
|
||||
instance._ttl = ts
|
||||
return instance
|
||||
return ttl_setter
|
||||
|
||||
qs = model.__queryset__(model)
|
||||
|
||||
def ttl_setter(ts):
|
||||
qs._ttl = ts
|
||||
return qs
|
||||
|
||||
return ttl_setter
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
class TimestampDescriptor(object):
|
||||
"""
|
||||
returns a query set descriptor with a timestamp specified
|
||||
"""
|
||||
def __get__(self, instance, model):
|
||||
if instance:
|
||||
# instance method
|
||||
def timestamp_setter(ts):
|
||||
instance._timestamp = ts
|
||||
return instance
|
||||
return timestamp_setter
|
||||
|
||||
return model.objects.timestamp
|
||||
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
class IfNotExistsDescriptor(object):
|
||||
"""
|
||||
return a query set descriptor with a if_not_exists flag specified
|
||||
"""
|
||||
def __get__(self, instance, model):
|
||||
if instance:
|
||||
# instance method
|
||||
def ifnotexists_setter(ife):
|
||||
instance._if_not_exists = ife
|
||||
return instance
|
||||
return ifnotexists_setter
|
||||
|
||||
return model.objects.if_not_exists
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
class ConsistencyDescriptor(object):
|
||||
"""
|
||||
returns a query set descriptor if called on Class, instance if it was an instance call
|
||||
"""
|
||||
def __get__(self, instance, model):
|
||||
if instance:
|
||||
#instance = copy.deepcopy(instance)
|
||||
def consistency_setter(consistency):
|
||||
instance.__consistency__ = consistency
|
||||
return instance
|
||||
return consistency_setter
|
||||
|
||||
qs = model.__queryset__(model)
|
||||
|
||||
def consistency_setter(consistency):
|
||||
qs._consistency = consistency
|
||||
return qs
|
||||
|
||||
return consistency_setter
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class ColumnQueryEvaluator(AbstractQueryableColumn):
|
||||
"""
|
||||
Wraps a column and allows it to be used in comparator
|
||||
expressions, returning query operators
|
||||
|
||||
ie:
|
||||
Model.column == 5
|
||||
"""
|
||||
|
||||
def __init__(self, column):
|
||||
self.column = column
|
||||
|
||||
def __unicode__(self):
|
||||
return self.column.db_field_name
|
||||
|
||||
def _get_column(self):
|
||||
""" :rtype: ColumnQueryEvaluator """
|
||||
return self.column
|
||||
|
||||
|
||||
class ColumnDescriptor(object):
|
||||
"""
|
||||
Handles the reading and writing of column values to and from
|
||||
a model instance's value manager, as well as creating
|
||||
comparator queries
|
||||
"""
|
||||
|
||||
def __init__(self, column):
|
||||
"""
|
||||
:param column:
|
||||
:type column: columns.Column
|
||||
:return:
|
||||
"""
|
||||
self.column = column
|
||||
self.query_evaluator = ColumnQueryEvaluator(self.column)
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
"""
|
||||
Returns either the value or column, depending
|
||||
on if an instance is provided or not
|
||||
|
||||
:param instance: the model instance
|
||||
:type instance: Model
|
||||
"""
|
||||
try:
|
||||
return instance._values[self.column.column_name].getval()
|
||||
except AttributeError as e:
|
||||
return self.query_evaluator
|
||||
|
||||
def __set__(self, instance, value):
|
||||
"""
|
||||
Sets the value on an instance, raises an exception with classes
|
||||
TODO: use None instance to create update statements
|
||||
"""
|
||||
if instance:
|
||||
return instance._values[self.column.column_name].setval(value)
|
||||
else:
|
||||
raise AttributeError('cannot reassign column values')
|
||||
|
||||
def __delete__(self, instance):
|
||||
"""
|
||||
Sets the column value to None, if possible
|
||||
"""
|
||||
if instance:
|
||||
if self.column.can_delete:
|
||||
instance._values[self.column.column_name].delval()
|
||||
else:
|
||||
raise AttributeError('cannot delete {} columns'.format(self.column.column_name))
|
||||
|
||||
|
||||
class BaseModel(object):
|
||||
"""
|
||||
The base model class, don't inherit from this, inherit from Model, defined below
|
||||
"""
|
||||
|
||||
class DoesNotExist(_DoesNotExist): pass
|
||||
|
||||
class MultipleObjectsReturned(_MultipleObjectsReturned): pass
|
||||
|
||||
objects = QuerySetDescriptor()
|
||||
ttl = TTLDescriptor()
|
||||
consistency = ConsistencyDescriptor()
|
||||
iff = TransactionDescriptor()
|
||||
|
||||
# custom timestamps, see USING TIMESTAMP X
|
||||
timestamp = TimestampDescriptor()
|
||||
|
||||
if_not_exists = IfNotExistsDescriptor()
|
||||
|
||||
# _len is lazily created by __len__
|
||||
|
||||
# table names will be generated automatically from it's model
|
||||
# however, you can also define them manually here
|
||||
__table_name__ = None
|
||||
|
||||
# the keyspace for this model
|
||||
__keyspace__ = None
|
||||
|
||||
# polymorphism options
|
||||
__polymorphic_key__ = None
|
||||
|
||||
# compaction options
|
||||
__compaction__ = None
|
||||
__compaction_tombstone_compaction_interval__ = None
|
||||
__compaction_tombstone_threshold__ = None
|
||||
|
||||
# compaction - size tiered options
|
||||
__compaction_bucket_high__ = None
|
||||
__compaction_bucket_low__ = None
|
||||
__compaction_max_threshold__ = None
|
||||
__compaction_min_threshold__ = None
|
||||
__compaction_min_sstable_size__ = None
|
||||
|
||||
# compaction - leveled options
|
||||
__compaction_sstable_size_in_mb__ = None
|
||||
|
||||
# end compaction
|
||||
# the queryset class used for this class
|
||||
__queryset__ = ModelQuerySet
|
||||
__dmlquery__ = DMLQuery
|
||||
|
||||
__default_ttl__ = None # default ttl value to use
|
||||
__consistency__ = None # can be set per query
|
||||
|
||||
# Additional table properties
|
||||
__bloom_filter_fp_chance__ = None
|
||||
__caching__ = None
|
||||
__comment__ = None
|
||||
__dclocal_read_repair_chance__ = None
|
||||
__default_time_to_live__ = None
|
||||
__gc_grace_seconds__ = None
|
||||
__index_interval__ = None
|
||||
__memtable_flush_period_in_ms__ = None
|
||||
__populate_io_cache_on_flush__ = None
|
||||
__read_repair_chance__ = None
|
||||
__replicate_on_write__ = None
|
||||
|
||||
_timestamp = None # optional timestamp to include with the operation (USING TIMESTAMP)
|
||||
|
||||
_if_not_exists = False # optional if_not_exists flag to check existence before insertion
|
||||
|
||||
def __init__(self, **values):
|
||||
self._values = {}
|
||||
self._ttl = self.__default_ttl__
|
||||
self._timestamp = None
|
||||
self._transaction = None
|
||||
|
||||
for name, column in self._columns.items():
|
||||
value = values.get(name, None)
|
||||
if value is not None or isinstance(column, columns.BaseContainerColumn):
|
||||
value = column.to_python(value)
|
||||
value_mngr = column.value_manager(self, column, value)
|
||||
if name in values:
|
||||
value_mngr.explicit = True
|
||||
self._values[name] = value_mngr
|
||||
|
||||
# a flag set by the deserializer to indicate
|
||||
# that update should be used when persisting changes
|
||||
self._is_persisted = False
|
||||
self._batch = None
|
||||
self._timeout = NOT_SET
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
"""
|
||||
Pretty printing of models by their primary key
|
||||
"""
|
||||
return '{} <{}>'.format(self.__class__.__name__,
|
||||
', '.join(('{}={}'.format(k, getattr(self, k)) for k,v in six.iteritems(self._primary_keys)))
|
||||
)
|
||||
|
||||
|
||||
|
||||
@classmethod
|
||||
def _discover_polymorphic_submodels(cls):
|
||||
if not cls._is_polymorphic_base:
|
||||
raise ModelException('_discover_polymorphic_submodels can only be called on polymorphic base classes')
|
||||
def _discover(klass):
|
||||
if not klass._is_polymorphic_base and klass.__polymorphic_key__ is not None:
|
||||
cls._polymorphic_map[klass.__polymorphic_key__] = klass
|
||||
for subklass in klass.__subclasses__():
|
||||
_discover(subklass)
|
||||
_discover(cls)
|
||||
|
||||
@classmethod
|
||||
def _get_model_by_polymorphic_key(cls, key):
|
||||
if not cls._is_polymorphic_base:
|
||||
raise ModelException('_get_model_by_polymorphic_key can only be called on polymorphic base classes')
|
||||
return cls._polymorphic_map.get(key)
|
||||
|
||||
@classmethod
|
||||
def _construct_instance(cls, values):
|
||||
"""
|
||||
method used to construct instances from query results
|
||||
this is where polymorphic deserialization occurs
|
||||
"""
|
||||
# we're going to take the values, which is from the DB as a dict
|
||||
# and translate that into our local fields
|
||||
# the db_map is a db_field -> model field map
|
||||
items = values.items()
|
||||
field_dict = dict([(cls._db_map.get(k, k),v) for k,v in items])
|
||||
|
||||
if cls._is_polymorphic:
|
||||
poly_key = field_dict.get(cls._polymorphic_column_name)
|
||||
|
||||
if poly_key is None:
|
||||
raise PolyMorphicModelException('polymorphic key was not found in values')
|
||||
|
||||
poly_base = cls if cls._is_polymorphic_base else cls._polymorphic_base
|
||||
|
||||
klass = poly_base._get_model_by_polymorphic_key(poly_key)
|
||||
if klass is None:
|
||||
poly_base._discover_polymorphic_submodels()
|
||||
klass = poly_base._get_model_by_polymorphic_key(poly_key)
|
||||
if klass is None:
|
||||
raise PolyMorphicModelException(
|
||||
'unrecognized polymorphic key {} for class {}'.format(poly_key, poly_base.__name__)
|
||||
)
|
||||
|
||||
if not issubclass(klass, cls):
|
||||
raise PolyMorphicModelException(
|
||||
'{} is not a subclass of {}'.format(klass.__name__, cls.__name__)
|
||||
)
|
||||
|
||||
field_dict = {k: v for k, v in field_dict.items() if k in klass._columns.keys()}
|
||||
|
||||
else:
|
||||
klass = cls
|
||||
|
||||
instance = klass(**field_dict)
|
||||
instance._is_persisted = True
|
||||
return instance
|
||||
|
||||
def _can_update(self):
|
||||
"""
|
||||
Called by the save function to check if this should be
|
||||
persisted with update or insert
|
||||
|
||||
:return:
|
||||
"""
|
||||
if not self._is_persisted: return False
|
||||
pks = self._primary_keys.keys()
|
||||
return all([not self._values[k].changed for k in self._primary_keys])
|
||||
|
||||
@classmethod
|
||||
def _get_keyspace(cls):
|
||||
""" Returns the manual keyspace, if set, otherwise the default keyspace """
|
||||
return cls.__keyspace__ or DEFAULT_KEYSPACE
|
||||
|
||||
@classmethod
|
||||
def _get_column(cls, name):
|
||||
"""
|
||||
Returns the column matching the given name, raising a key error if
|
||||
it doesn't exist
|
||||
|
||||
:param name: the name of the column to return
|
||||
:rtype: Column
|
||||
"""
|
||||
return cls._columns[name]
|
||||
|
||||
def __eq__(self, other):
|
||||
if self.__class__ != other.__class__:
|
||||
return False
|
||||
|
||||
# check attribute keys
|
||||
keys = set(self._columns.keys())
|
||||
other_keys = set(other._columns.keys())
|
||||
if keys != other_keys:
|
||||
return False
|
||||
|
||||
# check that all of the attributes match
|
||||
for key in other_keys:
|
||||
if getattr(self, key, None) != getattr(other, key, None):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
@classmethod
|
||||
def column_family_name(cls, include_keyspace=True):
|
||||
"""
|
||||
Returns the column family name if it's been defined
|
||||
otherwise, it creates it from the module and class name
|
||||
"""
|
||||
cf_name = ''
|
||||
if cls.__table_name__:
|
||||
cf_name = cls.__table_name__.lower()
|
||||
else:
|
||||
# get polymorphic base table names if model is polymorphic
|
||||
if cls._is_polymorphic and not cls._is_polymorphic_base:
|
||||
return cls._polymorphic_base.column_family_name(include_keyspace=include_keyspace)
|
||||
|
||||
camelcase = re.compile(r'([a-z])([A-Z])')
|
||||
ccase = lambda s: camelcase.sub(lambda v: '{}_{}'.format(v.group(1), v.group(2).lower()), s)
|
||||
|
||||
cf_name += ccase(cls.__name__)
|
||||
#trim to less than 48 characters or cassandra will complain
|
||||
cf_name = cf_name[-48:]
|
||||
cf_name = cf_name.lower()
|
||||
cf_name = re.sub(r'^_+', '', cf_name)
|
||||
if not include_keyspace: return cf_name
|
||||
return '{}.{}'.format(cls._get_keyspace(), cf_name)
|
||||
|
||||
def validate(self):
|
||||
""" Cleans and validates the field values """
|
||||
for name, col in self._columns.items():
|
||||
v = getattr(self, name)
|
||||
if v is None and not self._values[name].explicit and col.has_default:
|
||||
v = col.get_default()
|
||||
val = col.validate(v)
|
||||
setattr(self, name, val)
|
||||
|
||||
### Let an instance be used like a dict of its columns keys/values
|
||||
|
||||
def __iter__(self):
|
||||
""" Iterate over column ids. """
|
||||
for column_id in self._columns.keys():
|
||||
yield column_id
|
||||
|
||||
def __getitem__(self, key):
|
||||
""" Returns column's value. """
|
||||
if not isinstance(key, six.string_types):
|
||||
raise TypeError
|
||||
if key not in self._columns.keys():
|
||||
raise KeyError
|
||||
return getattr(self, key)
|
||||
|
||||
def __setitem__(self, key, val):
|
||||
""" Sets a column's value. """
|
||||
if not isinstance(key, six.string_types):
|
||||
raise TypeError
|
||||
if key not in self._columns.keys():
|
||||
raise KeyError
|
||||
return setattr(self, key, val)
|
||||
|
||||
def __len__(self):
|
||||
""" Returns the number of columns defined on that model. """
|
||||
try:
|
||||
return self._len
|
||||
except:
|
||||
self._len = len(self._columns.keys())
|
||||
return self._len
|
||||
|
||||
def keys(self):
|
||||
""" Returns list of column's IDs. """
|
||||
return [k for k in self]
|
||||
|
||||
def values(self):
|
||||
""" Returns list of column's values. """
|
||||
return [self[k] for k in self]
|
||||
|
||||
def items(self):
|
||||
""" Returns a list of columns's IDs/values. """
|
||||
return [(k, self[k]) for k in self]
|
||||
|
||||
def _as_dict(self):
|
||||
""" Returns a map of column names to cleaned values """
|
||||
values = self._dynamic_columns or {}
|
||||
for name, col in self._columns.items():
|
||||
values[name] = col.to_database(getattr(self, name, None))
|
||||
return values
|
||||
|
||||
@classmethod
|
||||
def create(cls, **kwargs):
|
||||
extra_columns = set(kwargs.keys()) - set(cls._columns.keys())
|
||||
if extra_columns:
|
||||
raise ValidationError("Incorrect columns passed: {}".format(extra_columns))
|
||||
return cls.objects.create(**kwargs)
|
||||
|
||||
@classmethod
|
||||
def all(cls):
|
||||
return cls.objects.all()
|
||||
|
||||
@classmethod
|
||||
def filter(cls, *args, **kwargs):
|
||||
# if kwargs.values().count(None):
|
||||
# raise CQLEngineException("Cannot pass None as a filter")
|
||||
|
||||
return cls.objects.filter(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def get(cls, *args, **kwargs):
|
||||
return cls.objects.get(*args, **kwargs)
|
||||
|
||||
def timeout(self, timeout):
|
||||
assert self._batch is None, 'Setting both timeout and batch is not supported'
|
||||
self._timeout = timeout
|
||||
return self
|
||||
|
||||
def save(self):
|
||||
# handle polymorphic models
|
||||
if self._is_polymorphic:
|
||||
if self._is_polymorphic_base:
|
||||
raise PolyMorphicModelException('cannot save polymorphic base model')
|
||||
else:
|
||||
setattr(self, self._polymorphic_column_name, self.__polymorphic_key__)
|
||||
|
||||
is_new = self.pk is None
|
||||
self.validate()
|
||||
self.__dmlquery__(self.__class__, self,
|
||||
batch=self._batch,
|
||||
ttl=self._ttl,
|
||||
timestamp=self._timestamp,
|
||||
consistency=self.__consistency__,
|
||||
if_not_exists=self._if_not_exists,
|
||||
transaction=self._transaction,
|
||||
timeout=self._timeout).save()
|
||||
|
||||
#reset the value managers
|
||||
for v in self._values.values():
|
||||
v.reset_previous_value()
|
||||
self._is_persisted = True
|
||||
|
||||
self._ttl = self.__default_ttl__
|
||||
self._timestamp = None
|
||||
|
||||
return self
|
||||
|
||||
def update(self, **values):
|
||||
for k, v in values.items():
|
||||
col = self._columns.get(k)
|
||||
|
||||
# check for nonexistant columns
|
||||
if col is None:
|
||||
raise ValidationError("{}.{} has no column named: {}".format(self.__module__, self.__class__.__name__, k))
|
||||
|
||||
# check for primary key update attempts
|
||||
if col.is_primary_key:
|
||||
raise ValidationError("Cannot apply update to primary key '{}' for {}.{}".format(k, self.__module__, self.__class__.__name__))
|
||||
|
||||
setattr(self, k, v)
|
||||
|
||||
# handle polymorphic models
|
||||
if self._is_polymorphic:
|
||||
if self._is_polymorphic_base:
|
||||
raise PolyMorphicModelException('cannot update polymorphic base model')
|
||||
else:
|
||||
setattr(self, self._polymorphic_column_name, self.__polymorphic_key__)
|
||||
|
||||
self.validate()
|
||||
self.__dmlquery__(self.__class__, self,
|
||||
batch=self._batch,
|
||||
ttl=self._ttl,
|
||||
timestamp=self._timestamp,
|
||||
consistency=self.__consistency__,
|
||||
transaction=self._transaction,
|
||||
timeout=self._timeout).update()
|
||||
|
||||
#reset the value managers
|
||||
for v in self._values.values():
|
||||
v.reset_previous_value()
|
||||
self._is_persisted = True
|
||||
|
||||
self._ttl = self.__default_ttl__
|
||||
self._timestamp = None
|
||||
|
||||
return self
|
||||
|
||||
def delete(self):
|
||||
""" Deletes this instance """
|
||||
self.__dmlquery__(self.__class__, self,
|
||||
batch=self._batch,
|
||||
timestamp=self._timestamp,
|
||||
consistency=self.__consistency__,
|
||||
timeout=self._timeout).delete()
|
||||
|
||||
def get_changed_columns(self):
|
||||
""" returns a list of the columns that have been updated since instantiation or save """
|
||||
return [k for k,v in self._values.items() if v.changed]
|
||||
|
||||
@classmethod
|
||||
def _class_batch(cls, batch):
|
||||
return cls.objects.batch(batch)
|
||||
|
||||
def _inst_batch(self, batch):
|
||||
assert self._timeout is NOT_SET, 'Setting both timeout and batch is not supported'
|
||||
self._batch = batch
|
||||
return self
|
||||
|
||||
|
||||
batch = hybrid_classmethod(_class_batch, _inst_batch)
|
||||
|
||||
|
||||
|
||||
class ModelMetaClass(type):
|
||||
|
||||
def __new__(cls, name, bases, attrs):
|
||||
"""
|
||||
"""
|
||||
#move column definitions into columns dict
|
||||
#and set default column names
|
||||
column_dict = OrderedDict()
|
||||
primary_keys = OrderedDict()
|
||||
pk_name = None
|
||||
|
||||
#get inherited properties
|
||||
inherited_columns = OrderedDict()
|
||||
for base in bases:
|
||||
for k,v in getattr(base, '_defined_columns', {}).items():
|
||||
inherited_columns.setdefault(k,v)
|
||||
|
||||
#short circuit __abstract__ inheritance
|
||||
is_abstract = attrs['__abstract__'] = attrs.get('__abstract__', False)
|
||||
|
||||
#short circuit __polymorphic_key__ inheritance
|
||||
attrs['__polymorphic_key__'] = attrs.get('__polymorphic_key__', None)
|
||||
|
||||
def _transform_column(col_name, col_obj):
|
||||
column_dict[col_name] = col_obj
|
||||
if col_obj.primary_key:
|
||||
primary_keys[col_name] = col_obj
|
||||
col_obj.set_column_name(col_name)
|
||||
#set properties
|
||||
attrs[col_name] = ColumnDescriptor(col_obj)
|
||||
|
||||
column_definitions = [(k,v) for k,v in attrs.items() if isinstance(v, columns.Column)]
|
||||
#column_definitions = sorted(column_definitions, lambda x,y: cmp(x[1].position, y[1].position))
|
||||
column_definitions = sorted(column_definitions, key=lambda x: x[1].position)
|
||||
|
||||
is_polymorphic_base = any([c[1].polymorphic_key for c in column_definitions])
|
||||
|
||||
column_definitions = [x for x in inherited_columns.items()] + column_definitions
|
||||
polymorphic_columns = [c for c in column_definitions if c[1].polymorphic_key]
|
||||
is_polymorphic = len(polymorphic_columns) > 0
|
||||
if len(polymorphic_columns) > 1:
|
||||
raise ModelDefinitionException('only one polymorphic_key can be defined in a model, {} found'.format(len(polymorphic_columns)))
|
||||
|
||||
polymorphic_column_name, polymorphic_column = polymorphic_columns[0] if polymorphic_columns else (None, None)
|
||||
|
||||
if isinstance(polymorphic_column, (columns.BaseContainerColumn, columns.Counter)):
|
||||
raise ModelDefinitionException('counter and container columns cannot be used for polymorphic keys')
|
||||
|
||||
# find polymorphic base class
|
||||
polymorphic_base = None
|
||||
if is_polymorphic and not is_polymorphic_base:
|
||||
def _get_polymorphic_base(bases):
|
||||
for base in bases:
|
||||
if getattr(base, '_is_polymorphic_base', False):
|
||||
return base
|
||||
klass = _get_polymorphic_base(base.__bases__)
|
||||
if klass:
|
||||
return klass
|
||||
polymorphic_base = _get_polymorphic_base(bases)
|
||||
|
||||
defined_columns = OrderedDict(column_definitions)
|
||||
|
||||
# check for primary key
|
||||
if not is_abstract and not any([v.primary_key for k,v in column_definitions]):
|
||||
raise ModelDefinitionException("At least 1 primary key is required.")
|
||||
|
||||
counter_columns = [c for c in defined_columns.values() if isinstance(c, columns.Counter)]
|
||||
data_columns = [c for c in defined_columns.values() if not c.primary_key and not isinstance(c, columns.Counter)]
|
||||
if counter_columns and data_columns:
|
||||
raise ModelDefinitionException('counter models may not have data columns')
|
||||
|
||||
has_partition_keys = any(v.partition_key for (k, v) in column_definitions)
|
||||
|
||||
#transform column definitions
|
||||
for k, v in column_definitions:
|
||||
# don't allow a column with the same name as a built-in attribute or method
|
||||
if k in BaseModel.__dict__:
|
||||
raise ModelDefinitionException("column '{}' conflicts with built-in attribute/method".format(k))
|
||||
|
||||
# counter column primary keys are not allowed
|
||||
if (v.primary_key or v.partition_key) and isinstance(v, (columns.Counter, columns.BaseContainerColumn)):
|
||||
raise ModelDefinitionException('counter columns and container columns cannot be used as primary keys')
|
||||
|
||||
# this will mark the first primary key column as a partition
|
||||
# key, if one hasn't been set already
|
||||
if not has_partition_keys and v.primary_key:
|
||||
v.partition_key = True
|
||||
has_partition_keys = True
|
||||
_transform_column(k, v)
|
||||
|
||||
partition_keys = OrderedDict(k for k in primary_keys.items() if k[1].partition_key)
|
||||
clustering_keys = OrderedDict(k for k in primary_keys.items() if not k[1].partition_key)
|
||||
|
||||
#setup partition key shortcut
|
||||
if len(partition_keys) == 0:
|
||||
if not is_abstract:
|
||||
raise ModelException("at least one partition key must be defined")
|
||||
if len(partition_keys) == 1:
|
||||
pk_name = [x for x in partition_keys.keys()][0]
|
||||
attrs['pk'] = attrs[pk_name]
|
||||
else:
|
||||
# composite partition key case, get/set a tuple of values
|
||||
_get = lambda self: tuple(self._values[c].getval() for c in partition_keys.keys())
|
||||
_set = lambda self, val: tuple(self._values[c].setval(v) for (c, v) in zip(partition_keys.keys(), val))
|
||||
attrs['pk'] = property(_get, _set)
|
||||
|
||||
# some validation
|
||||
col_names = set()
|
||||
for v in column_dict.values():
|
||||
# check for duplicate column names
|
||||
if v.db_field_name in col_names:
|
||||
raise ModelException("{} defines the column {} more than once".format(name, v.db_field_name))
|
||||
if v.clustering_order and not (v.primary_key and not v.partition_key):
|
||||
raise ModelException("clustering_order may be specified only for clustering primary keys")
|
||||
if v.clustering_order and v.clustering_order.lower() not in ('asc', 'desc'):
|
||||
raise ModelException("invalid clustering order {} for column {}".format(repr(v.clustering_order), v.db_field_name))
|
||||
col_names.add(v.db_field_name)
|
||||
|
||||
#create db_name -> model name map for loading
|
||||
db_map = {}
|
||||
for field_name, col in column_dict.items():
|
||||
db_map[col.db_field_name] = field_name
|
||||
|
||||
#add management members to the class
|
||||
attrs['_columns'] = column_dict
|
||||
attrs['_primary_keys'] = primary_keys
|
||||
attrs['_defined_columns'] = defined_columns
|
||||
|
||||
# maps the database field to the models key
|
||||
attrs['_db_map'] = db_map
|
||||
attrs['_pk_name'] = pk_name
|
||||
attrs['_dynamic_columns'] = {}
|
||||
|
||||
attrs['_partition_keys'] = partition_keys
|
||||
attrs['_clustering_keys'] = clustering_keys
|
||||
attrs['_has_counter'] = len(counter_columns) > 0
|
||||
|
||||
# add polymorphic management attributes
|
||||
attrs['_is_polymorphic_base'] = is_polymorphic_base
|
||||
attrs['_is_polymorphic'] = is_polymorphic
|
||||
attrs['_polymorphic_base'] = polymorphic_base
|
||||
attrs['_polymorphic_column'] = polymorphic_column
|
||||
attrs['_polymorphic_column_name'] = polymorphic_column_name
|
||||
attrs['_polymorphic_map'] = {} if is_polymorphic_base else None
|
||||
|
||||
#setup class exceptions
|
||||
DoesNotExistBase = None
|
||||
for base in bases:
|
||||
DoesNotExistBase = getattr(base, 'DoesNotExist', None)
|
||||
if DoesNotExistBase is not None: break
|
||||
DoesNotExistBase = DoesNotExistBase or attrs.pop('DoesNotExist', BaseModel.DoesNotExist)
|
||||
attrs['DoesNotExist'] = type('DoesNotExist', (DoesNotExistBase,), {})
|
||||
|
||||
MultipleObjectsReturnedBase = None
|
||||
for base in bases:
|
||||
MultipleObjectsReturnedBase = getattr(base, 'MultipleObjectsReturned', None)
|
||||
if MultipleObjectsReturnedBase is not None: break
|
||||
MultipleObjectsReturnedBase = DoesNotExistBase or attrs.pop('MultipleObjectsReturned', BaseModel.MultipleObjectsReturned)
|
||||
attrs['MultipleObjectsReturned'] = type('MultipleObjectsReturned', (MultipleObjectsReturnedBase,), {})
|
||||
|
||||
#create the class and add a QuerySet to it
|
||||
klass = super(ModelMetaClass, cls).__new__(cls, name, bases, attrs)
|
||||
|
||||
return klass
|
||||
|
||||
|
||||
import six
|
||||
|
||||
@six.add_metaclass(ModelMetaClass)
|
||||
class Model(BaseModel):
|
||||
"""
|
||||
the db name for the column family can be set as the attribute db_name, or
|
||||
it will be generated from the class name
|
||||
"""
|
||||
__abstract__ = True
|
||||
# __metaclass__ = ModelMetaClass
|
||||
|
||||
|
122
cqlengine/named.py
Normal file
122
cqlengine/named.py
Normal file
@ -0,0 +1,122 @@
|
||||
from cqlengine.exceptions import CQLEngineException
|
||||
from cqlengine.query import AbstractQueryableColumn, SimpleQuerySet
|
||||
|
||||
from cqlengine.query import DoesNotExist as _DoesNotExist
|
||||
from cqlengine.query import MultipleObjectsReturned as _MultipleObjectsReturned
|
||||
|
||||
class QuerySetDescriptor(object):
|
||||
"""
|
||||
returns a fresh queryset for the given model
|
||||
it's declared on everytime it's accessed
|
||||
"""
|
||||
|
||||
def __get__(self, obj, model):
|
||||
""" :rtype: ModelQuerySet """
|
||||
if model.__abstract__:
|
||||
raise CQLEngineException('cannot execute queries against abstract models')
|
||||
return SimpleQuerySet(obj)
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""
|
||||
Just a hint to IDEs that it's ok to call this
|
||||
|
||||
:rtype: ModelQuerySet
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class NamedColumn(AbstractQueryableColumn):
|
||||
"""
|
||||
A column that is not coupled to a model class, or type
|
||||
"""
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
def _get_column(self):
|
||||
""" :rtype: NamedColumn """
|
||||
return self
|
||||
|
||||
@property
|
||||
def db_field_name(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def cql(self):
|
||||
return self.get_cql()
|
||||
|
||||
def get_cql(self):
|
||||
return '"{}"'.format(self.name)
|
||||
|
||||
def to_database(self, val):
|
||||
return val
|
||||
|
||||
|
||||
class NamedTable(object):
|
||||
"""
|
||||
A Table that is not coupled to a model class
|
||||
"""
|
||||
|
||||
__abstract__ = False
|
||||
|
||||
objects = QuerySetDescriptor()
|
||||
|
||||
class DoesNotExist(_DoesNotExist): pass
|
||||
class MultipleObjectsReturned(_MultipleObjectsReturned): pass
|
||||
|
||||
def __init__(self, keyspace, name):
|
||||
self.keyspace = keyspace
|
||||
self.name = name
|
||||
|
||||
def column(self, name):
|
||||
return NamedColumn(name)
|
||||
|
||||
def column_family_name(self, include_keyspace=True):
|
||||
"""
|
||||
Returns the column family name if it's been defined
|
||||
otherwise, it creates it from the module and class name
|
||||
"""
|
||||
if include_keyspace:
|
||||
return '{}.{}'.format(self.keyspace, self.name)
|
||||
else:
|
||||
return self.name
|
||||
|
||||
def _get_column(self, name):
|
||||
"""
|
||||
Returns the column matching the given name
|
||||
|
||||
:rtype: Column
|
||||
"""
|
||||
return self.column(name)
|
||||
|
||||
# def create(self, **kwargs):
|
||||
# return self.objects.create(**kwargs)
|
||||
|
||||
def all(self):
|
||||
return self.objects.all()
|
||||
|
||||
def filter(self, *args, **kwargs):
|
||||
return self.objects.filter(*args, **kwargs)
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return self.objects.get(*args, **kwargs)
|
||||
|
||||
|
||||
class NamedKeyspace(object):
|
||||
"""
|
||||
A keyspace
|
||||
"""
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def table(self, name):
|
||||
"""
|
||||
returns a table descriptor with the given
|
||||
name that belongs to this keyspace
|
||||
"""
|
||||
return NamedTable(self.name, name)
|
||||
|
91
cqlengine/operators.py
Normal file
91
cqlengine/operators.py
Normal file
@ -0,0 +1,91 @@
|
||||
import six
|
||||
|
||||
|
||||
class QueryOperatorException(Exception): pass
|
||||
|
||||
import sys
|
||||
|
||||
# move to central spot
|
||||
class UnicodeMixin(object):
|
||||
if sys.version_info > (3, 0):
|
||||
__str__ = lambda x: x.__unicode__()
|
||||
else:
|
||||
__str__ = lambda x: six.text_type(x).encode('utf-8')
|
||||
|
||||
|
||||
class BaseQueryOperator(UnicodeMixin):
|
||||
# The symbol that identifies this operator in kwargs
|
||||
# ie: colname__<symbol>
|
||||
symbol = None
|
||||
|
||||
# The comparator symbol this operator uses in cql
|
||||
cql_symbol = None
|
||||
|
||||
def __unicode__(self):
|
||||
if self.cql_symbol is None:
|
||||
raise QueryOperatorException("cql symbol is None")
|
||||
return self.cql_symbol
|
||||
|
||||
@classmethod
|
||||
def get_operator(cls, symbol):
|
||||
if cls == BaseQueryOperator:
|
||||
raise QueryOperatorException("get_operator can only be called from a BaseQueryOperator subclass")
|
||||
if not hasattr(cls, 'opmap'):
|
||||
cls.opmap = {}
|
||||
def _recurse(klass):
|
||||
if klass.symbol:
|
||||
cls.opmap[klass.symbol.upper()] = klass
|
||||
for subklass in klass.__subclasses__():
|
||||
_recurse(subklass)
|
||||
pass
|
||||
_recurse(cls)
|
||||
try:
|
||||
return cls.opmap[symbol.upper()]
|
||||
except KeyError:
|
||||
raise QueryOperatorException("{} doesn't map to a QueryOperator".format(symbol))
|
||||
|
||||
|
||||
class BaseWhereOperator(BaseQueryOperator):
|
||||
""" base operator used for where clauses """
|
||||
|
||||
|
||||
class EqualsOperator(BaseWhereOperator):
|
||||
symbol = 'EQ'
|
||||
cql_symbol = '='
|
||||
|
||||
|
||||
class InOperator(EqualsOperator):
|
||||
symbol = 'IN'
|
||||
cql_symbol = 'IN'
|
||||
|
||||
|
||||
class GreaterThanOperator(BaseWhereOperator):
|
||||
symbol = "GT"
|
||||
cql_symbol = '>'
|
||||
|
||||
|
||||
class GreaterThanOrEqualOperator(BaseWhereOperator):
|
||||
symbol = "GTE"
|
||||
cql_symbol = '>='
|
||||
|
||||
|
||||
class LessThanOperator(BaseWhereOperator):
|
||||
symbol = "LT"
|
||||
cql_symbol = '<'
|
||||
|
||||
|
||||
class LessThanOrEqualOperator(BaseWhereOperator):
|
||||
symbol = "LTE"
|
||||
cql_symbol = '<='
|
||||
|
||||
|
||||
class BaseAssignmentOperator(BaseQueryOperator):
|
||||
""" base operator used for insert and delete statements """
|
||||
|
||||
|
||||
class AssignmentOperator(BaseAssignmentOperator):
|
||||
cql_symbol = "="
|
||||
|
||||
|
||||
class AddSymbol(BaseAssignmentOperator):
|
||||
cql_symbol = "+"
|
1032
cqlengine/query.py
Normal file
1032
cqlengine/query.py
Normal file
File diff suppressed because it is too large
Load Diff
817
cqlengine/statements.py
Normal file
817
cqlengine/statements.py
Normal file
@ -0,0 +1,817 @@
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
import six
|
||||
from cqlengine.functions import QueryValue
|
||||
from cqlengine.operators import BaseWhereOperator, InOperator
|
||||
|
||||
|
||||
class StatementException(Exception): pass
|
||||
|
||||
|
||||
|
||||
import sys
|
||||
|
||||
class UnicodeMixin(object):
|
||||
if sys.version_info > (3, 0):
|
||||
__str__ = lambda x: x.__unicode__()
|
||||
else:
|
||||
__str__ = lambda x: six.text_type(x).encode('utf-8')
|
||||
|
||||
class ValueQuoter(UnicodeMixin):
|
||||
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
|
||||
def __unicode__(self):
|
||||
from cassandra.encoder import cql_quote
|
||||
if isinstance(self.value, bool):
|
||||
return 'true' if self.value else 'false'
|
||||
elif isinstance(self.value, (list, tuple)):
|
||||
return '[' + ', '.join([cql_quote(v) for v in self.value]) + ']'
|
||||
elif isinstance(self.value, dict):
|
||||
return '{' + ', '.join([cql_quote(k) + ':' + cql_quote(v) for k,v in self.value.items()]) + '}'
|
||||
elif isinstance(self.value, set):
|
||||
return '{' + ', '.join([cql_quote(v) for v in self.value]) + '}'
|
||||
return cql_quote(self.value)
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, self.__class__):
|
||||
return self.value == other.value
|
||||
return False
|
||||
|
||||
|
||||
class InQuoter(ValueQuoter):
|
||||
|
||||
def __unicode__(self):
|
||||
from cassandra.encoder import cql_quote
|
||||
return '(' + ', '.join([cql_quote(v) for v in self.value]) + ')'
|
||||
|
||||
|
||||
class BaseClause(UnicodeMixin):
|
||||
|
||||
def __init__(self, field, value):
|
||||
self.field = field
|
||||
self.value = value
|
||||
self.context_id = None
|
||||
|
||||
def __unicode__(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.field) ^ hash(self.value)
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, self.__class__):
|
||||
return self.field == other.field and self.value == other.value
|
||||
return False
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def get_context_size(self):
|
||||
""" returns the number of entries this clause will add to the query context """
|
||||
return 1
|
||||
|
||||
def set_context_id(self, i):
|
||||
""" sets the value placeholder that will be used in the query """
|
||||
self.context_id = i
|
||||
|
||||
def update_context(self, ctx):
|
||||
""" updates the query context with this clauses values """
|
||||
assert isinstance(ctx, dict)
|
||||
ctx[str(self.context_id)] = self.value
|
||||
|
||||
|
||||
class WhereClause(BaseClause):
|
||||
""" a single where statement used in queries """
|
||||
|
||||
def __init__(self, field, operator, value, quote_field=True):
|
||||
"""
|
||||
|
||||
:param field:
|
||||
:param operator:
|
||||
:param value:
|
||||
:param quote_field: hack to get the token function rendering properly
|
||||
:return:
|
||||
"""
|
||||
if not isinstance(operator, BaseWhereOperator):
|
||||
raise StatementException(
|
||||
"operator must be of type {}, got {}".format(BaseWhereOperator, type(operator))
|
||||
)
|
||||
super(WhereClause, self).__init__(field, value)
|
||||
self.operator = operator
|
||||
self.query_value = self.value if isinstance(self.value, QueryValue) else QueryValue(self.value)
|
||||
self.quote_field = quote_field
|
||||
|
||||
def __unicode__(self):
|
||||
field = ('"{}"' if self.quote_field else '{}').format(self.field)
|
||||
return u'{} {} {}'.format(field, self.operator, six.text_type(self.query_value))
|
||||
|
||||
def __hash__(self):
|
||||
return super(WhereClause, self).__hash__() ^ hash(self.operator)
|
||||
|
||||
def __eq__(self, other):
|
||||
if super(WhereClause, self).__eq__(other):
|
||||
return self.operator.__class__ == other.operator.__class__
|
||||
return False
|
||||
|
||||
def get_context_size(self):
|
||||
return self.query_value.get_context_size()
|
||||
|
||||
def set_context_id(self, i):
|
||||
super(WhereClause, self).set_context_id(i)
|
||||
self.query_value.set_context_id(i)
|
||||
|
||||
def update_context(self, ctx):
|
||||
if isinstance(self.operator, InOperator):
|
||||
ctx[str(self.context_id)] = InQuoter(self.value)
|
||||
else:
|
||||
self.query_value.update_context(ctx)
|
||||
|
||||
|
||||
class AssignmentClause(BaseClause):
|
||||
""" a single variable st statement """
|
||||
|
||||
def __unicode__(self):
|
||||
return u'"{}" = %({})s'.format(self.field, self.context_id)
|
||||
|
||||
def insert_tuple(self):
|
||||
return self.field, self.context_id
|
||||
|
||||
|
||||
class TransactionClause(BaseClause):
|
||||
""" A single variable iff statement """
|
||||
|
||||
def __unicode__(self):
|
||||
return u'"{}" = %({})s'.format(self.field, self.context_id)
|
||||
|
||||
def insert_tuple(self):
|
||||
return self.field, self.context_id
|
||||
|
||||
|
||||
class ContainerUpdateClause(AssignmentClause):
|
||||
|
||||
def __init__(self, field, value, operation=None, previous=None, column=None):
|
||||
super(ContainerUpdateClause, self).__init__(field, value)
|
||||
self.previous = previous
|
||||
self._assignments = None
|
||||
self._operation = operation
|
||||
self._analyzed = False
|
||||
self._column = column
|
||||
|
||||
def _to_database(self, val):
|
||||
return self._column.to_database(val) if self._column else val
|
||||
|
||||
def _analyze(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_context_size(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def update_context(self, ctx):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class SetUpdateClause(ContainerUpdateClause):
|
||||
""" updates a set collection """
|
||||
|
||||
def __init__(self, field, value, operation=None, previous=None, column=None):
|
||||
super(SetUpdateClause, self).__init__(field, value, operation, previous, column=column)
|
||||
self._additions = None
|
||||
self._removals = None
|
||||
|
||||
def __unicode__(self):
|
||||
qs = []
|
||||
ctx_id = self.context_id
|
||||
if (self.previous is None and
|
||||
self._assignments is None and
|
||||
self._additions is None and
|
||||
self._removals is None):
|
||||
qs += ['"{}" = %({})s'.format(self.field, ctx_id)]
|
||||
if self._assignments is not None:
|
||||
qs += ['"{}" = %({})s'.format(self.field, ctx_id)]
|
||||
ctx_id += 1
|
||||
if self._additions is not None:
|
||||
qs += ['"{0}" = "{0}" + %({1})s'.format(self.field, ctx_id)]
|
||||
ctx_id += 1
|
||||
if self._removals is not None:
|
||||
qs += ['"{0}" = "{0}" - %({1})s'.format(self.field, ctx_id)]
|
||||
|
||||
return ', '.join(qs)
|
||||
|
||||
def _analyze(self):
|
||||
""" works out the updates to be performed """
|
||||
if self.value is None or self.value == self.previous:
|
||||
pass
|
||||
elif self._operation == "add":
|
||||
self._additions = self.value
|
||||
elif self._operation == "remove":
|
||||
self._removals = self.value
|
||||
elif self.previous is None:
|
||||
self._assignments = self.value
|
||||
else:
|
||||
# partial update time
|
||||
self._additions = (self.value - self.previous) or None
|
||||
self._removals = (self.previous - self.value) or None
|
||||
self._analyzed = True
|
||||
|
||||
def get_context_size(self):
|
||||
if not self._analyzed: self._analyze()
|
||||
if (self.previous is None and
|
||||
not self._assignments and
|
||||
self._additions is None and
|
||||
self._removals is None):
|
||||
return 1
|
||||
return int(bool(self._assignments)) + int(bool(self._additions)) + int(bool(self._removals))
|
||||
|
||||
def update_context(self, ctx):
|
||||
if not self._analyzed: self._analyze()
|
||||
ctx_id = self.context_id
|
||||
if (self.previous is None and
|
||||
self._assignments is None and
|
||||
self._additions is None and
|
||||
self._removals is None):
|
||||
ctx[str(ctx_id)] = self._to_database({})
|
||||
if self._assignments is not None:
|
||||
ctx[str(ctx_id)] = self._to_database(self._assignments)
|
||||
ctx_id += 1
|
||||
if self._additions is not None:
|
||||
ctx[str(ctx_id)] = self._to_database(self._additions)
|
||||
ctx_id += 1
|
||||
if self._removals is not None:
|
||||
ctx[str(ctx_id)] = self._to_database(self._removals)
|
||||
|
||||
|
||||
class ListUpdateClause(ContainerUpdateClause):
|
||||
""" updates a list collection """
|
||||
|
||||
def __init__(self, field, value, operation=None, previous=None, column=None):
|
||||
super(ListUpdateClause, self).__init__(field, value, operation, previous, column=column)
|
||||
self._append = None
|
||||
self._prepend = None
|
||||
|
||||
def __unicode__(self):
|
||||
if not self._analyzed: self._analyze()
|
||||
qs = []
|
||||
ctx_id = self.context_id
|
||||
if self._assignments is not None:
|
||||
qs += ['"{}" = %({})s'.format(self.field, ctx_id)]
|
||||
ctx_id += 1
|
||||
|
||||
if self._prepend is not None:
|
||||
qs += ['"{0}" = %({1})s + "{0}"'.format(self.field, ctx_id)]
|
||||
ctx_id += 1
|
||||
|
||||
if self._append is not None:
|
||||
qs += ['"{0}" = "{0}" + %({1})s'.format(self.field, ctx_id)]
|
||||
|
||||
return ', '.join(qs)
|
||||
|
||||
def get_context_size(self):
|
||||
if not self._analyzed: self._analyze()
|
||||
return int(self._assignments is not None) + int(bool(self._append)) + int(bool(self._prepend))
|
||||
|
||||
def update_context(self, ctx):
|
||||
if not self._analyzed: self._analyze()
|
||||
ctx_id = self.context_id
|
||||
if self._assignments is not None:
|
||||
ctx[str(ctx_id)] = self._to_database(self._assignments)
|
||||
ctx_id += 1
|
||||
if self._prepend is not None:
|
||||
# CQL seems to prepend element at a time, starting
|
||||
# with the element at idx 0, we can either reverse
|
||||
# it here, or have it inserted in reverse
|
||||
ctx[str(ctx_id)] = self._to_database(list(reversed(self._prepend)))
|
||||
ctx_id += 1
|
||||
if self._append is not None:
|
||||
ctx[str(ctx_id)] = self._to_database(self._append)
|
||||
|
||||
def _analyze(self):
|
||||
""" works out the updates to be performed """
|
||||
if self.value is None or self.value == self.previous:
|
||||
pass
|
||||
|
||||
elif self._operation == "append":
|
||||
self._append = self.value
|
||||
|
||||
elif self._operation == "prepend":
|
||||
# self.value is a Quoter but we reverse self._prepend later as if
|
||||
# it's a list, so we have to set it to the underlying list
|
||||
self._prepend = self.value.value
|
||||
|
||||
elif self.previous is None:
|
||||
self._assignments = self.value
|
||||
|
||||
elif len(self.value) < len(self.previous):
|
||||
# if elements have been removed,
|
||||
# rewrite the whole list
|
||||
self._assignments = self.value
|
||||
|
||||
elif len(self.previous) == 0:
|
||||
# if we're updating from an empty
|
||||
# list, do a complete insert
|
||||
self._assignments = self.value
|
||||
else:
|
||||
|
||||
# the max start idx we want to compare
|
||||
search_space = len(self.value) - max(0, len(self.previous)-1)
|
||||
|
||||
# the size of the sub lists we want to look at
|
||||
search_size = len(self.previous)
|
||||
|
||||
for i in range(search_space):
|
||||
#slice boundary
|
||||
j = i + search_size
|
||||
sub = self.value[i:j]
|
||||
idx_cmp = lambda idx: self.previous[idx] == sub[idx]
|
||||
if idx_cmp(0) and idx_cmp(-1) and self.previous == sub:
|
||||
self._prepend = self.value[:i] or None
|
||||
self._append = self.value[j:] or None
|
||||
break
|
||||
|
||||
# if both append and prepend are still None after looking
|
||||
# at both lists, an insert statement will be created
|
||||
if self._prepend is self._append is None:
|
||||
self._assignments = self.value
|
||||
|
||||
self._analyzed = True
|
||||
|
||||
|
||||
class MapUpdateClause(ContainerUpdateClause):
|
||||
""" updates a map collection """
|
||||
|
||||
def __init__(self, field, value, operation=None, previous=None, column=None):
|
||||
super(MapUpdateClause, self).__init__(field, value, operation, previous, column=column)
|
||||
self._updates = None
|
||||
|
||||
def _analyze(self):
|
||||
if self._operation == "update":
|
||||
self._updates = self.value.keys()
|
||||
else:
|
||||
if self.previous is None:
|
||||
self._updates = sorted([k for k, v in self.value.items()])
|
||||
else:
|
||||
self._updates = sorted([k for k, v in self.value.items() if v != self.previous.get(k)]) or None
|
||||
self._analyzed = True
|
||||
|
||||
def get_context_size(self):
|
||||
if not self._analyzed: self._analyze()
|
||||
if self.previous is None and not self._updates:
|
||||
return 1
|
||||
return len(self._updates or []) * 2
|
||||
|
||||
def update_context(self, ctx):
|
||||
if not self._analyzed: self._analyze()
|
||||
ctx_id = self.context_id
|
||||
if self.previous is None and not self._updates:
|
||||
ctx[str(ctx_id)] = {}
|
||||
else:
|
||||
for key in self._updates or []:
|
||||
val = self.value.get(key)
|
||||
ctx[str(ctx_id)] = self._column.key_col.to_database(key) if self._column else key
|
||||
ctx[str(ctx_id + 1)] = self._column.value_col.to_database(val) if self._column else val
|
||||
ctx_id += 2
|
||||
|
||||
def __unicode__(self):
|
||||
if not self._analyzed: self._analyze()
|
||||
qs = []
|
||||
|
||||
ctx_id = self.context_id
|
||||
if self.previous is None and not self._updates:
|
||||
qs += ['"{}" = %({})s'.format(self.field, ctx_id)]
|
||||
else:
|
||||
for _ in self._updates or []:
|
||||
qs += ['"{}"[%({})s] = %({})s'.format(self.field, ctx_id, ctx_id + 1)]
|
||||
ctx_id += 2
|
||||
|
||||
return ', '.join(qs)
|
||||
|
||||
|
||||
class CounterUpdateClause(ContainerUpdateClause):
|
||||
|
||||
def __init__(self, field, value, previous=None, column=None):
|
||||
super(CounterUpdateClause, self).__init__(field, value, previous=previous, column=column)
|
||||
self.previous = self.previous or 0
|
||||
|
||||
def get_context_size(self):
|
||||
return 1
|
||||
|
||||
def update_context(self, ctx):
|
||||
ctx[str(self.context_id)] = self._to_database(abs(self.value - self.previous))
|
||||
|
||||
def __unicode__(self):
|
||||
delta = self.value - self.previous
|
||||
sign = '-' if delta < 0 else '+'
|
||||
return '"{0}" = "{0}" {1} %({2})s'.format(self.field, sign, self.context_id)
|
||||
|
||||
|
||||
class BaseDeleteClause(BaseClause):
|
||||
pass
|
||||
|
||||
|
||||
class FieldDeleteClause(BaseDeleteClause):
|
||||
""" deletes a field from a row """
|
||||
|
||||
def __init__(self, field):
|
||||
super(FieldDeleteClause, self).__init__(field, None)
|
||||
|
||||
def __unicode__(self):
|
||||
return '"{}"'.format(self.field)
|
||||
|
||||
def update_context(self, ctx):
|
||||
pass
|
||||
|
||||
def get_context_size(self):
|
||||
return 0
|
||||
|
||||
|
||||
class MapDeleteClause(BaseDeleteClause):
|
||||
""" removes keys from a map """
|
||||
|
||||
def __init__(self, field, value, previous=None):
|
||||
super(MapDeleteClause, self).__init__(field, value)
|
||||
self.value = self.value or {}
|
||||
self.previous = previous or {}
|
||||
self._analyzed = False
|
||||
self._removals = None
|
||||
|
||||
def _analyze(self):
|
||||
self._removals = sorted([k for k in self.previous if k not in self.value])
|
||||
self._analyzed = True
|
||||
|
||||
def update_context(self, ctx):
|
||||
if not self._analyzed: self._analyze()
|
||||
for idx, key in enumerate(self._removals):
|
||||
ctx[str(self.context_id + idx)] = key
|
||||
|
||||
def get_context_size(self):
|
||||
if not self._analyzed: self._analyze()
|
||||
return len(self._removals)
|
||||
|
||||
def __unicode__(self):
|
||||
if not self._analyzed: self._analyze()
|
||||
return ', '.join(['"{}"[%({})s]'.format(self.field, self.context_id + i) for i in range(len(self._removals))])
|
||||
|
||||
|
||||
class BaseCQLStatement(UnicodeMixin):
|
||||
""" The base cql statement class """
|
||||
|
||||
def __init__(self, table, consistency=None, timestamp=None, where=None):
|
||||
super(BaseCQLStatement, self).__init__()
|
||||
self.table = table
|
||||
self.consistency = consistency
|
||||
self.context_id = 0
|
||||
self.context_counter = self.context_id
|
||||
self.timestamp = timestamp
|
||||
|
||||
self.where_clauses = []
|
||||
for clause in where or []:
|
||||
self.add_where_clause(clause)
|
||||
|
||||
def add_where_clause(self, clause):
|
||||
"""
|
||||
adds a where clause to this statement
|
||||
:param clause: the clause to add
|
||||
:type clause: WhereClause
|
||||
"""
|
||||
if not isinstance(clause, WhereClause):
|
||||
raise StatementException("only instances of WhereClause can be added to statements")
|
||||
clause.set_context_id(self.context_counter)
|
||||
self.context_counter += clause.get_context_size()
|
||||
self.where_clauses.append(clause)
|
||||
|
||||
def get_context(self):
|
||||
"""
|
||||
returns the context dict for this statement
|
||||
:rtype: dict
|
||||
"""
|
||||
ctx = {}
|
||||
for clause in self.where_clauses or []:
|
||||
clause.update_context(ctx)
|
||||
return ctx
|
||||
|
||||
def get_context_size(self):
|
||||
return len(self.get_context())
|
||||
|
||||
def update_context_id(self, i):
|
||||
self.context_id = i
|
||||
self.context_counter = self.context_id
|
||||
for clause in self.where_clauses:
|
||||
clause.set_context_id(self.context_counter)
|
||||
self.context_counter += clause.get_context_size()
|
||||
|
||||
@property
|
||||
def timestamp_normalized(self):
|
||||
"""
|
||||
we're expecting self.timestamp to be either a long, int, a datetime, or a timedelta
|
||||
:return:
|
||||
"""
|
||||
if not self.timestamp:
|
||||
return None
|
||||
|
||||
if isinstance(self.timestamp, six.integer_types):
|
||||
return self.timestamp
|
||||
|
||||
if isinstance(self.timestamp, timedelta):
|
||||
tmp = datetime.now() + self.timestamp
|
||||
else:
|
||||
tmp = self.timestamp
|
||||
|
||||
return int(time.mktime(tmp.timetuple()) * 1e+6 + tmp.microsecond)
|
||||
|
||||
def __unicode__(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return self.__unicode__()
|
||||
|
||||
@property
|
||||
def _where(self):
|
||||
return 'WHERE {}'.format(' AND '.join([six.text_type(c) for c in self.where_clauses]))
|
||||
|
||||
|
||||
class SelectStatement(BaseCQLStatement):
|
||||
""" a cql select statement """
|
||||
|
||||
def __init__(self,
|
||||
table,
|
||||
fields=None,
|
||||
count=False,
|
||||
consistency=None,
|
||||
where=None,
|
||||
order_by=None,
|
||||
limit=None,
|
||||
allow_filtering=False):
|
||||
|
||||
"""
|
||||
:param where
|
||||
:type where list of cqlengine.statements.WhereClause
|
||||
"""
|
||||
super(SelectStatement, self).__init__(
|
||||
table,
|
||||
consistency=consistency,
|
||||
where=where
|
||||
)
|
||||
|
||||
self.fields = [fields] if isinstance(fields, six.string_types) else (fields or [])
|
||||
self.count = count
|
||||
self.order_by = [order_by] if isinstance(order_by, six.string_types) else order_by
|
||||
self.limit = limit
|
||||
self.allow_filtering = allow_filtering
|
||||
|
||||
def __unicode__(self):
|
||||
qs = ['SELECT']
|
||||
if self.count:
|
||||
qs += ['COUNT(*)']
|
||||
else:
|
||||
qs += [', '.join(['"{}"'.format(f) for f in self.fields]) if self.fields else '*']
|
||||
qs += ['FROM', self.table]
|
||||
|
||||
if self.where_clauses:
|
||||
qs += [self._where]
|
||||
|
||||
if self.order_by and not self.count:
|
||||
qs += ['ORDER BY {}'.format(', '.join(six.text_type(o) for o in self.order_by))]
|
||||
|
||||
if self.limit:
|
||||
qs += ['LIMIT {}'.format(self.limit)]
|
||||
|
||||
if self.allow_filtering:
|
||||
qs += ['ALLOW FILTERING']
|
||||
|
||||
return ' '.join(qs)
|
||||
|
||||
|
||||
class AssignmentStatement(BaseCQLStatement):
|
||||
""" value assignment statements """
|
||||
|
||||
def __init__(self,
|
||||
table,
|
||||
assignments=None,
|
||||
consistency=None,
|
||||
where=None,
|
||||
ttl=None,
|
||||
timestamp=None):
|
||||
super(AssignmentStatement, self).__init__(
|
||||
table,
|
||||
consistency=consistency,
|
||||
where=where,
|
||||
)
|
||||
self.ttl = ttl
|
||||
self.timestamp = timestamp
|
||||
|
||||
# add assignments
|
||||
self.assignments = []
|
||||
for assignment in assignments or []:
|
||||
self.add_assignment_clause(assignment)
|
||||
|
||||
def update_context_id(self, i):
|
||||
super(AssignmentStatement, self).update_context_id(i)
|
||||
for assignment in self.assignments:
|
||||
assignment.set_context_id(self.context_counter)
|
||||
self.context_counter += assignment.get_context_size()
|
||||
|
||||
def add_assignment_clause(self, clause):
|
||||
"""
|
||||
adds an assignment clause to this statement
|
||||
:param clause: the clause to add
|
||||
:type clause: AssignmentClause
|
||||
"""
|
||||
if not isinstance(clause, AssignmentClause):
|
||||
raise StatementException("only instances of AssignmentClause can be added to statements")
|
||||
clause.set_context_id(self.context_counter)
|
||||
self.context_counter += clause.get_context_size()
|
||||
self.assignments.append(clause)
|
||||
|
||||
@property
|
||||
def is_empty(self):
|
||||
return len(self.assignments) == 0
|
||||
|
||||
def get_context(self):
|
||||
ctx = super(AssignmentStatement, self).get_context()
|
||||
for clause in self.assignments:
|
||||
clause.update_context(ctx)
|
||||
return ctx
|
||||
|
||||
|
||||
class InsertStatement(AssignmentStatement):
|
||||
""" an cql insert select statement """
|
||||
|
||||
def __init__(self,
|
||||
table,
|
||||
assignments=None,
|
||||
consistency=None,
|
||||
where=None,
|
||||
ttl=None,
|
||||
timestamp=None,
|
||||
if_not_exists=False):
|
||||
super(InsertStatement, self).__init__(
|
||||
table,
|
||||
assignments=assignments,
|
||||
consistency=consistency,
|
||||
where=where,
|
||||
ttl=ttl,
|
||||
timestamp=timestamp)
|
||||
|
||||
self.if_not_exists = if_not_exists
|
||||
|
||||
def add_where_clause(self, clause):
|
||||
raise StatementException("Cannot add where clauses to insert statements")
|
||||
|
||||
def __unicode__(self):
|
||||
qs = ['INSERT INTO {}'.format(self.table)]
|
||||
|
||||
# get column names and context placeholders
|
||||
fields = [a.insert_tuple() for a in self.assignments]
|
||||
columns, values = zip(*fields)
|
||||
|
||||
qs += ["({})".format(', '.join(['"{}"'.format(c) for c in columns]))]
|
||||
qs += ['VALUES']
|
||||
qs += ["({})".format(', '.join(['%({})s'.format(v) for v in values]))]
|
||||
|
||||
if self.if_not_exists:
|
||||
qs += ["IF NOT EXISTS"]
|
||||
|
||||
if self.ttl:
|
||||
qs += ["USING TTL {}".format(self.ttl)]
|
||||
|
||||
if self.timestamp:
|
||||
qs += ["USING TIMESTAMP {}".format(self.timestamp_normalized)]
|
||||
|
||||
return ' '.join(qs)
|
||||
|
||||
|
||||
class UpdateStatement(AssignmentStatement):
|
||||
""" an cql update select statement """
|
||||
|
||||
def __init__(self,
|
||||
table,
|
||||
assignments=None,
|
||||
consistency=None,
|
||||
where=None,
|
||||
ttl=None,
|
||||
timestamp=None,
|
||||
transactions=None):
|
||||
super(UpdateStatement, self). __init__(table,
|
||||
assignments=assignments,
|
||||
consistency=consistency,
|
||||
where=where,
|
||||
ttl=ttl,
|
||||
timestamp=timestamp)
|
||||
|
||||
# Add iff statements
|
||||
self.transactions = []
|
||||
for transaction in transactions or []:
|
||||
self.add_transaction_clause(transaction)
|
||||
|
||||
def __unicode__(self):
|
||||
qs = ['UPDATE', self.table]
|
||||
|
||||
using_options = []
|
||||
|
||||
if self.ttl:
|
||||
using_options += ["TTL {}".format(self.ttl)]
|
||||
|
||||
if self.timestamp:
|
||||
using_options += ["TIMESTAMP {}".format(self.timestamp_normalized)]
|
||||
|
||||
if using_options:
|
||||
qs += ["USING {}".format(" AND ".join(using_options))]
|
||||
|
||||
qs += ['SET']
|
||||
qs += [', '.join([six.text_type(c) for c in self.assignments])]
|
||||
|
||||
if self.where_clauses:
|
||||
qs += [self._where]
|
||||
|
||||
if len(self.transactions) > 0:
|
||||
qs += [self._get_transactions()]
|
||||
|
||||
return ' '.join(qs)
|
||||
|
||||
def add_transaction_clause(self, clause):
|
||||
"""
|
||||
Adds a iff clause to this statement
|
||||
|
||||
:param clause: The clause that will be added to the iff statement
|
||||
:type clause: TransactionClause
|
||||
"""
|
||||
if not isinstance(clause, TransactionClause):
|
||||
raise StatementException('only instances of AssignmentClause can be added to statements')
|
||||
clause.set_context_id(self.context_counter)
|
||||
self.context_counter += clause.get_context_size()
|
||||
self.transactions.append(clause)
|
||||
|
||||
def get_context(self):
|
||||
ctx = super(UpdateStatement, self).get_context()
|
||||
for clause in self.transactions or []:
|
||||
clause.update_context(ctx)
|
||||
return ctx
|
||||
|
||||
def _get_transactions(self):
|
||||
return 'IF {}'.format(' AND '.join([six.text_type(c) for c in self.transactions]))
|
||||
|
||||
def update_context_id(self, i):
|
||||
super(UpdateStatement, self).update_context_id(i)
|
||||
for transaction in self.transactions:
|
||||
transaction.set_context_id(self.context_counter)
|
||||
self.context_counter += transaction.get_context_size()
|
||||
|
||||
|
||||
class DeleteStatement(BaseCQLStatement):
|
||||
""" a cql delete statement """
|
||||
|
||||
def __init__(self, table, fields=None, consistency=None, where=None, timestamp=None):
|
||||
super(DeleteStatement, self).__init__(
|
||||
table,
|
||||
consistency=consistency,
|
||||
where=where,
|
||||
timestamp=timestamp
|
||||
)
|
||||
self.fields = []
|
||||
if isinstance(fields, six.string_types):
|
||||
fields = [fields]
|
||||
for field in fields or []:
|
||||
self.add_field(field)
|
||||
|
||||
def update_context_id(self, i):
|
||||
super(DeleteStatement, self).update_context_id(i)
|
||||
for field in self.fields:
|
||||
field.set_context_id(self.context_counter)
|
||||
self.context_counter += field.get_context_size()
|
||||
|
||||
def get_context(self):
|
||||
ctx = super(DeleteStatement, self).get_context()
|
||||
for field in self.fields:
|
||||
field.update_context(ctx)
|
||||
return ctx
|
||||
|
||||
def add_field(self, field):
|
||||
if isinstance(field, six.string_types):
|
||||
field = FieldDeleteClause(field)
|
||||
if not isinstance(field, BaseClause):
|
||||
raise StatementException("only instances of AssignmentClause can be added to statements")
|
||||
field.set_context_id(self.context_counter)
|
||||
self.context_counter += field.get_context_size()
|
||||
self.fields.append(field)
|
||||
|
||||
def __unicode__(self):
|
||||
qs = ['DELETE']
|
||||
if self.fields:
|
||||
qs += [', '.join(['{}'.format(f) for f in self.fields])]
|
||||
qs += ['FROM', self.table]
|
||||
|
||||
delete_option = []
|
||||
|
||||
if self.timestamp:
|
||||
delete_option += ["TIMESTAMP {}".format(self.timestamp_normalized)]
|
||||
|
||||
if delete_option:
|
||||
qs += [" USING {} ".format(" AND ".join(delete_option))]
|
||||
|
||||
if self.where_clauses:
|
||||
qs += [self._where]
|
||||
|
||||
return ' '.join(qs)
|
||||
|
30
cqlengine/tests/__init__.py
Normal file
30
cqlengine/tests/__init__.py
Normal file
@ -0,0 +1,30 @@
|
||||
import os
|
||||
|
||||
|
||||
from cqlengine import connection
|
||||
from cqlengine.management import create_keyspace
|
||||
|
||||
|
||||
def setup_package():
|
||||
try:
|
||||
CASSANDRA_VERSION = int(os.environ["CASSANDRA_VERSION"])
|
||||
except:
|
||||
print("CASSANDRA_VERSION must be set as an environment variable. "
|
||||
"One of (12, 20, 21)")
|
||||
raise
|
||||
|
||||
if os.environ.get('CASSANDRA_TEST_HOST'):
|
||||
CASSANDRA_TEST_HOST = os.environ['CASSANDRA_TEST_HOST']
|
||||
else:
|
||||
CASSANDRA_TEST_HOST = 'localhost'
|
||||
|
||||
if CASSANDRA_VERSION < 20:
|
||||
protocol_version = 1
|
||||
else:
|
||||
protocol_version = 2
|
||||
|
||||
connection.setup([CASSANDRA_TEST_HOST],
|
||||
protocol_version=protocol_version,
|
||||
default_keyspace='cqlengine_test')
|
||||
|
||||
create_keyspace("cqlengine_test", replication_factor=1, strategy_class="SimpleStrategy")
|
33
cqlengine/tests/base.py
Normal file
33
cqlengine/tests/base.py
Normal file
@ -0,0 +1,33 @@
|
||||
from unittest import TestCase
|
||||
import os
|
||||
import sys
|
||||
import six
|
||||
from cqlengine.connection import get_session
|
||||
|
||||
|
||||
CASSANDRA_VERSION = int(os.environ['CASSANDRA_VERSION'])
|
||||
PROTOCOL_VERSION = 1 if CASSANDRA_VERSION < 20 else 2
|
||||
|
||||
|
||||
class BaseCassEngTestCase(TestCase):
|
||||
|
||||
# @classmethod
|
||||
# def setUpClass(cls):
|
||||
# super(BaseCassEngTestCase, cls).setUpClass()
|
||||
session = None
|
||||
|
||||
def setUp(self):
|
||||
self.session = get_session()
|
||||
super(BaseCassEngTestCase, self).setUp()
|
||||
|
||||
def assertHasAttr(self, obj, attr):
|
||||
self.assertTrue(hasattr(obj, attr),
|
||||
"{} doesn't have attribute: {}".format(obj, attr))
|
||||
|
||||
def assertNotHasAttr(self, obj, attr):
|
||||
self.assertFalse(hasattr(obj, attr),
|
||||
"{} shouldn't have the attribute: {}".format(obj, attr))
|
||||
|
||||
if sys.version_info > (3, 0):
|
||||
def assertItemsEqual(self, first, second, msg=None):
|
||||
return self.assertCountEqual(first, second, msg)
|
0
cqlengine/tests/columns/__init__.py
Normal file
0
cqlengine/tests/columns/__init__.py
Normal file
532
cqlengine/tests/columns/test_container_columns.py
Normal file
532
cqlengine/tests/columns/test_container_columns.py
Normal file
@ -0,0 +1,532 @@
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
from uuid import uuid4
|
||||
import six
|
||||
|
||||
from cqlengine import Model, ValidationError
|
||||
from cqlengine import columns
|
||||
from cqlengine.management import sync_table, drop_table
|
||||
from cqlengine.tests.base import BaseCassEngTestCase
|
||||
|
||||
|
||||
class TestSetModel(Model):
|
||||
|
||||
|
||||
partition = columns.UUID(primary_key=True, default=uuid4)
|
||||
int_set = columns.Set(columns.Integer, required=False)
|
||||
text_set = columns.Set(columns.Text, required=False)
|
||||
|
||||
|
||||
class JsonTestColumn(columns.Column):
|
||||
|
||||
db_type = 'text'
|
||||
|
||||
def to_python(self, value):
|
||||
if value is None: return
|
||||
if isinstance(value, six.string_types):
|
||||
return json.loads(value)
|
||||
else:
|
||||
return value
|
||||
|
||||
def to_database(self, value):
|
||||
if value is None: return
|
||||
return json.dumps(value)
|
||||
|
||||
|
||||
class TestSetColumn(BaseCassEngTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestSetColumn, cls).setUpClass()
|
||||
drop_table(TestSetModel)
|
||||
sync_table(TestSetModel)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(TestSetColumn, cls).tearDownClass()
|
||||
drop_table(TestSetModel)
|
||||
|
||||
def test_add_none_fails(self):
|
||||
with self.assertRaises(ValidationError):
|
||||
m = TestSetModel.create(int_set=set([None]))
|
||||
|
||||
def test_empty_set_initial(self):
|
||||
"""
|
||||
tests that sets are set() by default, should never be none
|
||||
:return:
|
||||
"""
|
||||
m = TestSetModel.create()
|
||||
m.int_set.add(5)
|
||||
m.save()
|
||||
|
||||
def test_deleting_last_item_should_succeed(self):
|
||||
m = TestSetModel.create()
|
||||
m.int_set.add(5)
|
||||
m.save()
|
||||
m.int_set.remove(5)
|
||||
m.save()
|
||||
|
||||
m = TestSetModel.get(partition=m.partition)
|
||||
self.assertNotIn(5, m.int_set)
|
||||
|
||||
def test_blind_deleting_last_item_should_succeed(self):
|
||||
m = TestSetModel.create()
|
||||
m.int_set.add(5)
|
||||
m.save()
|
||||
|
||||
TestSetModel.objects(partition=m.partition).update(int_set=set())
|
||||
|
||||
m = TestSetModel.get(partition=m.partition)
|
||||
self.assertNotIn(5, m.int_set)
|
||||
|
||||
def test_empty_set_retrieval(self):
|
||||
m = TestSetModel.create()
|
||||
m2 = TestSetModel.get(partition=m.partition)
|
||||
m2.int_set.add(3)
|
||||
|
||||
def test_io_success(self):
|
||||
""" Tests that a basic usage works as expected """
|
||||
m1 = TestSetModel.create(int_set={1, 2}, text_set={'kai', 'andreas'})
|
||||
m2 = TestSetModel.get(partition=m1.partition)
|
||||
|
||||
assert isinstance(m2.int_set, set)
|
||||
assert isinstance(m2.text_set, set)
|
||||
|
||||
assert 1 in m2.int_set
|
||||
assert 2 in m2.int_set
|
||||
|
||||
assert 'kai' in m2.text_set
|
||||
assert 'andreas' in m2.text_set
|
||||
|
||||
def test_type_validation(self):
|
||||
"""
|
||||
Tests that attempting to use the wrong types will raise an exception
|
||||
"""
|
||||
with self.assertRaises(ValidationError):
|
||||
TestSetModel.create(int_set={'string', True}, text_set={1, 3.0})
|
||||
|
||||
def test_element_count_validation(self):
|
||||
"""
|
||||
Tests that big collections are detected and raise an exception.
|
||||
"""
|
||||
TestSetModel.create(text_set={str(uuid4()) for i in range(65535)})
|
||||
with self.assertRaises(ValidationError):
|
||||
TestSetModel.create(text_set={str(uuid4()) for i in range(65536)})
|
||||
|
||||
def test_partial_updates(self):
|
||||
""" Tests that partial udpates work as expected """
|
||||
m1 = TestSetModel.create(int_set={1, 2, 3, 4})
|
||||
|
||||
m1.int_set.add(5)
|
||||
m1.int_set.remove(1)
|
||||
assert m1.int_set == {2, 3, 4, 5}
|
||||
|
||||
m1.save()
|
||||
|
||||
m2 = TestSetModel.get(partition=m1.partition)
|
||||
assert m2.int_set == {2, 3, 4, 5}
|
||||
|
||||
def test_instantiation_with_column_class(self):
|
||||
"""
|
||||
Tests that columns instantiated with a column class work properly
|
||||
and that the class is instantiated in the constructor
|
||||
"""
|
||||
column = columns.Set(columns.Text)
|
||||
assert isinstance(column.value_col, columns.Text)
|
||||
|
||||
def test_instantiation_with_column_instance(self):
|
||||
"""
|
||||
Tests that columns instantiated with a column instance work properly
|
||||
"""
|
||||
column = columns.Set(columns.Text(min_length=100))
|
||||
assert isinstance(column.value_col, columns.Text)
|
||||
|
||||
def test_to_python(self):
|
||||
""" Tests that to_python of value column is called """
|
||||
column = columns.Set(JsonTestColumn)
|
||||
val = {1, 2, 3}
|
||||
db_val = column.to_database(val)
|
||||
assert db_val.value == {json.dumps(v) for v in val}
|
||||
py_val = column.to_python(db_val.value)
|
||||
assert py_val == val
|
||||
|
||||
def test_default_empty_container_saving(self):
|
||||
""" tests that the default empty container is not saved if it hasn't been updated """
|
||||
pkey = uuid4()
|
||||
# create a row with set data
|
||||
TestSetModel.create(partition=pkey, int_set={3, 4})
|
||||
# create another with no set data
|
||||
TestSetModel.create(partition=pkey)
|
||||
|
||||
m = TestSetModel.get(partition=pkey)
|
||||
self.assertEqual(m.int_set, {3, 4})
|
||||
|
||||
|
||||
class TestListModel(Model):
|
||||
|
||||
|
||||
partition = columns.UUID(primary_key=True, default=uuid4)
|
||||
int_list = columns.List(columns.Integer, required=False)
|
||||
text_list = columns.List(columns.Text, required=False)
|
||||
|
||||
|
||||
class TestListColumn(BaseCassEngTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestListColumn, cls).setUpClass()
|
||||
drop_table(TestListModel)
|
||||
sync_table(TestListModel)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(TestListColumn, cls).tearDownClass()
|
||||
drop_table(TestListModel)
|
||||
|
||||
def test_initial(self):
|
||||
tmp = TestListModel.create()
|
||||
tmp.int_list.append(1)
|
||||
|
||||
def test_initial(self):
|
||||
tmp = TestListModel.create()
|
||||
tmp2 = TestListModel.get(partition=tmp.partition)
|
||||
tmp2.int_list.append(1)
|
||||
|
||||
def test_io_success(self):
|
||||
""" Tests that a basic usage works as expected """
|
||||
m1 = TestListModel.create(int_list=[1, 2], text_list=['kai', 'andreas'])
|
||||
m2 = TestListModel.get(partition=m1.partition)
|
||||
|
||||
assert isinstance(m2.int_list, list)
|
||||
assert isinstance(m2.text_list, list)
|
||||
|
||||
assert len(m2.int_list) == 2
|
||||
assert len(m2.text_list) == 2
|
||||
|
||||
assert m2.int_list[0] == 1
|
||||
assert m2.int_list[1] == 2
|
||||
|
||||
assert m2.text_list[0] == 'kai'
|
||||
assert m2.text_list[1] == 'andreas'
|
||||
|
||||
def test_type_validation(self):
|
||||
"""
|
||||
Tests that attempting to use the wrong types will raise an exception
|
||||
"""
|
||||
with self.assertRaises(ValidationError):
|
||||
TestListModel.create(int_list=['string', True], text_list=[1, 3.0])
|
||||
|
||||
def test_element_count_validation(self):
|
||||
"""
|
||||
Tests that big collections are detected and raise an exception.
|
||||
"""
|
||||
TestListModel.create(text_list=[str(uuid4()) for i in range(65535)])
|
||||
with self.assertRaises(ValidationError):
|
||||
TestListModel.create(text_list=[str(uuid4()) for i in range(65536)])
|
||||
|
||||
def test_partial_updates(self):
|
||||
""" Tests that partial udpates work as expected """
|
||||
final = list(range(10))
|
||||
initial = final[3:7]
|
||||
m1 = TestListModel.create(int_list=initial)
|
||||
|
||||
m1.int_list = final
|
||||
m1.save()
|
||||
|
||||
m2 = TestListModel.get(partition=m1.partition)
|
||||
assert list(m2.int_list) == final
|
||||
|
||||
def test_instantiation_with_column_class(self):
|
||||
"""
|
||||
Tests that columns instantiated with a column class work properly
|
||||
and that the class is instantiated in the constructor
|
||||
"""
|
||||
column = columns.List(columns.Text)
|
||||
assert isinstance(column.value_col, columns.Text)
|
||||
|
||||
def test_instantiation_with_column_instance(self):
|
||||
"""
|
||||
Tests that columns instantiated with a column instance work properly
|
||||
"""
|
||||
column = columns.List(columns.Text(min_length=100))
|
||||
assert isinstance(column.value_col, columns.Text)
|
||||
|
||||
def test_to_python(self):
|
||||
""" Tests that to_python of value column is called """
|
||||
column = columns.List(JsonTestColumn)
|
||||
val = [1, 2, 3]
|
||||
db_val = column.to_database(val)
|
||||
assert db_val.value == [json.dumps(v) for v in val]
|
||||
py_val = column.to_python(db_val.value)
|
||||
assert py_val == val
|
||||
|
||||
def test_default_empty_container_saving(self):
|
||||
""" tests that the default empty container is not saved if it hasn't been updated """
|
||||
pkey = uuid4()
|
||||
# create a row with list data
|
||||
TestListModel.create(partition=pkey, int_list=[1,2,3,4])
|
||||
# create another with no list data
|
||||
TestListModel.create(partition=pkey)
|
||||
|
||||
m = TestListModel.get(partition=pkey)
|
||||
self.assertEqual(m.int_list, [1,2,3,4])
|
||||
|
||||
def test_remove_entry_works(self):
|
||||
pkey = uuid4()
|
||||
tmp = TestListModel.create(partition=pkey, int_list=[1,2])
|
||||
tmp.int_list.pop()
|
||||
tmp.update()
|
||||
tmp = TestListModel.get(partition=pkey)
|
||||
self.assertEqual(tmp.int_list, [1])
|
||||
|
||||
def test_update_from_non_empty_to_empty(self):
|
||||
pkey = uuid4()
|
||||
tmp = TestListModel.create(partition=pkey, int_list=[1,2])
|
||||
tmp.int_list = []
|
||||
tmp.update()
|
||||
|
||||
tmp = TestListModel.get(partition=pkey)
|
||||
self.assertEqual(tmp.int_list, [])
|
||||
|
||||
def test_insert_none(self):
|
||||
pkey = uuid4()
|
||||
with self.assertRaises(ValidationError):
|
||||
TestListModel.create(partition=pkey, int_list=[None])
|
||||
|
||||
def test_blind_list_updates_from_none(self):
|
||||
""" Tests that updates from None work as expected """
|
||||
m = TestListModel.create(int_list=None)
|
||||
expected = [1, 2]
|
||||
m.int_list = expected
|
||||
m.save()
|
||||
|
||||
m2 = TestListModel.get(partition=m.partition)
|
||||
assert m2.int_list == expected
|
||||
|
||||
TestListModel.objects(partition=m.partition).update(int_list=[])
|
||||
|
||||
m3 = TestListModel.get(partition=m.partition)
|
||||
assert m3.int_list == []
|
||||
|
||||
class TestMapModel(Model):
|
||||
|
||||
|
||||
partition = columns.UUID(primary_key=True, default=uuid4)
|
||||
int_map = columns.Map(columns.Integer, columns.UUID, required=False)
|
||||
text_map = columns.Map(columns.Text, columns.DateTime, required=False)
|
||||
|
||||
|
||||
class TestMapColumn(BaseCassEngTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMapColumn, cls).setUpClass()
|
||||
drop_table(TestMapModel)
|
||||
sync_table(TestMapModel)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(TestMapColumn, cls).tearDownClass()
|
||||
drop_table(TestMapModel)
|
||||
|
||||
def test_empty_default(self):
|
||||
tmp = TestMapModel.create()
|
||||
tmp.int_map['blah'] = 1
|
||||
|
||||
def test_add_none_as_map_key(self):
|
||||
with self.assertRaises(ValidationError):
|
||||
TestMapModel.create(int_map={None:1})
|
||||
|
||||
def test_add_none_as_map_value(self):
|
||||
with self.assertRaises(ValidationError):
|
||||
TestMapModel.create(int_map={None:1})
|
||||
|
||||
def test_empty_retrieve(self):
|
||||
tmp = TestMapModel.create()
|
||||
tmp2 = TestMapModel.get(partition=tmp.partition)
|
||||
tmp2.int_map['blah'] = 1
|
||||
|
||||
def test_remove_last_entry_works(self):
|
||||
tmp = TestMapModel.create()
|
||||
tmp.text_map["blah"] = datetime.now()
|
||||
tmp.save()
|
||||
del tmp.text_map["blah"]
|
||||
tmp.save()
|
||||
|
||||
tmp = TestMapModel.get(partition=tmp.partition)
|
||||
self.assertNotIn("blah", tmp.int_map)
|
||||
|
||||
def test_io_success(self):
|
||||
""" Tests that a basic usage works as expected """
|
||||
k1 = uuid4()
|
||||
k2 = uuid4()
|
||||
now = datetime.now()
|
||||
then = now + timedelta(days=1)
|
||||
m1 = TestMapModel.create(int_map={1: k1, 2: k2}, text_map={'now': now, 'then': then})
|
||||
m2 = TestMapModel.get(partition=m1.partition)
|
||||
|
||||
assert isinstance(m2.int_map, dict)
|
||||
assert isinstance(m2.text_map, dict)
|
||||
|
||||
assert 1 in m2.int_map
|
||||
assert 2 in m2.int_map
|
||||
assert m2.int_map[1] == k1
|
||||
assert m2.int_map[2] == k2
|
||||
|
||||
assert 'now' in m2.text_map
|
||||
assert 'then' in m2.text_map
|
||||
assert (now - m2.text_map['now']).total_seconds() < 0.001
|
||||
assert (then - m2.text_map['then']).total_seconds() < 0.001
|
||||
|
||||
def test_type_validation(self):
|
||||
"""
|
||||
Tests that attempting to use the wrong types will raise an exception
|
||||
"""
|
||||
with self.assertRaises(ValidationError):
|
||||
TestMapModel.create(int_map={'key': 2, uuid4(): 'val'}, text_map={2: 5})
|
||||
|
||||
def test_element_count_validation(self):
|
||||
"""
|
||||
Tests that big collections are detected and raise an exception.
|
||||
"""
|
||||
TestMapModel.create(text_map={str(uuid4()): i for i in range(65535)})
|
||||
with self.assertRaises(ValidationError):
|
||||
TestMapModel.create(text_map={str(uuid4()): i for i in range(65536)})
|
||||
|
||||
def test_partial_updates(self):
|
||||
""" Tests that partial udpates work as expected """
|
||||
now = datetime.now()
|
||||
#derez it a bit
|
||||
now = datetime(*now.timetuple()[:-3])
|
||||
early = now - timedelta(minutes=30)
|
||||
earlier = early - timedelta(minutes=30)
|
||||
later = now + timedelta(minutes=30)
|
||||
|
||||
initial = {'now': now, 'early': earlier}
|
||||
final = {'later': later, 'early': early}
|
||||
|
||||
m1 = TestMapModel.create(text_map=initial)
|
||||
|
||||
m1.text_map = final
|
||||
m1.save()
|
||||
|
||||
m2 = TestMapModel.get(partition=m1.partition)
|
||||
assert m2.text_map == final
|
||||
|
||||
def test_updates_from_none(self):
|
||||
""" Tests that updates from None work as expected """
|
||||
m = TestMapModel.create(int_map=None)
|
||||
expected = {1: uuid4()}
|
||||
m.int_map = expected
|
||||
m.save()
|
||||
|
||||
m2 = TestMapModel.get(partition=m.partition)
|
||||
assert m2.int_map == expected
|
||||
|
||||
m2.int_map = None
|
||||
m2.save()
|
||||
m3 = TestMapModel.get(partition=m.partition)
|
||||
assert m3.int_map != expected
|
||||
|
||||
def test_blind_updates_from_none(self):
|
||||
""" Tests that updates from None work as expected """
|
||||
m = TestMapModel.create(int_map=None)
|
||||
expected = {1: uuid4()}
|
||||
m.int_map = expected
|
||||
m.save()
|
||||
|
||||
m2 = TestMapModel.get(partition=m.partition)
|
||||
assert m2.int_map == expected
|
||||
|
||||
TestMapModel.objects(partition=m.partition).update(int_map={})
|
||||
|
||||
m3 = TestMapModel.get(partition=m.partition)
|
||||
assert m3.int_map != expected
|
||||
|
||||
def test_updates_to_none(self):
|
||||
""" Tests that setting the field to None works as expected """
|
||||
m = TestMapModel.create(int_map={1: uuid4()})
|
||||
m.int_map = None
|
||||
m.save()
|
||||
|
||||
m2 = TestMapModel.get(partition=m.partition)
|
||||
assert m2.int_map == {}
|
||||
|
||||
def test_instantiation_with_column_class(self):
|
||||
"""
|
||||
Tests that columns instantiated with a column class work properly
|
||||
and that the class is instantiated in the constructor
|
||||
"""
|
||||
column = columns.Map(columns.Text, columns.Integer)
|
||||
assert isinstance(column.key_col, columns.Text)
|
||||
assert isinstance(column.value_col, columns.Integer)
|
||||
|
||||
def test_instantiation_with_column_instance(self):
|
||||
"""
|
||||
Tests that columns instantiated with a column instance work properly
|
||||
"""
|
||||
column = columns.Map(columns.Text(min_length=100), columns.Integer())
|
||||
assert isinstance(column.key_col, columns.Text)
|
||||
assert isinstance(column.value_col, columns.Integer)
|
||||
|
||||
def test_to_python(self):
|
||||
""" Tests that to_python of value column is called """
|
||||
column = columns.Map(JsonTestColumn, JsonTestColumn)
|
||||
val = {1: 2, 3: 4, 5: 6}
|
||||
db_val = column.to_database(val)
|
||||
assert db_val.value == {json.dumps(k):json.dumps(v) for k,v in val.items()}
|
||||
py_val = column.to_python(db_val.value)
|
||||
assert py_val == val
|
||||
|
||||
def test_default_empty_container_saving(self):
|
||||
""" tests that the default empty container is not saved if it hasn't been updated """
|
||||
pkey = uuid4()
|
||||
tmap = {1: uuid4(), 2: uuid4()}
|
||||
# create a row with set data
|
||||
TestMapModel.create(partition=pkey, int_map=tmap)
|
||||
# create another with no set data
|
||||
TestMapModel.create(partition=pkey)
|
||||
|
||||
m = TestMapModel.get(partition=pkey)
|
||||
self.assertEqual(m.int_map, tmap)
|
||||
|
||||
# def test_partial_update_creation(self):
|
||||
# """
|
||||
# Tests that proper update statements are created for a partial list update
|
||||
# :return:
|
||||
# """
|
||||
# final = range(10)
|
||||
# initial = final[3:7]
|
||||
#
|
||||
# ctx = {}
|
||||
# col = columns.List(columns.Integer, db_field="TEST")
|
||||
# statements = col.get_update_statement(final, initial, ctx)
|
||||
#
|
||||
# assert len([v for v in ctx.values() if [0,1,2] == v.value]) == 1
|
||||
# assert len([v for v in ctx.values() if [7,8,9] == v.value]) == 1
|
||||
# assert len([s for s in statements if '"TEST" = "TEST" +' in s]) == 1
|
||||
# assert len([s for s in statements if '+ "TEST"' in s]) == 1
|
||||
|
||||
|
||||
class TestCamelMapModel(Model):
|
||||
|
||||
|
||||
partition = columns.UUID(primary_key=True, default=uuid4)
|
||||
camelMap = columns.Map(columns.Text, columns.Integer, required=False)
|
||||
|
||||
|
||||
class TestCamelMapColumn(BaseCassEngTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestCamelMapColumn, cls).setUpClass()
|
||||
drop_table(TestCamelMapModel)
|
||||
sync_table(TestCamelMapModel)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(TestCamelMapColumn, cls).tearDownClass()
|
||||
drop_table(TestCamelMapModel)
|
||||
|
||||
def test_camelcase_column(self):
|
||||
TestCamelMapModel.create(camelMap={'blah': 1})
|
94
cqlengine/tests/columns/test_counter_column.py
Normal file
94
cqlengine/tests/columns/test_counter_column.py
Normal file
@ -0,0 +1,94 @@
|
||||
from uuid import uuid4
|
||||
|
||||
from cqlengine import Model
|
||||
from cqlengine import columns
|
||||
from cqlengine.management import sync_table, drop_table
|
||||
from cqlengine.models import ModelDefinitionException
|
||||
from cqlengine.tests.base import BaseCassEngTestCase
|
||||
|
||||
|
||||
class TestCounterModel(Model):
|
||||
|
||||
partition = columns.UUID(primary_key=True, default=uuid4)
|
||||
cluster = columns.UUID(primary_key=True, default=uuid4)
|
||||
counter = columns.Counter()
|
||||
|
||||
|
||||
class TestClassConstruction(BaseCassEngTestCase):
|
||||
|
||||
def test_defining_a_non_counter_column_fails(self):
|
||||
""" Tests that defining a non counter column field in a model with a counter column fails """
|
||||
with self.assertRaises(ModelDefinitionException):
|
||||
class model(Model):
|
||||
partition = columns.UUID(primary_key=True, default=uuid4)
|
||||
counter = columns.Counter()
|
||||
text = columns.Text()
|
||||
|
||||
|
||||
def test_defining_a_primary_key_counter_column_fails(self):
|
||||
""" Tests that defining primary keys on counter columns fails """
|
||||
with self.assertRaises(TypeError):
|
||||
class model(Model):
|
||||
partition = columns.UUID(primary_key=True, default=uuid4)
|
||||
cluster = columns.Counter(primary_ley=True)
|
||||
counter = columns.Counter()
|
||||
|
||||
# force it
|
||||
with self.assertRaises(ModelDefinitionException):
|
||||
class model(Model):
|
||||
partition = columns.UUID(primary_key=True, default=uuid4)
|
||||
cluster = columns.Counter()
|
||||
cluster.primary_key = True
|
||||
counter = columns.Counter()
|
||||
|
||||
|
||||
class TestCounterColumn(BaseCassEngTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestCounterColumn, cls).setUpClass()
|
||||
drop_table(TestCounterModel)
|
||||
sync_table(TestCounterModel)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(TestCounterColumn, cls).tearDownClass()
|
||||
drop_table(TestCounterModel)
|
||||
|
||||
def test_updates(self):
|
||||
""" Tests that counter updates work as intended """
|
||||
instance = TestCounterModel.create()
|
||||
instance.counter += 5
|
||||
instance.save()
|
||||
|
||||
actual = TestCounterModel.get(partition=instance.partition)
|
||||
assert actual.counter == 5
|
||||
|
||||
def test_concurrent_updates(self):
|
||||
""" Tests updates from multiple queries reaches the correct value """
|
||||
instance = TestCounterModel.create()
|
||||
new1 = TestCounterModel.get(partition=instance.partition)
|
||||
new2 = TestCounterModel.get(partition=instance.partition)
|
||||
|
||||
new1.counter += 5
|
||||
new1.save()
|
||||
new2.counter += 5
|
||||
new2.save()
|
||||
|
||||
actual = TestCounterModel.get(partition=instance.partition)
|
||||
assert actual.counter == 10
|
||||
|
||||
def test_update_from_none(self):
|
||||
""" Tests that updating from None uses a create statement """
|
||||
instance = TestCounterModel()
|
||||
instance.counter += 1
|
||||
instance.save()
|
||||
|
||||
new = TestCounterModel.get(partition=instance.partition)
|
||||
assert new.counter == 1
|
||||
|
||||
def test_new_instance_defaults_to_zero(self):
|
||||
""" Tests that instantiating a new model instance will set the counter column to zero """
|
||||
instance = TestCounterModel()
|
||||
assert instance.counter == 0
|
||||
|
72
cqlengine/tests/columns/test_static_column.py
Normal file
72
cqlengine/tests/columns/test_static_column.py
Normal file
@ -0,0 +1,72 @@
|
||||
from uuid import uuid4
|
||||
from unittest import skipUnless
|
||||
from cqlengine import Model
|
||||
from cqlengine import columns
|
||||
from cqlengine.management import sync_table, drop_table
|
||||
from cqlengine.models import ModelDefinitionException
|
||||
from cqlengine.tests.base import BaseCassEngTestCase
|
||||
from cqlengine.tests.base import CASSANDRA_VERSION, PROTOCOL_VERSION
|
||||
|
||||
|
||||
class TestStaticModel(Model):
|
||||
partition = columns.UUID(primary_key=True, default=uuid4)
|
||||
cluster = columns.UUID(primary_key=True, default=uuid4)
|
||||
static = columns.Text(static=True)
|
||||
text = columns.Text()
|
||||
|
||||
|
||||
class TestStaticColumn(BaseCassEngTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestStaticColumn, cls).setUpClass()
|
||||
drop_table(TestStaticModel)
|
||||
if CASSANDRA_VERSION >= 20:
|
||||
sync_table(TestStaticModel)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(TestStaticColumn, cls).tearDownClass()
|
||||
drop_table(TestStaticModel)
|
||||
|
||||
@skipUnless(PROTOCOL_VERSION >= 2, "only runs against the cql3 protocol v2.0")
|
||||
def test_mixed_updates(self):
|
||||
""" Tests that updates on both static and non-static columns work as intended """
|
||||
instance = TestStaticModel.create()
|
||||
instance.static = "it's shared"
|
||||
instance.text = "some text"
|
||||
instance.save()
|
||||
|
||||
u = TestStaticModel.get(partition=instance.partition)
|
||||
u.static = "it's still shared"
|
||||
u.text = "another text"
|
||||
u.update()
|
||||
actual = TestStaticModel.get(partition=u.partition)
|
||||
|
||||
assert actual.static == "it's still shared"
|
||||
|
||||
@skipUnless(PROTOCOL_VERSION >= 2, "only runs against the cql3 protocol v2.0")
|
||||
def test_static_only_updates(self):
|
||||
""" Tests that updates on static only column work as intended """
|
||||
instance = TestStaticModel.create()
|
||||
instance.static = "it's shared"
|
||||
instance.text = "some text"
|
||||
instance.save()
|
||||
|
||||
u = TestStaticModel.get(partition=instance.partition)
|
||||
u.static = "it's still shared"
|
||||
u.update()
|
||||
actual = TestStaticModel.get(partition=u.partition)
|
||||
assert actual.static == "it's still shared"
|
||||
|
||||
@skipUnless(PROTOCOL_VERSION >= 2, "only runs against the cql3 protocol v2.0")
|
||||
def test_static_with_null_cluster_key(self):
|
||||
""" Tests that save/update/delete works for static column works when clustering key is null"""
|
||||
instance = TestStaticModel.create(cluster=None, static = "it's shared")
|
||||
instance.save()
|
||||
|
||||
u = TestStaticModel.get(partition=instance.partition)
|
||||
u.static = "it's still shared"
|
||||
u.update()
|
||||
actual = TestStaticModel.get(partition=u.partition)
|
||||
assert actual.static == "it's still shared"
|
386
cqlengine/tests/columns/test_validation.py
Normal file
386
cqlengine/tests/columns/test_validation.py
Normal file
@ -0,0 +1,386 @@
|
||||
#tests the behavior of the column classes
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import date
|
||||
from datetime import tzinfo
|
||||
from decimal import Decimal as D
|
||||
from unittest import TestCase
|
||||
from uuid import uuid4, uuid1
|
||||
from cassandra import InvalidRequest
|
||||
import six
|
||||
from cqlengine import ValidationError
|
||||
from cqlengine.connection import execute
|
||||
|
||||
from cqlengine.tests.base import BaseCassEngTestCase
|
||||
|
||||
from cqlengine.columns import Column, TimeUUID
|
||||
from cqlengine.columns import Bytes
|
||||
from cqlengine.columns import Ascii
|
||||
from cqlengine.columns import Text
|
||||
from cqlengine.columns import Integer
|
||||
from cqlengine.columns import BigInt
|
||||
from cqlengine.columns import VarInt
|
||||
from cqlengine.columns import DateTime
|
||||
from cqlengine.columns import Date
|
||||
from cqlengine.columns import UUID
|
||||
from cqlengine.columns import Boolean
|
||||
from cqlengine.columns import Float
|
||||
from cqlengine.columns import Decimal
|
||||
from cqlengine.columns import Inet
|
||||
|
||||
from cqlengine.management import sync_table, drop_table
|
||||
from cqlengine.models import Model
|
||||
|
||||
|
||||
class TestDatetime(BaseCassEngTestCase):
|
||||
class DatetimeTest(Model):
|
||||
|
||||
test_id = Integer(primary_key=True)
|
||||
created_at = DateTime()
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestDatetime, cls).setUpClass()
|
||||
sync_table(cls.DatetimeTest)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(TestDatetime, cls).tearDownClass()
|
||||
drop_table(cls.DatetimeTest)
|
||||
|
||||
def test_datetime_io(self):
|
||||
now = datetime.now()
|
||||
dt = self.DatetimeTest.objects.create(test_id=0, created_at=now)
|
||||
dt2 = self.DatetimeTest.objects(test_id=0).first()
|
||||
assert dt2.created_at.timetuple()[:6] == now.timetuple()[:6]
|
||||
|
||||
def test_datetime_tzinfo_io(self):
|
||||
class TZ(tzinfo):
|
||||
def utcoffset(self, date_time):
|
||||
return timedelta(hours=-1)
|
||||
def dst(self, date_time):
|
||||
return None
|
||||
|
||||
now = datetime(1982, 1, 1, tzinfo=TZ())
|
||||
dt = self.DatetimeTest.objects.create(test_id=0, created_at=now)
|
||||
dt2 = self.DatetimeTest.objects(test_id=0).first()
|
||||
assert dt2.created_at.timetuple()[:6] == (now + timedelta(hours=1)).timetuple()[:6]
|
||||
|
||||
def test_datetime_date_support(self):
|
||||
today = date.today()
|
||||
self.DatetimeTest.objects.create(test_id=0, created_at=today)
|
||||
dt2 = self.DatetimeTest.objects(test_id=0).first()
|
||||
assert dt2.created_at.isoformat() == datetime(today.year, today.month, today.day).isoformat()
|
||||
|
||||
def test_datetime_none(self):
|
||||
dt = self.DatetimeTest.objects.create(test_id=1, created_at=None)
|
||||
dt2 = self.DatetimeTest.objects(test_id=1).first()
|
||||
assert dt2.created_at is None
|
||||
|
||||
dts = self.DatetimeTest.objects.filter(test_id=1).values_list('created_at')
|
||||
assert dts[0][0] is None
|
||||
|
||||
|
||||
class TestBoolDefault(BaseCassEngTestCase):
|
||||
class BoolDefaultValueTest(Model):
|
||||
|
||||
test_id = Integer(primary_key=True)
|
||||
stuff = Boolean(default=True)
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestBoolDefault, cls).setUpClass()
|
||||
sync_table(cls.BoolDefaultValueTest)
|
||||
|
||||
def test_default_is_set(self):
|
||||
tmp = self.BoolDefaultValueTest.create(test_id=1)
|
||||
self.assertEqual(True, tmp.stuff)
|
||||
tmp2 = self.BoolDefaultValueTest.get(test_id=1)
|
||||
self.assertEqual(True, tmp2.stuff)
|
||||
|
||||
class TestBoolValidation(BaseCassEngTestCase):
|
||||
class BoolValidationTest(Model):
|
||||
|
||||
test_id = Integer(primary_key=True)
|
||||
bool_column = Boolean()
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestBoolValidation, cls).setUpClass()
|
||||
sync_table(cls.BoolValidationTest)
|
||||
|
||||
def test_validation_preserves_none(self):
|
||||
test_obj = self.BoolValidationTest(test_id=1)
|
||||
|
||||
test_obj.validate()
|
||||
self.assertIsNone(test_obj.bool_column)
|
||||
|
||||
class TestVarInt(BaseCassEngTestCase):
|
||||
class VarIntTest(Model):
|
||||
|
||||
test_id = Integer(primary_key=True)
|
||||
bignum = VarInt(primary_key=True)
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestVarInt, cls).setUpClass()
|
||||
sync_table(cls.VarIntTest)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(TestVarInt, cls).tearDownClass()
|
||||
sync_table(cls.VarIntTest)
|
||||
|
||||
def test_varint_io(self):
|
||||
# TODO: this is a weird test. i changed the number from sys.maxint (which doesn't exist in python 3)
|
||||
# to the giant number below and it broken between runs.
|
||||
long_int = 92834902384092834092384028340283048239048203480234823048230482304820348239
|
||||
int1 = self.VarIntTest.objects.create(test_id=0, bignum=long_int)
|
||||
int2 = self.VarIntTest.objects(test_id=0).first()
|
||||
self.assertEqual(int1.bignum, int2.bignum)
|
||||
|
||||
|
||||
class TestDate(BaseCassEngTestCase):
|
||||
class DateTest(Model):
|
||||
|
||||
test_id = Integer(primary_key=True)
|
||||
created_at = Date()
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestDate, cls).setUpClass()
|
||||
sync_table(cls.DateTest)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(TestDate, cls).tearDownClass()
|
||||
drop_table(cls.DateTest)
|
||||
|
||||
def test_date_io(self):
|
||||
today = date.today()
|
||||
self.DateTest.objects.create(test_id=0, created_at=today)
|
||||
dt2 = self.DateTest.objects(test_id=0).first()
|
||||
assert dt2.created_at.isoformat() == today.isoformat()
|
||||
|
||||
def test_date_io_using_datetime(self):
|
||||
now = datetime.utcnow()
|
||||
self.DateTest.objects.create(test_id=0, created_at=now)
|
||||
dt2 = self.DateTest.objects(test_id=0).first()
|
||||
assert not isinstance(dt2.created_at, datetime)
|
||||
assert isinstance(dt2.created_at, date)
|
||||
assert dt2.created_at.isoformat() == now.date().isoformat()
|
||||
|
||||
def test_date_none(self):
|
||||
self.DateTest.objects.create(test_id=1, created_at=None)
|
||||
dt2 = self.DateTest.objects(test_id=1).first()
|
||||
assert dt2.created_at is None
|
||||
|
||||
dts = self.DateTest.objects(test_id=1).values_list('created_at')
|
||||
assert dts[0][0] is None
|
||||
|
||||
|
||||
class TestDecimal(BaseCassEngTestCase):
|
||||
class DecimalTest(Model):
|
||||
|
||||
test_id = Integer(primary_key=True)
|
||||
dec_val = Decimal()
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestDecimal, cls).setUpClass()
|
||||
sync_table(cls.DecimalTest)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(TestDecimal, cls).tearDownClass()
|
||||
drop_table(cls.DecimalTest)
|
||||
|
||||
def test_decimal_io(self):
|
||||
dt = self.DecimalTest.objects.create(test_id=0, dec_val=D('0.00'))
|
||||
dt2 = self.DecimalTest.objects(test_id=0).first()
|
||||
assert dt2.dec_val == dt.dec_val
|
||||
|
||||
dt = self.DecimalTest.objects.create(test_id=0, dec_val=5)
|
||||
dt2 = self.DecimalTest.objects(test_id=0).first()
|
||||
assert dt2.dec_val == D('5')
|
||||
|
||||
class TestUUID(BaseCassEngTestCase):
|
||||
class UUIDTest(Model):
|
||||
|
||||
test_id = Integer(primary_key=True)
|
||||
a_uuid = UUID(default=uuid4())
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestUUID, cls).setUpClass()
|
||||
sync_table(cls.UUIDTest)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(TestUUID, cls).tearDownClass()
|
||||
drop_table(cls.UUIDTest)
|
||||
|
||||
def test_uuid_str_with_dashes(self):
|
||||
a_uuid = uuid4()
|
||||
t0 = self.UUIDTest.create(test_id=0, a_uuid=str(a_uuid))
|
||||
t1 = self.UUIDTest.get(test_id=0)
|
||||
assert a_uuid == t1.a_uuid
|
||||
|
||||
def test_uuid_str_no_dashes(self):
|
||||
a_uuid = uuid4()
|
||||
t0 = self.UUIDTest.create(test_id=1, a_uuid=a_uuid.hex)
|
||||
t1 = self.UUIDTest.get(test_id=1)
|
||||
assert a_uuid == t1.a_uuid
|
||||
|
||||
class TestTimeUUID(BaseCassEngTestCase):
|
||||
class TimeUUIDTest(Model):
|
||||
|
||||
test_id = Integer(primary_key=True)
|
||||
timeuuid = TimeUUID(default=uuid1())
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestTimeUUID, cls).setUpClass()
|
||||
sync_table(cls.TimeUUIDTest)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(TestTimeUUID, cls).tearDownClass()
|
||||
drop_table(cls.TimeUUIDTest)
|
||||
|
||||
def test_timeuuid_io(self):
|
||||
"""
|
||||
ensures that
|
||||
:return:
|
||||
"""
|
||||
t0 = self.TimeUUIDTest.create(test_id=0)
|
||||
t1 = self.TimeUUIDTest.get(test_id=0)
|
||||
|
||||
assert t1.timeuuid.time == t1.timeuuid.time
|
||||
|
||||
class TestInteger(BaseCassEngTestCase):
|
||||
class IntegerTest(Model):
|
||||
|
||||
test_id = UUID(primary_key=True, default=lambda:uuid4())
|
||||
value = Integer(default=0, required=True)
|
||||
|
||||
def test_default_zero_fields_validate(self):
|
||||
""" Tests that integer columns with a default value of 0 validate """
|
||||
it = self.IntegerTest()
|
||||
it.validate()
|
||||
|
||||
class TestBigInt(BaseCassEngTestCase):
|
||||
class BigIntTest(Model):
|
||||
|
||||
test_id = UUID(primary_key=True, default=lambda:uuid4())
|
||||
value = BigInt(default=0, required=True)
|
||||
|
||||
def test_default_zero_fields_validate(self):
|
||||
""" Tests that bigint columns with a default value of 0 validate """
|
||||
it = self.BigIntTest()
|
||||
it.validate()
|
||||
|
||||
class TestText(BaseCassEngTestCase):
|
||||
|
||||
def test_min_length(self):
|
||||
#min len defaults to 1
|
||||
col = Text()
|
||||
col.validate('')
|
||||
|
||||
col.validate('b')
|
||||
|
||||
#test not required defaults to 0
|
||||
Text(required=False).validate('')
|
||||
|
||||
#test arbitrary lengths
|
||||
Text(min_length=0).validate('')
|
||||
Text(min_length=5).validate('blake')
|
||||
Text(min_length=5).validate('blaketastic')
|
||||
with self.assertRaises(ValidationError):
|
||||
Text(min_length=6).validate('blake')
|
||||
|
||||
def test_max_length(self):
|
||||
|
||||
Text(max_length=5).validate('blake')
|
||||
with self.assertRaises(ValidationError):
|
||||
Text(max_length=5).validate('blaketastic')
|
||||
|
||||
def test_type_checking(self):
|
||||
Text().validate('string')
|
||||
Text().validate(u'unicode')
|
||||
Text().validate(bytearray('bytearray', encoding='ascii'))
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
Text(required=True).validate(None)
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
Text().validate(5)
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
Text().validate(True)
|
||||
|
||||
def test_non_required_validation(self):
|
||||
""" Tests that validation is ok on none and blank values if required is False """
|
||||
Text().validate('')
|
||||
Text().validate(None)
|
||||
|
||||
|
||||
|
||||
|
||||
class TestExtraFieldsRaiseException(BaseCassEngTestCase):
|
||||
class TestModel(Model):
|
||||
|
||||
id = UUID(primary_key=True, default=uuid4)
|
||||
|
||||
def test_extra_field(self):
|
||||
with self.assertRaises(ValidationError):
|
||||
self.TestModel.create(bacon=5000)
|
||||
|
||||
class TestPythonDoesntDieWhenExtraFieldIsInCassandra(BaseCassEngTestCase):
|
||||
class TestModel(Model):
|
||||
|
||||
__table_name__ = 'alter_doesnt_break_running_app'
|
||||
id = UUID(primary_key=True, default=uuid4)
|
||||
|
||||
def test_extra_field(self):
|
||||
drop_table(self.TestModel)
|
||||
sync_table(self.TestModel)
|
||||
self.TestModel.create()
|
||||
execute("ALTER TABLE {} add blah int".format(self.TestModel.column_family_name(include_keyspace=True)))
|
||||
self.TestModel.objects().all()
|
||||
|
||||
class TestTimeUUIDFromDatetime(TestCase):
|
||||
def test_conversion_specific_date(self):
|
||||
dt = datetime(1981, 7, 11, microsecond=555000)
|
||||
|
||||
uuid = TimeUUID.from_datetime(dt)
|
||||
|
||||
from uuid import UUID
|
||||
assert isinstance(uuid, UUID)
|
||||
|
||||
ts = (uuid.time - 0x01b21dd213814000) / 1e7 # back to a timestamp
|
||||
new_dt = datetime.utcfromtimestamp(ts)
|
||||
|
||||
# checks that we created a UUID1 with the proper timestamp
|
||||
assert new_dt == dt
|
||||
|
||||
class TestInet(BaseCassEngTestCase):
|
||||
|
||||
class InetTestModel(Model):
|
||||
id = UUID(primary_key=True, default=uuid4)
|
||||
address = Inet()
|
||||
|
||||
def setUp(self):
|
||||
drop_table(self.InetTestModel)
|
||||
sync_table(self.InetTestModel)
|
||||
|
||||
def test_inet_saves(self):
|
||||
tmp = self.InetTestModel.create(address="192.168.1.1")
|
||||
|
||||
m = self.InetTestModel.get(id=tmp.id)
|
||||
|
||||
assert m.address == "192.168.1.1"
|
||||
|
||||
def test_non_address_fails(self):
|
||||
with self.assertRaises(InvalidRequest):
|
||||
self.InetTestModel.create(address="ham sandwich")
|
||||
|
178
cqlengine/tests/columns/test_value_io.py
Normal file
178
cqlengine/tests/columns/test_value_io.py
Normal file
@ -0,0 +1,178 @@
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
from uuid import uuid1, uuid4, UUID
|
||||
import six
|
||||
|
||||
from cqlengine.tests.base import BaseCassEngTestCase
|
||||
|
||||
from cqlengine.management import sync_table
|
||||
from cqlengine.management import drop_table
|
||||
from cqlengine.models import Model
|
||||
from cqlengine.columns import ValueQuoter
|
||||
from cqlengine import columns
|
||||
import unittest
|
||||
|
||||
|
||||
class BaseColumnIOTest(BaseCassEngTestCase):
|
||||
"""
|
||||
Tests that values are come out of cassandra in the format we expect
|
||||
|
||||
To test a column type, subclass this test, define the column, and the primary key
|
||||
and data values you want to test
|
||||
"""
|
||||
|
||||
# The generated test model is assigned here
|
||||
_generated_model = None
|
||||
|
||||
# the column we want to test
|
||||
column = None
|
||||
|
||||
# the values we want to test against, you can
|
||||
# use a single value, or multiple comma separated values
|
||||
pkey_val = None
|
||||
data_val = None
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(BaseColumnIOTest, cls).setUpClass()
|
||||
|
||||
#if the test column hasn't been defined, bail out
|
||||
if not cls.column: return
|
||||
|
||||
# create a table with the given column
|
||||
class IOTestModel(Model):
|
||||
|
||||
table_name = cls.column.db_type + "_io_test_model_{}".format(uuid4().hex[:8])
|
||||
pkey = cls.column(primary_key=True)
|
||||
data = cls.column()
|
||||
cls._generated_model = IOTestModel
|
||||
sync_table(cls._generated_model)
|
||||
|
||||
#tupleify the tested values
|
||||
if not isinstance(cls.pkey_val, tuple):
|
||||
cls.pkey_val = cls.pkey_val,
|
||||
if not isinstance(cls.data_val, tuple):
|
||||
cls.data_val = cls.data_val,
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(BaseColumnIOTest, cls).tearDownClass()
|
||||
if not cls.column: return
|
||||
drop_table(cls._generated_model)
|
||||
|
||||
def comparator_converter(self, val):
|
||||
""" If you want to convert the original value used to compare the model vales """
|
||||
return val
|
||||
|
||||
def test_column_io(self):
|
||||
""" Tests the given models class creates and retrieves values as expected """
|
||||
if not self.column: return
|
||||
for pkey, data in zip(self.pkey_val, self.data_val):
|
||||
#create
|
||||
m1 = self._generated_model.create(pkey=pkey, data=data)
|
||||
|
||||
#get
|
||||
m2 = self._generated_model.get(pkey=pkey)
|
||||
assert m1.pkey == m2.pkey == self.comparator_converter(pkey), self.column
|
||||
assert m1.data == m2.data == self.comparator_converter(data), self.column
|
||||
|
||||
#delete
|
||||
self._generated_model.filter(pkey=pkey).delete()
|
||||
|
||||
class TestBlobIO(BaseColumnIOTest):
|
||||
|
||||
column = columns.Blob
|
||||
pkey_val = six.b('blake'), uuid4().bytes
|
||||
data_val = six.b('eggleston'), uuid4().bytes
|
||||
|
||||
class TestBlobIO2(BaseColumnIOTest):
|
||||
|
||||
column = columns.Blob
|
||||
pkey_val = bytearray(six.b('blake')), uuid4().bytes
|
||||
data_val = bytearray(six.b('eggleston')), uuid4().bytes
|
||||
|
||||
class TestTextIO(BaseColumnIOTest):
|
||||
|
||||
column = columns.Text
|
||||
pkey_val = 'bacon'
|
||||
data_val = 'monkey'
|
||||
|
||||
|
||||
class TestNonBinaryTextIO(BaseColumnIOTest):
|
||||
|
||||
column = columns.Text
|
||||
pkey_val = 'bacon'
|
||||
data_val = '0xmonkey'
|
||||
|
||||
class TestInteger(BaseColumnIOTest):
|
||||
|
||||
column = columns.Integer
|
||||
pkey_val = 5
|
||||
data_val = 6
|
||||
|
||||
class TestBigInt(BaseColumnIOTest):
|
||||
|
||||
column = columns.BigInt
|
||||
pkey_val = 6
|
||||
data_val = pow(2, 63) - 1
|
||||
|
||||
class TestDateTime(BaseColumnIOTest):
|
||||
|
||||
column = columns.DateTime
|
||||
|
||||
now = datetime(*datetime.now().timetuple()[:6])
|
||||
pkey_val = now
|
||||
data_val = now + timedelta(days=1)
|
||||
|
||||
class TestDate(BaseColumnIOTest):
|
||||
|
||||
column = columns.Date
|
||||
|
||||
now = datetime.now().date()
|
||||
pkey_val = now
|
||||
data_val = now + timedelta(days=1)
|
||||
|
||||
class TestUUID(BaseColumnIOTest):
|
||||
|
||||
column = columns.UUID
|
||||
|
||||
pkey_val = str(uuid4()), uuid4()
|
||||
data_val = str(uuid4()), uuid4()
|
||||
|
||||
def comparator_converter(self, val):
|
||||
return val if isinstance(val, UUID) else UUID(val)
|
||||
|
||||
class TestTimeUUID(BaseColumnIOTest):
|
||||
|
||||
column = columns.TimeUUID
|
||||
|
||||
pkey_val = str(uuid1()), uuid1()
|
||||
data_val = str(uuid1()), uuid1()
|
||||
|
||||
def comparator_converter(self, val):
|
||||
return val if isinstance(val, UUID) else UUID(val)
|
||||
|
||||
class TestFloatIO(BaseColumnIOTest):
|
||||
|
||||
column = columns.Float
|
||||
|
||||
pkey_val = 3.14
|
||||
data_val = -1982.11
|
||||
|
||||
class TestDecimalIO(BaseColumnIOTest):
|
||||
|
||||
column = columns.Decimal
|
||||
|
||||
pkey_val = Decimal('1.35'), 5, '2.4'
|
||||
data_val = Decimal('0.005'), 3.5, '8'
|
||||
|
||||
def comparator_converter(self, val):
|
||||
return Decimal(val)
|
||||
|
||||
class TestQuoter(unittest.TestCase):
|
||||
|
||||
def test_equals(self):
|
||||
assert ValueQuoter(False) == ValueQuoter(False)
|
||||
assert ValueQuoter(1) == ValueQuoter(1)
|
||||
assert ValueQuoter("foo") == ValueQuoter("foo")
|
||||
assert ValueQuoter(1.55) == ValueQuoter(1.55)
|
0
cqlengine/tests/connections/__init__.py
Normal file
0
cqlengine/tests/connections/__init__.py
Normal file
0
cqlengine/tests/management/__init__.py
Normal file
0
cqlengine/tests/management/__init__.py
Normal file
237
cqlengine/tests/management/test_compaction_settings.py
Normal file
237
cqlengine/tests/management/test_compaction_settings.py
Normal file
@ -0,0 +1,237 @@
|
||||
import copy
|
||||
import json
|
||||
from time import sleep
|
||||
from mock import patch, MagicMock
|
||||
from cqlengine import Model, columns, SizeTieredCompactionStrategy, LeveledCompactionStrategy
|
||||
from cqlengine.exceptions import CQLEngineException
|
||||
from cqlengine.management import get_compaction_options, drop_table, sync_table, get_table_settings
|
||||
from cqlengine.tests.base import BaseCassEngTestCase
|
||||
|
||||
|
||||
class CompactionModel(Model):
|
||||
|
||||
__compaction__ = None
|
||||
cid = columns.UUID(primary_key=True)
|
||||
name = columns.Text()
|
||||
|
||||
|
||||
class BaseCompactionTest(BaseCassEngTestCase):
|
||||
def assert_option_fails(self, key):
|
||||
# key is a normal_key, converted to
|
||||
# __compaction_key__
|
||||
|
||||
key = "__compaction_{}__".format(key)
|
||||
|
||||
with patch.object(self.model, key, 10), \
|
||||
self.assertRaises(CQLEngineException):
|
||||
get_compaction_options(self.model)
|
||||
|
||||
|
||||
class SizeTieredCompactionTest(BaseCompactionTest):
|
||||
|
||||
def setUp(self):
|
||||
self.model = copy.deepcopy(CompactionModel)
|
||||
self.model.__compaction__ = SizeTieredCompactionStrategy
|
||||
|
||||
def test_size_tiered(self):
|
||||
result = get_compaction_options(self.model)
|
||||
assert result['class'] == SizeTieredCompactionStrategy
|
||||
|
||||
def test_min_threshold(self):
|
||||
self.model.__compaction_min_threshold__ = 2
|
||||
result = get_compaction_options(self.model)
|
||||
assert result['min_threshold'] == '2'
|
||||
|
||||
|
||||
class LeveledCompactionTest(BaseCompactionTest):
|
||||
def setUp(self):
|
||||
self.model = copy.deepcopy(CompactionLeveledStrategyModel)
|
||||
|
||||
def test_simple_leveled(self):
|
||||
result = get_compaction_options(self.model)
|
||||
assert result['class'] == LeveledCompactionStrategy
|
||||
|
||||
def test_bucket_high_fails(self):
|
||||
self.assert_option_fails('bucket_high')
|
||||
|
||||
def test_bucket_low_fails(self):
|
||||
self.assert_option_fails('bucket_low')
|
||||
|
||||
def test_max_threshold_fails(self):
|
||||
self.assert_option_fails('max_threshold')
|
||||
|
||||
def test_min_threshold_fails(self):
|
||||
self.assert_option_fails('min_threshold')
|
||||
|
||||
def test_min_sstable_size_fails(self):
|
||||
self.assert_option_fails('min_sstable_size')
|
||||
|
||||
def test_sstable_size_in_mb(self):
|
||||
with patch.object(self.model, '__compaction_sstable_size_in_mb__', 32):
|
||||
result = get_compaction_options(self.model)
|
||||
|
||||
assert result['sstable_size_in_mb'] == '32'
|
||||
|
||||
|
||||
class LeveledcompactionTestTable(Model):
|
||||
|
||||
__compaction__ = LeveledCompactionStrategy
|
||||
__compaction_sstable_size_in_mb__ = 64
|
||||
|
||||
user_id = columns.UUID(primary_key=True)
|
||||
name = columns.Text()
|
||||
|
||||
from cqlengine.management import schema_columnfamilies
|
||||
|
||||
class AlterTableTest(BaseCassEngTestCase):
|
||||
|
||||
def test_alter_is_called_table(self):
|
||||
drop_table(LeveledcompactionTestTable)
|
||||
sync_table(LeveledcompactionTestTable)
|
||||
with patch('cqlengine.management.update_compaction') as mock:
|
||||
sync_table(LeveledcompactionTestTable)
|
||||
assert mock.called == 1
|
||||
|
||||
def test_compaction_not_altered_without_changes_leveled(self):
|
||||
from cqlengine.management import update_compaction
|
||||
|
||||
class LeveledCompactionChangesDetectionTest(Model):
|
||||
|
||||
__compaction__ = LeveledCompactionStrategy
|
||||
__compaction_sstable_size_in_mb__ = 160
|
||||
__compaction_tombstone_threshold__ = 0.125
|
||||
__compaction_tombstone_compaction_interval__ = 3600
|
||||
|
||||
pk = columns.Integer(primary_key=True)
|
||||
|
||||
drop_table(LeveledCompactionChangesDetectionTest)
|
||||
sync_table(LeveledCompactionChangesDetectionTest)
|
||||
|
||||
assert not update_compaction(LeveledCompactionChangesDetectionTest)
|
||||
|
||||
def test_compaction_not_altered_without_changes_sizetiered(self):
|
||||
from cqlengine.management import update_compaction
|
||||
|
||||
class SizeTieredCompactionChangesDetectionTest(Model):
|
||||
|
||||
__compaction__ = SizeTieredCompactionStrategy
|
||||
__compaction_bucket_high__ = 20
|
||||
__compaction_bucket_low__ = 10
|
||||
__compaction_max_threshold__ = 200
|
||||
__compaction_min_threshold__ = 100
|
||||
__compaction_min_sstable_size__ = 1000
|
||||
__compaction_tombstone_threshold__ = 0.125
|
||||
__compaction_tombstone_compaction_interval__ = 3600
|
||||
|
||||
pk = columns.Integer(primary_key=True)
|
||||
|
||||
drop_table(SizeTieredCompactionChangesDetectionTest)
|
||||
sync_table(SizeTieredCompactionChangesDetectionTest)
|
||||
|
||||
assert not update_compaction(SizeTieredCompactionChangesDetectionTest)
|
||||
|
||||
def test_alter_actually_alters(self):
|
||||
tmp = copy.deepcopy(LeveledcompactionTestTable)
|
||||
drop_table(tmp)
|
||||
sync_table(tmp)
|
||||
tmp.__compaction__ = SizeTieredCompactionStrategy
|
||||
tmp.__compaction_sstable_size_in_mb__ = None
|
||||
sync_table(tmp)
|
||||
|
||||
table_settings = get_table_settings(tmp)
|
||||
|
||||
self.assertRegexpMatches(table_settings.options['compaction_strategy_class'], '.*SizeTieredCompactionStrategy$')
|
||||
|
||||
|
||||
def test_alter_options(self):
|
||||
|
||||
class AlterTable(Model):
|
||||
|
||||
__compaction__ = LeveledCompactionStrategy
|
||||
__compaction_sstable_size_in_mb__ = 64
|
||||
|
||||
user_id = columns.UUID(primary_key=True)
|
||||
name = columns.Text()
|
||||
|
||||
drop_table(AlterTable)
|
||||
sync_table(AlterTable)
|
||||
AlterTable.__compaction_sstable_size_in_mb__ = 128
|
||||
sync_table(AlterTable)
|
||||
|
||||
|
||||
|
||||
class EmptyCompactionTest(BaseCassEngTestCase):
|
||||
def test_empty_compaction(self):
|
||||
class EmptyCompactionModel(Model):
|
||||
|
||||
__compaction__ = None
|
||||
cid = columns.UUID(primary_key=True)
|
||||
name = columns.Text()
|
||||
|
||||
result = get_compaction_options(EmptyCompactionModel)
|
||||
self.assertEqual({}, result)
|
||||
|
||||
|
||||
class CompactionLeveledStrategyModel(Model):
|
||||
|
||||
__compaction__ = LeveledCompactionStrategy
|
||||
cid = columns.UUID(primary_key=True)
|
||||
name = columns.Text()
|
||||
|
||||
|
||||
class CompactionSizeTieredModel(Model):
|
||||
|
||||
__compaction__ = SizeTieredCompactionStrategy
|
||||
cid = columns.UUID(primary_key=True)
|
||||
name = columns.Text()
|
||||
|
||||
|
||||
|
||||
class OptionsTest(BaseCassEngTestCase):
|
||||
|
||||
def test_all_size_tiered_options(self):
|
||||
class AllSizeTieredOptionsModel(Model):
|
||||
|
||||
__compaction__ = SizeTieredCompactionStrategy
|
||||
__compaction_bucket_low__ = .3
|
||||
__compaction_bucket_high__ = 2
|
||||
__compaction_min_threshold__ = 2
|
||||
__compaction_max_threshold__ = 64
|
||||
__compaction_tombstone_compaction_interval__ = 86400
|
||||
|
||||
cid = columns.UUID(primary_key=True)
|
||||
name = columns.Text()
|
||||
|
||||
drop_table(AllSizeTieredOptionsModel)
|
||||
sync_table(AllSizeTieredOptionsModel)
|
||||
|
||||
options = get_table_settings(AllSizeTieredOptionsModel).options['compaction_strategy_options']
|
||||
options = json.loads(options)
|
||||
|
||||
expected = {u'min_threshold': u'2',
|
||||
u'bucket_low': u'0.3',
|
||||
u'tombstone_compaction_interval': u'86400',
|
||||
u'bucket_high': u'2',
|
||||
u'max_threshold': u'64'}
|
||||
|
||||
self.assertDictEqual(options, expected)
|
||||
|
||||
|
||||
def test_all_leveled_options(self):
|
||||
|
||||
class AllLeveledOptionsModel(Model):
|
||||
|
||||
__compaction__ = LeveledCompactionStrategy
|
||||
__compaction_sstable_size_in_mb__ = 64
|
||||
|
||||
cid = columns.UUID(primary_key=True)
|
||||
name = columns.Text()
|
||||
|
||||
drop_table(AllLeveledOptionsModel)
|
||||
sync_table(AllLeveledOptionsModel)
|
||||
|
||||
settings = get_table_settings(AllLeveledOptionsModel).options
|
||||
|
||||
options = json.loads(settings['compaction_strategy_options'])
|
||||
self.assertDictEqual(options, {u'sstable_size_in_mb': u'64'})
|
||||
|
292
cqlengine/tests/management/test_management.py
Normal file
292
cqlengine/tests/management/test_management.py
Normal file
@ -0,0 +1,292 @@
|
||||
import mock
|
||||
from cqlengine import ALL, CACHING_ALL, CACHING_NONE
|
||||
from cqlengine.connection import get_session
|
||||
from cqlengine.exceptions import CQLEngineException
|
||||
from cqlengine.management import get_fields, sync_table, drop_table
|
||||
from cqlengine.tests.base import BaseCassEngTestCase
|
||||
from cqlengine.tests.base import CASSANDRA_VERSION, PROTOCOL_VERSION
|
||||
from cqlengine import management
|
||||
from cqlengine.tests.query.test_queryset import TestModel
|
||||
from cqlengine.models import Model
|
||||
from cqlengine import columns, SizeTieredCompactionStrategy, LeveledCompactionStrategy
|
||||
from unittest import skipUnless
|
||||
|
||||
|
||||
class CreateKeyspaceTest(BaseCassEngTestCase):
|
||||
def test_create_succeeeds(self):
|
||||
management.create_keyspace('test_keyspace', strategy_class="SimpleStrategy", replication_factor=1)
|
||||
management.delete_keyspace('test_keyspace')
|
||||
|
||||
class DeleteTableTest(BaseCassEngTestCase):
|
||||
|
||||
def test_multiple_deletes_dont_fail(self):
|
||||
"""
|
||||
|
||||
"""
|
||||
sync_table(TestModel)
|
||||
|
||||
drop_table(TestModel)
|
||||
drop_table(TestModel)
|
||||
|
||||
class LowercaseKeyModel(Model):
|
||||
|
||||
first_key = columns.Integer(primary_key=True)
|
||||
second_key = columns.Integer(primary_key=True)
|
||||
some_data = columns.Text()
|
||||
|
||||
class CapitalizedKeyModel(Model):
|
||||
|
||||
firstKey = columns.Integer(primary_key=True)
|
||||
secondKey = columns.Integer(primary_key=True)
|
||||
someData = columns.Text()
|
||||
|
||||
class PrimaryKeysOnlyModel(Model):
|
||||
|
||||
__compaction__ = LeveledCompactionStrategy
|
||||
|
||||
first_ey = columns.Integer(primary_key=True)
|
||||
second_key = columns.Integer(primary_key=True)
|
||||
|
||||
|
||||
class CapitalizedKeyTest(BaseCassEngTestCase):
|
||||
|
||||
def test_table_definition(self):
|
||||
""" Tests that creating a table with capitalized column names succeedso """
|
||||
sync_table(LowercaseKeyModel)
|
||||
sync_table(CapitalizedKeyModel)
|
||||
|
||||
drop_table(LowercaseKeyModel)
|
||||
drop_table(CapitalizedKeyModel)
|
||||
|
||||
|
||||
class FirstModel(Model):
|
||||
|
||||
__table_name__ = 'first_model'
|
||||
first_key = columns.UUID(primary_key=True)
|
||||
second_key = columns.UUID()
|
||||
third_key = columns.Text()
|
||||
|
||||
class SecondModel(Model):
|
||||
|
||||
__table_name__ = 'first_model'
|
||||
first_key = columns.UUID(primary_key=True)
|
||||
second_key = columns.UUID()
|
||||
third_key = columns.Text()
|
||||
fourth_key = columns.Text()
|
||||
|
||||
class ThirdModel(Model):
|
||||
|
||||
__table_name__ = 'first_model'
|
||||
first_key = columns.UUID(primary_key=True)
|
||||
second_key = columns.UUID()
|
||||
third_key = columns.Text()
|
||||
# removed fourth key, but it should stay in the DB
|
||||
blah = columns.Map(columns.Text, columns.Text)
|
||||
|
||||
class FourthModel(Model):
|
||||
|
||||
__table_name__ = 'first_model'
|
||||
first_key = columns.UUID(primary_key=True)
|
||||
second_key = columns.UUID()
|
||||
third_key = columns.Text()
|
||||
# removed fourth key, but it should stay in the DB
|
||||
renamed = columns.Map(columns.Text, columns.Text, db_field='blah')
|
||||
|
||||
class AddColumnTest(BaseCassEngTestCase):
|
||||
def setUp(self):
|
||||
drop_table(FirstModel)
|
||||
|
||||
def test_add_column(self):
|
||||
sync_table(FirstModel)
|
||||
fields = get_fields(FirstModel)
|
||||
|
||||
# this should contain the second key
|
||||
self.assertEqual(len(fields), 2)
|
||||
# get schema
|
||||
sync_table(SecondModel)
|
||||
|
||||
fields = get_fields(FirstModel)
|
||||
self.assertEqual(len(fields), 3)
|
||||
|
||||
sync_table(ThirdModel)
|
||||
fields = get_fields(FirstModel)
|
||||
self.assertEqual(len(fields), 4)
|
||||
|
||||
sync_table(FourthModel)
|
||||
fields = get_fields(FirstModel)
|
||||
self.assertEqual(len(fields), 4)
|
||||
|
||||
|
||||
class ModelWithTableProperties(Model):
|
||||
|
||||
# Set random table properties
|
||||
__bloom_filter_fp_chance__ = 0.76328
|
||||
__caching__ = CACHING_ALL
|
||||
__comment__ = 'TxfguvBdzwROQALmQBOziRMbkqVGFjqcJfVhwGR'
|
||||
__gc_grace_seconds__ = 2063
|
||||
__populate_io_cache_on_flush__ = True
|
||||
__read_repair_chance__ = 0.17985
|
||||
__replicate_on_write__ = False
|
||||
__dclocal_read_repair_chance__ = 0.50811
|
||||
|
||||
key = columns.UUID(primary_key=True)
|
||||
|
||||
# kind of a hack, but we only test this property on C >= 2.0
|
||||
if CASSANDRA_VERSION >= 20:
|
||||
ModelWithTableProperties.__memtable_flush_period_in_ms__ = 43681
|
||||
ModelWithTableProperties.__index_interval__ = 98706
|
||||
ModelWithTableProperties.__default_time_to_live__ = 4756
|
||||
|
||||
class TablePropertiesTests(BaseCassEngTestCase):
|
||||
|
||||
def setUp(self):
|
||||
drop_table(ModelWithTableProperties)
|
||||
|
||||
def test_set_table_properties(self):
|
||||
|
||||
sync_table(ModelWithTableProperties)
|
||||
expected = {'bloom_filter_fp_chance': 0.76328,
|
||||
'comment': 'TxfguvBdzwROQALmQBOziRMbkqVGFjqcJfVhwGR',
|
||||
'gc_grace_seconds': 2063,
|
||||
'read_repair_chance': 0.17985,
|
||||
# For some reason 'dclocal_read_repair_chance' in CQL is called
|
||||
# just 'local_read_repair_chance' in the schema table.
|
||||
# Source: https://issues.apache.org/jira/browse/CASSANDRA-6717
|
||||
# TODO: due to a bug in the native driver i'm not seeing the local read repair chance show up
|
||||
# 'local_read_repair_chance': 0.50811,
|
||||
}
|
||||
if CASSANDRA_VERSION <= 20:
|
||||
expected['caching'] = CACHING_ALL
|
||||
expected['replicate_on_write'] = False
|
||||
|
||||
if CASSANDRA_VERSION == 20:
|
||||
expected['populate_io_cache_on_flush'] = True
|
||||
expected['index_interval'] = 98706
|
||||
|
||||
if CASSANDRA_VERSION >= 20:
|
||||
expected['default_time_to_live'] = 4756
|
||||
expected['memtable_flush_period_in_ms'] = 43681
|
||||
|
||||
self.assertDictContainsSubset(expected, management.get_table_settings(ModelWithTableProperties).options)
|
||||
|
||||
def test_table_property_update(self):
|
||||
ModelWithTableProperties.__bloom_filter_fp_chance__ = 0.66778
|
||||
ModelWithTableProperties.__caching__ = CACHING_NONE
|
||||
ModelWithTableProperties.__comment__ = 'xirAkRWZVVvsmzRvXamiEcQkshkUIDINVJZgLYSdnGHweiBrAiJdLJkVohdRy'
|
||||
ModelWithTableProperties.__gc_grace_seconds__ = 96362
|
||||
|
||||
ModelWithTableProperties.__populate_io_cache_on_flush__ = False
|
||||
ModelWithTableProperties.__read_repair_chance__ = 0.2989
|
||||
ModelWithTableProperties.__replicate_on_write__ = True
|
||||
ModelWithTableProperties.__dclocal_read_repair_chance__ = 0.12732
|
||||
|
||||
if CASSANDRA_VERSION >= 20:
|
||||
ModelWithTableProperties.__default_time_to_live__ = 65178
|
||||
ModelWithTableProperties.__memtable_flush_period_in_ms__ = 60210
|
||||
ModelWithTableProperties.__index_interval__ = 94207
|
||||
|
||||
sync_table(ModelWithTableProperties)
|
||||
|
||||
table_settings = management.get_table_settings(ModelWithTableProperties).options
|
||||
|
||||
expected = {'bloom_filter_fp_chance': 0.66778,
|
||||
'comment': 'xirAkRWZVVvsmzRvXamiEcQkshkUIDINVJZgLYSdnGHweiBrAiJdLJkVohdRy',
|
||||
'gc_grace_seconds': 96362,
|
||||
'read_repair_chance': 0.2989,
|
||||
#'local_read_repair_chance': 0.12732,
|
||||
}
|
||||
if CASSANDRA_VERSION >= 20:
|
||||
expected['memtable_flush_period_in_ms'] = 60210
|
||||
expected['default_time_to_live'] = 65178
|
||||
|
||||
if CASSANDRA_VERSION == 20:
|
||||
expected['index_interval'] = 94207
|
||||
|
||||
# these featuers removed in cassandra 2.1
|
||||
if CASSANDRA_VERSION <= 20:
|
||||
expected['caching'] = CACHING_NONE
|
||||
expected['replicate_on_write'] = True
|
||||
expected['populate_io_cache_on_flush'] = False
|
||||
|
||||
self.assertDictContainsSubset(expected, table_settings)
|
||||
|
||||
|
||||
class SyncTableTests(BaseCassEngTestCase):
|
||||
|
||||
def setUp(self):
|
||||
drop_table(PrimaryKeysOnlyModel)
|
||||
|
||||
def test_sync_table_works_with_primary_keys_only_tables(self):
|
||||
|
||||
# This is "create table":
|
||||
|
||||
sync_table(PrimaryKeysOnlyModel)
|
||||
|
||||
# let's make sure settings persisted correctly:
|
||||
|
||||
assert PrimaryKeysOnlyModel.__compaction__ == LeveledCompactionStrategy
|
||||
# blows up with DoesNotExist if table does not exist
|
||||
table_settings = management.get_table_settings(PrimaryKeysOnlyModel)
|
||||
# let make sure the flag we care about
|
||||
|
||||
assert LeveledCompactionStrategy in table_settings.options['compaction_strategy_class']
|
||||
|
||||
|
||||
# Now we are "updating" the table:
|
||||
|
||||
# setting up something to change
|
||||
PrimaryKeysOnlyModel.__compaction__ = SizeTieredCompactionStrategy
|
||||
|
||||
# primary-keys-only tables do not create entries in system.schema_columns
|
||||
# table. Only non-primary keys are added to that table.
|
||||
# Our code must deal with that eventuality properly (not crash)
|
||||
# on subsequent runs of sync_table (which runs get_fields internally)
|
||||
get_fields(PrimaryKeysOnlyModel)
|
||||
sync_table(PrimaryKeysOnlyModel)
|
||||
|
||||
table_settings = management.get_table_settings(PrimaryKeysOnlyModel)
|
||||
assert SizeTieredCompactionStrategy in table_settings.options['compaction_strategy_class']
|
||||
|
||||
class NonModelFailureTest(BaseCassEngTestCase):
|
||||
class FakeModel(object):
|
||||
pass
|
||||
|
||||
def test_failure(self):
|
||||
with self.assertRaises(CQLEngineException):
|
||||
sync_table(self.FakeModel)
|
||||
|
||||
|
||||
@skipUnless(PROTOCOL_VERSION >= 2, "only runs against the cql3 protocol v2.0")
|
||||
def test_static_columns():
|
||||
class StaticModel(Model):
|
||||
id = columns.Integer(primary_key=True)
|
||||
c = columns.Integer(primary_key=True)
|
||||
name = columns.Text(static=True)
|
||||
|
||||
drop_table(StaticModel)
|
||||
|
||||
from mock import patch
|
||||
|
||||
from cqlengine.connection import get_session
|
||||
session = get_session()
|
||||
|
||||
with patch.object(session, "execute", wraps=session.execute) as m:
|
||||
sync_table(StaticModel)
|
||||
|
||||
assert m.call_count > 0
|
||||
statement = m.call_args[0][0].query_string
|
||||
assert '"name" text static' in statement, statement
|
||||
|
||||
# if we sync again, we should not apply an alter w/ a static
|
||||
sync_table(StaticModel)
|
||||
|
||||
with patch.object(session, "execute", wraps=session.execute) as m2:
|
||||
sync_table(StaticModel)
|
||||
|
||||
assert len(m2.call_args_list) == 1
|
||||
assert "ALTER" not in m2.call_args[0][0].query_string
|
||||
|
||||
|
||||
|
||||
|
||||
|
0
cqlengine/tests/model/__init__.py
Normal file
0
cqlengine/tests/model/__init__.py
Normal file
377
cqlengine/tests/model/test_class_construction.py
Normal file
377
cqlengine/tests/model/test_class_construction.py
Normal file
@ -0,0 +1,377 @@
|
||||
from uuid import uuid4
|
||||
import warnings
|
||||
from cqlengine.query import QueryException, ModelQuerySet, DMLQuery
|
||||
from cqlengine.tests.base import BaseCassEngTestCase
|
||||
|
||||
from cqlengine.exceptions import ModelException, CQLEngineException
|
||||
from cqlengine.models import Model, ModelDefinitionException, ColumnQueryEvaluator, UndefinedKeyspaceWarning
|
||||
from cqlengine import columns
|
||||
import cqlengine
|
||||
|
||||
class TestModelClassFunction(BaseCassEngTestCase):
|
||||
"""
|
||||
Tests verifying the behavior of the Model metaclass
|
||||
"""
|
||||
|
||||
def test_column_attributes_handled_correctly(self):
|
||||
"""
|
||||
Tests that column attributes are moved to a _columns dict
|
||||
and replaced with simple value attributes
|
||||
"""
|
||||
|
||||
class TestModel(Model):
|
||||
|
||||
id = columns.UUID(primary_key=True, default=lambda:uuid4())
|
||||
text = columns.Text()
|
||||
|
||||
#check class attibutes
|
||||
self.assertHasAttr(TestModel, '_columns')
|
||||
self.assertHasAttr(TestModel, 'id')
|
||||
self.assertHasAttr(TestModel, 'text')
|
||||
|
||||
#check instance attributes
|
||||
inst = TestModel()
|
||||
self.assertHasAttr(inst, 'id')
|
||||
self.assertHasAttr(inst, 'text')
|
||||
self.assertIsNone(inst.id)
|
||||
self.assertIsNone(inst.text)
|
||||
|
||||
def test_db_map(self):
|
||||
"""
|
||||
Tests that the db_map is properly defined
|
||||
-the db_map allows columns
|
||||
"""
|
||||
class WildDBNames(Model):
|
||||
|
||||
id = columns.UUID(primary_key=True, default=lambda:uuid4())
|
||||
content = columns.Text(db_field='words_and_whatnot')
|
||||
numbers = columns.Integer(db_field='integers_etc')
|
||||
|
||||
db_map = WildDBNames._db_map
|
||||
self.assertEquals(db_map['words_and_whatnot'], 'content')
|
||||
self.assertEquals(db_map['integers_etc'], 'numbers')
|
||||
|
||||
def test_attempting_to_make_duplicate_column_names_fails(self):
|
||||
"""
|
||||
Tests that trying to create conflicting db column names will fail
|
||||
"""
|
||||
|
||||
with self.assertRaises(ModelException):
|
||||
class BadNames(Model):
|
||||
words = columns.Text()
|
||||
content = columns.Text(db_field='words')
|
||||
|
||||
def test_column_ordering_is_preserved(self):
|
||||
"""
|
||||
Tests that the _columns dics retains the ordering of the class definition
|
||||
"""
|
||||
|
||||
class Stuff(Model):
|
||||
|
||||
id = columns.UUID(primary_key=True, default=lambda:uuid4())
|
||||
words = columns.Text()
|
||||
content = columns.Text()
|
||||
numbers = columns.Integer()
|
||||
|
||||
self.assertEquals([x for x in Stuff._columns.keys()], ['id', 'words', 'content', 'numbers'])
|
||||
|
||||
def test_exception_raised_when_creating_class_without_pk(self):
|
||||
with self.assertRaises(ModelDefinitionException):
|
||||
class TestModel(Model):
|
||||
|
||||
count = columns.Integer()
|
||||
text = columns.Text(required=False)
|
||||
|
||||
|
||||
def test_value_managers_are_keeping_model_instances_isolated(self):
|
||||
"""
|
||||
Tests that instance value managers are isolated from other instances
|
||||
"""
|
||||
class Stuff(Model):
|
||||
|
||||
id = columns.UUID(primary_key=True, default=lambda:uuid4())
|
||||
num = columns.Integer()
|
||||
|
||||
inst1 = Stuff(num=5)
|
||||
inst2 = Stuff(num=7)
|
||||
|
||||
self.assertNotEquals(inst1.num, inst2.num)
|
||||
self.assertEquals(inst1.num, 5)
|
||||
self.assertEquals(inst2.num, 7)
|
||||
|
||||
def test_superclass_fields_are_inherited(self):
|
||||
"""
|
||||
Tests that fields defined on the super class are inherited properly
|
||||
"""
|
||||
class TestModel(Model):
|
||||
|
||||
id = columns.UUID(primary_key=True, default=lambda:uuid4())
|
||||
text = columns.Text()
|
||||
|
||||
class InheritedModel(TestModel):
|
||||
numbers = columns.Integer()
|
||||
|
||||
assert 'text' in InheritedModel._columns
|
||||
assert 'numbers' in InheritedModel._columns
|
||||
|
||||
def test_column_family_name_generation(self):
|
||||
""" Tests that auto column family name generation works as expected """
|
||||
class TestModel(Model):
|
||||
|
||||
id = columns.UUID(primary_key=True, default=lambda:uuid4())
|
||||
text = columns.Text()
|
||||
|
||||
assert TestModel.column_family_name(include_keyspace=False) == 'test_model'
|
||||
|
||||
def test_normal_fields_can_be_defined_between_primary_keys(self):
|
||||
"""
|
||||
Tests tha non primary key fields can be defined between primary key fields
|
||||
"""
|
||||
|
||||
def test_at_least_one_non_primary_key_column_is_required(self):
|
||||
"""
|
||||
Tests that an error is raised if a model doesn't contain at least one primary key field
|
||||
"""
|
||||
|
||||
def test_model_keyspace_attribute_must_be_a_string(self):
|
||||
"""
|
||||
Tests that users can't set the keyspace to None, or something else
|
||||
"""
|
||||
|
||||
def test_indexes_arent_allowed_on_models_with_multiple_primary_keys(self):
|
||||
"""
|
||||
Tests that attempting to define an index on a model with multiple primary keys fails
|
||||
"""
|
||||
|
||||
def test_meta_data_is_not_inherited(self):
|
||||
"""
|
||||
Test that metadata defined in one class, is not inherited by subclasses
|
||||
"""
|
||||
|
||||
def test_partition_keys(self):
|
||||
"""
|
||||
Test compound partition key definition
|
||||
"""
|
||||
class ModelWithPartitionKeys(cqlengine.Model):
|
||||
|
||||
id = columns.UUID(primary_key=True, default=lambda:uuid4())
|
||||
c1 = cqlengine.Text(primary_key=True)
|
||||
p1 = cqlengine.Text(partition_key=True)
|
||||
p2 = cqlengine.Text(partition_key=True)
|
||||
|
||||
cols = ModelWithPartitionKeys._columns
|
||||
|
||||
self.assertTrue(cols['c1'].primary_key)
|
||||
self.assertFalse(cols['c1'].partition_key)
|
||||
|
||||
self.assertTrue(cols['p1'].primary_key)
|
||||
self.assertTrue(cols['p1'].partition_key)
|
||||
self.assertTrue(cols['p2'].primary_key)
|
||||
self.assertTrue(cols['p2'].partition_key)
|
||||
|
||||
obj = ModelWithPartitionKeys(p1='a', p2='b')
|
||||
self.assertEquals(obj.pk, ('a', 'b'))
|
||||
|
||||
def test_del_attribute_is_assigned_properly(self):
|
||||
""" Tests that columns that can be deleted have the del attribute """
|
||||
class DelModel(Model):
|
||||
|
||||
id = columns.UUID(primary_key=True, default=lambda:uuid4())
|
||||
key = columns.Integer(primary_key=True)
|
||||
data = columns.Integer(required=False)
|
||||
|
||||
model = DelModel(key=4, data=5)
|
||||
del model.data
|
||||
with self.assertRaises(AttributeError):
|
||||
del model.key
|
||||
|
||||
def test_does_not_exist_exceptions_are_not_shared_between_model(self):
|
||||
""" Tests that DoesNotExist exceptions are not the same exception between models """
|
||||
|
||||
class Model1(Model):
|
||||
|
||||
id = columns.UUID(primary_key=True, default=lambda:uuid4())
|
||||
|
||||
class Model2(Model):
|
||||
|
||||
id = columns.UUID(primary_key=True, default=lambda:uuid4())
|
||||
|
||||
try:
|
||||
raise Model1.DoesNotExist
|
||||
except Model2.DoesNotExist:
|
||||
assert False, "Model1 exception should not be caught by Model2"
|
||||
except Model1.DoesNotExist:
|
||||
#expected
|
||||
pass
|
||||
|
||||
def test_does_not_exist_inherits_from_superclass(self):
|
||||
""" Tests that a DoesNotExist exception can be caught by it's parent class DoesNotExist """
|
||||
class Model1(Model):
|
||||
|
||||
id = columns.UUID(primary_key=True, default=lambda:uuid4())
|
||||
|
||||
class Model2(Model1):
|
||||
pass
|
||||
|
||||
try:
|
||||
raise Model2.DoesNotExist
|
||||
except Model1.DoesNotExist:
|
||||
#expected
|
||||
pass
|
||||
except Exception:
|
||||
assert False, "Model2 exception should not be caught by Model1"
|
||||
|
||||
def test_abstract_model_keyspace_warning_is_skipped(self):
|
||||
with warnings.catch_warnings(record=True) as warn:
|
||||
class NoKeyspace(Model):
|
||||
__abstract__ = True
|
||||
key = columns.UUID(primary_key=True)
|
||||
|
||||
self.assertEqual(len(warn), 0)
|
||||
|
||||
class TestManualTableNaming(BaseCassEngTestCase):
|
||||
|
||||
class RenamedTest(cqlengine.Model):
|
||||
__keyspace__ = 'whatever'
|
||||
__table_name__ = 'manual_name'
|
||||
|
||||
id = cqlengine.UUID(primary_key=True)
|
||||
data = cqlengine.Text()
|
||||
|
||||
def test_proper_table_naming(self):
|
||||
assert self.RenamedTest.column_family_name(include_keyspace=False) == 'manual_name'
|
||||
assert self.RenamedTest.column_family_name(include_keyspace=True) == 'whatever.manual_name'
|
||||
|
||||
class AbstractModel(Model):
|
||||
__abstract__ = True
|
||||
|
||||
|
||||
class ConcreteModel(AbstractModel):
|
||||
pkey = columns.Integer(primary_key=True)
|
||||
data = columns.Integer()
|
||||
|
||||
class AbstractModelWithCol(Model):
|
||||
|
||||
__abstract__ = True
|
||||
pkey = columns.Integer(primary_key=True)
|
||||
|
||||
class ConcreteModelWithCol(AbstractModelWithCol):
|
||||
data = columns.Integer()
|
||||
|
||||
class AbstractModelWithFullCols(Model):
|
||||
__abstract__ = True
|
||||
|
||||
pkey = columns.Integer(primary_key=True)
|
||||
data = columns.Integer()
|
||||
|
||||
class TestAbstractModelClasses(BaseCassEngTestCase):
|
||||
|
||||
def test_id_field_is_not_created(self):
|
||||
""" Tests that an id field is not automatically generated on abstract classes """
|
||||
assert not hasattr(AbstractModel, 'id')
|
||||
assert not hasattr(AbstractModelWithCol, 'id')
|
||||
|
||||
def test_id_field_is_not_created_on_subclass(self):
|
||||
assert not hasattr(ConcreteModel, 'id')
|
||||
|
||||
def test_abstract_attribute_is_not_inherited(self):
|
||||
""" Tests that __abstract__ attribute is not inherited """
|
||||
assert not ConcreteModel.__abstract__
|
||||
assert not ConcreteModelWithCol.__abstract__
|
||||
|
||||
def test_attempting_to_save_abstract_model_fails(self):
|
||||
""" Attempting to save a model from an abstract model should fail """
|
||||
with self.assertRaises(CQLEngineException):
|
||||
AbstractModelWithFullCols.create(pkey=1, data=2)
|
||||
|
||||
def test_attempting_to_create_abstract_table_fails(self):
|
||||
""" Attempting to create a table from an abstract model should fail """
|
||||
from cqlengine.management import sync_table
|
||||
with self.assertRaises(CQLEngineException):
|
||||
sync_table(AbstractModelWithFullCols)
|
||||
|
||||
def test_attempting_query_on_abstract_model_fails(self):
|
||||
""" Tests attempting to execute query with an abstract model fails """
|
||||
with self.assertRaises(CQLEngineException):
|
||||
iter(AbstractModelWithFullCols.objects(pkey=5)).next()
|
||||
|
||||
def test_abstract_columns_are_inherited(self):
|
||||
""" Tests that columns defined in the abstract class are inherited into the concrete class """
|
||||
assert hasattr(ConcreteModelWithCol, 'pkey')
|
||||
assert isinstance(ConcreteModelWithCol.pkey, ColumnQueryEvaluator)
|
||||
assert isinstance(ConcreteModelWithCol._columns['pkey'], columns.Column)
|
||||
|
||||
def test_concrete_class_table_creation_cycle(self):
|
||||
""" Tests that models with inherited abstract classes can be created, and have io performed """
|
||||
from cqlengine.management import sync_table, drop_table
|
||||
sync_table(ConcreteModelWithCol)
|
||||
|
||||
w1 = ConcreteModelWithCol.create(pkey=5, data=6)
|
||||
w2 = ConcreteModelWithCol.create(pkey=6, data=7)
|
||||
|
||||
r1 = ConcreteModelWithCol.get(pkey=5)
|
||||
r2 = ConcreteModelWithCol.get(pkey=6)
|
||||
|
||||
assert w1.pkey == r1.pkey
|
||||
assert w1.data == r1.data
|
||||
assert w2.pkey == r2.pkey
|
||||
assert w2.data == r2.data
|
||||
|
||||
drop_table(ConcreteModelWithCol)
|
||||
|
||||
|
||||
class TestCustomQuerySet(BaseCassEngTestCase):
|
||||
""" Tests overriding the default queryset class """
|
||||
|
||||
class TestException(Exception): pass
|
||||
|
||||
def test_overriding_queryset(self):
|
||||
|
||||
class QSet(ModelQuerySet):
|
||||
def create(iself, **kwargs):
|
||||
raise self.TestException
|
||||
|
||||
class CQModel(Model):
|
||||
__queryset__ = QSet
|
||||
|
||||
part = columns.UUID(primary_key=True)
|
||||
data = columns.Text()
|
||||
|
||||
with self.assertRaises(self.TestException):
|
||||
CQModel.create(part=uuid4(), data='s')
|
||||
|
||||
def test_overriding_dmlqueryset(self):
|
||||
|
||||
class DMLQ(DMLQuery):
|
||||
def save(iself):
|
||||
raise self.TestException
|
||||
|
||||
class CDQModel(Model):
|
||||
|
||||
__dmlquery__ = DMLQ
|
||||
part = columns.UUID(primary_key=True)
|
||||
data = columns.Text()
|
||||
|
||||
with self.assertRaises(self.TestException):
|
||||
CDQModel().save()
|
||||
|
||||
|
||||
class TestCachedLengthIsNotCarriedToSubclasses(BaseCassEngTestCase):
|
||||
def test_subclassing(self):
|
||||
|
||||
length = len(ConcreteModelWithCol())
|
||||
|
||||
class AlreadyLoadedTest(ConcreteModelWithCol):
|
||||
new_field = columns.Integer()
|
||||
|
||||
self.assertGreater(len(AlreadyLoadedTest()), length)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
55
cqlengine/tests/model/test_equality_operations.py
Normal file
55
cqlengine/tests/model/test_equality_operations.py
Normal file
@ -0,0 +1,55 @@
|
||||
from unittest import skip
|
||||
from uuid import uuid4
|
||||
from cqlengine.tests.base import BaseCassEngTestCase
|
||||
|
||||
from cqlengine.management import sync_table
|
||||
from cqlengine.management import drop_table
|
||||
from cqlengine.models import Model
|
||||
from cqlengine import columns
|
||||
|
||||
class TestModel(Model):
|
||||
|
||||
id = columns.UUID(primary_key=True, default=lambda:uuid4())
|
||||
count = columns.Integer()
|
||||
text = columns.Text(required=False)
|
||||
|
||||
class TestEqualityOperators(BaseCassEngTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestEqualityOperators, cls).setUpClass()
|
||||
sync_table(TestModel)
|
||||
|
||||
def setUp(self):
|
||||
super(TestEqualityOperators, self).setUp()
|
||||
self.t0 = TestModel.create(count=5, text='words')
|
||||
self.t1 = TestModel.create(count=5, text='words')
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(TestEqualityOperators, cls).tearDownClass()
|
||||
drop_table(TestModel)
|
||||
|
||||
def test_an_instance_evaluates_as_equal_to_itself(self):
|
||||
"""
|
||||
"""
|
||||
assert self.t0 == self.t0
|
||||
|
||||
def test_two_instances_referencing_the_same_rows_and_different_values_evaluate_not_equal(self):
|
||||
"""
|
||||
"""
|
||||
t0 = TestModel.get(id=self.t0.id)
|
||||
t0.text = 'bleh'
|
||||
assert t0 != self.t0
|
||||
|
||||
def test_two_instances_referencing_the_same_rows_and_values_evaluate_equal(self):
|
||||
"""
|
||||
"""
|
||||
t0 = TestModel.get(id=self.t0.id)
|
||||
assert t0 == self.t0
|
||||
|
||||
def test_two_instances_referencing_different_rows_evaluate_to_not_equal(self):
|
||||
"""
|
||||
"""
|
||||
assert self.t0 != self.t1
|
||||
|
58
cqlengine/tests/model/test_model.py
Normal file
58
cqlengine/tests/model/test_model.py
Normal file
@ -0,0 +1,58 @@
|
||||
from unittest import TestCase
|
||||
|
||||
from cqlengine.models import Model, ModelDefinitionException
|
||||
from cqlengine import columns
|
||||
|
||||
|
||||
class TestModel(TestCase):
|
||||
""" Tests the non-io functionality of models """
|
||||
|
||||
def test_instance_equality(self):
|
||||
""" tests the model equality functionality """
|
||||
class EqualityModel(Model):
|
||||
|
||||
pk = columns.Integer(primary_key=True)
|
||||
|
||||
m0 = EqualityModel(pk=0)
|
||||
m1 = EqualityModel(pk=1)
|
||||
|
||||
self.assertEqual(m0, m0)
|
||||
self.assertNotEqual(m0, m1)
|
||||
|
||||
def test_model_equality(self):
|
||||
""" tests the model equality functionality """
|
||||
class EqualityModel0(Model):
|
||||
|
||||
pk = columns.Integer(primary_key=True)
|
||||
|
||||
class EqualityModel1(Model):
|
||||
|
||||
kk = columns.Integer(primary_key=True)
|
||||
|
||||
m0 = EqualityModel0(pk=0)
|
||||
m1 = EqualityModel1(kk=1)
|
||||
|
||||
self.assertEqual(m0, m0)
|
||||
self.assertNotEqual(m0, m1)
|
||||
|
||||
|
||||
class BuiltInAttributeConflictTest(TestCase):
|
||||
"""tests Model definitions that conflict with built-in attributes/methods"""
|
||||
|
||||
def test_model_with_attribute_name_conflict(self):
|
||||
"""should raise exception when model defines column that conflicts with built-in attribute"""
|
||||
with self.assertRaises(ModelDefinitionException):
|
||||
class IllegalTimestampColumnModel(Model):
|
||||
|
||||
my_primary_key = columns.Integer(primary_key=True)
|
||||
timestamp = columns.BigInt()
|
||||
|
||||
def test_model_with_method_name_conflict(self):
|
||||
"""should raise exception when model defines column that conflicts with built-in method"""
|
||||
with self.assertRaises(ModelDefinitionException):
|
||||
class IllegalFilterColumnModel(Model):
|
||||
|
||||
my_primary_key = columns.Integer(primary_key=True)
|
||||
filter = columns.Text()
|
||||
|
||||
|
326
cqlengine/tests/model/test_model_io.py
Normal file
326
cqlengine/tests/model/test_model_io.py
Normal file
@ -0,0 +1,326 @@
|
||||
from uuid import uuid4
|
||||
import random
|
||||
from datetime import date
|
||||
from operator import itemgetter
|
||||
from cqlengine.exceptions import CQLEngineException
|
||||
from cqlengine.tests.base import BaseCassEngTestCase
|
||||
|
||||
from cqlengine.management import sync_table
|
||||
from cqlengine.management import drop_table
|
||||
from cqlengine.models import Model
|
||||
from cqlengine import columns
|
||||
|
||||
class TestModel(Model):
|
||||
|
||||
id = columns.UUID(primary_key=True, default=lambda:uuid4())
|
||||
count = columns.Integer()
|
||||
text = columns.Text(required=False)
|
||||
a_bool = columns.Boolean(default=False)
|
||||
|
||||
class TestModel(Model):
|
||||
|
||||
id = columns.UUID(primary_key=True, default=lambda:uuid4())
|
||||
count = columns.Integer()
|
||||
text = columns.Text(required=False)
|
||||
a_bool = columns.Boolean(default=False)
|
||||
|
||||
|
||||
class TestModelIO(BaseCassEngTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestModelIO, cls).setUpClass()
|
||||
sync_table(TestModel)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(TestModelIO, cls).tearDownClass()
|
||||
drop_table(TestModel)
|
||||
|
||||
def test_model_save_and_load(self):
|
||||
"""
|
||||
Tests that models can be saved and retrieved
|
||||
"""
|
||||
tm = TestModel.create(count=8, text='123456789')
|
||||
self.assertIsInstance(tm, TestModel)
|
||||
|
||||
tm2 = TestModel.objects(id=tm.pk).first()
|
||||
self.assertIsInstance(tm2, TestModel)
|
||||
|
||||
for cname in tm._columns.keys():
|
||||
self.assertEquals(getattr(tm, cname), getattr(tm2, cname))
|
||||
|
||||
def test_model_read_as_dict(self):
|
||||
"""
|
||||
Tests that columns of an instance can be read as a dict.
|
||||
"""
|
||||
tm = TestModel.create(count=8, text='123456789', a_bool=True)
|
||||
column_dict = {
|
||||
'id': tm.id,
|
||||
'count': tm.count,
|
||||
'text': tm.text,
|
||||
'a_bool': tm.a_bool,
|
||||
}
|
||||
self.assertEquals(sorted(tm.keys()), sorted(column_dict.keys()))
|
||||
|
||||
self.assertItemsEqual(tm.values(), column_dict.values())
|
||||
self.assertEquals(
|
||||
sorted(tm.items(), key=itemgetter(0)),
|
||||
sorted(column_dict.items(), key=itemgetter(0)))
|
||||
self.assertEquals(len(tm), len(column_dict))
|
||||
for column_id in column_dict.keys():
|
||||
self.assertEqual(tm[column_id], column_dict[column_id])
|
||||
|
||||
tm['count'] = 6
|
||||
self.assertEqual(tm.count, 6)
|
||||
|
||||
def test_model_updating_works_properly(self):
|
||||
"""
|
||||
Tests that subsequent saves after initial model creation work
|
||||
"""
|
||||
tm = TestModel.objects.create(count=8, text='123456789')
|
||||
|
||||
tm.count = 100
|
||||
tm.a_bool = True
|
||||
tm.save()
|
||||
|
||||
tm2 = TestModel.objects(id=tm.pk).first()
|
||||
self.assertEquals(tm.count, tm2.count)
|
||||
self.assertEquals(tm.a_bool, tm2.a_bool)
|
||||
|
||||
def test_model_deleting_works_properly(self):
|
||||
"""
|
||||
Tests that an instance's delete method deletes the instance
|
||||
"""
|
||||
tm = TestModel.create(count=8, text='123456789')
|
||||
tm.delete()
|
||||
tm2 = TestModel.objects(id=tm.pk).first()
|
||||
self.assertIsNone(tm2)
|
||||
|
||||
def test_column_deleting_works_properly(self):
|
||||
"""
|
||||
"""
|
||||
tm = TestModel.create(count=8, text='123456789')
|
||||
tm.text = None
|
||||
tm.save()
|
||||
|
||||
tm2 = TestModel.objects(id=tm.pk).first()
|
||||
self.assertIsInstance(tm2, TestModel)
|
||||
|
||||
assert tm2.text is None
|
||||
assert tm2._values['text'].previous_value is None
|
||||
|
||||
def test_a_sensical_error_is_raised_if_you_try_to_create_a_table_twice(self):
|
||||
"""
|
||||
"""
|
||||
sync_table(TestModel)
|
||||
sync_table(TestModel)
|
||||
|
||||
|
||||
class TestMultiKeyModel(Model):
|
||||
|
||||
partition = columns.Integer(primary_key=True)
|
||||
cluster = columns.Integer(primary_key=True)
|
||||
count = columns.Integer(required=False)
|
||||
text = columns.Text(required=False)
|
||||
|
||||
|
||||
class TestDeleting(BaseCassEngTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestDeleting, cls).setUpClass()
|
||||
drop_table(TestMultiKeyModel)
|
||||
sync_table(TestMultiKeyModel)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(TestDeleting, cls).tearDownClass()
|
||||
drop_table(TestMultiKeyModel)
|
||||
|
||||
def test_deleting_only_deletes_one_object(self):
|
||||
partition = random.randint(0,1000)
|
||||
for i in range(5):
|
||||
TestMultiKeyModel.create(partition=partition, cluster=i, count=i, text=str(i))
|
||||
|
||||
assert TestMultiKeyModel.filter(partition=partition).count() == 5
|
||||
|
||||
TestMultiKeyModel.get(partition=partition, cluster=0).delete()
|
||||
|
||||
assert TestMultiKeyModel.filter(partition=partition).count() == 4
|
||||
|
||||
TestMultiKeyModel.filter(partition=partition).delete()
|
||||
|
||||
|
||||
class TestUpdating(BaseCassEngTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestUpdating, cls).setUpClass()
|
||||
drop_table(TestMultiKeyModel)
|
||||
sync_table(TestMultiKeyModel)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(TestUpdating, cls).tearDownClass()
|
||||
drop_table(TestMultiKeyModel)
|
||||
|
||||
def setUp(self):
|
||||
super(TestUpdating, self).setUp()
|
||||
self.instance = TestMultiKeyModel.create(
|
||||
partition=random.randint(0, 1000),
|
||||
cluster=random.randint(0, 1000),
|
||||
count=0,
|
||||
text='happy'
|
||||
)
|
||||
|
||||
def test_vanilla_update(self):
|
||||
self.instance.count = 5
|
||||
self.instance.save()
|
||||
|
||||
check = TestMultiKeyModel.get(partition=self.instance.partition, cluster=self.instance.cluster)
|
||||
assert check.count == 5
|
||||
assert check.text == 'happy'
|
||||
|
||||
def test_deleting_only(self):
|
||||
self.instance.count = None
|
||||
self.instance.text = None
|
||||
self.instance.save()
|
||||
|
||||
check = TestMultiKeyModel.get(partition=self.instance.partition, cluster=self.instance.cluster)
|
||||
assert check.count is None
|
||||
assert check.text is None
|
||||
|
||||
def test_get_changed_columns(self):
|
||||
assert self.instance.get_changed_columns() == []
|
||||
self.instance.count = 1
|
||||
changes = self.instance.get_changed_columns()
|
||||
assert len(changes) == 1
|
||||
assert changes == ['count']
|
||||
self.instance.save()
|
||||
assert self.instance.get_changed_columns() == []
|
||||
|
||||
|
||||
class TestCanUpdate(BaseCassEngTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestCanUpdate, cls).setUpClass()
|
||||
drop_table(TestModel)
|
||||
sync_table(TestModel)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(TestCanUpdate, cls).tearDownClass()
|
||||
drop_table(TestModel)
|
||||
|
||||
def test_success_case(self):
|
||||
tm = TestModel(count=8, text='123456789')
|
||||
|
||||
# object hasn't been saved,
|
||||
# shouldn't be able to update
|
||||
assert not tm._is_persisted
|
||||
assert not tm._can_update()
|
||||
|
||||
tm.save()
|
||||
|
||||
# object has been saved,
|
||||
# should be able to update
|
||||
assert tm._is_persisted
|
||||
assert tm._can_update()
|
||||
|
||||
tm.count = 200
|
||||
|
||||
# primary keys haven't changed,
|
||||
# should still be able to update
|
||||
assert tm._can_update()
|
||||
tm.save()
|
||||
|
||||
tm.id = uuid4()
|
||||
|
||||
# primary keys have changed,
|
||||
# should not be able to update
|
||||
assert not tm._can_update()
|
||||
|
||||
|
||||
class IndexDefinitionModel(Model):
|
||||
|
||||
key = columns.UUID(primary_key=True)
|
||||
val = columns.Text(index=True)
|
||||
|
||||
class TestIndexedColumnDefinition(BaseCassEngTestCase):
|
||||
|
||||
def test_exception_isnt_raised_if_an_index_is_defined_more_than_once(self):
|
||||
sync_table(IndexDefinitionModel)
|
||||
sync_table(IndexDefinitionModel)
|
||||
|
||||
class ReservedWordModel(Model):
|
||||
|
||||
token = columns.Text(primary_key=True)
|
||||
insert = columns.Integer(index=True)
|
||||
|
||||
class TestQueryQuoting(BaseCassEngTestCase):
|
||||
|
||||
def test_reserved_cql_words_can_be_used_as_column_names(self):
|
||||
"""
|
||||
"""
|
||||
sync_table(ReservedWordModel)
|
||||
|
||||
model1 = ReservedWordModel.create(token='1', insert=5)
|
||||
|
||||
model2 = ReservedWordModel.filter(token='1')
|
||||
|
||||
assert len(model2) == 1
|
||||
assert model1.token == model2[0].token
|
||||
assert model1.insert == model2[0].insert
|
||||
|
||||
|
||||
class TestQueryModel(Model):
|
||||
|
||||
test_id = columns.UUID(primary_key=True, default=uuid4)
|
||||
date = columns.Date(primary_key=True)
|
||||
description = columns.Text()
|
||||
|
||||
|
||||
class TestQuerying(BaseCassEngTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestQuerying, cls).setUpClass()
|
||||
drop_table(TestQueryModel)
|
||||
sync_table(TestQueryModel)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(TestQuerying, cls).tearDownClass()
|
||||
drop_table(TestQueryModel)
|
||||
|
||||
def test_query_with_date(self):
|
||||
uid = uuid4()
|
||||
day = date(2013, 11, 26)
|
||||
obj = TestQueryModel.create(test_id=uid, date=day, description=u'foo')
|
||||
|
||||
self.assertEqual(obj.description, u'foo')
|
||||
|
||||
inst = TestQueryModel.filter(
|
||||
TestQueryModel.test_id == uid,
|
||||
TestQueryModel.date == day).limit(1).first()
|
||||
|
||||
assert inst.test_id == uid
|
||||
assert inst.date == day
|
||||
|
||||
def test_none_filter_fails():
|
||||
class NoneFilterModel(Model):
|
||||
|
||||
pk = columns.Integer(primary_key=True)
|
||||
v = columns.Integer()
|
||||
sync_table(NoneFilterModel)
|
||||
|
||||
try:
|
||||
NoneFilterModel.objects(pk=None)
|
||||
raise Exception("fail")
|
||||
except CQLEngineException as e:
|
||||
pass
|
||||
|
||||
|
||||
|
241
cqlengine/tests/model/test_polymorphism.py
Normal file
241
cqlengine/tests/model/test_polymorphism.py
Normal file
@ -0,0 +1,241 @@
|
||||
import uuid
|
||||
import mock
|
||||
|
||||
from cqlengine import columns
|
||||
from cqlengine import models
|
||||
from cqlengine.connection import get_session
|
||||
from cqlengine.tests.base import BaseCassEngTestCase
|
||||
from cqlengine import management
|
||||
|
||||
|
||||
class TestPolymorphicClassConstruction(BaseCassEngTestCase):
|
||||
|
||||
def test_multiple_polymorphic_key_failure(self):
|
||||
""" Tests that defining a model with more than one polymorphic key fails """
|
||||
with self.assertRaises(models.ModelDefinitionException):
|
||||
class M(models.Model):
|
||||
|
||||
partition = columns.Integer(primary_key=True)
|
||||
type1 = columns.Integer(polymorphic_key=True)
|
||||
type2 = columns.Integer(polymorphic_key=True)
|
||||
|
||||
def test_polymorphic_key_inheritance(self):
|
||||
""" Tests that polymorphic_key attribute is not inherited """
|
||||
class Base(models.Model):
|
||||
|
||||
partition = columns.Integer(primary_key=True)
|
||||
type1 = columns.Integer(polymorphic_key=True)
|
||||
|
||||
class M1(Base):
|
||||
__polymorphic_key__ = 1
|
||||
|
||||
class M2(M1):
|
||||
pass
|
||||
|
||||
assert M2.__polymorphic_key__ is None
|
||||
|
||||
def test_polymorphic_metaclass(self):
|
||||
""" Tests that the model meta class configures polymorphic models properly """
|
||||
class Base(models.Model):
|
||||
|
||||
partition = columns.Integer(primary_key=True)
|
||||
type1 = columns.Integer(polymorphic_key=True)
|
||||
|
||||
class M1(Base):
|
||||
__polymorphic_key__ = 1
|
||||
|
||||
assert Base._is_polymorphic
|
||||
assert M1._is_polymorphic
|
||||
|
||||
assert Base._is_polymorphic_base
|
||||
assert not M1._is_polymorphic_base
|
||||
|
||||
assert Base._polymorphic_column is Base._columns['type1']
|
||||
assert M1._polymorphic_column is M1._columns['type1']
|
||||
|
||||
assert Base._polymorphic_column_name == 'type1'
|
||||
assert M1._polymorphic_column_name == 'type1'
|
||||
|
||||
def test_table_names_are_inherited_from_poly_base(self):
|
||||
class Base(models.Model):
|
||||
|
||||
partition = columns.Integer(primary_key=True)
|
||||
type1 = columns.Integer(polymorphic_key=True)
|
||||
|
||||
class M1(Base):
|
||||
__polymorphic_key__ = 1
|
||||
|
||||
assert Base.column_family_name() == M1.column_family_name()
|
||||
|
||||
def test_collection_columns_cant_be_polymorphic_keys(self):
|
||||
with self.assertRaises(models.ModelDefinitionException):
|
||||
class Base(models.Model):
|
||||
|
||||
partition = columns.Integer(primary_key=True)
|
||||
type1 = columns.Set(columns.Integer, polymorphic_key=True)
|
||||
|
||||
|
||||
class PolyBase(models.Model):
|
||||
|
||||
partition = columns.UUID(primary_key=True, default=uuid.uuid4)
|
||||
row_type = columns.Integer(polymorphic_key=True)
|
||||
|
||||
|
||||
class Poly1(PolyBase):
|
||||
__polymorphic_key__ = 1
|
||||
data1 = columns.Text()
|
||||
|
||||
|
||||
class Poly2(PolyBase):
|
||||
__polymorphic_key__ = 2
|
||||
data2 = columns.Text()
|
||||
|
||||
|
||||
class TestPolymorphicModel(BaseCassEngTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestPolymorphicModel, cls).setUpClass()
|
||||
management.sync_table(Poly1)
|
||||
management.sync_table(Poly2)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(TestPolymorphicModel, cls).tearDownClass()
|
||||
management.drop_table(Poly1)
|
||||
management.drop_table(Poly2)
|
||||
|
||||
def test_saving_base_model_fails(self):
|
||||
with self.assertRaises(models.PolyMorphicModelException):
|
||||
PolyBase.create()
|
||||
|
||||
def test_saving_subclass_saves_poly_key(self):
|
||||
p1 = Poly1.create(data1='pickle')
|
||||
p2 = Poly2.create(data2='bacon')
|
||||
|
||||
assert p1.row_type == Poly1.__polymorphic_key__
|
||||
assert p2.row_type == Poly2.__polymorphic_key__
|
||||
|
||||
def test_query_deserialization(self):
|
||||
p1 = Poly1.create(data1='pickle')
|
||||
p2 = Poly2.create(data2='bacon')
|
||||
|
||||
p1r = PolyBase.get(partition=p1.partition)
|
||||
p2r = PolyBase.get(partition=p2.partition)
|
||||
|
||||
assert isinstance(p1r, Poly1)
|
||||
assert isinstance(p2r, Poly2)
|
||||
|
||||
def test_delete_on_polymorphic_subclass_does_not_include_polymorphic_key(self):
|
||||
p1 = Poly1.create()
|
||||
session = get_session()
|
||||
with mock.patch.object(session, 'execute') as m:
|
||||
Poly1.objects(partition=p1.partition).delete()
|
||||
|
||||
# make sure our polymorphic key isn't in the CQL
|
||||
# not sure how we would even get here if it was in there
|
||||
# since the CQL would fail.
|
||||
|
||||
self.assertNotIn("row_type", m.call_args[0][0].query_string)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class UnindexedPolyBase(models.Model):
|
||||
|
||||
partition = columns.UUID(primary_key=True, default=uuid.uuid4)
|
||||
cluster = columns.UUID(primary_key=True, default=uuid.uuid4)
|
||||
row_type = columns.Integer(polymorphic_key=True)
|
||||
|
||||
|
||||
class UnindexedPoly1(UnindexedPolyBase):
|
||||
__polymorphic_key__ = 1
|
||||
data1 = columns.Text()
|
||||
|
||||
|
||||
class UnindexedPoly2(UnindexedPolyBase):
|
||||
__polymorphic_key__ = 2
|
||||
data2 = columns.Text()
|
||||
|
||||
|
||||
class UnindexedPoly3(UnindexedPoly2):
|
||||
__polymorphic_key__ = 3
|
||||
data3 = columns.Text()
|
||||
|
||||
|
||||
class TestUnindexedPolymorphicQuery(BaseCassEngTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestUnindexedPolymorphicQuery, cls).setUpClass()
|
||||
management.sync_table(UnindexedPoly1)
|
||||
management.sync_table(UnindexedPoly2)
|
||||
management.sync_table(UnindexedPoly3)
|
||||
|
||||
cls.p1 = UnindexedPoly1.create(data1='pickle')
|
||||
cls.p2 = UnindexedPoly2.create(partition=cls.p1.partition, data2='bacon')
|
||||
cls.p3 = UnindexedPoly3.create(partition=cls.p1.partition, data3='turkey')
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(TestUnindexedPolymorphicQuery, cls).tearDownClass()
|
||||
management.drop_table(UnindexedPoly1)
|
||||
management.drop_table(UnindexedPoly2)
|
||||
management.drop_table(UnindexedPoly3)
|
||||
|
||||
def test_non_conflicting_type_results_work(self):
|
||||
p1, p2, p3 = self.p1, self.p2, self.p3
|
||||
assert len(list(UnindexedPoly1.objects(partition=p1.partition, cluster=p1.cluster))) == 1
|
||||
assert len(list(UnindexedPoly2.objects(partition=p1.partition, cluster=p2.cluster))) == 1
|
||||
|
||||
def test_subclassed_model_results_work_properly(self):
|
||||
p1, p2, p3 = self.p1, self.p2, self.p3
|
||||
assert len(list(UnindexedPoly2.objects(partition=p1.partition, cluster__in=[p2.cluster, p3.cluster]))) == 2
|
||||
|
||||
def test_conflicting_type_results(self):
|
||||
with self.assertRaises(models.PolyMorphicModelException):
|
||||
list(UnindexedPoly1.objects(partition=self.p1.partition))
|
||||
with self.assertRaises(models.PolyMorphicModelException):
|
||||
list(UnindexedPoly2.objects(partition=self.p1.partition))
|
||||
|
||||
|
||||
class IndexedPolyBase(models.Model):
|
||||
|
||||
partition = columns.UUID(primary_key=True, default=uuid.uuid4)
|
||||
cluster = columns.UUID(primary_key=True, default=uuid.uuid4)
|
||||
row_type = columns.Integer(polymorphic_key=True, index=True)
|
||||
|
||||
|
||||
class IndexedPoly1(IndexedPolyBase):
|
||||
__polymorphic_key__ = 1
|
||||
data1 = columns.Text()
|
||||
|
||||
|
||||
class IndexedPoly2(IndexedPolyBase):
|
||||
__polymorphic_key__ = 2
|
||||
data2 = columns.Text()
|
||||
|
||||
|
||||
class TestIndexedPolymorphicQuery(BaseCassEngTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestIndexedPolymorphicQuery, cls).setUpClass()
|
||||
management.sync_table(IndexedPoly1)
|
||||
management.sync_table(IndexedPoly2)
|
||||
|
||||
cls.p1 = IndexedPoly1.create(data1='pickle')
|
||||
cls.p2 = IndexedPoly2.create(partition=cls.p1.partition, data2='bacon')
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(TestIndexedPolymorphicQuery, cls).tearDownClass()
|
||||
management.drop_table(IndexedPoly1)
|
||||
management.drop_table(IndexedPoly2)
|
||||
|
||||
def test_success_case(self):
|
||||
assert len(list(IndexedPoly1.objects(partition=self.p1.partition))) == 1
|
||||
assert len(list(IndexedPoly2.objects(partition=self.p1.partition))) == 1
|
||||
|
||||
|
91
cqlengine/tests/model/test_updates.py
Normal file
91
cqlengine/tests/model/test_updates.py
Normal file
@ -0,0 +1,91 @@
|
||||
from uuid import uuid4
|
||||
|
||||
from mock import patch
|
||||
from cqlengine.exceptions import ValidationError
|
||||
|
||||
from cqlengine.tests.base import BaseCassEngTestCase
|
||||
from cqlengine.models import Model
|
||||
from cqlengine import columns
|
||||
from cqlengine.management import sync_table, drop_table
|
||||
|
||||
|
||||
class TestUpdateModel(Model):
|
||||
|
||||
partition = columns.UUID(primary_key=True, default=uuid4)
|
||||
cluster = columns.UUID(primary_key=True, default=uuid4)
|
||||
count = columns.Integer(required=False)
|
||||
text = columns.Text(required=False, index=True)
|
||||
|
||||
|
||||
class ModelUpdateTests(BaseCassEngTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(ModelUpdateTests, cls).setUpClass()
|
||||
sync_table(TestUpdateModel)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(ModelUpdateTests, cls).tearDownClass()
|
||||
drop_table(TestUpdateModel)
|
||||
|
||||
def test_update_model(self):
|
||||
""" tests calling udpate on models with no values passed in """
|
||||
m0 = TestUpdateModel.create(count=5, text='monkey')
|
||||
|
||||
# independently save over a new count value, unknown to original instance
|
||||
m1 = TestUpdateModel.get(partition=m0.partition, cluster=m0.cluster)
|
||||
m1.count = 6
|
||||
m1.save()
|
||||
|
||||
# update the text, and call update
|
||||
m0.text = 'monkey land'
|
||||
m0.update()
|
||||
|
||||
# database should reflect both updates
|
||||
m2 = TestUpdateModel.get(partition=m0.partition, cluster=m0.cluster)
|
||||
self.assertEqual(m2.count, m1.count)
|
||||
self.assertEqual(m2.text, m0.text)
|
||||
|
||||
def test_update_values(self):
|
||||
""" tests calling update on models with values passed in """
|
||||
m0 = TestUpdateModel.create(count=5, text='monkey')
|
||||
|
||||
# independently save over a new count value, unknown to original instance
|
||||
m1 = TestUpdateModel.get(partition=m0.partition, cluster=m0.cluster)
|
||||
m1.count = 6
|
||||
m1.save()
|
||||
|
||||
# update the text, and call update
|
||||
m0.update(text='monkey land')
|
||||
self.assertEqual(m0.text, 'monkey land')
|
||||
|
||||
# database should reflect both updates
|
||||
m2 = TestUpdateModel.get(partition=m0.partition, cluster=m0.cluster)
|
||||
self.assertEqual(m2.count, m1.count)
|
||||
self.assertEqual(m2.text, m0.text)
|
||||
|
||||
def test_noop_model_update(self):
|
||||
""" tests that calling update on a model with no changes will do nothing. """
|
||||
m0 = TestUpdateModel.create(count=5, text='monkey')
|
||||
|
||||
with patch.object(self.session, 'execute') as execute:
|
||||
m0.update()
|
||||
assert execute.call_count == 0
|
||||
|
||||
with patch.object(self.session, 'execute') as execute:
|
||||
m0.update(count=5)
|
||||
assert execute.call_count == 0
|
||||
|
||||
def test_invalid_update_kwarg(self):
|
||||
""" tests that passing in a kwarg to the update method that isn't a column will fail """
|
||||
m0 = TestUpdateModel.create(count=5, text='monkey')
|
||||
with self.assertRaises(ValidationError):
|
||||
m0.update(numbers=20)
|
||||
|
||||
def test_primary_key_update_failure(self):
|
||||
""" tests that attempting to update the value of a primary key will fail """
|
||||
m0 = TestUpdateModel.create(count=5, text='monkey')
|
||||
with self.assertRaises(ValidationError):
|
||||
m0.update(partition=uuid4())
|
||||
|
1
cqlengine/tests/model/test_validation.py
Normal file
1
cqlengine/tests/model/test_validation.py
Normal file
@ -0,0 +1 @@
|
||||
|
61
cqlengine/tests/model/test_value_lists.py
Normal file
61
cqlengine/tests/model/test_value_lists.py
Normal file
@ -0,0 +1,61 @@
|
||||
import random
|
||||
from cqlengine.tests.base import BaseCassEngTestCase
|
||||
|
||||
from cqlengine.management import sync_table
|
||||
from cqlengine.management import drop_table
|
||||
from cqlengine.models import Model
|
||||
from cqlengine import columns
|
||||
|
||||
|
||||
class TestModel(Model):
|
||||
|
||||
id = columns.Integer(primary_key=True)
|
||||
clustering_key = columns.Integer(primary_key=True, clustering_order='desc')
|
||||
|
||||
class TestClusteringComplexModel(Model):
|
||||
|
||||
id = columns.Integer(primary_key=True)
|
||||
clustering_key = columns.Integer(primary_key=True, clustering_order='desc')
|
||||
some_value = columns.Integer()
|
||||
|
||||
class TestClusteringOrder(BaseCassEngTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestClusteringOrder, cls).setUpClass()
|
||||
sync_table(TestModel)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(TestClusteringOrder, cls).tearDownClass()
|
||||
drop_table(TestModel)
|
||||
|
||||
def test_clustering_order(self):
|
||||
"""
|
||||
Tests that models can be saved and retrieved
|
||||
"""
|
||||
items = list(range(20))
|
||||
random.shuffle(items)
|
||||
for i in items:
|
||||
TestModel.create(id=1, clustering_key=i)
|
||||
|
||||
values = list(TestModel.objects.values_list('clustering_key', flat=True))
|
||||
# [19L, 18L, 17L, 16L, 15L, 14L, 13L, 12L, 11L, 10L, 9L, 8L, 7L, 6L, 5L, 4L, 3L, 2L, 1L, 0L]
|
||||
self.assertEquals(values, sorted(items, reverse=True))
|
||||
|
||||
def test_clustering_order_more_complex(self):
|
||||
"""
|
||||
Tests that models can be saved and retrieved
|
||||
"""
|
||||
sync_table(TestClusteringComplexModel)
|
||||
|
||||
items = list(range(20))
|
||||
random.shuffle(items)
|
||||
for i in items:
|
||||
TestClusteringComplexModel.create(id=1, clustering_key=i, some_value=2)
|
||||
|
||||
values = list(TestClusteringComplexModel.objects.values_list('some_value', flat=True))
|
||||
|
||||
self.assertEquals([2] * 20, values)
|
||||
drop_table(TestClusteringComplexModel)
|
||||
|
1
cqlengine/tests/operators/__init__.py
Normal file
1
cqlengine/tests/operators/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
__author__ = 'bdeggleston'
|
9
cqlengine/tests/operators/test_base_operator.py
Normal file
9
cqlengine/tests/operators/test_base_operator.py
Normal file
@ -0,0 +1,9 @@
|
||||
from unittest import TestCase
|
||||
from cqlengine.operators import BaseQueryOperator, QueryOperatorException
|
||||
|
||||
|
||||
class BaseOperatorTest(TestCase):
|
||||
|
||||
def test_get_operator_cannot_be_called_from_base_class(self):
|
||||
with self.assertRaises(QueryOperatorException):
|
||||
BaseQueryOperator.get_operator('*')
|
31
cqlengine/tests/operators/test_where_operators.py
Normal file
31
cqlengine/tests/operators/test_where_operators.py
Normal file
@ -0,0 +1,31 @@
|
||||
from unittest import TestCase
|
||||
from cqlengine.operators import *
|
||||
|
||||
import six
|
||||
|
||||
class TestWhereOperators(TestCase):
|
||||
|
||||
def test_symbol_lookup(self):
|
||||
""" tests where symbols are looked up properly """
|
||||
|
||||
def check_lookup(symbol, expected):
|
||||
op = BaseWhereOperator.get_operator(symbol)
|
||||
self.assertEqual(op, expected)
|
||||
|
||||
check_lookup('EQ', EqualsOperator)
|
||||
check_lookup('IN', InOperator)
|
||||
check_lookup('GT', GreaterThanOperator)
|
||||
check_lookup('GTE', GreaterThanOrEqualOperator)
|
||||
check_lookup('LT', LessThanOperator)
|
||||
check_lookup('LTE', LessThanOrEqualOperator)
|
||||
|
||||
def test_operator_rendering(self):
|
||||
""" tests symbols are rendered properly """
|
||||
self.assertEqual("=", six.text_type(EqualsOperator()))
|
||||
self.assertEqual("IN", six.text_type(InOperator()))
|
||||
self.assertEqual(">", six.text_type(GreaterThanOperator()))
|
||||
self.assertEqual(">=", six.text_type(GreaterThanOrEqualOperator()))
|
||||
self.assertEqual("<", six.text_type(LessThanOperator()))
|
||||
self.assertEqual("<=", six.text_type(LessThanOrEqualOperator()))
|
||||
|
||||
|
0
cqlengine/tests/query/__init__.py
Normal file
0
cqlengine/tests/query/__init__.py
Normal file
187
cqlengine/tests/query/test_batch_query.py
Normal file
187
cqlengine/tests/query/test_batch_query.py
Normal file
@ -0,0 +1,187 @@
|
||||
from datetime import datetime
|
||||
from unittest import skip
|
||||
from uuid import uuid4
|
||||
import random
|
||||
from cqlengine import Model, columns
|
||||
from cqlengine.connection import NOT_SET
|
||||
from cqlengine.management import drop_table, sync_table
|
||||
from cqlengine.query import BatchQuery, DMLQuery
|
||||
from cqlengine.tests.base import BaseCassEngTestCase
|
||||
from cassandra.cluster import Session
|
||||
import mock
|
||||
|
||||
|
||||
class TestMultiKeyModel(Model):
|
||||
|
||||
partition = columns.Integer(primary_key=True)
|
||||
cluster = columns.Integer(primary_key=True)
|
||||
count = columns.Integer(required=False)
|
||||
text = columns.Text(required=False)
|
||||
|
||||
class BatchQueryLogModel(Model):
|
||||
|
||||
# simple k/v table
|
||||
k = columns.Integer(primary_key=True)
|
||||
v = columns.Integer()
|
||||
|
||||
class BatchQueryTests(BaseCassEngTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(BatchQueryTests, cls).setUpClass()
|
||||
drop_table(TestMultiKeyModel)
|
||||
sync_table(TestMultiKeyModel)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(BatchQueryTests, cls).tearDownClass()
|
||||
drop_table(TestMultiKeyModel)
|
||||
|
||||
def setUp(self):
|
||||
super(BatchQueryTests, self).setUp()
|
||||
self.pkey = 1
|
||||
for obj in TestMultiKeyModel.filter(partition=self.pkey):
|
||||
obj.delete()
|
||||
|
||||
def test_insert_success_case(self):
|
||||
|
||||
b = BatchQuery()
|
||||
inst = TestMultiKeyModel.batch(b).create(partition=self.pkey, cluster=2, count=3, text='4')
|
||||
|
||||
with self.assertRaises(TestMultiKeyModel.DoesNotExist):
|
||||
TestMultiKeyModel.get(partition=self.pkey, cluster=2)
|
||||
|
||||
b.execute()
|
||||
|
||||
TestMultiKeyModel.get(partition=self.pkey, cluster=2)
|
||||
|
||||
def test_update_success_case(self):
|
||||
|
||||
inst = TestMultiKeyModel.create(partition=self.pkey, cluster=2, count=3, text='4')
|
||||
|
||||
b = BatchQuery()
|
||||
|
||||
inst.count = 4
|
||||
inst.batch(b).save()
|
||||
|
||||
inst2 = TestMultiKeyModel.get(partition=self.pkey, cluster=2)
|
||||
assert inst2.count == 3
|
||||
|
||||
b.execute()
|
||||
|
||||
inst3 = TestMultiKeyModel.get(partition=self.pkey, cluster=2)
|
||||
assert inst3.count == 4
|
||||
|
||||
def test_delete_success_case(self):
|
||||
|
||||
inst = TestMultiKeyModel.create(partition=self.pkey, cluster=2, count=3, text='4')
|
||||
|
||||
b = BatchQuery()
|
||||
|
||||
inst.batch(b).delete()
|
||||
|
||||
TestMultiKeyModel.get(partition=self.pkey, cluster=2)
|
||||
|
||||
b.execute()
|
||||
|
||||
with self.assertRaises(TestMultiKeyModel.DoesNotExist):
|
||||
TestMultiKeyModel.get(partition=self.pkey, cluster=2)
|
||||
|
||||
def test_context_manager(self):
|
||||
|
||||
with BatchQuery() as b:
|
||||
for i in range(5):
|
||||
TestMultiKeyModel.batch(b).create(partition=self.pkey, cluster=i, count=3, text='4')
|
||||
|
||||
for i in range(5):
|
||||
with self.assertRaises(TestMultiKeyModel.DoesNotExist):
|
||||
TestMultiKeyModel.get(partition=self.pkey, cluster=i)
|
||||
|
||||
for i in range(5):
|
||||
TestMultiKeyModel.get(partition=self.pkey, cluster=i)
|
||||
|
||||
def test_bulk_delete_success_case(self):
|
||||
|
||||
for i in range(1):
|
||||
for j in range(5):
|
||||
TestMultiKeyModel.create(partition=i, cluster=j, count=i*j, text='{}:{}'.format(i,j))
|
||||
|
||||
with BatchQuery() as b:
|
||||
TestMultiKeyModel.objects.batch(b).filter(partition=0).delete()
|
||||
assert TestMultiKeyModel.filter(partition=0).count() == 5
|
||||
|
||||
assert TestMultiKeyModel.filter(partition=0).count() == 0
|
||||
#cleanup
|
||||
for m in TestMultiKeyModel.all():
|
||||
m.delete()
|
||||
|
||||
def test_none_success_case(self):
|
||||
""" Tests that passing None into the batch call clears any batch object """
|
||||
b = BatchQuery()
|
||||
|
||||
q = TestMultiKeyModel.objects.batch(b)
|
||||
assert q._batch == b
|
||||
|
||||
q = q.batch(None)
|
||||
assert q._batch is None
|
||||
|
||||
def test_dml_none_success_case(self):
|
||||
""" Tests that passing None into the batch call clears any batch object """
|
||||
b = BatchQuery()
|
||||
|
||||
q = DMLQuery(TestMultiKeyModel, batch=b)
|
||||
assert q._batch == b
|
||||
|
||||
q.batch(None)
|
||||
assert q._batch is None
|
||||
|
||||
def test_batch_execute_on_exception_succeeds(self):
|
||||
# makes sure if execute_on_exception == True we still apply the batch
|
||||
drop_table(BatchQueryLogModel)
|
||||
sync_table(BatchQueryLogModel)
|
||||
|
||||
obj = BatchQueryLogModel.objects(k=1)
|
||||
self.assertEqual(0, len(obj))
|
||||
|
||||
try:
|
||||
with BatchQuery(execute_on_exception=True) as b:
|
||||
BatchQueryLogModel.batch(b).create(k=1, v=1)
|
||||
raise Exception("Blah")
|
||||
except:
|
||||
pass
|
||||
|
||||
obj = BatchQueryLogModel.objects(k=1)
|
||||
# should be 1 because the batch should execute
|
||||
self.assertEqual(1, len(obj))
|
||||
|
||||
def test_batch_execute_on_exception_skips_if_not_specified(self):
|
||||
# makes sure if execute_on_exception == True we still apply the batch
|
||||
drop_table(BatchQueryLogModel)
|
||||
sync_table(BatchQueryLogModel)
|
||||
|
||||
obj = BatchQueryLogModel.objects(k=2)
|
||||
self.assertEqual(0, len(obj))
|
||||
|
||||
try:
|
||||
with BatchQuery() as b:
|
||||
BatchQueryLogModel.batch(b).create(k=2, v=2)
|
||||
raise Exception("Blah")
|
||||
except:
|
||||
pass
|
||||
|
||||
obj = BatchQueryLogModel.objects(k=2)
|
||||
|
||||
# should be 0 because the batch should not execute
|
||||
self.assertEqual(0, len(obj))
|
||||
|
||||
def test_batch_execute_timeout(self):
|
||||
with mock.patch.object(Session, 'execute', autospec=True) as mock_execute:
|
||||
with BatchQuery(timeout=1) as b:
|
||||
BatchQueryLogModel.batch(b).create(k=2, v=2)
|
||||
mock_execute.assert_called_once_with(mock.ANY, mock.ANY, mock.ANY, timeout=1)
|
||||
|
||||
def test_batch_execute_no_timeout(self):
|
||||
with mock.patch.object(Session, 'execute', autospec=True) as mock_execute:
|
||||
with BatchQuery() as b:
|
||||
BatchQueryLogModel.batch(b).create(k=2, v=2)
|
||||
mock_execute.assert_called_once_with(mock.ANY, mock.ANY, mock.ANY, timeout=NOT_SET)
|
58
cqlengine/tests/query/test_datetime_queries.py
Normal file
58
cqlengine/tests/query/test_datetime_queries.py
Normal file
@ -0,0 +1,58 @@
|
||||
from datetime import datetime, timedelta
|
||||
from uuid import uuid4
|
||||
|
||||
from cqlengine.tests.base import BaseCassEngTestCase
|
||||
|
||||
from cqlengine.exceptions import ModelException
|
||||
from cqlengine.management import sync_table
|
||||
from cqlengine.management import drop_table
|
||||
from cqlengine.models import Model
|
||||
from cqlengine import columns
|
||||
from cqlengine import query
|
||||
|
||||
class DateTimeQueryTestModel(Model):
|
||||
|
||||
user = columns.Integer(primary_key=True)
|
||||
day = columns.DateTime(primary_key=True)
|
||||
data = columns.Text()
|
||||
|
||||
class TestDateTimeQueries(BaseCassEngTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestDateTimeQueries, cls).setUpClass()
|
||||
sync_table(DateTimeQueryTestModel)
|
||||
|
||||
cls.base_date = datetime.now() - timedelta(days=10)
|
||||
for x in range(7):
|
||||
for y in range(10):
|
||||
DateTimeQueryTestModel.create(
|
||||
user=x,
|
||||
day=(cls.base_date+timedelta(days=y)),
|
||||
data=str(uuid4())
|
||||
)
|
||||
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(TestDateTimeQueries, cls).tearDownClass()
|
||||
drop_table(DateTimeQueryTestModel)
|
||||
|
||||
def test_range_query(self):
|
||||
""" Tests that loading from a range of dates works properly """
|
||||
start = datetime(*self.base_date.timetuple()[:3])
|
||||
end = start + timedelta(days=3)
|
||||
|
||||
results = DateTimeQueryTestModel.filter(user=0, day__gte=start, day__lt=end)
|
||||
assert len(results) == 3
|
||||
|
||||
def test_datetime_precision(self):
|
||||
""" Tests that millisecond resolution is preserved when saving datetime objects """
|
||||
now = datetime.now()
|
||||
pk = 1000
|
||||
obj = DateTimeQueryTestModel.create(user=pk, day=now, data='energy cheese')
|
||||
load = DateTimeQueryTestModel.get(user=pk)
|
||||
|
||||
assert abs(now - load.day).total_seconds() < 0.001
|
||||
obj.delete()
|
||||
|
246
cqlengine/tests/query/test_named.py
Normal file
246
cqlengine/tests/query/test_named.py
Normal file
@ -0,0 +1,246 @@
|
||||
from cqlengine import operators
|
||||
from cqlengine.named import NamedKeyspace
|
||||
from cqlengine.operators import EqualsOperator, GreaterThanOrEqualOperator
|
||||
from cqlengine.query import ResultObject
|
||||
from cqlengine.tests.query.test_queryset import BaseQuerySetUsage
|
||||
from cqlengine.tests.base import BaseCassEngTestCase
|
||||
|
||||
|
||||
class TestQuerySetOperation(BaseCassEngTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestQuerySetOperation, cls).setUpClass()
|
||||
cls.keyspace = NamedKeyspace('cqlengine_test')
|
||||
cls.table = cls.keyspace.table('test_model')
|
||||
|
||||
def test_query_filter_parsing(self):
|
||||
"""
|
||||
Tests the queryset filter method parses it's kwargs properly
|
||||
"""
|
||||
query1 = self.table.objects(test_id=5)
|
||||
assert len(query1._where) == 1
|
||||
|
||||
op = query1._where[0]
|
||||
assert isinstance(op.operator, operators.EqualsOperator)
|
||||
assert op.value == 5
|
||||
|
||||
query2 = query1.filter(expected_result__gte=1)
|
||||
assert len(query2._where) == 2
|
||||
|
||||
op = query2._where[1]
|
||||
assert isinstance(op.operator, operators.GreaterThanOrEqualOperator)
|
||||
assert op.value == 1
|
||||
|
||||
def test_query_expression_parsing(self):
|
||||
""" Tests that query experessions are evaluated properly """
|
||||
query1 = self.table.filter(self.table.column('test_id') == 5)
|
||||
assert len(query1._where) == 1
|
||||
|
||||
op = query1._where[0]
|
||||
assert isinstance(op.operator, operators.EqualsOperator)
|
||||
assert op.value == 5
|
||||
|
||||
query2 = query1.filter(self.table.column('expected_result') >= 1)
|
||||
assert len(query2._where) == 2
|
||||
|
||||
op = query2._where[1]
|
||||
assert isinstance(op.operator, operators.GreaterThanOrEqualOperator)
|
||||
assert op.value == 1
|
||||
|
||||
def test_filter_method_where_clause_generation(self):
|
||||
"""
|
||||
Tests the where clause creation
|
||||
"""
|
||||
query1 = self.table.objects(test_id=5)
|
||||
self.assertEqual(len(query1._where), 1)
|
||||
where = query1._where[0]
|
||||
self.assertEqual(where.field, 'test_id')
|
||||
self.assertEqual(where.value, 5)
|
||||
|
||||
query2 = query1.filter(expected_result__gte=1)
|
||||
self.assertEqual(len(query2._where), 2)
|
||||
|
||||
where = query2._where[0]
|
||||
self.assertEqual(where.field, 'test_id')
|
||||
self.assertIsInstance(where.operator, EqualsOperator)
|
||||
self.assertEqual(where.value, 5)
|
||||
|
||||
where = query2._where[1]
|
||||
self.assertEqual(where.field, 'expected_result')
|
||||
self.assertIsInstance(where.operator, GreaterThanOrEqualOperator)
|
||||
self.assertEqual(where.value, 1)
|
||||
|
||||
def test_query_expression_where_clause_generation(self):
|
||||
"""
|
||||
Tests the where clause creation
|
||||
"""
|
||||
query1 = self.table.objects(self.table.column('test_id') == 5)
|
||||
self.assertEqual(len(query1._where), 1)
|
||||
where = query1._where[0]
|
||||
self.assertEqual(where.field, 'test_id')
|
||||
self.assertEqual(where.value, 5)
|
||||
|
||||
query2 = query1.filter(self.table.column('expected_result') >= 1)
|
||||
self.assertEqual(len(query2._where), 2)
|
||||
|
||||
where = query2._where[0]
|
||||
self.assertEqual(where.field, 'test_id')
|
||||
self.assertIsInstance(where.operator, EqualsOperator)
|
||||
self.assertEqual(where.value, 5)
|
||||
|
||||
where = query2._where[1]
|
||||
self.assertEqual(where.field, 'expected_result')
|
||||
self.assertIsInstance(where.operator, GreaterThanOrEqualOperator)
|
||||
self.assertEqual(where.value, 1)
|
||||
|
||||
|
||||
class TestQuerySetCountSelectionAndIteration(BaseQuerySetUsage):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestQuerySetCountSelectionAndIteration, cls).setUpClass()
|
||||
|
||||
from cqlengine.tests.query.test_queryset import TestModel
|
||||
|
||||
ks,tn = TestModel.column_family_name().split('.')
|
||||
cls.keyspace = NamedKeyspace(ks)
|
||||
cls.table = cls.keyspace.table(tn)
|
||||
|
||||
def test_count(self):
|
||||
""" Tests that adding filtering statements affects the count query as expected """
|
||||
assert self.table.objects.count() == 12
|
||||
|
||||
q = self.table.objects(test_id=0)
|
||||
assert q.count() == 4
|
||||
|
||||
def test_query_expression_count(self):
|
||||
""" Tests that adding query statements affects the count query as expected """
|
||||
assert self.table.objects.count() == 12
|
||||
|
||||
q = self.table.objects(self.table.column('test_id') == 0)
|
||||
assert q.count() == 4
|
||||
|
||||
def test_iteration(self):
|
||||
""" Tests that iterating over a query set pulls back all of the expected results """
|
||||
q = self.table.objects(test_id=0)
|
||||
#tuple of expected attempt_id, expected_result values
|
||||
compare_set = set([(0,5), (1,10), (2,15), (3,20)])
|
||||
for t in q:
|
||||
val = t.attempt_id, t.expected_result
|
||||
assert val in compare_set
|
||||
compare_set.remove(val)
|
||||
assert len(compare_set) == 0
|
||||
|
||||
# test with regular filtering
|
||||
q = self.table.objects(attempt_id=3).allow_filtering()
|
||||
assert len(q) == 3
|
||||
#tuple of expected test_id, expected_result values
|
||||
compare_set = set([(0,20), (1,20), (2,75)])
|
||||
for t in q:
|
||||
val = t.test_id, t.expected_result
|
||||
assert val in compare_set
|
||||
compare_set.remove(val)
|
||||
assert len(compare_set) == 0
|
||||
|
||||
# test with query method
|
||||
q = self.table.objects(self.table.column('attempt_id') == 3).allow_filtering()
|
||||
assert len(q) == 3
|
||||
#tuple of expected test_id, expected_result values
|
||||
compare_set = set([(0,20), (1,20), (2,75)])
|
||||
for t in q:
|
||||
val = t.test_id, t.expected_result
|
||||
assert val in compare_set
|
||||
compare_set.remove(val)
|
||||
assert len(compare_set) == 0
|
||||
|
||||
def test_multiple_iterations_work_properly(self):
|
||||
""" Tests that iterating over a query set more than once works """
|
||||
# test with both the filtering method and the query method
|
||||
for q in (self.table.objects(test_id=0), self.table.objects(self.table.column('test_id') == 0)):
|
||||
#tuple of expected attempt_id, expected_result values
|
||||
compare_set = set([(0,5), (1,10), (2,15), (3,20)])
|
||||
for t in q:
|
||||
val = t.attempt_id, t.expected_result
|
||||
assert val in compare_set
|
||||
compare_set.remove(val)
|
||||
assert len(compare_set) == 0
|
||||
|
||||
#try it again
|
||||
compare_set = set([(0,5), (1,10), (2,15), (3,20)])
|
||||
for t in q:
|
||||
val = t.attempt_id, t.expected_result
|
||||
assert val in compare_set
|
||||
compare_set.remove(val)
|
||||
assert len(compare_set) == 0
|
||||
|
||||
def test_multiple_iterators_are_isolated(self):
|
||||
"""
|
||||
tests that the use of one iterator does not affect the behavior of another
|
||||
"""
|
||||
for q in (self.table.objects(test_id=0), self.table.objects(self.table.column('test_id') == 0)):
|
||||
q = q.order_by('attempt_id')
|
||||
expected_order = [0,1,2,3]
|
||||
iter1 = iter(q)
|
||||
iter2 = iter(q)
|
||||
for attempt_id in expected_order:
|
||||
assert next(iter1).attempt_id == attempt_id
|
||||
assert next(iter2).attempt_id == attempt_id
|
||||
|
||||
def test_get_success_case(self):
|
||||
"""
|
||||
Tests that the .get() method works on new and existing querysets
|
||||
"""
|
||||
m = self.table.objects.get(test_id=0, attempt_id=0)
|
||||
assert isinstance(m, ResultObject)
|
||||
assert m.test_id == 0
|
||||
assert m.attempt_id == 0
|
||||
|
||||
q = self.table.objects(test_id=0, attempt_id=0)
|
||||
m = q.get()
|
||||
assert isinstance(m, ResultObject)
|
||||
assert m.test_id == 0
|
||||
assert m.attempt_id == 0
|
||||
|
||||
q = self.table.objects(test_id=0)
|
||||
m = q.get(attempt_id=0)
|
||||
assert isinstance(m, ResultObject)
|
||||
assert m.test_id == 0
|
||||
assert m.attempt_id == 0
|
||||
|
||||
def test_query_expression_get_success_case(self):
|
||||
"""
|
||||
Tests that the .get() method works on new and existing querysets
|
||||
"""
|
||||
m = self.table.get(self.table.column('test_id') == 0, self.table.column('attempt_id') == 0)
|
||||
assert isinstance(m, ResultObject)
|
||||
assert m.test_id == 0
|
||||
assert m.attempt_id == 0
|
||||
|
||||
q = self.table.objects(self.table.column('test_id') == 0, self.table.column('attempt_id') == 0)
|
||||
m = q.get()
|
||||
assert isinstance(m, ResultObject)
|
||||
assert m.test_id == 0
|
||||
assert m.attempt_id == 0
|
||||
|
||||
q = self.table.objects(self.table.column('test_id') == 0)
|
||||
m = q.get(self.table.column('attempt_id') == 0)
|
||||
assert isinstance(m, ResultObject)
|
||||
assert m.test_id == 0
|
||||
assert m.attempt_id == 0
|
||||
|
||||
def test_get_doesnotexist_exception(self):
|
||||
"""
|
||||
Tests that get calls that don't return a result raises a DoesNotExist error
|
||||
"""
|
||||
with self.assertRaises(self.table.DoesNotExist):
|
||||
self.table.objects.get(test_id=100)
|
||||
|
||||
def test_get_multipleobjects_exception(self):
|
||||
"""
|
||||
Tests that get calls that return multiple results raise a MultipleObjectsReturned error
|
||||
"""
|
||||
with self.assertRaises(self.table.MultipleObjectsReturned):
|
||||
self.table.objects.get(test_id=1)
|
||||
|
||||
|
111
cqlengine/tests/query/test_queryoperators.py
Normal file
111
cqlengine/tests/query/test_queryoperators.py
Normal file
@ -0,0 +1,111 @@
|
||||
from datetime import datetime
|
||||
from cqlengine.columns import DateTime
|
||||
|
||||
from cqlengine.tests.base import BaseCassEngTestCase
|
||||
from cqlengine import columns, Model
|
||||
from cqlengine import functions
|
||||
from cqlengine import query
|
||||
from cqlengine.statements import WhereClause
|
||||
from cqlengine.operators import EqualsOperator
|
||||
from cqlengine.management import sync_table, drop_table
|
||||
|
||||
class TestQuerySetOperation(BaseCassEngTestCase):
|
||||
|
||||
def test_maxtimeuuid_function(self):
|
||||
"""
|
||||
Tests that queries with helper functions are generated properly
|
||||
"""
|
||||
now = datetime.now()
|
||||
where = WhereClause('time', EqualsOperator(), functions.MaxTimeUUID(now))
|
||||
where.set_context_id(5)
|
||||
|
||||
self.assertEqual(str(where), '"time" = MaxTimeUUID(%(5)s)')
|
||||
ctx = {}
|
||||
where.update_context(ctx)
|
||||
self.assertEqual(ctx, {'5': DateTime().to_database(now)})
|
||||
|
||||
def test_mintimeuuid_function(self):
|
||||
"""
|
||||
Tests that queries with helper functions are generated properly
|
||||
"""
|
||||
now = datetime.now()
|
||||
where = WhereClause('time', EqualsOperator(), functions.MinTimeUUID(now))
|
||||
where.set_context_id(5)
|
||||
|
||||
self.assertEqual(str(where), '"time" = MinTimeUUID(%(5)s)')
|
||||
ctx = {}
|
||||
where.update_context(ctx)
|
||||
self.assertEqual(ctx, {'5': DateTime().to_database(now)})
|
||||
|
||||
|
||||
class TokenTestModel(Model):
|
||||
|
||||
key = columns.Integer(primary_key=True)
|
||||
val = columns.Integer()
|
||||
|
||||
|
||||
class TestTokenFunction(BaseCassEngTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestTokenFunction, self).setUp()
|
||||
sync_table(TokenTestModel)
|
||||
|
||||
def tearDown(self):
|
||||
super(TestTokenFunction, self).tearDown()
|
||||
drop_table(TokenTestModel)
|
||||
|
||||
def test_token_function(self):
|
||||
""" Tests that token functions work properly """
|
||||
assert TokenTestModel.objects().count() == 0
|
||||
for i in range(10):
|
||||
TokenTestModel.create(key=i, val=i)
|
||||
assert TokenTestModel.objects().count() == 10
|
||||
seen_keys = set()
|
||||
last_token = None
|
||||
for instance in TokenTestModel.objects().limit(5):
|
||||
last_token = instance.key
|
||||
seen_keys.add(last_token)
|
||||
assert len(seen_keys) == 5
|
||||
for instance in TokenTestModel.objects(pk__token__gt=functions.Token(last_token)):
|
||||
seen_keys.add(instance.key)
|
||||
|
||||
assert len(seen_keys) == 10
|
||||
assert all([i in seen_keys for i in range(10)])
|
||||
|
||||
def test_compound_pk_token_function(self):
|
||||
|
||||
class TestModel(Model):
|
||||
|
||||
p1 = columns.Text(partition_key=True)
|
||||
p2 = columns.Text(partition_key=True)
|
||||
|
||||
func = functions.Token('a', 'b')
|
||||
|
||||
q = TestModel.objects.filter(pk__token__gt=func)
|
||||
where = q._where[0]
|
||||
where.set_context_id(1)
|
||||
self.assertEquals(str(where), 'token("p1", "p2") > token(%({})s, %({})s)'.format(1, 2))
|
||||
|
||||
# Verify that a SELECT query can be successfully generated
|
||||
str(q._select_query())
|
||||
|
||||
# Token(tuple()) is also possible for convenience
|
||||
# it (allows for Token(obj.pk) syntax)
|
||||
func = functions.Token(('a', 'b'))
|
||||
|
||||
q = TestModel.objects.filter(pk__token__gt=func)
|
||||
where = q._where[0]
|
||||
where.set_context_id(1)
|
||||
self.assertEquals(str(where), 'token("p1", "p2") > token(%({})s, %({})s)'.format(1, 2))
|
||||
str(q._select_query())
|
||||
|
||||
# The 'pk__token' virtual column may only be compared to a Token
|
||||
self.assertRaises(query.QueryException, TestModel.objects.filter, pk__token__gt=10)
|
||||
|
||||
# A Token may only be compared to the `pk__token' virtual column
|
||||
func = functions.Token('a', 'b')
|
||||
self.assertRaises(query.QueryException, TestModel.objects.filter, p1__gt=func)
|
||||
|
||||
# The # of arguments to Token must match the # of partition keys
|
||||
func = functions.Token('a')
|
||||
self.assertRaises(query.QueryException, TestModel.objects.filter, pk__token__gt=func)
|
756
cqlengine/tests/query/test_queryset.py
Normal file
756
cqlengine/tests/query/test_queryset.py
Normal file
@ -0,0 +1,756 @@
|
||||
from __future__ import absolute_import
|
||||
from datetime import datetime
|
||||
import time
|
||||
from unittest import TestCase, skipUnless
|
||||
from uuid import uuid1, uuid4
|
||||
import uuid
|
||||
|
||||
from cassandra.cluster import Session
|
||||
from cqlengine.tests.base import BaseCassEngTestCase
|
||||
from cqlengine.connection import NOT_SET
|
||||
import mock
|
||||
from cqlengine import functions
|
||||
from cqlengine.management import sync_table, drop_table
|
||||
from cqlengine.models import Model
|
||||
from cqlengine import columns
|
||||
from cqlengine import query
|
||||
from datetime import timedelta
|
||||
from datetime import tzinfo
|
||||
|
||||
from cqlengine import statements
|
||||
from cqlengine import operators
|
||||
|
||||
from cqlengine.connection import get_session
|
||||
from cqlengine.tests.base import PROTOCOL_VERSION
|
||||
|
||||
|
||||
class TzOffset(tzinfo):
|
||||
"""Minimal implementation of a timezone offset to help testing with timezone
|
||||
aware datetimes.
|
||||
"""
|
||||
|
||||
def __init__(self, offset):
|
||||
self._offset = timedelta(hours=offset)
|
||||
|
||||
def utcoffset(self, dt):
|
||||
return self._offset
|
||||
|
||||
def tzname(self, dt):
|
||||
return 'TzOffset: {}'.format(self._offset.hours)
|
||||
|
||||
def dst(self, dt):
|
||||
return timedelta(0)
|
||||
|
||||
|
||||
class TestModel(Model):
|
||||
|
||||
test_id = columns.Integer(primary_key=True)
|
||||
attempt_id = columns.Integer(primary_key=True)
|
||||
description = columns.Text()
|
||||
expected_result = columns.Integer()
|
||||
test_result = columns.Integer()
|
||||
|
||||
|
||||
class IndexedTestModel(Model):
|
||||
|
||||
test_id = columns.Integer(primary_key=True)
|
||||
attempt_id = columns.Integer(index=True)
|
||||
description = columns.Text()
|
||||
expected_result = columns.Integer()
|
||||
test_result = columns.Integer(index=True)
|
||||
|
||||
|
||||
class TestMultiClusteringModel(Model):
|
||||
|
||||
one = columns.Integer(primary_key=True)
|
||||
two = columns.Integer(primary_key=True)
|
||||
three = columns.Integer(primary_key=True)
|
||||
|
||||
|
||||
class TestQuerySetOperation(BaseCassEngTestCase):
|
||||
def test_query_filter_parsing(self):
|
||||
"""
|
||||
Tests the queryset filter method parses it's kwargs properly
|
||||
"""
|
||||
query1 = TestModel.objects(test_id=5)
|
||||
assert len(query1._where) == 1
|
||||
|
||||
op = query1._where[0]
|
||||
|
||||
assert isinstance(op, statements.WhereClause)
|
||||
assert isinstance(op.operator, operators.EqualsOperator)
|
||||
assert op.value == 5
|
||||
|
||||
query2 = query1.filter(expected_result__gte=1)
|
||||
assert len(query2._where) == 2
|
||||
|
||||
op = query2._where[1]
|
||||
self.assertIsInstance(op, statements.WhereClause)
|
||||
self.assertIsInstance(op.operator, operators.GreaterThanOrEqualOperator)
|
||||
assert op.value == 1
|
||||
|
||||
def test_query_expression_parsing(self):
|
||||
""" Tests that query experessions are evaluated properly """
|
||||
query1 = TestModel.filter(TestModel.test_id == 5)
|
||||
assert len(query1._where) == 1
|
||||
|
||||
op = query1._where[0]
|
||||
assert isinstance(op, statements.WhereClause)
|
||||
assert isinstance(op.operator, operators.EqualsOperator)
|
||||
assert op.value == 5
|
||||
|
||||
query2 = query1.filter(TestModel.expected_result >= 1)
|
||||
assert len(query2._where) == 2
|
||||
|
||||
op = query2._where[1]
|
||||
self.assertIsInstance(op, statements.WhereClause)
|
||||
self.assertIsInstance(op.operator, operators.GreaterThanOrEqualOperator)
|
||||
assert op.value == 1
|
||||
|
||||
def test_using_invalid_column_names_in_filter_kwargs_raises_error(self):
|
||||
"""
|
||||
Tests that using invalid or nonexistant column names for filter args raises an error
|
||||
"""
|
||||
with self.assertRaises(query.QueryException):
|
||||
TestModel.objects(nonsense=5)
|
||||
|
||||
def test_using_nonexistant_column_names_in_query_args_raises_error(self):
|
||||
"""
|
||||
Tests that using invalid or nonexistant columns for query args raises an error
|
||||
"""
|
||||
with self.assertRaises(AttributeError):
|
||||
TestModel.objects(TestModel.nonsense == 5)
|
||||
|
||||
def test_using_non_query_operators_in_query_args_raises_error(self):
|
||||
"""
|
||||
Tests that providing query args that are not query operator instances raises an error
|
||||
"""
|
||||
with self.assertRaises(query.QueryException):
|
||||
TestModel.objects(5)
|
||||
|
||||
def test_queryset_is_immutable(self):
|
||||
"""
|
||||
Tests that calling a queryset function that changes it's state returns a new queryset
|
||||
"""
|
||||
query1 = TestModel.objects(test_id=5)
|
||||
assert len(query1._where) == 1
|
||||
|
||||
query2 = query1.filter(expected_result__gte=1)
|
||||
assert len(query2._where) == 2
|
||||
assert len(query1._where) == 1
|
||||
|
||||
def test_queryset_limit_immutability(self):
|
||||
"""
|
||||
Tests that calling a queryset function that changes it's state returns a new queryset with same limit
|
||||
"""
|
||||
query1 = TestModel.objects(test_id=5).limit(1)
|
||||
assert query1._limit == 1
|
||||
|
||||
query2 = query1.filter(expected_result__gte=1)
|
||||
assert query2._limit == 1
|
||||
|
||||
query3 = query1.filter(expected_result__gte=1).limit(2)
|
||||
assert query1._limit == 1
|
||||
assert query3._limit == 2
|
||||
|
||||
def test_the_all_method_duplicates_queryset(self):
|
||||
"""
|
||||
Tests that calling all on a queryset with previously defined filters duplicates queryset
|
||||
"""
|
||||
query1 = TestModel.objects(test_id=5)
|
||||
assert len(query1._where) == 1
|
||||
|
||||
query2 = query1.filter(expected_result__gte=1)
|
||||
assert len(query2._where) == 2
|
||||
|
||||
query3 = query2.all()
|
||||
assert query3 == query2
|
||||
|
||||
def test_defining_only_and_defer_fails(self):
|
||||
"""
|
||||
Tests that trying to add fields to either only or defer, or doing so more than once fails
|
||||
"""
|
||||
|
||||
def test_defining_only_or_defer_on_nonexistant_fields_fails(self):
|
||||
"""
|
||||
Tests that setting only or defer fields that don't exist raises an exception
|
||||
"""
|
||||
|
||||
|
||||
class BaseQuerySetUsage(BaseCassEngTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(BaseQuerySetUsage, cls).setUpClass()
|
||||
drop_table(TestModel)
|
||||
drop_table(IndexedTestModel)
|
||||
sync_table(TestModel)
|
||||
sync_table(IndexedTestModel)
|
||||
sync_table(TestMultiClusteringModel)
|
||||
|
||||
TestModel.objects.create(test_id=0, attempt_id=0, description='try1', expected_result=5, test_result=30)
|
||||
TestModel.objects.create(test_id=0, attempt_id=1, description='try2', expected_result=10, test_result=30)
|
||||
TestModel.objects.create(test_id=0, attempt_id=2, description='try3', expected_result=15, test_result=30)
|
||||
TestModel.objects.create(test_id=0, attempt_id=3, description='try4', expected_result=20, test_result=25)
|
||||
|
||||
TestModel.objects.create(test_id=1, attempt_id=0, description='try5', expected_result=5, test_result=25)
|
||||
TestModel.objects.create(test_id=1, attempt_id=1, description='try6', expected_result=10, test_result=25)
|
||||
TestModel.objects.create(test_id=1, attempt_id=2, description='try7', expected_result=15, test_result=25)
|
||||
TestModel.objects.create(test_id=1, attempt_id=3, description='try8', expected_result=20, test_result=20)
|
||||
|
||||
TestModel.objects.create(test_id=2, attempt_id=0, description='try9', expected_result=50, test_result=40)
|
||||
TestModel.objects.create(test_id=2, attempt_id=1, description='try10', expected_result=60, test_result=40)
|
||||
TestModel.objects.create(test_id=2, attempt_id=2, description='try11', expected_result=70, test_result=45)
|
||||
TestModel.objects.create(test_id=2, attempt_id=3, description='try12', expected_result=75, test_result=45)
|
||||
|
||||
IndexedTestModel.objects.create(test_id=0, attempt_id=0, description='try1', expected_result=5, test_result=30)
|
||||
IndexedTestModel.objects.create(test_id=1, attempt_id=1, description='try2', expected_result=10, test_result=30)
|
||||
IndexedTestModel.objects.create(test_id=2, attempt_id=2, description='try3', expected_result=15, test_result=30)
|
||||
IndexedTestModel.objects.create(test_id=3, attempt_id=3, description='try4', expected_result=20, test_result=25)
|
||||
|
||||
IndexedTestModel.objects.create(test_id=4, attempt_id=0, description='try5', expected_result=5, test_result=25)
|
||||
IndexedTestModel.objects.create(test_id=5, attempt_id=1, description='try6', expected_result=10, test_result=25)
|
||||
IndexedTestModel.objects.create(test_id=6, attempt_id=2, description='try7', expected_result=15, test_result=25)
|
||||
IndexedTestModel.objects.create(test_id=7, attempt_id=3, description='try8', expected_result=20, test_result=20)
|
||||
|
||||
IndexedTestModel.objects.create(test_id=8, attempt_id=0, description='try9', expected_result=50, test_result=40)
|
||||
IndexedTestModel.objects.create(test_id=9, attempt_id=1, description='try10', expected_result=60,
|
||||
test_result=40)
|
||||
IndexedTestModel.objects.create(test_id=10, attempt_id=2, description='try11', expected_result=70,
|
||||
test_result=45)
|
||||
IndexedTestModel.objects.create(test_id=11, attempt_id=3, description='try12', expected_result=75,
|
||||
test_result=45)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(BaseQuerySetUsage, cls).tearDownClass()
|
||||
drop_table(TestModel)
|
||||
drop_table(IndexedTestModel)
|
||||
drop_table(TestMultiClusteringModel)
|
||||
|
||||
|
||||
class TestQuerySetCountSelectionAndIteration(BaseQuerySetUsage):
|
||||
def test_count(self):
|
||||
""" Tests that adding filtering statements affects the count query as expected """
|
||||
assert TestModel.objects.count() == 12
|
||||
|
||||
q = TestModel.objects(test_id=0)
|
||||
assert q.count() == 4
|
||||
|
||||
def test_query_expression_count(self):
|
||||
""" Tests that adding query statements affects the count query as expected """
|
||||
assert TestModel.objects.count() == 12
|
||||
|
||||
q = TestModel.objects(TestModel.test_id == 0)
|
||||
assert q.count() == 4
|
||||
|
||||
def test_query_limit_count(self):
|
||||
""" Tests that adding query with a limit affects the count as expected """
|
||||
assert TestModel.objects.count() == 12
|
||||
|
||||
q = TestModel.objects(TestModel.test_id == 0).limit(2)
|
||||
result = q.count()
|
||||
self.assertEqual(2, result)
|
||||
|
||||
def test_iteration(self):
|
||||
""" Tests that iterating over a query set pulls back all of the expected results """
|
||||
q = TestModel.objects(test_id=0)
|
||||
#tuple of expected attempt_id, expected_result values
|
||||
compare_set = set([(0, 5), (1, 10), (2, 15), (3, 20)])
|
||||
for t in q:
|
||||
val = t.attempt_id, t.expected_result
|
||||
assert val in compare_set
|
||||
compare_set.remove(val)
|
||||
assert len(compare_set) == 0
|
||||
|
||||
# test with regular filtering
|
||||
q = TestModel.objects(attempt_id=3).allow_filtering()
|
||||
assert len(q) == 3
|
||||
#tuple of expected test_id, expected_result values
|
||||
compare_set = set([(0, 20), (1, 20), (2, 75)])
|
||||
for t in q:
|
||||
val = t.test_id, t.expected_result
|
||||
assert val in compare_set
|
||||
compare_set.remove(val)
|
||||
assert len(compare_set) == 0
|
||||
|
||||
# test with query method
|
||||
q = TestModel.objects(TestModel.attempt_id == 3).allow_filtering()
|
||||
assert len(q) == 3
|
||||
#tuple of expected test_id, expected_result values
|
||||
compare_set = set([(0, 20), (1, 20), (2, 75)])
|
||||
for t in q:
|
||||
val = t.test_id, t.expected_result
|
||||
assert val in compare_set
|
||||
compare_set.remove(val)
|
||||
assert len(compare_set) == 0
|
||||
|
||||
def test_multiple_iterations_work_properly(self):
|
||||
""" Tests that iterating over a query set more than once works """
|
||||
# test with both the filtering method and the query method
|
||||
for q in (TestModel.objects(test_id=0), TestModel.objects(TestModel.test_id == 0)):
|
||||
#tuple of expected attempt_id, expected_result values
|
||||
compare_set = set([(0, 5), (1, 10), (2, 15), (3, 20)])
|
||||
for t in q:
|
||||
val = t.attempt_id, t.expected_result
|
||||
assert val in compare_set
|
||||
compare_set.remove(val)
|
||||
assert len(compare_set) == 0
|
||||
|
||||
#try it again
|
||||
compare_set = set([(0, 5), (1, 10), (2, 15), (3, 20)])
|
||||
for t in q:
|
||||
val = t.attempt_id, t.expected_result
|
||||
assert val in compare_set
|
||||
compare_set.remove(val)
|
||||
assert len(compare_set) == 0
|
||||
|
||||
def test_multiple_iterators_are_isolated(self):
|
||||
"""
|
||||
tests that the use of one iterator does not affect the behavior of another
|
||||
"""
|
||||
for q in (TestModel.objects(test_id=0), TestModel.objects(TestModel.test_id == 0)):
|
||||
q = q.order_by('attempt_id')
|
||||
expected_order = [0, 1, 2, 3]
|
||||
iter1 = iter(q)
|
||||
iter2 = iter(q)
|
||||
for attempt_id in expected_order:
|
||||
assert next(iter1).attempt_id == attempt_id
|
||||
assert next(iter2).attempt_id == attempt_id
|
||||
|
||||
def test_get_success_case(self):
|
||||
"""
|
||||
Tests that the .get() method works on new and existing querysets
|
||||
"""
|
||||
m = TestModel.objects.get(test_id=0, attempt_id=0)
|
||||
assert isinstance(m, TestModel)
|
||||
assert m.test_id == 0
|
||||
assert m.attempt_id == 0
|
||||
|
||||
q = TestModel.objects(test_id=0, attempt_id=0)
|
||||
m = q.get()
|
||||
assert isinstance(m, TestModel)
|
||||
assert m.test_id == 0
|
||||
assert m.attempt_id == 0
|
||||
|
||||
q = TestModel.objects(test_id=0)
|
||||
m = q.get(attempt_id=0)
|
||||
assert isinstance(m, TestModel)
|
||||
assert m.test_id == 0
|
||||
assert m.attempt_id == 0
|
||||
|
||||
def test_query_expression_get_success_case(self):
|
||||
"""
|
||||
Tests that the .get() method works on new and existing querysets
|
||||
"""
|
||||
m = TestModel.get(TestModel.test_id == 0, TestModel.attempt_id == 0)
|
||||
assert isinstance(m, TestModel)
|
||||
assert m.test_id == 0
|
||||
assert m.attempt_id == 0
|
||||
|
||||
q = TestModel.objects(TestModel.test_id == 0, TestModel.attempt_id == 0)
|
||||
m = q.get()
|
||||
assert isinstance(m, TestModel)
|
||||
assert m.test_id == 0
|
||||
assert m.attempt_id == 0
|
||||
|
||||
q = TestModel.objects(TestModel.test_id == 0)
|
||||
m = q.get(TestModel.attempt_id == 0)
|
||||
assert isinstance(m, TestModel)
|
||||
assert m.test_id == 0
|
||||
assert m.attempt_id == 0
|
||||
|
||||
def test_get_doesnotexist_exception(self):
|
||||
"""
|
||||
Tests that get calls that don't return a result raises a DoesNotExist error
|
||||
"""
|
||||
with self.assertRaises(TestModel.DoesNotExist):
|
||||
TestModel.objects.get(test_id=100)
|
||||
|
||||
def test_get_multipleobjects_exception(self):
|
||||
"""
|
||||
Tests that get calls that return multiple results raise a MultipleObjectsReturned error
|
||||
"""
|
||||
with self.assertRaises(TestModel.MultipleObjectsReturned):
|
||||
TestModel.objects.get(test_id=1)
|
||||
|
||||
def test_allow_filtering_flag(self):
|
||||
"""
|
||||
"""
|
||||
|
||||
|
||||
def test_non_quality_filtering():
|
||||
class NonEqualityFilteringModel(Model):
|
||||
|
||||
example_id = columns.UUID(primary_key=True, default=uuid.uuid4)
|
||||
sequence_id = columns.Integer(primary_key=True) # sequence_id is a clustering key
|
||||
example_type = columns.Integer(index=True)
|
||||
created_at = columns.DateTime()
|
||||
|
||||
drop_table(NonEqualityFilteringModel)
|
||||
sync_table(NonEqualityFilteringModel)
|
||||
|
||||
# setup table, etc.
|
||||
|
||||
NonEqualityFilteringModel.create(sequence_id=1, example_type=0, created_at=datetime.now())
|
||||
NonEqualityFilteringModel.create(sequence_id=3, example_type=0, created_at=datetime.now())
|
||||
NonEqualityFilteringModel.create(sequence_id=5, example_type=1, created_at=datetime.now())
|
||||
|
||||
qA = NonEqualityFilteringModel.objects(NonEqualityFilteringModel.sequence_id > 3).allow_filtering()
|
||||
num = qA.count()
|
||||
assert num == 1, num
|
||||
|
||||
|
||||
|
||||
class TestQuerySetOrdering(BaseQuerySetUsage):
|
||||
|
||||
def test_order_by_success_case(self):
|
||||
q = TestModel.objects(test_id=0).order_by('attempt_id')
|
||||
expected_order = [0, 1, 2, 3]
|
||||
for model, expect in zip(q, expected_order):
|
||||
assert model.attempt_id == expect
|
||||
|
||||
q = q.order_by('-attempt_id')
|
||||
expected_order.reverse()
|
||||
for model, expect in zip(q, expected_order):
|
||||
assert model.attempt_id == expect
|
||||
|
||||
def test_ordering_by_non_second_primary_keys_fail(self):
|
||||
# kwarg filtering
|
||||
with self.assertRaises(query.QueryException):
|
||||
q = TestModel.objects(test_id=0).order_by('test_id')
|
||||
|
||||
# kwarg filtering
|
||||
with self.assertRaises(query.QueryException):
|
||||
q = TestModel.objects(TestModel.test_id == 0).order_by('test_id')
|
||||
|
||||
def test_ordering_by_non_primary_keys_fails(self):
|
||||
with self.assertRaises(query.QueryException):
|
||||
q = TestModel.objects(test_id=0).order_by('description')
|
||||
|
||||
def test_ordering_on_indexed_columns_fails(self):
|
||||
with self.assertRaises(query.QueryException):
|
||||
q = IndexedTestModel.objects(test_id=0).order_by('attempt_id')
|
||||
|
||||
def test_ordering_on_multiple_clustering_columns(self):
|
||||
TestMultiClusteringModel.create(one=1, two=1, three=4)
|
||||
TestMultiClusteringModel.create(one=1, two=1, three=2)
|
||||
TestMultiClusteringModel.create(one=1, two=1, three=5)
|
||||
TestMultiClusteringModel.create(one=1, two=1, three=1)
|
||||
TestMultiClusteringModel.create(one=1, two=1, three=3)
|
||||
|
||||
results = TestMultiClusteringModel.objects.filter(one=1, two=1).order_by('-two', '-three')
|
||||
assert [r.three for r in results] == [5, 4, 3, 2, 1]
|
||||
|
||||
results = TestMultiClusteringModel.objects.filter(one=1, two=1).order_by('two', 'three')
|
||||
assert [r.three for r in results] == [1, 2, 3, 4, 5]
|
||||
|
||||
results = TestMultiClusteringModel.objects.filter(one=1, two=1).order_by('two').order_by('three')
|
||||
assert [r.three for r in results] == [1, 2, 3, 4, 5]
|
||||
|
||||
|
||||
class TestQuerySetSlicing(BaseQuerySetUsage):
|
||||
def test_out_of_range_index_raises_error(self):
|
||||
q = TestModel.objects(test_id=0).order_by('attempt_id')
|
||||
with self.assertRaises(IndexError):
|
||||
q[10]
|
||||
|
||||
def test_array_indexing_works_properly(self):
|
||||
q = TestModel.objects(test_id=0).order_by('attempt_id')
|
||||
expected_order = [0, 1, 2, 3]
|
||||
for i in range(len(q)):
|
||||
assert q[i].attempt_id == expected_order[i]
|
||||
|
||||
def test_negative_indexing_works_properly(self):
|
||||
q = TestModel.objects(test_id=0).order_by('attempt_id')
|
||||
expected_order = [0, 1, 2, 3]
|
||||
assert q[-1].attempt_id == expected_order[-1]
|
||||
assert q[-2].attempt_id == expected_order[-2]
|
||||
|
||||
def test_slicing_works_properly(self):
|
||||
q = TestModel.objects(test_id=0).order_by('attempt_id')
|
||||
expected_order = [0, 1, 2, 3]
|
||||
for model, expect in zip(q[1:3], expected_order[1:3]):
|
||||
assert model.attempt_id == expect
|
||||
|
||||
def test_negative_slicing(self):
|
||||
q = TestModel.objects(test_id=0).order_by('attempt_id')
|
||||
expected_order = [0, 1, 2, 3]
|
||||
for model, expect in zip(q[-3:], expected_order[-3:]):
|
||||
assert model.attempt_id == expect
|
||||
for model, expect in zip(q[:-1], expected_order[:-1]):
|
||||
assert model.attempt_id == expect
|
||||
|
||||
|
||||
class TestQuerySetValidation(BaseQuerySetUsage):
|
||||
def test_primary_key_or_index_must_be_specified(self):
|
||||
"""
|
||||
Tests that queries that don't have an equals relation to a primary key or indexed field fail
|
||||
"""
|
||||
with self.assertRaises(query.QueryException):
|
||||
q = TestModel.objects(test_result=25)
|
||||
list([i for i in q])
|
||||
|
||||
def test_primary_key_or_index_must_have_equal_relation_filter(self):
|
||||
"""
|
||||
Tests that queries that don't have non equal (>,<, etc) relation to a primary key or indexed field fail
|
||||
"""
|
||||
with self.assertRaises(query.QueryException):
|
||||
q = TestModel.objects(test_id__gt=0)
|
||||
list([i for i in q])
|
||||
|
||||
def test_indexed_field_can_be_queried(self):
|
||||
"""
|
||||
Tests that queries on an indexed field will work without any primary key relations specified
|
||||
"""
|
||||
q = IndexedTestModel.objects(test_result=25)
|
||||
assert q.count() == 4
|
||||
|
||||
|
||||
class TestQuerySetDelete(BaseQuerySetUsage):
|
||||
def test_delete(self):
|
||||
TestModel.objects.create(test_id=3, attempt_id=0, description='try9', expected_result=50, test_result=40)
|
||||
TestModel.objects.create(test_id=3, attempt_id=1, description='try10', expected_result=60, test_result=40)
|
||||
TestModel.objects.create(test_id=3, attempt_id=2, description='try11', expected_result=70, test_result=45)
|
||||
TestModel.objects.create(test_id=3, attempt_id=3, description='try12', expected_result=75, test_result=45)
|
||||
|
||||
assert TestModel.objects.count() == 16
|
||||
assert TestModel.objects(test_id=3).count() == 4
|
||||
|
||||
TestModel.objects(test_id=3).delete()
|
||||
|
||||
assert TestModel.objects.count() == 12
|
||||
assert TestModel.objects(test_id=3).count() == 0
|
||||
|
||||
def test_delete_without_partition_key(self):
|
||||
""" Tests that attempting to delete a model without defining a partition key fails """
|
||||
with self.assertRaises(query.QueryException):
|
||||
TestModel.objects(attempt_id=0).delete()
|
||||
|
||||
def test_delete_without_any_where_args(self):
|
||||
""" Tests that attempting to delete a whole table without any arguments will fail """
|
||||
with self.assertRaises(query.QueryException):
|
||||
TestModel.objects(attempt_id=0).delete()
|
||||
|
||||
|
||||
class TestQuerySetConnectionHandling(BaseQuerySetUsage):
|
||||
def test_conn_is_returned_after_filling_cache(self):
|
||||
"""
|
||||
Tests that the queryset returns it's connection after it's fetched all of it's results
|
||||
"""
|
||||
q = TestModel.objects(test_id=0)
|
||||
#tuple of expected attempt_id, expected_result values
|
||||
compare_set = set([(0, 5), (1, 10), (2, 15), (3, 20)])
|
||||
for t in q:
|
||||
val = t.attempt_id, t.expected_result
|
||||
assert val in compare_set
|
||||
compare_set.remove(val)
|
||||
|
||||
assert q._con is None
|
||||
assert q._cur is None
|
||||
|
||||
|
||||
class TimeUUIDQueryModel(Model):
|
||||
|
||||
partition = columns.UUID(primary_key=True)
|
||||
time = columns.TimeUUID(primary_key=True)
|
||||
data = columns.Text(required=False)
|
||||
|
||||
|
||||
class TestMinMaxTimeUUIDFunctions(BaseCassEngTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMinMaxTimeUUIDFunctions, cls).setUpClass()
|
||||
sync_table(TimeUUIDQueryModel)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(TestMinMaxTimeUUIDFunctions, cls).tearDownClass()
|
||||
drop_table(TimeUUIDQueryModel)
|
||||
|
||||
def test_tzaware_datetime_support(self):
|
||||
"""Test that using timezone aware datetime instances works with the
|
||||
MinTimeUUID/MaxTimeUUID functions.
|
||||
"""
|
||||
pk = uuid4()
|
||||
midpoint_utc = datetime.utcnow().replace(tzinfo=TzOffset(0))
|
||||
midpoint_helsinki = midpoint_utc.astimezone(TzOffset(3))
|
||||
|
||||
# Assert pre-condition that we have the same logical point in time
|
||||
assert midpoint_utc.utctimetuple() == midpoint_helsinki.utctimetuple()
|
||||
assert midpoint_utc.timetuple() != midpoint_helsinki.timetuple()
|
||||
|
||||
TimeUUIDQueryModel.create(
|
||||
partition=pk,
|
||||
time=columns.TimeUUID.from_datetime(midpoint_utc - timedelta(minutes=1)),
|
||||
data='1')
|
||||
|
||||
TimeUUIDQueryModel.create(
|
||||
partition=pk,
|
||||
time=columns.TimeUUID.from_datetime(midpoint_utc),
|
||||
data='2')
|
||||
|
||||
TimeUUIDQueryModel.create(
|
||||
partition=pk,
|
||||
time=columns.TimeUUID.from_datetime(midpoint_utc + timedelta(minutes=1)),
|
||||
data='3')
|
||||
|
||||
assert ['1', '2'] == [o.data for o in TimeUUIDQueryModel.filter(
|
||||
TimeUUIDQueryModel.partition == pk,
|
||||
TimeUUIDQueryModel.time <= functions.MaxTimeUUID(midpoint_utc))]
|
||||
|
||||
assert ['1', '2'] == [o.data for o in TimeUUIDQueryModel.filter(
|
||||
TimeUUIDQueryModel.partition == pk,
|
||||
TimeUUIDQueryModel.time <= functions.MaxTimeUUID(midpoint_helsinki))]
|
||||
|
||||
assert ['2', '3'] == [o.data for o in TimeUUIDQueryModel.filter(
|
||||
TimeUUIDQueryModel.partition == pk,
|
||||
TimeUUIDQueryModel.time >= functions.MinTimeUUID(midpoint_utc))]
|
||||
|
||||
assert ['2', '3'] == [o.data for o in TimeUUIDQueryModel.filter(
|
||||
TimeUUIDQueryModel.partition == pk,
|
||||
TimeUUIDQueryModel.time >= functions.MinTimeUUID(midpoint_helsinki))]
|
||||
|
||||
def test_success_case(self):
|
||||
""" Test that the min and max time uuid functions work as expected """
|
||||
pk = uuid4()
|
||||
TimeUUIDQueryModel.create(partition=pk, time=uuid1(), data='1')
|
||||
time.sleep(0.2)
|
||||
TimeUUIDQueryModel.create(partition=pk, time=uuid1(), data='2')
|
||||
time.sleep(0.2)
|
||||
midpoint = datetime.utcnow()
|
||||
time.sleep(0.2)
|
||||
TimeUUIDQueryModel.create(partition=pk, time=uuid1(), data='3')
|
||||
time.sleep(0.2)
|
||||
TimeUUIDQueryModel.create(partition=pk, time=uuid1(), data='4')
|
||||
time.sleep(0.2)
|
||||
|
||||
# test kwarg filtering
|
||||
q = TimeUUIDQueryModel.filter(partition=pk, time__lte=functions.MaxTimeUUID(midpoint))
|
||||
q = [d for d in q]
|
||||
assert len(q) == 2
|
||||
datas = [d.data for d in q]
|
||||
assert '1' in datas
|
||||
assert '2' in datas
|
||||
|
||||
q = TimeUUIDQueryModel.filter(partition=pk, time__gte=functions.MinTimeUUID(midpoint))
|
||||
assert len(q) == 2
|
||||
datas = [d.data for d in q]
|
||||
assert '3' in datas
|
||||
assert '4' in datas
|
||||
|
||||
# test query expression filtering
|
||||
q = TimeUUIDQueryModel.filter(
|
||||
TimeUUIDQueryModel.partition == pk,
|
||||
TimeUUIDQueryModel.time <= functions.MaxTimeUUID(midpoint)
|
||||
)
|
||||
q = [d for d in q]
|
||||
assert len(q) == 2
|
||||
datas = [d.data for d in q]
|
||||
assert '1' in datas
|
||||
assert '2' in datas
|
||||
|
||||
q = TimeUUIDQueryModel.filter(
|
||||
TimeUUIDQueryModel.partition == pk,
|
||||
TimeUUIDQueryModel.time >= functions.MinTimeUUID(midpoint)
|
||||
)
|
||||
assert len(q) == 2
|
||||
datas = [d.data for d in q]
|
||||
assert '3' in datas
|
||||
assert '4' in datas
|
||||
|
||||
|
||||
class TestInOperator(BaseQuerySetUsage):
|
||||
def test_kwarg_success_case(self):
|
||||
""" Tests the in operator works with the kwarg query method """
|
||||
q = TestModel.filter(test_id__in=[0, 1])
|
||||
assert q.count() == 8
|
||||
|
||||
def test_query_expression_success_case(self):
|
||||
""" Tests the in operator works with the query expression query method """
|
||||
q = TestModel.filter(TestModel.test_id.in_([0, 1]))
|
||||
assert q.count() == 8
|
||||
|
||||
|
||||
class TestValuesList(BaseQuerySetUsage):
|
||||
def test_values_list(self):
|
||||
q = TestModel.objects.filter(test_id=0, attempt_id=1)
|
||||
item = q.values_list('test_id', 'attempt_id', 'description', 'expected_result', 'test_result').first()
|
||||
assert item == [0, 1, 'try2', 10, 30]
|
||||
|
||||
item = q.values_list('expected_result', flat=True).first()
|
||||
assert item == 10
|
||||
|
||||
|
||||
class TestObjectsProperty(BaseQuerySetUsage):
|
||||
def test_objects_property_returns_fresh_queryset(self):
|
||||
assert TestModel.objects._result_cache is None
|
||||
len(TestModel.objects) # evaluate queryset
|
||||
assert TestModel.objects._result_cache is None
|
||||
|
||||
|
||||
@skipUnless(PROTOCOL_VERSION >= 2, "only runs against the cql3 protocol v2.0")
|
||||
def test_paged_result_handling():
|
||||
# addresses #225
|
||||
class PagingTest(Model):
|
||||
id = columns.Integer(primary_key=True)
|
||||
val = columns.Integer()
|
||||
sync_table(PagingTest)
|
||||
|
||||
PagingTest.create(id=1, val=1)
|
||||
PagingTest.create(id=2, val=2)
|
||||
|
||||
session = get_session()
|
||||
with mock.patch.object(session, 'default_fetch_size', 1):
|
||||
results = PagingTest.objects()[:]
|
||||
|
||||
assert len(results) == 2
|
||||
|
||||
|
||||
class ModelQuerySetTimeoutTestCase(BaseQuerySetUsage):
|
||||
def test_default_timeout(self):
|
||||
with mock.patch.object(Session, 'execute', autospec=True) as mock_execute:
|
||||
list(TestModel.objects())
|
||||
mock_execute.assert_called_once_with(mock.ANY, mock.ANY, mock.ANY, timeout=NOT_SET)
|
||||
|
||||
def test_float_timeout(self):
|
||||
with mock.patch.object(Session, 'execute', autospec=True) as mock_execute:
|
||||
list(TestModel.objects().timeout(0.5))
|
||||
mock_execute.assert_called_once_with(mock.ANY, mock.ANY, mock.ANY, timeout=0.5)
|
||||
|
||||
def test_none_timeout(self):
|
||||
with mock.patch.object(Session, 'execute', autospec=True) as mock_execute:
|
||||
list(TestModel.objects().timeout(None))
|
||||
mock_execute.assert_called_once_with(mock.ANY, mock.ANY, mock.ANY, timeout=None)
|
||||
|
||||
|
||||
class DMLQueryTimeoutTestCase(BaseQuerySetUsage):
|
||||
def setUp(self):
|
||||
self.model = TestModel(test_id=1, attempt_id=1, description='timeout test')
|
||||
super(DMLQueryTimeoutTestCase, self).setUp()
|
||||
|
||||
def test_default_timeout(self):
|
||||
with mock.patch.object(Session, 'execute', autospec=True) as mock_execute:
|
||||
self.model.save()
|
||||
mock_execute.assert_called_once_with(mock.ANY, mock.ANY, mock.ANY, timeout=NOT_SET)
|
||||
|
||||
def test_float_timeout(self):
|
||||
with mock.patch.object(Session, 'execute', autospec=True) as mock_execute:
|
||||
self.model.timeout(0.5).save()
|
||||
mock_execute.assert_called_once_with(mock.ANY, mock.ANY, mock.ANY, timeout=0.5)
|
||||
|
||||
def test_none_timeout(self):
|
||||
with mock.patch.object(Session, 'execute', autospec=True) as mock_execute:
|
||||
self.model.timeout(None).save()
|
||||
mock_execute.assert_called_once_with(mock.ANY, mock.ANY, mock.ANY, timeout=None)
|
||||
|
||||
def test_timeout_then_batch(self):
|
||||
b = query.BatchQuery()
|
||||
m = self.model.timeout(None)
|
||||
with self.assertRaises(AssertionError):
|
||||
m.batch(b)
|
||||
|
||||
def test_batch_then_timeout(self):
|
||||
b = query.BatchQuery()
|
||||
m = self.model.batch(b)
|
||||
with self.assertRaises(AssertionError):
|
||||
m.timeout(0.5)
|
219
cqlengine/tests/query/test_updates.py
Normal file
219
cqlengine/tests/query/test_updates.py
Normal file
@ -0,0 +1,219 @@
|
||||
from uuid import uuid4
|
||||
from cqlengine.exceptions import ValidationError
|
||||
from cqlengine.query import QueryException
|
||||
|
||||
from cqlengine.tests.base import BaseCassEngTestCase
|
||||
from cqlengine.models import Model
|
||||
from cqlengine.management import sync_table, drop_table
|
||||
from cqlengine import columns
|
||||
|
||||
|
||||
class TestQueryUpdateModel(Model):
|
||||
|
||||
partition = columns.UUID(primary_key=True, default=uuid4)
|
||||
cluster = columns.Integer(primary_key=True)
|
||||
count = columns.Integer(required=False)
|
||||
text = columns.Text(required=False, index=True)
|
||||
text_set = columns.Set(columns.Text, required=False)
|
||||
text_list = columns.List(columns.Text, required=False)
|
||||
text_map = columns.Map(columns.Text, columns.Text, required=False)
|
||||
|
||||
class QueryUpdateTests(BaseCassEngTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(QueryUpdateTests, cls).setUpClass()
|
||||
sync_table(TestQueryUpdateModel)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(QueryUpdateTests, cls).tearDownClass()
|
||||
drop_table(TestQueryUpdateModel)
|
||||
|
||||
def test_update_values(self):
|
||||
""" tests calling udpate on a queryset """
|
||||
partition = uuid4()
|
||||
for i in range(5):
|
||||
TestQueryUpdateModel.create(partition=partition, cluster=i, count=i, text=str(i))
|
||||
|
||||
# sanity check
|
||||
for i, row in enumerate(TestQueryUpdateModel.objects(partition=partition)):
|
||||
assert row.cluster == i
|
||||
assert row.count == i
|
||||
assert row.text == str(i)
|
||||
|
||||
# perform update
|
||||
TestQueryUpdateModel.objects(partition=partition, cluster=3).update(count=6)
|
||||
|
||||
for i, row in enumerate(TestQueryUpdateModel.objects(partition=partition)):
|
||||
assert row.cluster == i
|
||||
assert row.count == (6 if i == 3 else i)
|
||||
assert row.text == str(i)
|
||||
|
||||
def test_update_values_validation(self):
|
||||
""" tests calling udpate on models with values passed in """
|
||||
partition = uuid4()
|
||||
for i in range(5):
|
||||
TestQueryUpdateModel.create(partition=partition, cluster=i, count=i, text=str(i))
|
||||
|
||||
# sanity check
|
||||
for i, row in enumerate(TestQueryUpdateModel.objects(partition=partition)):
|
||||
assert row.cluster == i
|
||||
assert row.count == i
|
||||
assert row.text == str(i)
|
||||
|
||||
# perform update
|
||||
with self.assertRaises(ValidationError):
|
||||
TestQueryUpdateModel.objects(partition=partition, cluster=3).update(count='asdf')
|
||||
|
||||
def test_invalid_update_kwarg(self):
|
||||
""" tests that passing in a kwarg to the update method that isn't a column will fail """
|
||||
with self.assertRaises(ValidationError):
|
||||
TestQueryUpdateModel.objects(partition=uuid4(), cluster=3).update(bacon=5000)
|
||||
|
||||
def test_primary_key_update_failure(self):
|
||||
""" tests that attempting to update the value of a primary key will fail """
|
||||
with self.assertRaises(ValidationError):
|
||||
TestQueryUpdateModel.objects(partition=uuid4(), cluster=3).update(cluster=5000)
|
||||
|
||||
def test_null_update_deletes_column(self):
|
||||
""" setting a field to null in the update should issue a delete statement """
|
||||
partition = uuid4()
|
||||
for i in range(5):
|
||||
TestQueryUpdateModel.create(partition=partition, cluster=i, count=i, text=str(i))
|
||||
|
||||
# sanity check
|
||||
for i, row in enumerate(TestQueryUpdateModel.objects(partition=partition)):
|
||||
assert row.cluster == i
|
||||
assert row.count == i
|
||||
assert row.text == str(i)
|
||||
|
||||
# perform update
|
||||
TestQueryUpdateModel.objects(partition=partition, cluster=3).update(text=None)
|
||||
|
||||
for i, row in enumerate(TestQueryUpdateModel.objects(partition=partition)):
|
||||
assert row.cluster == i
|
||||
assert row.count == i
|
||||
assert row.text == (None if i == 3 else str(i))
|
||||
|
||||
def test_mixed_value_and_null_update(self):
|
||||
""" tests that updating a columns value, and removing another works properly """
|
||||
partition = uuid4()
|
||||
for i in range(5):
|
||||
TestQueryUpdateModel.create(partition=partition, cluster=i, count=i, text=str(i))
|
||||
|
||||
# sanity check
|
||||
for i, row in enumerate(TestQueryUpdateModel.objects(partition=partition)):
|
||||
assert row.cluster == i
|
||||
assert row.count == i
|
||||
assert row.text == str(i)
|
||||
|
||||
# perform update
|
||||
TestQueryUpdateModel.objects(partition=partition, cluster=3).update(count=6, text=None)
|
||||
|
||||
for i, row in enumerate(TestQueryUpdateModel.objects(partition=partition)):
|
||||
assert row.cluster == i
|
||||
assert row.count == (6 if i == 3 else i)
|
||||
assert row.text == (None if i == 3 else str(i))
|
||||
|
||||
def test_counter_updates(self):
|
||||
pass
|
||||
|
||||
def test_set_add_updates(self):
|
||||
partition = uuid4()
|
||||
cluster = 1
|
||||
TestQueryUpdateModel.objects.create(
|
||||
partition=partition, cluster=cluster, text_set={"foo"})
|
||||
TestQueryUpdateModel.objects(
|
||||
partition=partition, cluster=cluster).update(text_set__add={'bar'})
|
||||
obj = TestQueryUpdateModel.objects.get(partition=partition, cluster=cluster)
|
||||
self.assertEqual(obj.text_set, {"foo", "bar"})
|
||||
|
||||
def test_set_add_updates_new_record(self):
|
||||
""" If the key doesn't exist yet, an update creates the record
|
||||
"""
|
||||
partition = uuid4()
|
||||
cluster = 1
|
||||
TestQueryUpdateModel.objects(
|
||||
partition=partition, cluster=cluster).update(text_set__add={'bar'})
|
||||
obj = TestQueryUpdateModel.objects.get(partition=partition, cluster=cluster)
|
||||
self.assertEqual(obj.text_set, {"bar"})
|
||||
|
||||
def test_set_remove_updates(self):
|
||||
partition = uuid4()
|
||||
cluster = 1
|
||||
TestQueryUpdateModel.objects.create(
|
||||
partition=partition, cluster=cluster, text_set={"foo", "baz"})
|
||||
TestQueryUpdateModel.objects(
|
||||
partition=partition, cluster=cluster).update(
|
||||
text_set__remove={'foo'})
|
||||
obj = TestQueryUpdateModel.objects.get(partition=partition, cluster=cluster)
|
||||
self.assertEqual(obj.text_set, {"baz"})
|
||||
|
||||
def test_set_remove_new_record(self):
|
||||
""" Removing something not in the set should silently do nothing
|
||||
"""
|
||||
partition = uuid4()
|
||||
cluster = 1
|
||||
TestQueryUpdateModel.objects.create(
|
||||
partition=partition, cluster=cluster, text_set={"foo"})
|
||||
TestQueryUpdateModel.objects(
|
||||
partition=partition, cluster=cluster).update(
|
||||
text_set__remove={'afsd'})
|
||||
obj = TestQueryUpdateModel.objects.get(partition=partition, cluster=cluster)
|
||||
self.assertEqual(obj.text_set, {"foo"})
|
||||
|
||||
def test_list_append_updates(self):
|
||||
partition = uuid4()
|
||||
cluster = 1
|
||||
TestQueryUpdateModel.objects.create(
|
||||
partition=partition, cluster=cluster, text_list=["foo"])
|
||||
TestQueryUpdateModel.objects(
|
||||
partition=partition, cluster=cluster).update(
|
||||
text_list__append=['bar'])
|
||||
obj = TestQueryUpdateModel.objects.get(partition=partition, cluster=cluster)
|
||||
self.assertEqual(obj.text_list, ["foo", "bar"])
|
||||
|
||||
def test_list_prepend_updates(self):
|
||||
""" Prepend two things since order is reversed by default by CQL """
|
||||
partition = uuid4()
|
||||
cluster = 1
|
||||
TestQueryUpdateModel.objects.create(
|
||||
partition=partition, cluster=cluster, text_list=["foo"])
|
||||
TestQueryUpdateModel.objects(
|
||||
partition=partition, cluster=cluster).update(
|
||||
text_list__prepend=['bar', 'baz'])
|
||||
obj = TestQueryUpdateModel.objects.get(partition=partition, cluster=cluster)
|
||||
self.assertEqual(obj.text_list, ["bar", "baz", "foo"])
|
||||
|
||||
def test_map_update_updates(self):
|
||||
""" Merge a dictionary into existing value """
|
||||
partition = uuid4()
|
||||
cluster = 1
|
||||
TestQueryUpdateModel.objects.create(
|
||||
partition=partition, cluster=cluster,
|
||||
text_map={"foo": '1', "bar": '2'})
|
||||
TestQueryUpdateModel.objects(
|
||||
partition=partition, cluster=cluster).update(
|
||||
text_map__update={"bar": '3', "baz": '4'})
|
||||
obj = TestQueryUpdateModel.objects.get(partition=partition, cluster=cluster)
|
||||
self.assertEqual(obj.text_map, {"foo": '1', "bar": '3', "baz": '4'})
|
||||
|
||||
def test_map_update_none_deletes_key(self):
|
||||
""" The CQL behavior is if you set a key in a map to null it deletes
|
||||
that key from the map. Test that this works with __update.
|
||||
|
||||
This test fails because of a bug in the cql python library not
|
||||
converting None to null (and the cql library is no longer in active
|
||||
developement).
|
||||
"""
|
||||
# partition = uuid4()
|
||||
# cluster = 1
|
||||
# TestQueryUpdateModel.objects.create(
|
||||
# partition=partition, cluster=cluster,
|
||||
# text_map={"foo": '1', "bar": '2'})
|
||||
# TestQueryUpdateModel.objects(
|
||||
# partition=partition, cluster=cluster).update(
|
||||
# text_map__update={"bar": None})
|
||||
# obj = TestQueryUpdateModel.objects.get(partition=partition, cluster=cluster)
|
||||
# self.assertEqual(obj.text_map, {"foo": '1'})
|
1
cqlengine/tests/statements/__init__.py
Normal file
1
cqlengine/tests/statements/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
__author__ = 'bdeggleston'
|
345
cqlengine/tests/statements/test_assignment_clauses.py
Normal file
345
cqlengine/tests/statements/test_assignment_clauses.py
Normal file
@ -0,0 +1,345 @@
|
||||
from unittest import TestCase
|
||||
from cqlengine.statements import AssignmentClause, SetUpdateClause, ListUpdateClause, MapUpdateClause, MapDeleteClause, FieldDeleteClause, CounterUpdateClause
|
||||
|
||||
|
||||
class AssignmentClauseTests(TestCase):
|
||||
|
||||
def test_rendering(self):
|
||||
pass
|
||||
|
||||
def test_insert_tuple(self):
|
||||
ac = AssignmentClause('a', 'b')
|
||||
ac.set_context_id(10)
|
||||
self.assertEqual(ac.insert_tuple(), ('a', 10))
|
||||
|
||||
|
||||
class SetUpdateClauseTests(TestCase):
|
||||
|
||||
def test_update_from_none(self):
|
||||
c = SetUpdateClause('s', {1, 2}, previous=None)
|
||||
c._analyze()
|
||||
c.set_context_id(0)
|
||||
|
||||
self.assertEqual(c._assignments, {1, 2})
|
||||
self.assertIsNone(c._additions)
|
||||
self.assertIsNone(c._removals)
|
||||
|
||||
self.assertEqual(c.get_context_size(), 1)
|
||||
self.assertEqual(str(c), '"s" = %(0)s')
|
||||
|
||||
ctx = {}
|
||||
c.update_context(ctx)
|
||||
self.assertEqual(ctx, {'0': {1, 2}})
|
||||
|
||||
def test_null_update(self):
|
||||
""" tests setting a set to None creates an empty update statement """
|
||||
c = SetUpdateClause('s', None, previous={1, 2})
|
||||
c._analyze()
|
||||
c.set_context_id(0)
|
||||
|
||||
self.assertIsNone(c._assignments)
|
||||
self.assertIsNone(c._additions)
|
||||
self.assertIsNone(c._removals)
|
||||
|
||||
self.assertEqual(c.get_context_size(), 0)
|
||||
self.assertEqual(str(c), '')
|
||||
|
||||
ctx = {}
|
||||
c.update_context(ctx)
|
||||
self.assertEqual(ctx, {})
|
||||
|
||||
def test_no_update(self):
|
||||
""" tests an unchanged value creates an empty update statement """
|
||||
c = SetUpdateClause('s', {1, 2}, previous={1, 2})
|
||||
c._analyze()
|
||||
c.set_context_id(0)
|
||||
|
||||
self.assertIsNone(c._assignments)
|
||||
self.assertIsNone(c._additions)
|
||||
self.assertIsNone(c._removals)
|
||||
|
||||
self.assertEqual(c.get_context_size(), 0)
|
||||
self.assertEqual(str(c), '')
|
||||
|
||||
ctx = {}
|
||||
c.update_context(ctx)
|
||||
self.assertEqual(ctx, {})
|
||||
|
||||
def test_update_empty_set(self):
|
||||
"""tests assigning a set to an empty set creates a nonempty
|
||||
update statement and nonzero context size."""
|
||||
c = SetUpdateClause(field='s', value=set())
|
||||
c._analyze()
|
||||
c.set_context_id(0)
|
||||
|
||||
self.assertEqual(c._assignments, set())
|
||||
self.assertIsNone(c._additions)
|
||||
self.assertIsNone(c._removals)
|
||||
|
||||
self.assertEqual(c.get_context_size(), 1)
|
||||
self.assertEqual(str(c), '"s" = %(0)s')
|
||||
|
||||
ctx = {}
|
||||
c.update_context(ctx)
|
||||
self.assertEqual(ctx, {'0' : set()})
|
||||
|
||||
def test_additions(self):
|
||||
c = SetUpdateClause('s', {1, 2, 3}, previous={1, 2})
|
||||
c._analyze()
|
||||
c.set_context_id(0)
|
||||
|
||||
self.assertIsNone(c._assignments)
|
||||
self.assertEqual(c._additions, {3})
|
||||
self.assertIsNone(c._removals)
|
||||
|
||||
self.assertEqual(c.get_context_size(), 1)
|
||||
self.assertEqual(str(c), '"s" = "s" + %(0)s')
|
||||
|
||||
ctx = {}
|
||||
c.update_context(ctx)
|
||||
self.assertEqual(ctx, {'0': {3}})
|
||||
|
||||
def test_removals(self):
|
||||
c = SetUpdateClause('s', {1, 2}, previous={1, 2, 3})
|
||||
c._analyze()
|
||||
c.set_context_id(0)
|
||||
|
||||
self.assertIsNone(c._assignments)
|
||||
self.assertIsNone(c._additions)
|
||||
self.assertEqual(c._removals, {3})
|
||||
|
||||
self.assertEqual(c.get_context_size(), 1)
|
||||
self.assertEqual(str(c), '"s" = "s" - %(0)s')
|
||||
|
||||
ctx = {}
|
||||
c.update_context(ctx)
|
||||
self.assertEqual(ctx, {'0': {3}})
|
||||
|
||||
def test_additions_and_removals(self):
|
||||
c = SetUpdateClause('s', {2, 3}, previous={1, 2})
|
||||
c._analyze()
|
||||
c.set_context_id(0)
|
||||
|
||||
self.assertIsNone(c._assignments)
|
||||
self.assertEqual(c._additions, {3})
|
||||
self.assertEqual(c._removals, {1})
|
||||
|
||||
self.assertEqual(c.get_context_size(), 2)
|
||||
self.assertEqual(str(c), '"s" = "s" + %(0)s, "s" = "s" - %(1)s')
|
||||
|
||||
ctx = {}
|
||||
c.update_context(ctx)
|
||||
self.assertEqual(ctx, {'0': {3}, '1': {1}})
|
||||
|
||||
|
||||
class ListUpdateClauseTests(TestCase):
|
||||
|
||||
def test_update_from_none(self):
|
||||
c = ListUpdateClause('s', [1, 2, 3])
|
||||
c._analyze()
|
||||
c.set_context_id(0)
|
||||
|
||||
self.assertEqual(c._assignments, [1, 2, 3])
|
||||
self.assertIsNone(c._append)
|
||||
self.assertIsNone(c._prepend)
|
||||
|
||||
self.assertEqual(c.get_context_size(), 1)
|
||||
self.assertEqual(str(c), '"s" = %(0)s')
|
||||
|
||||
ctx = {}
|
||||
c.update_context(ctx)
|
||||
self.assertEqual(ctx, {'0': [1, 2, 3]})
|
||||
|
||||
def test_update_from_empty(self):
|
||||
c = ListUpdateClause('s', [1, 2, 3], previous=[])
|
||||
c._analyze()
|
||||
c.set_context_id(0)
|
||||
|
||||
self.assertEqual(c._assignments, [1, 2, 3])
|
||||
self.assertIsNone(c._append)
|
||||
self.assertIsNone(c._prepend)
|
||||
|
||||
self.assertEqual(c.get_context_size(), 1)
|
||||
self.assertEqual(str(c), '"s" = %(0)s')
|
||||
|
||||
ctx = {}
|
||||
c.update_context(ctx)
|
||||
self.assertEqual(ctx, {'0': [1, 2, 3]})
|
||||
|
||||
def test_update_from_different_list(self):
|
||||
c = ListUpdateClause('s', [1, 2, 3], previous=[3, 2, 1])
|
||||
c._analyze()
|
||||
c.set_context_id(0)
|
||||
|
||||
self.assertEqual(c._assignments, [1, 2, 3])
|
||||
self.assertIsNone(c._append)
|
||||
self.assertIsNone(c._prepend)
|
||||
|
||||
self.assertEqual(c.get_context_size(), 1)
|
||||
self.assertEqual(str(c), '"s" = %(0)s')
|
||||
|
||||
ctx = {}
|
||||
c.update_context(ctx)
|
||||
self.assertEqual(ctx, {'0': [1, 2, 3]})
|
||||
|
||||
def test_append(self):
|
||||
c = ListUpdateClause('s', [1, 2, 3, 4], previous=[1, 2])
|
||||
c._analyze()
|
||||
c.set_context_id(0)
|
||||
|
||||
self.assertIsNone(c._assignments)
|
||||
self.assertEqual(c._append, [3, 4])
|
||||
self.assertIsNone(c._prepend)
|
||||
|
||||
self.assertEqual(c.get_context_size(), 1)
|
||||
self.assertEqual(str(c), '"s" = "s" + %(0)s')
|
||||
|
||||
ctx = {}
|
||||
c.update_context(ctx)
|
||||
self.assertEqual(ctx, {'0': [3, 4]})
|
||||
|
||||
def test_prepend(self):
|
||||
c = ListUpdateClause('s', [1, 2, 3, 4], previous=[3, 4])
|
||||
c._analyze()
|
||||
c.set_context_id(0)
|
||||
|
||||
self.assertIsNone(c._assignments)
|
||||
self.assertIsNone(c._append)
|
||||
self.assertEqual(c._prepend, [1, 2])
|
||||
|
||||
self.assertEqual(c.get_context_size(), 1)
|
||||
self.assertEqual(str(c), '"s" = %(0)s + "s"')
|
||||
|
||||
ctx = {}
|
||||
c.update_context(ctx)
|
||||
# test context list reversal
|
||||
self.assertEqual(ctx, {'0': [2, 1]})
|
||||
|
||||
def test_append_and_prepend(self):
|
||||
c = ListUpdateClause('s', [1, 2, 3, 4, 5, 6], previous=[3, 4])
|
||||
c._analyze()
|
||||
c.set_context_id(0)
|
||||
|
||||
self.assertIsNone(c._assignments)
|
||||
self.assertEqual(c._append, [5, 6])
|
||||
self.assertEqual(c._prepend, [1, 2])
|
||||
|
||||
self.assertEqual(c.get_context_size(), 2)
|
||||
self.assertEqual(str(c), '"s" = %(0)s + "s", "s" = "s" + %(1)s')
|
||||
|
||||
ctx = {}
|
||||
c.update_context(ctx)
|
||||
# test context list reversal
|
||||
self.assertEqual(ctx, {'0': [2, 1], '1': [5, 6]})
|
||||
|
||||
def test_shrinking_list_update(self):
|
||||
""" tests that updating to a smaller list results in an insert statement """
|
||||
c = ListUpdateClause('s', [1, 2, 3], previous=[1, 2, 3, 4])
|
||||
c._analyze()
|
||||
c.set_context_id(0)
|
||||
|
||||
self.assertEqual(c._assignments, [1, 2, 3])
|
||||
self.assertIsNone(c._append)
|
||||
self.assertIsNone(c._prepend)
|
||||
|
||||
self.assertEqual(c.get_context_size(), 1)
|
||||
self.assertEqual(str(c), '"s" = %(0)s')
|
||||
|
||||
ctx = {}
|
||||
c.update_context(ctx)
|
||||
self.assertEqual(ctx, {'0': [1, 2, 3]})
|
||||
|
||||
|
||||
class MapUpdateTests(TestCase):
|
||||
|
||||
def test_update(self):
|
||||
c = MapUpdateClause('s', {3: 0, 5: 6}, previous={5: 0, 3: 4})
|
||||
c._analyze()
|
||||
c.set_context_id(0)
|
||||
|
||||
self.assertEqual(c._updates, [3, 5])
|
||||
self.assertEqual(c.get_context_size(), 4)
|
||||
self.assertEqual(str(c), '"s"[%(0)s] = %(1)s, "s"[%(2)s] = %(3)s')
|
||||
|
||||
ctx = {}
|
||||
c.update_context(ctx)
|
||||
self.assertEqual(ctx, {'0': 3, "1": 0, '2': 5, '3': 6})
|
||||
|
||||
def test_update_from_null(self):
|
||||
c = MapUpdateClause('s', {3: 0, 5: 6})
|
||||
c._analyze()
|
||||
c.set_context_id(0)
|
||||
|
||||
self.assertEqual(c._updates, [3, 5])
|
||||
self.assertEqual(c.get_context_size(), 4)
|
||||
self.assertEqual(str(c), '"s"[%(0)s] = %(1)s, "s"[%(2)s] = %(3)s')
|
||||
|
||||
ctx = {}
|
||||
c.update_context(ctx)
|
||||
self.assertEqual(ctx, {'0': 3, "1": 0, '2': 5, '3': 6})
|
||||
|
||||
def test_nulled_columns_arent_included(self):
|
||||
c = MapUpdateClause('s', {3: 0}, {1: 2, 3: 4})
|
||||
c._analyze()
|
||||
c.set_context_id(0)
|
||||
|
||||
self.assertNotIn(1, c._updates)
|
||||
|
||||
|
||||
class CounterUpdateTests(TestCase):
|
||||
|
||||
def test_positive_update(self):
|
||||
c = CounterUpdateClause('a', 5, 3)
|
||||
c.set_context_id(5)
|
||||
|
||||
self.assertEqual(c.get_context_size(), 1)
|
||||
self.assertEqual(str(c), '"a" = "a" + %(5)s')
|
||||
|
||||
ctx = {}
|
||||
c.update_context(ctx)
|
||||
self.assertEqual(ctx, {'5': 2})
|
||||
|
||||
def test_negative_update(self):
|
||||
c = CounterUpdateClause('a', 4, 7)
|
||||
c.set_context_id(3)
|
||||
|
||||
self.assertEqual(c.get_context_size(), 1)
|
||||
self.assertEqual(str(c), '"a" = "a" - %(3)s')
|
||||
|
||||
ctx = {}
|
||||
c.update_context(ctx)
|
||||
self.assertEqual(ctx, {'3': 3})
|
||||
|
||||
def noop_update(self):
|
||||
c = CounterUpdateClause('a', 5, 5)
|
||||
c.set_context_id(5)
|
||||
|
||||
self.assertEqual(c.get_context_size(), 1)
|
||||
self.assertEqual(str(c), '"a" = "a" + %(0)s')
|
||||
|
||||
ctx = {}
|
||||
c.update_context(ctx)
|
||||
self.assertEqual(ctx, {'5': 0})
|
||||
|
||||
|
||||
class MapDeleteTests(TestCase):
|
||||
|
||||
def test_update(self):
|
||||
c = MapDeleteClause('s', {3: 0}, {1: 2, 3: 4, 5: 6})
|
||||
c._analyze()
|
||||
c.set_context_id(0)
|
||||
|
||||
self.assertEqual(c._removals, [1, 5])
|
||||
self.assertEqual(c.get_context_size(), 2)
|
||||
self.assertEqual(str(c), '"s"[%(0)s], "s"[%(1)s]')
|
||||
|
||||
ctx = {}
|
||||
c.update_context(ctx)
|
||||
self.assertEqual(ctx, {'0': 1, '1': 5})
|
||||
|
||||
|
||||
class FieldDeleteTests(TestCase):
|
||||
|
||||
def test_str(self):
|
||||
f = FieldDeleteClause("blake")
|
||||
assert str(f) == '"blake"'
|
11
cqlengine/tests/statements/test_assignment_statement.py
Normal file
11
cqlengine/tests/statements/test_assignment_statement.py
Normal file
@ -0,0 +1,11 @@
|
||||
from unittest import TestCase
|
||||
from cqlengine.statements import AssignmentStatement, StatementException
|
||||
|
||||
|
||||
class AssignmentStatementTest(TestCase):
|
||||
|
||||
def test_add_assignment_type_checking(self):
|
||||
""" tests that only assignment clauses can be added to queries """
|
||||
stmt = AssignmentStatement('table', [])
|
||||
with self.assertRaises(StatementException):
|
||||
stmt.add_assignment_clause('x=5')
|
16
cqlengine/tests/statements/test_base_clause.py
Normal file
16
cqlengine/tests/statements/test_base_clause.py
Normal file
@ -0,0 +1,16 @@
|
||||
from unittest import TestCase
|
||||
from cqlengine.statements import BaseClause
|
||||
|
||||
|
||||
class BaseClauseTests(TestCase):
|
||||
|
||||
def test_context_updating(self):
|
||||
ss = BaseClause('a', 'b')
|
||||
assert ss.get_context_size() == 1
|
||||
|
||||
ctx = {}
|
||||
ss.set_context_id(10)
|
||||
ss.update_context(ctx)
|
||||
assert ctx == {'10': 'b'}
|
||||
|
||||
|
11
cqlengine/tests/statements/test_base_statement.py
Normal file
11
cqlengine/tests/statements/test_base_statement.py
Normal file
@ -0,0 +1,11 @@
|
||||
from unittest import TestCase
|
||||
from cqlengine.statements import BaseCQLStatement, StatementException
|
||||
|
||||
|
||||
class BaseStatementTest(TestCase):
|
||||
|
||||
def test_where_clause_type_checking(self):
|
||||
""" tests that only assignment clauses can be added to queries """
|
||||
stmt = BaseCQLStatement('table', [])
|
||||
with self.assertRaises(StatementException):
|
||||
stmt.add_where_clause('x=5')
|
48
cqlengine/tests/statements/test_delete_statement.py
Normal file
48
cqlengine/tests/statements/test_delete_statement.py
Normal file
@ -0,0 +1,48 @@
|
||||
from unittest import TestCase
|
||||
from cqlengine.statements import DeleteStatement, WhereClause, MapDeleteClause
|
||||
from cqlengine.operators import *
|
||||
import six
|
||||
|
||||
class DeleteStatementTests(TestCase):
|
||||
|
||||
def test_single_field_is_listified(self):
|
||||
""" tests that passing a string field into the constructor puts it into a list """
|
||||
ds = DeleteStatement('table', 'field')
|
||||
self.assertEqual(len(ds.fields), 1)
|
||||
self.assertEqual(ds.fields[0].field, 'field')
|
||||
|
||||
def test_field_rendering(self):
|
||||
""" tests that fields are properly added to the select statement """
|
||||
ds = DeleteStatement('table', ['f1', 'f2'])
|
||||
self.assertTrue(six.text_type(ds).startswith('DELETE "f1", "f2"'), six.text_type(ds))
|
||||
self.assertTrue(str(ds).startswith('DELETE "f1", "f2"'), str(ds))
|
||||
|
||||
def test_none_fields_rendering(self):
|
||||
""" tests that a '*' is added if no fields are passed in """
|
||||
ds = DeleteStatement('table', None)
|
||||
self.assertTrue(six.text_type(ds).startswith('DELETE FROM'), six.text_type(ds))
|
||||
self.assertTrue(str(ds).startswith('DELETE FROM'), str(ds))
|
||||
|
||||
def test_table_rendering(self):
|
||||
ds = DeleteStatement('table', None)
|
||||
self.assertTrue(six.text_type(ds).startswith('DELETE FROM table'), six.text_type(ds))
|
||||
self.assertTrue(str(ds).startswith('DELETE FROM table'), str(ds))
|
||||
|
||||
def test_where_clause_rendering(self):
|
||||
ds = DeleteStatement('table', None)
|
||||
ds.add_where_clause(WhereClause('a', EqualsOperator(), 'b'))
|
||||
self.assertEqual(six.text_type(ds), 'DELETE FROM table WHERE "a" = %(0)s', six.text_type(ds))
|
||||
|
||||
def test_context_update(self):
|
||||
ds = DeleteStatement('table', None)
|
||||
ds.add_field(MapDeleteClause('d', {1: 2}, {1:2, 3: 4}))
|
||||
ds.add_where_clause(WhereClause('a', EqualsOperator(), 'b'))
|
||||
|
||||
ds.update_context_id(7)
|
||||
self.assertEqual(six.text_type(ds), 'DELETE "d"[%(8)s] FROM table WHERE "a" = %(7)s')
|
||||
self.assertEqual(ds.get_context(), {'7': 'b', '8': 3})
|
||||
|
||||
def test_context(self):
|
||||
ds = DeleteStatement('table', None)
|
||||
ds.add_where_clause(WhereClause('a', EqualsOperator(), 'b'))
|
||||
self.assertEqual(ds.get_context(), {'0': 'b'})
|
41
cqlengine/tests/statements/test_insert_statement.py
Normal file
41
cqlengine/tests/statements/test_insert_statement.py
Normal file
@ -0,0 +1,41 @@
|
||||
from unittest import TestCase
|
||||
from cqlengine.statements import InsertStatement, StatementException, AssignmentClause
|
||||
|
||||
import six
|
||||
|
||||
class InsertStatementTests(TestCase):
|
||||
|
||||
def test_where_clause_failure(self):
|
||||
""" tests that where clauses cannot be added to Insert statements """
|
||||
ist = InsertStatement('table', None)
|
||||
with self.assertRaises(StatementException):
|
||||
ist.add_where_clause('s')
|
||||
|
||||
def test_statement(self):
|
||||
ist = InsertStatement('table', None)
|
||||
ist.add_assignment_clause(AssignmentClause('a', 'b'))
|
||||
ist.add_assignment_clause(AssignmentClause('c', 'd'))
|
||||
|
||||
self.assertEqual(
|
||||
six.text_type(ist),
|
||||
'INSERT INTO table ("a", "c") VALUES (%(0)s, %(1)s)'
|
||||
)
|
||||
|
||||
def test_context_update(self):
|
||||
ist = InsertStatement('table', None)
|
||||
ist.add_assignment_clause(AssignmentClause('a', 'b'))
|
||||
ist.add_assignment_clause(AssignmentClause('c', 'd'))
|
||||
|
||||
ist.update_context_id(4)
|
||||
self.assertEqual(
|
||||
six.text_type(ist),
|
||||
'INSERT INTO table ("a", "c") VALUES (%(4)s, %(5)s)'
|
||||
)
|
||||
ctx = ist.get_context()
|
||||
self.assertEqual(ctx, {'4': 'b', '5': 'd'})
|
||||
|
||||
def test_additional_rendering(self):
|
||||
ist = InsertStatement('table', ttl=60)
|
||||
ist.add_assignment_clause(AssignmentClause('a', 'b'))
|
||||
ist.add_assignment_clause(AssignmentClause('c', 'd'))
|
||||
self.assertIn('USING TTL 60', six.text_type(ist))
|
0
cqlengine/tests/statements/test_quoter.py
Normal file
0
cqlengine/tests/statements/test_quoter.py
Normal file
70
cqlengine/tests/statements/test_select_statement.py
Normal file
70
cqlengine/tests/statements/test_select_statement.py
Normal file
@ -0,0 +1,70 @@
|
||||
from unittest import TestCase
|
||||
from cqlengine.statements import SelectStatement, WhereClause
|
||||
from cqlengine.operators import *
|
||||
import six
|
||||
|
||||
class SelectStatementTests(TestCase):
|
||||
|
||||
def test_single_field_is_listified(self):
|
||||
""" tests that passing a string field into the constructor puts it into a list """
|
||||
ss = SelectStatement('table', 'field')
|
||||
self.assertEqual(ss.fields, ['field'])
|
||||
|
||||
def test_field_rendering(self):
|
||||
""" tests that fields are properly added to the select statement """
|
||||
ss = SelectStatement('table', ['f1', 'f2'])
|
||||
self.assertTrue(six.text_type(ss).startswith('SELECT "f1", "f2"'), six.text_type(ss))
|
||||
self.assertTrue(str(ss).startswith('SELECT "f1", "f2"'), str(ss))
|
||||
|
||||
def test_none_fields_rendering(self):
|
||||
""" tests that a '*' is added if no fields are passed in """
|
||||
ss = SelectStatement('table')
|
||||
self.assertTrue(six.text_type(ss).startswith('SELECT *'), six.text_type(ss))
|
||||
self.assertTrue(str(ss).startswith('SELECT *'), str(ss))
|
||||
|
||||
def test_table_rendering(self):
|
||||
ss = SelectStatement('table')
|
||||
self.assertTrue(six.text_type(ss).startswith('SELECT * FROM table'), six.text_type(ss))
|
||||
self.assertTrue(str(ss).startswith('SELECT * FROM table'), str(ss))
|
||||
|
||||
def test_where_clause_rendering(self):
|
||||
ss = SelectStatement('table')
|
||||
ss.add_where_clause(WhereClause('a', EqualsOperator(), 'b'))
|
||||
self.assertEqual(six.text_type(ss), 'SELECT * FROM table WHERE "a" = %(0)s', six.text_type(ss))
|
||||
|
||||
def test_count(self):
|
||||
ss = SelectStatement('table', count=True, limit=10, order_by='d')
|
||||
ss.add_where_clause(WhereClause('a', EqualsOperator(), 'b'))
|
||||
self.assertEqual(six.text_type(ss), 'SELECT COUNT(*) FROM table WHERE "a" = %(0)s LIMIT 10', six.text_type(ss))
|
||||
self.assertIn('LIMIT', six.text_type(ss))
|
||||
self.assertNotIn('ORDER', six.text_type(ss))
|
||||
|
||||
def test_context(self):
|
||||
ss = SelectStatement('table')
|
||||
ss.add_where_clause(WhereClause('a', EqualsOperator(), 'b'))
|
||||
self.assertEqual(ss.get_context(), {'0': 'b'})
|
||||
|
||||
def test_context_id_update(self):
|
||||
""" tests that the right things happen the the context id """
|
||||
ss = SelectStatement('table')
|
||||
ss.add_where_clause(WhereClause('a', EqualsOperator(), 'b'))
|
||||
self.assertEqual(ss.get_context(), {'0': 'b'})
|
||||
self.assertEqual(str(ss), 'SELECT * FROM table WHERE "a" = %(0)s')
|
||||
|
||||
ss.update_context_id(5)
|
||||
self.assertEqual(ss.get_context(), {'5': 'b'})
|
||||
self.assertEqual(str(ss), 'SELECT * FROM table WHERE "a" = %(5)s')
|
||||
|
||||
def test_additional_rendering(self):
|
||||
ss = SelectStatement(
|
||||
'table',
|
||||
None,
|
||||
order_by=['x', 'y'],
|
||||
limit=15,
|
||||
allow_filtering=True
|
||||
)
|
||||
qstr = six.text_type(ss)
|
||||
self.assertIn('LIMIT 15', qstr)
|
||||
self.assertIn('ORDER BY x, y', qstr)
|
||||
self.assertIn('ALLOW FILTERING', qstr)
|
||||
|
70
cqlengine/tests/statements/test_update_statement.py
Normal file
70
cqlengine/tests/statements/test_update_statement.py
Normal file
@ -0,0 +1,70 @@
|
||||
from unittest import TestCase
|
||||
from cqlengine.columns import Set, List
|
||||
from cqlengine.operators import *
|
||||
from cqlengine.statements import (UpdateStatement, WhereClause,
|
||||
AssignmentClause, SetUpdateClause,
|
||||
ListUpdateClause)
|
||||
import six
|
||||
|
||||
|
||||
class UpdateStatementTests(TestCase):
|
||||
|
||||
def test_table_rendering(self):
|
||||
""" tests that fields are properly added to the select statement """
|
||||
us = UpdateStatement('table')
|
||||
self.assertTrue(six.text_type(us).startswith('UPDATE table SET'), six.text_type(us))
|
||||
self.assertTrue(str(us).startswith('UPDATE table SET'), str(us))
|
||||
|
||||
def test_rendering(self):
|
||||
us = UpdateStatement('table')
|
||||
us.add_assignment_clause(AssignmentClause('a', 'b'))
|
||||
us.add_assignment_clause(AssignmentClause('c', 'd'))
|
||||
us.add_where_clause(WhereClause('a', EqualsOperator(), 'x'))
|
||||
self.assertEqual(six.text_type(us), 'UPDATE table SET "a" = %(0)s, "c" = %(1)s WHERE "a" = %(2)s', six.text_type(us))
|
||||
|
||||
def test_context(self):
|
||||
us = UpdateStatement('table')
|
||||
us.add_assignment_clause(AssignmentClause('a', 'b'))
|
||||
us.add_assignment_clause(AssignmentClause('c', 'd'))
|
||||
us.add_where_clause(WhereClause('a', EqualsOperator(), 'x'))
|
||||
self.assertEqual(us.get_context(), {'0': 'b', '1': 'd', '2': 'x'})
|
||||
|
||||
def test_context_update(self):
|
||||
us = UpdateStatement('table')
|
||||
us.add_assignment_clause(AssignmentClause('a', 'b'))
|
||||
us.add_assignment_clause(AssignmentClause('c', 'd'))
|
||||
us.add_where_clause(WhereClause('a', EqualsOperator(), 'x'))
|
||||
us.update_context_id(3)
|
||||
self.assertEqual(six.text_type(us), 'UPDATE table SET "a" = %(4)s, "c" = %(5)s WHERE "a" = %(3)s')
|
||||
self.assertEqual(us.get_context(), {'4': 'b', '5': 'd', '3': 'x'})
|
||||
|
||||
def test_additional_rendering(self):
|
||||
us = UpdateStatement('table', ttl=60)
|
||||
us.add_assignment_clause(AssignmentClause('a', 'b'))
|
||||
us.add_where_clause(WhereClause('a', EqualsOperator(), 'x'))
|
||||
self.assertIn('USING TTL 60', six.text_type(us))
|
||||
|
||||
def test_update_set_add(self):
|
||||
us = UpdateStatement('table')
|
||||
us.add_assignment_clause(SetUpdateClause('a', Set.Quoter({1}), operation='add'))
|
||||
self.assertEqual(six.text_type(us), 'UPDATE table SET "a" = "a" + %(0)s')
|
||||
|
||||
def test_update_empty_set_add_does_not_assign(self):
|
||||
us = UpdateStatement('table')
|
||||
us.add_assignment_clause(SetUpdateClause('a', Set.Quoter(set()), operation='add'))
|
||||
self.assertEqual(six.text_type(us), 'UPDATE table SET "a" = "a" + %(0)s')
|
||||
|
||||
def test_update_empty_set_removal_does_not_assign(self):
|
||||
us = UpdateStatement('table')
|
||||
us.add_assignment_clause(SetUpdateClause('a', Set.Quoter(set()), operation='remove'))
|
||||
self.assertEqual(six.text_type(us), 'UPDATE table SET "a" = "a" - %(0)s')
|
||||
|
||||
def test_update_list_prepend_with_empty_list(self):
|
||||
us = UpdateStatement('table')
|
||||
us.add_assignment_clause(ListUpdateClause('a', List.Quoter([]), operation='prepend'))
|
||||
self.assertEqual(six.text_type(us), 'UPDATE table SET "a" = %(0)s + "a"')
|
||||
|
||||
def test_update_list_append_with_empty_list(self):
|
||||
us = UpdateStatement('table')
|
||||
us.add_assignment_clause(ListUpdateClause('a', List.Quoter([]), operation='append'))
|
||||
self.assertEqual(six.text_type(us), 'UPDATE table SET "a" = "a" + %(0)s')
|
26
cqlengine/tests/statements/test_where_clause.py
Normal file
26
cqlengine/tests/statements/test_where_clause.py
Normal file
@ -0,0 +1,26 @@
|
||||
from unittest import TestCase
|
||||
import six
|
||||
from cqlengine.operators import EqualsOperator
|
||||
from cqlengine.statements import StatementException, WhereClause
|
||||
|
||||
|
||||
class TestWhereClause(TestCase):
|
||||
|
||||
def test_operator_check(self):
|
||||
""" tests that creating a where statement with a non BaseWhereOperator object fails """
|
||||
with self.assertRaises(StatementException):
|
||||
WhereClause('a', 'b', 'c')
|
||||
|
||||
def test_where_clause_rendering(self):
|
||||
""" tests that where clauses are rendered properly """
|
||||
wc = WhereClause('a', EqualsOperator(), 'c')
|
||||
wc.set_context_id(5)
|
||||
|
||||
self.assertEqual('"a" = %(5)s', six.text_type(wc), six.text_type(wc))
|
||||
self.assertEqual('"a" = %(5)s', str(wc), type(wc))
|
||||
|
||||
def test_equality_method(self):
|
||||
""" tests that 2 identical where clauses evaluate as == """
|
||||
wc1 = WhereClause('a', EqualsOperator(), 'c')
|
||||
wc2 = WhereClause('a', EqualsOperator(), 'c')
|
||||
assert wc1 == wc2
|
194
cqlengine/tests/test_batch_query.py
Normal file
194
cqlengine/tests/test_batch_query.py
Normal file
@ -0,0 +1,194 @@
|
||||
from unittest import skip
|
||||
from uuid import uuid4
|
||||
import random
|
||||
|
||||
import mock
|
||||
import sure
|
||||
|
||||
from cqlengine import Model, columns
|
||||
from cqlengine.management import drop_table, sync_table
|
||||
from cqlengine.query import BatchQuery
|
||||
from cqlengine.tests.base import BaseCassEngTestCase
|
||||
|
||||
class TestMultiKeyModel(Model):
|
||||
|
||||
partition = columns.Integer(primary_key=True)
|
||||
cluster = columns.Integer(primary_key=True)
|
||||
count = columns.Integer(required=False)
|
||||
text = columns.Text(required=False)
|
||||
|
||||
|
||||
class BatchQueryTests(BaseCassEngTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(BatchQueryTests, cls).setUpClass()
|
||||
drop_table(TestMultiKeyModel)
|
||||
sync_table(TestMultiKeyModel)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(BatchQueryTests, cls).tearDownClass()
|
||||
drop_table(TestMultiKeyModel)
|
||||
|
||||
def setUp(self):
|
||||
super(BatchQueryTests, self).setUp()
|
||||
self.pkey = 1
|
||||
for obj in TestMultiKeyModel.filter(partition=self.pkey):
|
||||
obj.delete()
|
||||
|
||||
def test_insert_success_case(self):
|
||||
|
||||
b = BatchQuery()
|
||||
inst = TestMultiKeyModel.batch(b).create(partition=self.pkey, cluster=2, count=3, text='4')
|
||||
|
||||
with self.assertRaises(TestMultiKeyModel.DoesNotExist):
|
||||
TestMultiKeyModel.get(partition=self.pkey, cluster=2)
|
||||
|
||||
b.execute()
|
||||
|
||||
TestMultiKeyModel.get(partition=self.pkey, cluster=2)
|
||||
|
||||
def test_update_success_case(self):
|
||||
|
||||
inst = TestMultiKeyModel.create(partition=self.pkey, cluster=2, count=3, text='4')
|
||||
|
||||
b = BatchQuery()
|
||||
|
||||
inst.count = 4
|
||||
inst.batch(b).save()
|
||||
|
||||
inst2 = TestMultiKeyModel.get(partition=self.pkey, cluster=2)
|
||||
assert inst2.count == 3
|
||||
|
||||
b.execute()
|
||||
|
||||
inst3 = TestMultiKeyModel.get(partition=self.pkey, cluster=2)
|
||||
assert inst3.count == 4
|
||||
|
||||
def test_delete_success_case(self):
|
||||
|
||||
inst = TestMultiKeyModel.create(partition=self.pkey, cluster=2, count=3, text='4')
|
||||
|
||||
b = BatchQuery()
|
||||
|
||||
inst.batch(b).delete()
|
||||
|
||||
TestMultiKeyModel.get(partition=self.pkey, cluster=2)
|
||||
|
||||
b.execute()
|
||||
|
||||
with self.assertRaises(TestMultiKeyModel.DoesNotExist):
|
||||
TestMultiKeyModel.get(partition=self.pkey, cluster=2)
|
||||
|
||||
def test_context_manager(self):
|
||||
|
||||
with BatchQuery() as b:
|
||||
for i in range(5):
|
||||
TestMultiKeyModel.batch(b).create(partition=self.pkey, cluster=i, count=3, text='4')
|
||||
|
||||
for i in range(5):
|
||||
with self.assertRaises(TestMultiKeyModel.DoesNotExist):
|
||||
TestMultiKeyModel.get(partition=self.pkey, cluster=i)
|
||||
|
||||
for i in range(5):
|
||||
TestMultiKeyModel.get(partition=self.pkey, cluster=i)
|
||||
|
||||
def test_bulk_delete_success_case(self):
|
||||
|
||||
for i in range(1):
|
||||
for j in range(5):
|
||||
TestMultiKeyModel.create(partition=i, cluster=j, count=i*j, text='{}:{}'.format(i,j))
|
||||
|
||||
with BatchQuery() as b:
|
||||
TestMultiKeyModel.objects.batch(b).filter(partition=0).delete()
|
||||
assert TestMultiKeyModel.filter(partition=0).count() == 5
|
||||
|
||||
assert TestMultiKeyModel.filter(partition=0).count() == 0
|
||||
#cleanup
|
||||
for m in TestMultiKeyModel.all():
|
||||
m.delete()
|
||||
|
||||
def test_empty_batch(self):
|
||||
b = BatchQuery()
|
||||
b.execute()
|
||||
|
||||
with BatchQuery() as b:
|
||||
pass
|
||||
|
||||
class BatchQueryCallbacksTests(BaseCassEngTestCase):
|
||||
|
||||
def test_API_managing_callbacks(self):
|
||||
|
||||
# Callbacks can be added at init and after
|
||||
|
||||
def my_callback(*args, **kwargs):
|
||||
pass
|
||||
|
||||
# adding on init:
|
||||
batch = BatchQuery()
|
||||
|
||||
batch.add_callback(my_callback)
|
||||
batch.add_callback(my_callback, 2, named_arg='value')
|
||||
batch.add_callback(my_callback, 1, 3)
|
||||
|
||||
assert batch._callbacks == [
|
||||
(my_callback, (), {}),
|
||||
(my_callback, (2,), {'named_arg':'value'}),
|
||||
(my_callback, (1, 3), {})
|
||||
]
|
||||
|
||||
def test_callbacks_properly_execute_callables_and_tuples(self):
|
||||
|
||||
call_history = []
|
||||
def my_callback(*args, **kwargs):
|
||||
call_history.append(args)
|
||||
|
||||
# adding on init:
|
||||
batch = BatchQuery()
|
||||
|
||||
batch.add_callback(my_callback)
|
||||
batch.add_callback(my_callback, 'more', 'args')
|
||||
|
||||
batch.execute()
|
||||
|
||||
assert len(call_history) == 2
|
||||
assert [(), ('more', 'args')] == call_history
|
||||
|
||||
def test_callbacks_tied_to_execute(self):
|
||||
"""Batch callbacks should NOT fire if batch is not executed in context manager mode"""
|
||||
|
||||
call_history = []
|
||||
def my_callback(*args, **kwargs):
|
||||
call_history.append(args)
|
||||
|
||||
with BatchQuery() as batch:
|
||||
batch.add_callback(my_callback)
|
||||
pass
|
||||
|
||||
assert len(call_history) == 1
|
||||
|
||||
class SomeError(Exception):
|
||||
pass
|
||||
|
||||
with self.assertRaises(SomeError):
|
||||
with BatchQuery() as batch:
|
||||
batch.add_callback(my_callback)
|
||||
# this error bubbling up through context manager
|
||||
# should prevent callback runs (along with b.execute())
|
||||
raise SomeError
|
||||
|
||||
# still same call history. Nothing added
|
||||
assert len(call_history) == 1
|
||||
|
||||
# but if execute ran, even with an error bubbling through
|
||||
# the callbacks also would have fired
|
||||
with self.assertRaises(SomeError):
|
||||
with BatchQuery(execute_on_exception=True) as batch:
|
||||
batch.add_callback(my_callback)
|
||||
# this error bubbling up through context manager
|
||||
# should prevent callback runs (along with b.execute())
|
||||
raise SomeError
|
||||
|
||||
# still same call history
|
||||
assert len(call_history) == 2
|
95
cqlengine/tests/test_consistency.py
Normal file
95
cqlengine/tests/test_consistency.py
Normal file
@ -0,0 +1,95 @@
|
||||
from cqlengine.management import sync_table, drop_table
|
||||
from cqlengine.tests.base import BaseCassEngTestCase
|
||||
from cqlengine.models import Model
|
||||
from uuid import uuid4
|
||||
from cqlengine import columns
|
||||
import mock
|
||||
from cqlengine import ALL, BatchQuery
|
||||
|
||||
class TestConsistencyModel(Model):
|
||||
|
||||
id = columns.UUID(primary_key=True, default=lambda:uuid4())
|
||||
count = columns.Integer()
|
||||
text = columns.Text(required=False)
|
||||
|
||||
class BaseConsistencyTest(BaseCassEngTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(BaseConsistencyTest, cls).setUpClass()
|
||||
sync_table(TestConsistencyModel)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(BaseConsistencyTest, cls).tearDownClass()
|
||||
drop_table(TestConsistencyModel)
|
||||
|
||||
|
||||
class TestConsistency(BaseConsistencyTest):
|
||||
def test_create_uses_consistency(self):
|
||||
|
||||
qs = TestConsistencyModel.consistency(ALL)
|
||||
with mock.patch.object(self.session, 'execute') as m:
|
||||
qs.create(text="i am not fault tolerant this way")
|
||||
|
||||
args = m.call_args
|
||||
self.assertEqual(ALL, args[0][0].consistency_level)
|
||||
|
||||
def test_queryset_is_returned_on_create(self):
|
||||
qs = TestConsistencyModel.consistency(ALL)
|
||||
self.assertTrue(isinstance(qs, TestConsistencyModel.__queryset__), type(qs))
|
||||
|
||||
def test_update_uses_consistency(self):
|
||||
t = TestConsistencyModel.create(text="bacon and eggs")
|
||||
t.text = "ham sandwich"
|
||||
|
||||
with mock.patch.object(self.session, 'execute') as m:
|
||||
t.consistency(ALL).save()
|
||||
|
||||
args = m.call_args
|
||||
self.assertEqual(ALL, args[0][0].consistency_level)
|
||||
|
||||
|
||||
def test_batch_consistency(self):
|
||||
|
||||
with mock.patch.object(self.session, 'execute') as m:
|
||||
with BatchQuery(consistency=ALL) as b:
|
||||
TestConsistencyModel.batch(b).create(text="monkey")
|
||||
|
||||
args = m.call_args
|
||||
|
||||
self.assertEqual(ALL, args[0][0].consistency_level)
|
||||
|
||||
with mock.patch.object(self.session, 'execute') as m:
|
||||
with BatchQuery() as b:
|
||||
TestConsistencyModel.batch(b).create(text="monkey")
|
||||
|
||||
args = m.call_args
|
||||
self.assertNotEqual(ALL, args[0][0].consistency_level)
|
||||
|
||||
def test_blind_update(self):
|
||||
t = TestConsistencyModel.create(text="bacon and eggs")
|
||||
t.text = "ham sandwich"
|
||||
uid = t.id
|
||||
|
||||
with mock.patch.object(self.session, 'execute') as m:
|
||||
TestConsistencyModel.objects(id=uid).consistency(ALL).update(text="grilled cheese")
|
||||
|
||||
args = m.call_args
|
||||
self.assertEqual(ALL, args[0][0].consistency_level)
|
||||
|
||||
|
||||
def test_delete(self):
|
||||
# ensures we always carry consistency through on delete statements
|
||||
t = TestConsistencyModel.create(text="bacon and eggs")
|
||||
t.text = "ham and cheese sandwich"
|
||||
uid = t.id
|
||||
|
||||
with mock.patch.object(self.session, 'execute') as m:
|
||||
t.consistency(ALL).delete()
|
||||
|
||||
with mock.patch.object(self.session, 'execute') as m:
|
||||
TestConsistencyModel.objects(id=uid).consistency(ALL).delete()
|
||||
|
||||
args = m.call_args
|
||||
self.assertEqual(ALL, args[0][0].consistency_level)
|
200
cqlengine/tests/test_ifnotexists.py
Normal file
200
cqlengine/tests/test_ifnotexists.py
Normal file
@ -0,0 +1,200 @@
|
||||
from unittest import skipUnless
|
||||
from cqlengine.management import sync_table, drop_table, create_keyspace, delete_keyspace
|
||||
from cqlengine.tests.base import BaseCassEngTestCase
|
||||
from cqlengine.tests.base import PROTOCOL_VERSION
|
||||
from cqlengine.models import Model
|
||||
from cqlengine.exceptions import LWTException, IfNotExistsWithCounterColumn
|
||||
from cqlengine import columns, BatchQuery
|
||||
from uuid import uuid4
|
||||
import mock
|
||||
|
||||
|
||||
class TestIfNotExistsModel(Model):
|
||||
|
||||
id = columns.UUID(primary_key=True, default=lambda:uuid4())
|
||||
count = columns.Integer()
|
||||
text = columns.Text(required=False)
|
||||
|
||||
|
||||
class TestIfNotExistsWithCounterModel(Model):
|
||||
|
||||
id = columns.UUID(primary_key=True, default=lambda:uuid4())
|
||||
likes = columns.Counter()
|
||||
|
||||
|
||||
class BaseIfNotExistsTest(BaseCassEngTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(BaseIfNotExistsTest, cls).setUpClass()
|
||||
"""
|
||||
when receiving an insert statement with 'if not exist', cassandra would
|
||||
perform a read with QUORUM level. Unittest would be failed if replica_factor
|
||||
is 3 and one node only. Therefore I have create a new keyspace with
|
||||
replica_factor:1.
|
||||
"""
|
||||
sync_table(TestIfNotExistsModel)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(BaseIfNotExistsTest, cls).tearDownClass()
|
||||
drop_table(TestIfNotExistsModel)
|
||||
|
||||
|
||||
class BaseIfNotExistsWithCounterTest(BaseCassEngTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(BaseIfNotExistsWithCounterTest, cls).setUpClass()
|
||||
sync_table(TestIfNotExistsWithCounterModel)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(BaseIfNotExistsWithCounterTest, cls).tearDownClass()
|
||||
drop_table(TestIfNotExistsWithCounterModel)
|
||||
|
||||
|
||||
class IfNotExistsInsertTests(BaseIfNotExistsTest):
|
||||
|
||||
@skipUnless(PROTOCOL_VERSION >= 2, "only runs against the cql3 protocol v2.0")
|
||||
def test_insert_if_not_exists_success(self):
|
||||
""" tests that insertion with if_not_exists work as expected """
|
||||
|
||||
id = uuid4()
|
||||
|
||||
TestIfNotExistsModel.create(id=id, count=8, text='123456789')
|
||||
with self.assertRaises(LWTException):
|
||||
TestIfNotExistsModel.if_not_exists().create(id=id, count=9, text='111111111111')
|
||||
|
||||
q = TestIfNotExistsModel.objects(id=id)
|
||||
self.assertEqual(len(q), 1)
|
||||
|
||||
tm = q.first()
|
||||
self.assertEquals(tm.count, 8)
|
||||
self.assertEquals(tm.text, '123456789')
|
||||
|
||||
def test_insert_if_not_exists_failure(self):
|
||||
""" tests that insertion with if_not_exists failure """
|
||||
|
||||
id = uuid4()
|
||||
|
||||
TestIfNotExistsModel.create(id=id, count=8, text='123456789')
|
||||
TestIfNotExistsModel.create(id=id, count=9, text='111111111111')
|
||||
|
||||
q = TestIfNotExistsModel.objects(id=id)
|
||||
self.assertEquals(len(q), 1)
|
||||
|
||||
tm = q.first()
|
||||
self.assertEquals(tm.count, 9)
|
||||
self.assertEquals(tm.text, '111111111111')
|
||||
|
||||
@skipUnless(PROTOCOL_VERSION >= 2, "only runs against the cql3 protocol v2.0")
|
||||
def test_batch_insert_if_not_exists_success(self):
|
||||
""" tests that batch insertion with if_not_exists work as expected """
|
||||
|
||||
id = uuid4()
|
||||
|
||||
with BatchQuery() as b:
|
||||
TestIfNotExistsModel.batch(b).if_not_exists().create(id=id, count=8, text='123456789')
|
||||
|
||||
b = BatchQuery()
|
||||
TestIfNotExistsModel.batch(b).if_not_exists().create(id=id, count=9, text='111111111111')
|
||||
with self.assertRaises(LWTException):
|
||||
b.execute()
|
||||
|
||||
q = TestIfNotExistsModel.objects(id=id)
|
||||
self.assertEqual(len(q), 1)
|
||||
|
||||
tm = q.first()
|
||||
self.assertEquals(tm.count, 8)
|
||||
self.assertEquals(tm.text, '123456789')
|
||||
|
||||
def test_batch_insert_if_not_exists_failure(self):
|
||||
""" tests that batch insertion with if_not_exists failure """
|
||||
id = uuid4()
|
||||
|
||||
with BatchQuery() as b:
|
||||
TestIfNotExistsModel.batch(b).create(id=id, count=8, text='123456789')
|
||||
with BatchQuery() as b:
|
||||
TestIfNotExistsModel.batch(b).create(id=id, count=9, text='111111111111')
|
||||
|
||||
q = TestIfNotExistsModel.objects(id=id)
|
||||
self.assertEquals(len(q), 1)
|
||||
|
||||
tm = q.first()
|
||||
self.assertEquals(tm.count, 9)
|
||||
self.assertEquals(tm.text, '111111111111')
|
||||
|
||||
|
||||
class IfNotExistsModelTest(BaseIfNotExistsTest):
|
||||
|
||||
def test_if_not_exists_included_on_create(self):
|
||||
""" tests that if_not_exists on models works as expected """
|
||||
|
||||
with mock.patch.object(self.session, 'execute') as m:
|
||||
TestIfNotExistsModel.if_not_exists().create(count=8)
|
||||
|
||||
query = m.call_args[0][0].query_string
|
||||
self.assertIn("IF NOT EXISTS", query)
|
||||
|
||||
def test_if_not_exists_included_on_save(self):
|
||||
""" tests if we correctly put 'IF NOT EXISTS' for insert statement """
|
||||
|
||||
with mock.patch.object(self.session, 'execute') as m:
|
||||
tm = TestIfNotExistsModel(count=8)
|
||||
tm.if_not_exists(True).save()
|
||||
|
||||
query = m.call_args[0][0].query_string
|
||||
self.assertIn("IF NOT EXISTS", query)
|
||||
|
||||
def test_queryset_is_returned_on_class(self):
|
||||
""" ensure we get a queryset description back """
|
||||
qs = TestIfNotExistsModel.if_not_exists()
|
||||
self.assertTrue(isinstance(qs, TestIfNotExistsModel.__queryset__), type(qs))
|
||||
|
||||
def test_batch_if_not_exists(self):
|
||||
""" ensure 'IF NOT EXISTS' exists in statement when in batch """
|
||||
with mock.patch.object(self.session, 'execute') as m:
|
||||
with BatchQuery() as b:
|
||||
TestIfNotExistsModel.batch(b).if_not_exists().create(count=8)
|
||||
|
||||
self.assertIn("IF NOT EXISTS", m.call_args[0][0].query_string)
|
||||
|
||||
|
||||
class IfNotExistsInstanceTest(BaseIfNotExistsTest):
|
||||
|
||||
def test_instance_is_returned(self):
|
||||
"""
|
||||
ensures that we properly handle the instance.if_not_exists(True).save()
|
||||
scenario
|
||||
"""
|
||||
o = TestIfNotExistsModel.create(text="whatever")
|
||||
o.text = "new stuff"
|
||||
o = o.if_not_exists(True)
|
||||
self.assertEqual(True, o._if_not_exists)
|
||||
|
||||
def test_if_not_exists_is_not_include_with_query_on_update(self):
|
||||
"""
|
||||
make sure we don't put 'IF NOT EXIST' in update statements
|
||||
"""
|
||||
o = TestIfNotExistsModel.create(text="whatever")
|
||||
o.text = "new stuff"
|
||||
o = o.if_not_exists(True)
|
||||
|
||||
with mock.patch.object(self.session, 'execute') as m:
|
||||
o.save()
|
||||
|
||||
query = m.call_args[0][0].query_string
|
||||
self.assertNotIn("IF NOT EXIST", query)
|
||||
|
||||
|
||||
class IfNotExistWithCounterTest(BaseIfNotExistsWithCounterTest):
|
||||
|
||||
def test_instance_raise_exception(self):
|
||||
""" make sure exception is raised when calling
|
||||
if_not_exists on table with counter column
|
||||
"""
|
||||
id = uuid4()
|
||||
with self.assertRaises(IfNotExistsWithCounterColumn):
|
||||
TestIfNotExistsWithCounterModel.if_not_exists()
|
||||
|
34
cqlengine/tests/test_load.py
Normal file
34
cqlengine/tests/test_load.py
Normal file
@ -0,0 +1,34 @@
|
||||
import os
|
||||
from unittest import TestCase, skipUnless
|
||||
|
||||
from cqlengine import Model, Integer
|
||||
from cqlengine.management import sync_table
|
||||
from cqlengine.tests import base
|
||||
import resource
|
||||
import gc
|
||||
|
||||
class LoadTest(Model):
|
||||
|
||||
k = Integer(primary_key=True)
|
||||
v = Integer()
|
||||
|
||||
|
||||
@skipUnless("LOADTEST" in os.environ, "LOADTEST not on")
|
||||
def test_lots_of_queries():
|
||||
sync_table(LoadTest)
|
||||
import objgraph
|
||||
gc.collect()
|
||||
objgraph.show_most_common_types()
|
||||
|
||||
print("Starting...")
|
||||
|
||||
for i in range(1000000):
|
||||
if i % 25000 == 0:
|
||||
# print memory statistic
|
||||
print("Memory usage: %s" % (resource.getrusage(resource.RUSAGE_SELF).ru_maxrss))
|
||||
|
||||
LoadTest.create(k=i, v=i)
|
||||
|
||||
objgraph.show_most_common_types()
|
||||
|
||||
raise Exception("you shouldn't be here")
|
177
cqlengine/tests/test_timestamp.py
Normal file
177
cqlengine/tests/test_timestamp.py
Normal file
@ -0,0 +1,177 @@
|
||||
"""
|
||||
Tests surrounding the blah.timestamp( timedelta(seconds=30) ) format.
|
||||
"""
|
||||
from datetime import timedelta, datetime
|
||||
|
||||
from uuid import uuid4
|
||||
import mock
|
||||
import sure
|
||||
from cqlengine import Model, columns, BatchQuery
|
||||
from cqlengine.management import sync_table
|
||||
from cqlengine.tests.base import BaseCassEngTestCase
|
||||
|
||||
|
||||
class TestTimestampModel(Model):
|
||||
id = columns.UUID(primary_key=True, default=lambda:uuid4())
|
||||
count = columns.Integer()
|
||||
|
||||
|
||||
class BaseTimestampTest(BaseCassEngTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(BaseTimestampTest, cls).setUpClass()
|
||||
sync_table(TestTimestampModel)
|
||||
|
||||
|
||||
class BatchTest(BaseTimestampTest):
|
||||
|
||||
def test_batch_is_included(self):
|
||||
with mock.patch.object(self.session, "execute") as m, BatchQuery(timestamp=timedelta(seconds=30)) as b:
|
||||
TestTimestampModel.batch(b).create(count=1)
|
||||
|
||||
"USING TIMESTAMP".should.be.within(m.call_args[0][0].query_string)
|
||||
|
||||
|
||||
class CreateWithTimestampTest(BaseTimestampTest):
|
||||
|
||||
def test_batch(self):
|
||||
with mock.patch.object(self.session, "execute") as m, BatchQuery() as b:
|
||||
TestTimestampModel.timestamp(timedelta(seconds=10)).batch(b).create(count=1)
|
||||
|
||||
query = m.call_args[0][0].query_string
|
||||
|
||||
query.should.match(r"INSERT.*USING TIMESTAMP")
|
||||
query.should_not.match(r"TIMESTAMP.*INSERT")
|
||||
|
||||
def test_timestamp_not_included_on_normal_create(self):
|
||||
with mock.patch.object(self.session, "execute") as m:
|
||||
TestTimestampModel.create(count=2)
|
||||
|
||||
"USING TIMESTAMP".shouldnt.be.within(m.call_args[0][0].query_string)
|
||||
|
||||
def test_timestamp_is_set_on_model_queryset(self):
|
||||
delta = timedelta(seconds=30)
|
||||
tmp = TestTimestampModel.timestamp(delta)
|
||||
tmp._timestamp.should.equal(delta)
|
||||
|
||||
def test_non_batch_syntax_integration(self):
|
||||
tmp = TestTimestampModel.timestamp(timedelta(seconds=30)).create(count=1)
|
||||
tmp.should.be.ok
|
||||
|
||||
def test_non_batch_syntax_unit(self):
|
||||
|
||||
with mock.patch.object(self.session, "execute") as m:
|
||||
TestTimestampModel.timestamp(timedelta(seconds=30)).create(count=1)
|
||||
|
||||
query = m.call_args[0][0].query_string
|
||||
|
||||
"USING TIMESTAMP".should.be.within(query)
|
||||
|
||||
|
||||
class UpdateWithTimestampTest(BaseTimestampTest):
|
||||
def setUp(self):
|
||||
self.instance = TestTimestampModel.create(count=1)
|
||||
super(UpdateWithTimestampTest, self).setUp()
|
||||
|
||||
def test_instance_update_includes_timestamp_in_query(self):
|
||||
# not a batch
|
||||
|
||||
with mock.patch.object(self.session, "execute") as m:
|
||||
self.instance.timestamp(timedelta(seconds=30)).update(count=2)
|
||||
|
||||
"USING TIMESTAMP".should.be.within(m.call_args[0][0].query_string)
|
||||
|
||||
def test_instance_update_in_batch(self):
|
||||
with mock.patch.object(self.session, "execute") as m, BatchQuery() as b:
|
||||
self.instance.batch(b).timestamp(timedelta(seconds=30)).update(count=2)
|
||||
|
||||
query = m.call_args[0][0].query_string
|
||||
"USING TIMESTAMP".should.be.within(query)
|
||||
|
||||
|
||||
class DeleteWithTimestampTest(BaseTimestampTest):
|
||||
|
||||
def test_non_batch(self):
|
||||
"""
|
||||
we don't expect the model to come back at the end because the deletion timestamp should be in the future
|
||||
"""
|
||||
uid = uuid4()
|
||||
tmp = TestTimestampModel.create(id=uid, count=1)
|
||||
|
||||
TestTimestampModel.get(id=uid).should.be.ok
|
||||
|
||||
tmp.timestamp(timedelta(seconds=5)).delete()
|
||||
|
||||
with self.assertRaises(TestTimestampModel.DoesNotExist):
|
||||
TestTimestampModel.get(id=uid)
|
||||
|
||||
tmp = TestTimestampModel.create(id=uid, count=1)
|
||||
|
||||
with self.assertRaises(TestTimestampModel.DoesNotExist):
|
||||
TestTimestampModel.get(id=uid)
|
||||
|
||||
# calling .timestamp sets the TS on the model
|
||||
tmp.timestamp(timedelta(seconds=5))
|
||||
tmp._timestamp.should.be.ok
|
||||
|
||||
# calling save clears the set timestamp
|
||||
tmp.save()
|
||||
tmp._timestamp.shouldnt.be.ok
|
||||
|
||||
tmp.timestamp(timedelta(seconds=5))
|
||||
tmp.update()
|
||||
tmp._timestamp.shouldnt.be.ok
|
||||
|
||||
def test_blind_delete(self):
|
||||
"""
|
||||
we don't expect the model to come back at the end because the deletion timestamp should be in the future
|
||||
"""
|
||||
uid = uuid4()
|
||||
tmp = TestTimestampModel.create(id=uid, count=1)
|
||||
|
||||
TestTimestampModel.get(id=uid).should.be.ok
|
||||
|
||||
TestTimestampModel.objects(id=uid).timestamp(timedelta(seconds=5)).delete()
|
||||
|
||||
with self.assertRaises(TestTimestampModel.DoesNotExist):
|
||||
TestTimestampModel.get(id=uid)
|
||||
|
||||
tmp = TestTimestampModel.create(id=uid, count=1)
|
||||
|
||||
with self.assertRaises(TestTimestampModel.DoesNotExist):
|
||||
TestTimestampModel.get(id=uid)
|
||||
|
||||
def test_blind_delete_with_datetime(self):
|
||||
"""
|
||||
we don't expect the model to come back at the end because the deletion timestamp should be in the future
|
||||
"""
|
||||
uid = uuid4()
|
||||
tmp = TestTimestampModel.create(id=uid, count=1)
|
||||
|
||||
TestTimestampModel.get(id=uid).should.be.ok
|
||||
|
||||
plus_five_seconds = datetime.now() + timedelta(seconds=5)
|
||||
|
||||
TestTimestampModel.objects(id=uid).timestamp(plus_five_seconds).delete()
|
||||
|
||||
with self.assertRaises(TestTimestampModel.DoesNotExist):
|
||||
TestTimestampModel.get(id=uid)
|
||||
|
||||
tmp = TestTimestampModel.create(id=uid, count=1)
|
||||
|
||||
with self.assertRaises(TestTimestampModel.DoesNotExist):
|
||||
TestTimestampModel.get(id=uid)
|
||||
|
||||
def test_delete_in_the_past(self):
|
||||
uid = uuid4()
|
||||
tmp = TestTimestampModel.create(id=uid, count=1)
|
||||
|
||||
TestTimestampModel.get(id=uid).should.be.ok
|
||||
|
||||
# delete the in past, should not affect the object created above
|
||||
TestTimestampModel.objects(id=uid).timestamp(timedelta(seconds=-60)).delete()
|
||||
|
||||
TestTimestampModel.get(id=uid)
|
||||
|
||||
|
100
cqlengine/tests/test_transaction.py
Normal file
100
cqlengine/tests/test_transaction.py
Normal file
@ -0,0 +1,100 @@
|
||||
__author__ = 'Tim Martin'
|
||||
from unittest import skipUnless
|
||||
|
||||
from cqlengine.management import sync_table, drop_table
|
||||
from cqlengine.tests.base import BaseCassEngTestCase
|
||||
from cqlengine.tests.base import CASSANDRA_VERSION
|
||||
from cqlengine.models import Model
|
||||
from cqlengine.exceptions import LWTException
|
||||
from uuid import uuid4
|
||||
from cqlengine import columns, BatchQuery
|
||||
import mock
|
||||
from cqlengine import ALL, BatchQuery
|
||||
from cqlengine.statements import TransactionClause
|
||||
import six
|
||||
|
||||
|
||||
class TestTransactionModel(Model):
|
||||
id = columns.UUID(primary_key=True, default=lambda:uuid4())
|
||||
count = columns.Integer()
|
||||
text = columns.Text(required=False)
|
||||
|
||||
|
||||
@skipUnless(CASSANDRA_VERSION >= 20, "transactions only supported on cassandra 2.0 or higher")
|
||||
class TestTransaction(BaseCassEngTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestTransaction, cls).setUpClass()
|
||||
sync_table(TestTransactionModel)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(TestTransaction, cls).tearDownClass()
|
||||
drop_table(TestTransactionModel)
|
||||
|
||||
def test_update_using_transaction(self):
|
||||
t = TestTransactionModel.create(text='blah blah')
|
||||
t.text = 'new blah'
|
||||
with mock.patch.object(self.session, 'execute') as m:
|
||||
t.iff(text='blah blah').save()
|
||||
|
||||
args = m.call_args
|
||||
self.assertIn('IF "text" = %(0)s', args[0][0].query_string)
|
||||
|
||||
def test_update_transaction_success(self):
|
||||
t = TestTransactionModel.create(text='blah blah', count=5)
|
||||
id = t.id
|
||||
t.text = 'new blah'
|
||||
t.iff(text='blah blah').save()
|
||||
|
||||
updated = TestTransactionModel.objects(id=id).first()
|
||||
self.assertEqual(updated.count, 5)
|
||||
self.assertEqual(updated.text, 'new blah')
|
||||
|
||||
def test_update_failure(self):
|
||||
t = TestTransactionModel.create(text='blah blah')
|
||||
t.text = 'new blah'
|
||||
t = t.iff(text='something wrong')
|
||||
self.assertRaises(LWTException, t.save)
|
||||
|
||||
def test_blind_update(self):
|
||||
t = TestTransactionModel.create(text='blah blah')
|
||||
t.text = 'something else'
|
||||
uid = t.id
|
||||
|
||||
with mock.patch.object(self.session, 'execute') as m:
|
||||
TestTransactionModel.objects(id=uid).iff(text='blah blah').update(text='oh hey der')
|
||||
|
||||
args = m.call_args
|
||||
self.assertIn('IF "text" = %(1)s', args[0][0].query_string)
|
||||
|
||||
def test_blind_update_fail(self):
|
||||
t = TestTransactionModel.create(text='blah blah')
|
||||
t.text = 'something else'
|
||||
uid = t.id
|
||||
qs = TestTransactionModel.objects(id=uid).iff(text='Not dis!')
|
||||
self.assertRaises(LWTException, qs.update, text='this will never work')
|
||||
|
||||
def test_transaction_clause(self):
|
||||
tc = TransactionClause('some_value', 23)
|
||||
tc.set_context_id(3)
|
||||
|
||||
self.assertEqual('"some_value" = %(3)s', six.text_type(tc))
|
||||
self.assertEqual('"some_value" = %(3)s', str(tc))
|
||||
|
||||
def test_batch_update_transaction(self):
|
||||
t = TestTransactionModel.create(text='something', count=5)
|
||||
id = t.id
|
||||
with BatchQuery() as b:
|
||||
t.batch(b).iff(count=5).update(text='something else')
|
||||
|
||||
updated = TestTransactionModel.objects(id=id).first()
|
||||
self.assertEqual(updated.text, 'something else')
|
||||
|
||||
b = BatchQuery()
|
||||
updated.batch(b).iff(count=6).update(text='and another thing')
|
||||
self.assertRaises(LWTException, b.execute)
|
||||
|
||||
updated = TestTransactionModel.objects(id=id).first()
|
||||
self.assertEqual(updated.text, 'something else')
|
181
cqlengine/tests/test_ttl.py
Normal file
181
cqlengine/tests/test_ttl.py
Normal file
@ -0,0 +1,181 @@
|
||||
from cqlengine.management import sync_table, drop_table
|
||||
from cqlengine.tests.base import BaseCassEngTestCase
|
||||
from cqlengine.models import Model
|
||||
from uuid import uuid4
|
||||
from cqlengine import columns
|
||||
import mock
|
||||
from cqlengine.connection import get_session
|
||||
|
||||
|
||||
class TestTTLModel(Model):
|
||||
id = columns.UUID(primary_key=True, default=lambda:uuid4())
|
||||
count = columns.Integer()
|
||||
text = columns.Text(required=False)
|
||||
|
||||
|
||||
class BaseTTLTest(BaseCassEngTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(BaseTTLTest, cls).setUpClass()
|
||||
sync_table(TestTTLModel)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(BaseTTLTest, cls).tearDownClass()
|
||||
drop_table(TestTTLModel)
|
||||
|
||||
|
||||
class TestDefaultTTLModel(Model):
|
||||
__default_ttl__ = 20
|
||||
id = columns.UUID(primary_key=True, default=lambda:uuid4())
|
||||
count = columns.Integer()
|
||||
text = columns.Text(required=False)
|
||||
|
||||
|
||||
class BaseDefaultTTLTest(BaseCassEngTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(BaseDefaultTTLTest, cls).setUpClass()
|
||||
sync_table(TestDefaultTTLModel)
|
||||
sync_table(TestTTLModel)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(BaseDefaultTTLTest, cls).tearDownClass()
|
||||
drop_table(TestDefaultTTLModel)
|
||||
drop_table(TestTTLModel)
|
||||
|
||||
|
||||
class TTLQueryTests(BaseTTLTest):
|
||||
|
||||
def test_update_queryset_ttl_success_case(self):
|
||||
""" tests that ttls on querysets work as expected """
|
||||
|
||||
def test_select_ttl_failure(self):
|
||||
""" tests that ttls on select queries raise an exception """
|
||||
|
||||
|
||||
class TTLModelTests(BaseTTLTest):
|
||||
|
||||
def test_ttl_included_on_create(self):
|
||||
""" tests that ttls on models work as expected """
|
||||
session = get_session()
|
||||
|
||||
with mock.patch.object(session, 'execute') as m:
|
||||
TestTTLModel.ttl(60).create(text="hello blake")
|
||||
|
||||
query = m.call_args[0][0].query_string
|
||||
self.assertIn("USING TTL", query)
|
||||
|
||||
def test_queryset_is_returned_on_class(self):
|
||||
"""
|
||||
ensures we get a queryset descriptor back
|
||||
"""
|
||||
qs = TestTTLModel.ttl(60)
|
||||
self.assertTrue(isinstance(qs, TestTTLModel.__queryset__), type(qs))
|
||||
|
||||
|
||||
|
||||
class TTLInstanceUpdateTest(BaseTTLTest):
|
||||
def test_update_includes_ttl(self):
|
||||
session = get_session()
|
||||
|
||||
model = TestTTLModel.create(text="goodbye blake")
|
||||
with mock.patch.object(session, 'execute') as m:
|
||||
model.ttl(60).update(text="goodbye forever")
|
||||
|
||||
query = m.call_args[0][0].query_string
|
||||
self.assertIn("USING TTL", query)
|
||||
|
||||
def test_update_syntax_valid(self):
|
||||
# sanity test that ensures the TTL syntax is accepted by cassandra
|
||||
model = TestTTLModel.create(text="goodbye blake")
|
||||
model.ttl(60).update(text="goodbye forever")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class TTLInstanceTest(BaseTTLTest):
|
||||
def test_instance_is_returned(self):
|
||||
"""
|
||||
ensures that we properly handle the instance.ttl(60).save() scenario
|
||||
:return:
|
||||
"""
|
||||
o = TestTTLModel.create(text="whatever")
|
||||
o.text = "new stuff"
|
||||
o = o.ttl(60)
|
||||
self.assertEqual(60, o._ttl)
|
||||
|
||||
def test_ttl_is_include_with_query_on_update(self):
|
||||
session = get_session()
|
||||
|
||||
o = TestTTLModel.create(text="whatever")
|
||||
o.text = "new stuff"
|
||||
o = o.ttl(60)
|
||||
|
||||
with mock.patch.object(session, 'execute') as m:
|
||||
o.save()
|
||||
|
||||
query = m.call_args[0][0].query_string
|
||||
self.assertIn("USING TTL", query)
|
||||
|
||||
|
||||
class TTLBlindUpdateTest(BaseTTLTest):
|
||||
def test_ttl_included_with_blind_update(self):
|
||||
session = get_session()
|
||||
|
||||
o = TestTTLModel.create(text="whatever")
|
||||
tid = o.id
|
||||
|
||||
with mock.patch.object(session, 'execute') as m:
|
||||
TestTTLModel.objects(id=tid).ttl(60).update(text="bacon")
|
||||
|
||||
query = m.call_args[0][0].query_string
|
||||
self.assertIn("USING TTL", query)
|
||||
|
||||
|
||||
class TTLDefaultTest(BaseDefaultTTLTest):
|
||||
def test_default_ttl_not_set(self):
|
||||
session = get_session()
|
||||
|
||||
o = TestTTLModel.create(text="some text")
|
||||
tid = o.id
|
||||
|
||||
self.assertIsNone(o._ttl)
|
||||
|
||||
with mock.patch.object(session, 'execute') as m:
|
||||
TestTTLModel.objects(id=tid).update(text="aligators")
|
||||
|
||||
query = m.call_args[0][0].query_string
|
||||
self.assertNotIn("USING TTL", query)
|
||||
|
||||
def test_default_ttl_set(self):
|
||||
session = get_session()
|
||||
o = TestDefaultTTLModel.create(text="some text on ttl")
|
||||
tid = o.id
|
||||
|
||||
self.assertEqual(o._ttl, TestDefaultTTLModel.__default_ttl__)
|
||||
|
||||
with mock.patch.object(session, 'execute') as m:
|
||||
TestDefaultTTLModel.objects(id=tid).update(text="aligators expired")
|
||||
|
||||
query = m.call_args[0][0].query_string
|
||||
self.assertIn("USING TTL", query)
|
||||
|
||||
def test_override_default_ttl(self):
|
||||
session = get_session()
|
||||
o = TestDefaultTTLModel.create(text="some text on ttl")
|
||||
tid = o.id
|
||||
|
||||
self.assertEqual(o._ttl, TestDefaultTTLModel.__default_ttl__)
|
||||
o.ttl(3600)
|
||||
self.assertEqual(o._ttl, 3600)
|
||||
|
||||
with mock.patch.object(session, 'execute') as m:
|
||||
TestDefaultTTLModel.objects(id=tid).ttl(None).update(text="aligators expired")
|
||||
|
||||
query = m.call_args[0][0].query_string
|
||||
self.assertNotIn("USING TTL", query)
|
@ -72,17 +72,17 @@ qthelp:
|
||||
@echo
|
||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/CassandraDriver.qhcp"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/cassandra-driver.qhcp"
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/CassandraDriver.qhc"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/cassandra-driver.qhc"
|
||||
|
||||
devhelp:
|
||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||
@echo
|
||||
@echo "Build finished."
|
||||
@echo "To view the help file:"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/CassandraDriver"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/CassandraDriver"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/cassandra-driver"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/cassandra-driver"
|
||||
@echo "# devhelp"
|
||||
|
||||
epub:
|
||||
@ -100,7 +100,7 @@ latex:
|
||||
latexpdf:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through pdflatex..."
|
||||
make -C $(BUILDDIR)/latex all-pdf
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
text:
|
||||
|
@ -183,8 +183,7 @@ htmlhelp_basename = 'CassandraDriverdoc'
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||
latex_documents = [
|
||||
('index', 'CassandraDriver.tex', u'Cassandra Driver Documentation',
|
||||
u'DataStax', 'manual'),
|
||||
('index', 'cassandra-driver.tex', u'Cassandra Driver Documentation', u'DataStax', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
@ -216,6 +215,6 @@ latex_documents = [
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'cassandradriver', u'Cassandra Driver Documentation',
|
||||
('index', 'cassandra-driver', u'Cassandra Driver Documentation',
|
||||
[u'Tyler Hobbs'], 1)
|
||||
]
|
||||
|
112
docs/index.rst
112
docs/index.rst
@ -70,3 +70,115 @@ Indices and Tables
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
||||
..
|
||||
cqlengine documentation
|
||||
=======================
|
||||
|
||||
**Users of versions < 0.16, the default keyspace 'cqlengine' has been removed. Please read this before upgrading:** :ref:`Breaking Changes <keyspace-change>`
|
||||
|
||||
cqlengine is a Cassandra CQL 3 Object Mapper for Python
|
||||
|
||||
:ref:`getting-started`
|
||||
|
||||
Download
|
||||
========
|
||||
|
||||
`Github <https://github.com/cqlengine/cqlengine>`_
|
||||
|
||||
`PyPi <https://pypi.python.org/pypi/cqlengine>`_
|
||||
|
||||
Contents:
|
||||
=========
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
topics/models
|
||||
topics/queryset
|
||||
topics/columns
|
||||
topics/connection
|
||||
topics/manage_schemas
|
||||
topics/external_resources
|
||||
topics/related_projects
|
||||
topics/third_party
|
||||
topics/development
|
||||
topics/faq
|
||||
|
||||
.. _getting-started:
|
||||
|
||||
Getting Started
|
||||
===============
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
#first, define a model
|
||||
from cqlengine import columns
|
||||
from cqlengine import Model
|
||||
|
||||
class ExampleModel(Model):
|
||||
example_id = columns.UUID(primary_key=True, default=uuid.uuid4)
|
||||
example_type = columns.Integer(index=True)
|
||||
created_at = columns.DateTime()
|
||||
description = columns.Text(required=False)
|
||||
|
||||
#next, setup the connection to your cassandra server(s)...
|
||||
>>> from cqlengine import connection
|
||||
|
||||
# see http://datastax.github.io/python-driver/api/cassandra/cluster.html for options
|
||||
# the list of hosts will be passed to create a Cluster() instance
|
||||
>>> connection.setup(['127.0.0.1'], "cqlengine")
|
||||
|
||||
# if you're connecting to a 1.2 cluster
|
||||
>>> connection.setup(['127.0.0.1'], "cqlengine", protocol_version=1)
|
||||
|
||||
#...and create your CQL table
|
||||
>>> from cqlengine.management import sync_table
|
||||
>>> sync_table(ExampleModel)
|
||||
|
||||
#now we can create some rows:
|
||||
>>> em1 = ExampleModel.create(example_type=0, description="example1", created_at=datetime.now())
|
||||
>>> em2 = ExampleModel.create(example_type=0, description="example2", created_at=datetime.now())
|
||||
>>> em3 = ExampleModel.create(example_type=0, description="example3", created_at=datetime.now())
|
||||
>>> em4 = ExampleModel.create(example_type=0, description="example4", created_at=datetime.now())
|
||||
>>> em5 = ExampleModel.create(example_type=1, description="example5", created_at=datetime.now())
|
||||
>>> em6 = ExampleModel.create(example_type=1, description="example6", created_at=datetime.now())
|
||||
>>> em7 = ExampleModel.create(example_type=1, description="example7", created_at=datetime.now())
|
||||
>>> em8 = ExampleModel.create(example_type=1, description="example8", created_at=datetime.now())
|
||||
|
||||
#and now we can run some queries against our table
|
||||
>>> ExampleModel.objects.count()
|
||||
8
|
||||
>>> q = ExampleModel.objects(example_type=1)
|
||||
>>> q.count()
|
||||
4
|
||||
>>> for instance in q:
|
||||
>>> print instance.description
|
||||
example5
|
||||
example6
|
||||
example7
|
||||
example8
|
||||
|
||||
#here we are applying additional filtering to an existing query
|
||||
#query objects are immutable, so calling filter returns a new
|
||||
#query object
|
||||
>>> q2 = q.filter(example_id=em5.example_id)
|
||||
|
||||
>>> q2.count()
|
||||
1
|
||||
>>> for instance in q2:
|
||||
>>> print instance.description
|
||||
example5
|
||||
|
||||
|
||||
`Report a Bug <https://github.com/cqlengine/cqlengine/issues>`_
|
||||
|
||||
`Users Mailing List <https://groups.google.com/forum/?fromgroups#!forum/cqlengine-users>`_
|
||||
|
||||
|
||||
Indices and tables
|
||||
>>>>>>> cqlengine/master
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
190
docs/make.bat
Normal file
190
docs/make.bat
Normal file
@ -0,0 +1,190 @@
|
||||
@ECHO OFF
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set BUILDDIR=_build
|
||||
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
|
||||
set I18NSPHINXOPTS=%SPHINXOPTS% .
|
||||
if NOT "%PAPER%" == "" (
|
||||
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
|
||||
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
|
||||
)
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
if "%1" == "help" (
|
||||
:help
|
||||
echo.Please use `make ^<target^>` where ^<target^> is one of
|
||||
echo. html to make standalone HTML files
|
||||
echo. dirhtml to make HTML files named index.html in directories
|
||||
echo. singlehtml to make a single large HTML file
|
||||
echo. pickle to make pickle files
|
||||
echo. json to make JSON files
|
||||
echo. htmlhelp to make HTML files and a HTML help project
|
||||
echo. qthelp to make HTML files and a qthelp project
|
||||
echo. devhelp to make HTML files and a Devhelp project
|
||||
echo. epub to make an epub
|
||||
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
|
||||
echo. text to make text files
|
||||
echo. man to make manual pages
|
||||
echo. texinfo to make Texinfo files
|
||||
echo. gettext to make PO message catalogs
|
||||
echo. changes to make an overview over all changed/added/deprecated items
|
||||
echo. linkcheck to check all external links for integrity
|
||||
echo. doctest to run all doctests embedded in the documentation if enabled
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "clean" (
|
||||
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
|
||||
del /q /s %BUILDDIR%\*
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "html" (
|
||||
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "dirhtml" (
|
||||
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "singlehtml" (
|
||||
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "pickle" (
|
||||
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can process the pickle files.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "json" (
|
||||
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can process the JSON files.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "htmlhelp" (
|
||||
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can run HTML Help Workshop with the ^
|
||||
.hhp project file in %BUILDDIR%/htmlhelp.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "qthelp" (
|
||||
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can run "qcollectiongenerator" with the ^
|
||||
.qhcp project file in %BUILDDIR%/qthelp, like this:
|
||||
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\cqlengine.qhcp
|
||||
echo.To view the help file:
|
||||
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\cqlengine.ghc
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "devhelp" (
|
||||
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "epub" (
|
||||
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The epub file is in %BUILDDIR%/epub.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "latex" (
|
||||
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "text" (
|
||||
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The text files are in %BUILDDIR%/text.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "man" (
|
||||
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The manual pages are in %BUILDDIR%/man.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "texinfo" (
|
||||
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "gettext" (
|
||||
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "changes" (
|
||||
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.The overview file is in %BUILDDIR%/changes.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "linkcheck" (
|
||||
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Link check complete; look for any errors in the above output ^
|
||||
or in %BUILDDIR%/linkcheck/output.txt.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "doctest" (
|
||||
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Testing of doctests in the sources finished, look at the ^
|
||||
results in %BUILDDIR%/doctest/output.txt.
|
||||
goto end
|
||||
)
|
||||
|
||||
:end
|
210
docs/topics/columns.rst
Normal file
210
docs/topics/columns.rst
Normal file
@ -0,0 +1,210 @@
|
||||
=======
|
||||
Columns
|
||||
=======
|
||||
|
||||
**Users of versions < 0.4, please read this post before upgrading:** `Breaking Changes`_
|
||||
|
||||
.. _Breaking Changes: https://groups.google.com/forum/?fromgroups#!topic/cqlengine-users/erkSNe1JwuU
|
||||
|
||||
.. module:: cqlengine.columns
|
||||
|
||||
.. class:: Bytes()
|
||||
|
||||
Stores arbitrary bytes (no validation), expressed as hexadecimal ::
|
||||
|
||||
columns.Bytes()
|
||||
|
||||
|
||||
.. class:: Ascii()
|
||||
|
||||
Stores a US-ASCII character string ::
|
||||
|
||||
columns.Ascii()
|
||||
|
||||
|
||||
.. class:: Text()
|
||||
|
||||
Stores a UTF-8 encoded string ::
|
||||
|
||||
columns.Text()
|
||||
|
||||
**options**
|
||||
|
||||
:attr:`~columns.Text.min_length`
|
||||
Sets the minimum length of this string. If this field is not set , and the column is not a required field, it defaults to 0, otherwise 1.
|
||||
|
||||
:attr:`~columns.Text.max_length`
|
||||
Sets the maximum length of this string. Defaults to None
|
||||
|
||||
.. class:: Integer()
|
||||
|
||||
Stores a 32-bit signed integer value ::
|
||||
|
||||
columns.Integer()
|
||||
|
||||
.. class:: BigInt()
|
||||
|
||||
Stores a 64-bit signed long value ::
|
||||
|
||||
columns.BigInt()
|
||||
|
||||
.. class:: VarInt()
|
||||
|
||||
Stores an arbitrary-precision integer ::
|
||||
|
||||
columns.VarInt()
|
||||
|
||||
.. class:: DateTime()
|
||||
|
||||
Stores a datetime value.
|
||||
|
||||
columns.DateTime()
|
||||
|
||||
.. class:: UUID()
|
||||
|
||||
Stores a type 1 or type 4 UUID.
|
||||
|
||||
columns.UUID()
|
||||
|
||||
.. class:: TimeUUID()
|
||||
|
||||
Stores a UUID value as the cql type 'timeuuid' ::
|
||||
|
||||
columns.TimeUUID()
|
||||
|
||||
.. classmethod:: from_datetime(dt)
|
||||
|
||||
generates a TimeUUID for the given datetime
|
||||
|
||||
:param dt: the datetime to create a time uuid from
|
||||
:type dt: datetime.datetime
|
||||
|
||||
:returns: a time uuid created from the given datetime
|
||||
:rtype: uuid1
|
||||
|
||||
.. class:: Boolean()
|
||||
|
||||
Stores a boolean True or False value ::
|
||||
|
||||
columns.Boolean()
|
||||
|
||||
.. class:: Float()
|
||||
|
||||
Stores a floating point value ::
|
||||
|
||||
columns.Float()
|
||||
|
||||
**options**
|
||||
|
||||
:attr:`~columns.Float.double_precision`
|
||||
If True, stores a double precision float value, otherwise single precision. Defaults to True.
|
||||
|
||||
.. class:: Decimal()
|
||||
|
||||
Stores a variable precision decimal value ::
|
||||
|
||||
columns.Decimal()
|
||||
|
||||
.. class:: Counter()
|
||||
|
||||
Counters can be incremented and decremented ::
|
||||
|
||||
columns.Counter()
|
||||
|
||||
|
||||
Collection Type Columns
|
||||
----------------------------
|
||||
|
||||
CQLEngine also supports container column types. Each container column requires a column class argument to specify what type of objects it will hold. The Map column requires 2, one for the key, and the other for the value
|
||||
|
||||
*Example*
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class Person(Model):
|
||||
id = columns.UUID(primary_key=True, default=uuid.uuid4)
|
||||
first_name = columns.Text()
|
||||
last_name = columns.Text()
|
||||
|
||||
friends = columns.Set(columns.Text)
|
||||
enemies = columns.Set(columns.Text)
|
||||
todo_list = columns.List(columns.Text)
|
||||
birthdays = columns.Map(columns.Text, columns.DateTime)
|
||||
|
||||
|
||||
|
||||
.. class:: Set()
|
||||
|
||||
Stores a set of unordered, unique values. Available only with Cassandra 1.2 and above ::
|
||||
|
||||
columns.Set(value_type)
|
||||
|
||||
**options**
|
||||
|
||||
:attr:`~columns.Set.value_type`
|
||||
The type of objects the set will contain
|
||||
|
||||
:attr:`~columns.Set.strict`
|
||||
If True, adding this column will raise an exception during save if the value is not a python `set` instance. If False, it will attempt to coerce the value to a set. Defaults to True.
|
||||
|
||||
.. class:: List()
|
||||
|
||||
Stores a list of ordered values. Available only with Cassandra 1.2 and above ::
|
||||
|
||||
columns.List(value_type)
|
||||
|
||||
**options**
|
||||
|
||||
:attr:`~columns.List.value_type`
|
||||
The type of objects the set will contain
|
||||
|
||||
.. class:: Map()
|
||||
|
||||
Stores a map (dictionary) collection, available only with Cassandra 1.2 and above ::
|
||||
|
||||
columns.Map(key_type, value_type)
|
||||
|
||||
**options**
|
||||
|
||||
:attr:`~columns.Map.key_type`
|
||||
The type of the map keys
|
||||
|
||||
:attr:`~columns.Map.value_type`
|
||||
The type of the map values
|
||||
|
||||
Column Options
|
||||
==============
|
||||
|
||||
Each column can be defined with optional arguments to modify the way they behave. While some column types may define additional column options, these are the options that are available on all columns:
|
||||
|
||||
.. attribute:: BaseColumn.primary_key
|
||||
|
||||
If True, this column is created as a primary key field. A model can have multiple primary keys. Defaults to False.
|
||||
|
||||
*In CQL, there are 2 types of primary keys: partition keys and clustering keys. As with CQL, the first primary key is the partition key, and all others are clustering keys, unless partition keys are specified manually using* :attr:`BaseColumn.partition_key`
|
||||
|
||||
.. attribute:: BaseColumn.partition_key
|
||||
|
||||
If True, this column is created as partition primary key. There may be many partition keys defined, forming a *composite partition key*
|
||||
|
||||
.. attribute:: BaseColumn.index
|
||||
|
||||
If True, an index will be created for this column. Defaults to False.
|
||||
|
||||
*Note: Indexes can only be created on models with one primary key*
|
||||
|
||||
.. attribute:: BaseColumn.db_field
|
||||
|
||||
Explicitly sets the name of the column in the database table. If this is left blank, the column name will be the same as the name of the column attribute. Defaults to None.
|
||||
|
||||
.. attribute:: BaseColumn.default
|
||||
|
||||
The default value for this column. If a model instance is saved without a value for this column having been defined, the default value will be used. This can be either a value or a callable object (ie: datetime.now is a valid default argument).
|
||||
|
||||
.. attribute:: BaseColumn.required
|
||||
|
||||
If True, this model cannot be saved without a value defined for this column. Defaults to False. Primary key fields cannot have their required fields set to False.
|
||||
|
||||
.. attribute:: BaseColumn.clustering_order
|
||||
|
||||
Defines CLUSTERING ORDER for this column (valid choices are "asc" (default) or "desc"). It may be specified only for clustering primary keys - more: http://www.datastax.com/docs/1.2/cql_cli/cql/CREATE_TABLE#using-clustering-order
|
31
docs/topics/connection.rst
Normal file
31
docs/topics/connection.rst
Normal file
@ -0,0 +1,31 @@
|
||||
==========
|
||||
Connection
|
||||
==========
|
||||
|
||||
**Users of versions < 0.4, please read this post before upgrading:** `Breaking Changes`_
|
||||
|
||||
.. _Breaking Changes: https://groups.google.com/forum/?fromgroups#!topic/cqlengine-users/erkSNe1JwuU
|
||||
|
||||
.. module:: cqlengine.connection
|
||||
|
||||
The setup function in `cqlengine.connection` records the Cassandra servers to connect to.
|
||||
If there is a problem with one of the servers, cqlengine will try to connect to each of the other connections before failing.
|
||||
|
||||
.. function:: setup(hosts)
|
||||
|
||||
:param hosts: list of hosts, strings in the <hostname>:<port>, or just <hostname>
|
||||
:type hosts: list
|
||||
|
||||
:param default_keyspace: keyspace to default to
|
||||
:type default_keyspace: str
|
||||
|
||||
:param consistency: the consistency level of the connection, defaults to 'ONE'
|
||||
:type consistency: int
|
||||
|
||||
# see http://datastax.github.io/python-driver/api/cassandra.html#cassandra.ConsistencyLevel
|
||||
|
||||
Records the hosts and connects to one of them
|
||||
|
||||
See the example at :ref:`getting-started`
|
||||
|
||||
|
47
docs/topics/development.rst
Normal file
47
docs/topics/development.rst
Normal file
@ -0,0 +1,47 @@
|
||||
==================
|
||||
Development
|
||||
==================
|
||||
|
||||
Travis CI
|
||||
================
|
||||
|
||||
Tests are run using Travis CI using a Matrix to test different Cassandra and Python versions. It is located here: https://travis-ci.org/cqlengine/cqlengine
|
||||
|
||||
Python versions:
|
||||
|
||||
- 2.7
|
||||
- 3.4
|
||||
|
||||
Cassandra vesions:
|
||||
|
||||
- 1.2 (protocol_version 1)
|
||||
- 2.0 (protocol_version 2)
|
||||
- 2.1 (upcoming, protocol_version 3)
|
||||
|
||||
Pull Requests
|
||||
===============
|
||||
Only Pull Requests that have passed the entire matrix will be considered for merge into the main codebase.
|
||||
|
||||
Please see the contributing guidelines: https://github.com/cqlengine/cqlengine/blob/master/CONTRIBUTING.md
|
||||
|
||||
|
||||
Testing Locally
|
||||
=================
|
||||
|
||||
Before testing, you'll need to set an environment variable to the version of Cassandra that's being tested. The version cooresponds to the <Major><Minor> release, so for example if you're testing against Cassandra 2.1, you'd set the following:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
export CASSANDRA_VERSION=20
|
||||
|
||||
At the command line, execute:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
bin/test.py
|
||||
|
||||
This is a wrapper for nose that also sets up the database connection.
|
||||
|
||||
|
||||
|
||||
|
22
docs/topics/external_resources.rst
Normal file
22
docs/topics/external_resources.rst
Normal file
@ -0,0 +1,22 @@
|
||||
============================
|
||||
External Resources
|
||||
============================
|
||||
|
||||
Video Tutorials
|
||||
================
|
||||
|
||||
Introduction to CQLEngine: https://www.youtube.com/watch?v=zrbQcPNMbB0
|
||||
|
||||
TimeUUID and Table Polymorphism: https://www.youtube.com/watch?v=clXN9pnakvI
|
||||
|
||||
|
||||
Blog Posts
|
||||
===========
|
||||
|
||||
Blake Eggleston on Table Polymorphism in the .8 release: http://blakeeggleston.com/cqlengine-08-released.html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
50
docs/topics/faq.rst
Normal file
50
docs/topics/faq.rst
Normal file
@ -0,0 +1,50 @@
|
||||
==========================
|
||||
Frequently Asked Questions
|
||||
==========================
|
||||
|
||||
Q: Why don't updates work correctly on models instantiated as Model(field=blah, field2=blah2)?
|
||||
-------------------------------------------------------------------
|
||||
|
||||
A: The recommended way to create new rows is with the models .create method. The values passed into a model's init method are interpreted by the model as the values as they were read from a row. This allows the model to "know" which rows have changed since the row was read out of cassandra, and create suitable update statements.
|
||||
|
||||
Q: How to preserve ordering in batch query?
|
||||
-------------------------------------------
|
||||
|
||||
A: Statement Ordering is not supported by CQL3 batches. Therefore,
|
||||
once cassandra needs resolving conflict(Updating the same column in one batch),
|
||||
The algorithm below would be used.
|
||||
|
||||
* If timestamps are different, pick the column with the largest timestamp (the value being a regular column or a tombstone)
|
||||
* If timestamps are the same, and one of the columns in a tombstone ('null') - pick the tombstone
|
||||
* If timestamps are the same, and none of the columns are tombstones, pick the column with the largest value
|
||||
|
||||
Below is an example to show this scenario.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyMode(Model):
|
||||
id = columns.Integer(primary_key=True)
|
||||
count = columns.Integer()
|
||||
text = columns.Text()
|
||||
|
||||
with BatchQuery() as b:
|
||||
MyModel.batch(b).create(id=1, count=2, text='123')
|
||||
MyModel.batch(b).create(id=1, count=3, text='111')
|
||||
|
||||
assert MyModel.objects(id=1).first().count == 3
|
||||
assert MyModel.objects(id=1).first().text == '123'
|
||||
|
||||
The largest value of count is 3, and the largest value of text would be '123'.
|
||||
|
||||
The workaround is applying timestamp to each statement, then Cassandra would
|
||||
resolve to the statement with the lastest timestamp.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
with BatchQuery() as b:
|
||||
MyModel.timestamp(datetime.now()).batch(b).create(id=1, count=2, text='123')
|
||||
MyModel.timestamp(datetime.now()).batch(b).create(id=1, count=3, text='111')
|
||||
|
||||
assert MyModel.objects(id=1).first().count == 3
|
||||
assert MyModel.objects(id=1).first().text == '111'
|
||||
|
47
docs/topics/manage_schemas.rst
Normal file
47
docs/topics/manage_schemas.rst
Normal file
@ -0,0 +1,47 @@
|
||||
================
|
||||
Managing Schemas
|
||||
================
|
||||
|
||||
**Users of versions < 0.4, please read this post before upgrading:** `Breaking Changes`_
|
||||
|
||||
.. _Breaking Changes: https://groups.google.com/forum/?fromgroups#!topic/cqlengine-users/erkSNe1JwuU
|
||||
|
||||
.. module:: cqlengine.connection
|
||||
|
||||
.. module:: cqlengine.management
|
||||
|
||||
Once a connection has been made to Cassandra, you can use the functions in ``cqlengine.management`` to create and delete keyspaces, as well as create and delete tables for defined models
|
||||
|
||||
.. function:: create_keyspace(name)
|
||||
|
||||
:param name: the keyspace name to create
|
||||
:type name: string
|
||||
|
||||
creates a keyspace with the given name
|
||||
|
||||
.. function:: delete_keyspace(name)
|
||||
|
||||
:param name: the keyspace name to delete
|
||||
:type name: string
|
||||
|
||||
deletes the keyspace with the given name
|
||||
|
||||
.. function:: sync_table(model)
|
||||
|
||||
:param model: the :class:`~cqlengine.model.Model` class to make a table with
|
||||
:type model: :class:`~cqlengine.model.Model`
|
||||
|
||||
syncs a python model to cassandra (creates & alters)
|
||||
|
||||
.. function:: drop_table(model)
|
||||
|
||||
:param model: the :class:`~cqlengine.model.Model` class to delete a column family for
|
||||
:type model: :class:`~cqlengine.model.Model`
|
||||
|
||||
deletes the CQL table for the given model
|
||||
|
||||
|
||||
|
||||
See the example at :ref:`getting-started`
|
||||
|
||||
|
477
docs/topics/models.rst
Normal file
477
docs/topics/models.rst
Normal file
@ -0,0 +1,477 @@
|
||||
======
|
||||
Models
|
||||
======
|
||||
|
||||
**Users of versions < 0.4, please read this post before upgrading:** `Breaking Changes`_
|
||||
|
||||
.. _Breaking Changes: https://groups.google.com/forum/?fromgroups#!topic/cqlengine-users/erkSNe1JwuU
|
||||
|
||||
.. module:: cqlengine.connection
|
||||
|
||||
.. module:: cqlengine.models
|
||||
|
||||
A model is a python class representing a CQL table.
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
This example defines a Person table, with the columns ``first_name`` and ``last_name``
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from cqlengine import columns
|
||||
from cqlengine.models import Model
|
||||
|
||||
class Person(Model):
|
||||
id = columns.UUID(primary_key=True)
|
||||
first_name = columns.Text()
|
||||
last_name = columns.Text()
|
||||
|
||||
|
||||
The Person model would create this CQL table:
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
CREATE TABLE cqlengine.person (
|
||||
id uuid,
|
||||
first_name text,
|
||||
last_name text,
|
||||
PRIMARY KEY (id)
|
||||
)
|
||||
|
||||
Here's an example of a comment table created with clustering keys, in descending order:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from cqlengine import columns
|
||||
from cqlengine.models import Model
|
||||
|
||||
class Comment(Model):
|
||||
photo_id = columns.UUID(primary_key=True)
|
||||
comment_id = columns.TimeUUID(primary_key=True, clustering_order="DESC")
|
||||
comment = columns.Text()
|
||||
|
||||
The Comment model's ``create table`` would look like the following:
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
CREATE TABLE comment (
|
||||
photo_id uuid,
|
||||
comment_id timeuuid,
|
||||
comment text,
|
||||
PRIMARY KEY (photo_id, comment_id)
|
||||
) WITH CLUSTERING ORDER BY (comment_id DESC)
|
||||
|
||||
To sync the models to the database, you may do the following:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from cqlengine.management import sync_table
|
||||
sync_table(Person)
|
||||
sync_table(Comment)
|
||||
|
||||
|
||||
Columns
|
||||
=======
|
||||
|
||||
Columns in your models map to columns in your CQL table. You define CQL columns by defining column attributes on your model classes. For a model to be valid it needs at least one primary key column and one non-primary key column.
|
||||
|
||||
Just as in CQL, the order you define your columns in is important, and is the same order they are defined in on a model's corresponding table.
|
||||
|
||||
Column Types
|
||||
============
|
||||
|
||||
Each column on your model definitions needs to an instance of a Column class. The column types that are included with cqlengine as of this writing are:
|
||||
|
||||
* :class:`~cqlengine.columns.Bytes`
|
||||
* :class:`~cqlengine.columns.Ascii`
|
||||
* :class:`~cqlengine.columns.Text`
|
||||
* :class:`~cqlengine.columns.Integer`
|
||||
* :class:`~cqlengine.columns.BigInt`
|
||||
* :class:`~cqlengine.columns.DateTime`
|
||||
* :class:`~cqlengine.columns.UUID`
|
||||
* :class:`~cqlengine.columns.TimeUUID`
|
||||
* :class:`~cqlengine.columns.Boolean`
|
||||
* :class:`~cqlengine.columns.Float`
|
||||
* :class:`~cqlengine.columns.Decimal`
|
||||
* :class:`~cqlengine.columns.Set`
|
||||
* :class:`~cqlengine.columns.List`
|
||||
* :class:`~cqlengine.columns.Map`
|
||||
|
||||
Column Options
|
||||
--------------
|
||||
|
||||
Each column can be defined with optional arguments to modify the way they behave. While some column types may
|
||||
define additional column options, these are the options that are available on all columns:
|
||||
|
||||
:attr:`~cqlengine.columns.BaseColumn.primary_key`
|
||||
If True, this column is created as a primary key field. A model can have multiple primary keys. Defaults to False.
|
||||
|
||||
*In CQL, there are 2 types of primary keys: partition keys and clustering keys. As with CQL, the first
|
||||
primary key is the partition key, and all others are clustering keys, unless partition keys are specified
|
||||
manually using* :attr:`~cqlengine.columns.BaseColumn.partition_key`
|
||||
|
||||
:attr:`~cqlengine.columns.BaseColumn.partition_key`
|
||||
If True, this column is created as partition primary key. There may be many partition keys defined,
|
||||
forming a *composite partition key*
|
||||
|
||||
:attr:`~cqlengine.columns.BaseColumn.clustering_order`
|
||||
``ASC`` or ``DESC``, determines the clustering order of a clustering key.
|
||||
|
||||
:attr:`~cqlengine.columns.BaseColumn.index`
|
||||
If True, an index will be created for this column. Defaults to False.
|
||||
|
||||
:attr:`~cqlengine.columns.BaseColumn.db_field`
|
||||
Explicitly sets the name of the column in the database table. If this is left blank, the column name will be
|
||||
the same as the name of the column attribute. Defaults to None.
|
||||
|
||||
:attr:`~cqlengine.columns.BaseColumn.default`
|
||||
The default value for this column. If a model instance is saved without a value for this column having been
|
||||
defined, the default value will be used. This can be either a value or a callable object (ie: datetime.now is a valid default argument).
|
||||
Callable defaults will be called each time a default is assigned to a None value
|
||||
|
||||
:attr:`~cqlengine.columns.BaseColumn.required`
|
||||
If True, this model cannot be saved without a value defined for this column. Defaults to False. Primary key fields always require values.
|
||||
|
||||
:attr:`~cqlengine.columns.BaseColumn.static`
|
||||
Defined a column as static. Static columns are shared by all rows in a partition.
|
||||
|
||||
Model Methods
|
||||
=============
|
||||
Below are the methods that can be called on model instances.
|
||||
|
||||
.. class:: Model(\*\*values)
|
||||
|
||||
Creates an instance of the model. Pass in keyword arguments for columns you've defined on the model.
|
||||
|
||||
*Example*
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
#using the person model from earlier:
|
||||
class Person(Model):
|
||||
id = columns.UUID(primary_key=True)
|
||||
first_name = columns.Text()
|
||||
last_name = columns.Text()
|
||||
|
||||
person = Person(first_name='Blake', last_name='Eggleston')
|
||||
person.first_name #returns 'Blake'
|
||||
person.last_name #returns 'Eggleston'
|
||||
|
||||
|
||||
.. method:: save()
|
||||
|
||||
Saves an object to the database
|
||||
|
||||
*Example*
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
#create a person instance
|
||||
person = Person(first_name='Kimberly', last_name='Eggleston')
|
||||
#saves it to Cassandra
|
||||
person.save()
|
||||
|
||||
.. method:: delete()
|
||||
|
||||
Deletes the object from the database.
|
||||
|
||||
.. method:: batch(batch_object)
|
||||
|
||||
Sets the batch object to run instance updates and inserts queries with.
|
||||
|
||||
.. method:: timestamp(timedelta_or_datetime)
|
||||
|
||||
Sets the timestamp for the query
|
||||
|
||||
.. method:: ttl(ttl_in_sec)
|
||||
|
||||
Sets the ttl values to run instance updates and inserts queries with.
|
||||
|
||||
.. method:: if_not_exists()
|
||||
|
||||
Check the existence of an object before insertion. The existence of an
|
||||
object is determined by its primary key(s). And please note using this flag
|
||||
would incur performance cost.
|
||||
|
||||
if the insertion didn't applied, a LWTException exception would be raised.
|
||||
|
||||
*Example*
|
||||
|
||||
.. code-block:: python
|
||||
try:
|
||||
TestIfNotExistsModel.if_not_exists().create(id=id, count=9, text='111111111111')
|
||||
except LWTException as e:
|
||||
# handle failure case
|
||||
print e.existing # existing object
|
||||
|
||||
This method is supported on Cassandra 2.0 or later.
|
||||
|
||||
.. method:: iff(**values)
|
||||
|
||||
Checks to ensure that the values specified are correct on the Cassandra cluster.
|
||||
Simply specify the column(s) and the expected value(s). As with if_not_exists,
|
||||
this incurs a performance cost.
|
||||
|
||||
If the insertion isn't applied, a LWTException is raised
|
||||
|
||||
.. code-block::
|
||||
t = TestTransactionModel(text='some text', count=5)
|
||||
try:
|
||||
t.iff(count=5).update('other text')
|
||||
except LWTException as e:
|
||||
# handle failure
|
||||
|
||||
.. method:: update(**values)
|
||||
|
||||
Performs an update on the model instance. You can pass in values to set on the model
|
||||
for updating, or you can call without values to execute an update against any modified
|
||||
fields. If no fields on the model have been modified since loading, no query will be
|
||||
performed. Model validation is performed normally.
|
||||
|
||||
It is possible to do a blind update, that is, to update a field without having first selected the object out of the database. See :ref:`Blind Updates <blind_updates>`
|
||||
|
||||
.. method:: get_changed_columns()
|
||||
|
||||
Returns a list of column names that have changed since the model was instantiated or saved
|
||||
|
||||
Model Attributes
|
||||
================
|
||||
|
||||
.. attribute:: Model.__abstract__
|
||||
|
||||
*Optional.* Indicates that this model is only intended to be used as a base class for other models. You can't create tables for abstract models, but checks around schema validity are skipped during class construction.
|
||||
|
||||
.. attribute:: Model.__table_name__
|
||||
|
||||
*Optional.* Sets the name of the CQL table for this model. If left blank, the table name will be the name of the model, with it's module name as it's prefix. Manually defined table names are not inherited.
|
||||
|
||||
.. _keyspace-change:
|
||||
.. attribute:: Model.__keyspace__
|
||||
|
||||
Sets the name of the keyspace used by this model.
|
||||
|
||||
**Prior to cqlengine 0.16, this setting defaulted
|
||||
to 'cqlengine'. As of 0.16, this field needs to be set on all non-abstract models, or their base classes.**
|
||||
|
||||
.. _ttl-change:
|
||||
.. attribute:: Model.__default_ttl__
|
||||
|
||||
Sets the default ttl used by this model. This can be overridden by using the ``ttl(ttl_in_sec)`` method.
|
||||
|
||||
|
||||
Table Polymorphism
|
||||
==================
|
||||
|
||||
As of cqlengine 0.8, it is possible to save and load different model classes using a single CQL table.
|
||||
This is useful in situations where you have different object types that you want to store in a single cassandra row.
|
||||
|
||||
For instance, suppose you want a table that stores rows of pets owned by an owner:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class Pet(Model):
|
||||
__table_name__ = 'pet'
|
||||
owner_id = UUID(primary_key=True)
|
||||
pet_id = UUID(primary_key=True)
|
||||
pet_type = Text(polymorphic_key=True)
|
||||
name = Text()
|
||||
|
||||
def eat(self, food):
|
||||
pass
|
||||
|
||||
def sleep(self, time):
|
||||
pass
|
||||
|
||||
class Cat(Pet):
|
||||
__polymorphic_key__ = 'cat'
|
||||
cuteness = Float()
|
||||
|
||||
def tear_up_couch(self):
|
||||
pass
|
||||
|
||||
class Dog(Pet):
|
||||
__polymorphic_key__ = 'dog'
|
||||
fierceness = Float()
|
||||
|
||||
def bark_all_night(self):
|
||||
pass
|
||||
|
||||
After calling ``sync_table`` on each of these tables, the columns defined in each model will be added to the
|
||||
``pet`` table. Additionally, saving ``Cat`` and ``Dog`` models will save the meta data needed to identify each row
|
||||
as either a cat or dog.
|
||||
|
||||
To setup a polymorphic model structure, follow these steps
|
||||
|
||||
1. Create a base model with a column set as the polymorphic_key (set ``polymorphic_key=True`` in the column definition)
|
||||
2. Create subclass models, and define a unique ``__polymorphic_key__`` value on each
|
||||
3. Run ``sync_table`` on each of the sub tables
|
||||
|
||||
**About the polymorphic key**
|
||||
|
||||
The polymorphic key is what cqlengine uses under the covers to map logical cql rows to the appropriate model type. The
|
||||
base model maintains a map of polymorphic keys to subclasses. When a polymorphic model is saved, this value is automatically
|
||||
saved into the polymorphic key column. You can set the polymorphic key column to any column type that you like, with
|
||||
the exception of container and counter columns, although ``Integer`` columns make the most sense. Additionally, if you
|
||||
set ``index=True`` on your polymorphic key column, you can execute queries against polymorphic subclasses, and a
|
||||
``WHERE`` clause will be automatically added to your query, returning only rows of that type. Note that you must
|
||||
define a unique ``__polymorphic_key__`` value to each subclass, and that you can only assign a single polymorphic
|
||||
key column per model
|
||||
|
||||
|
||||
Extending Model Validation
|
||||
==========================
|
||||
|
||||
Each time you save a model instance in cqlengine, the data in the model is validated against the schema you've defined
|
||||
for your model. Most of the validation is fairly straightforward, it basically checks that you're not trying to do
|
||||
something like save text into an integer column, and it enforces the ``required`` flag set on column definitions.
|
||||
It also performs any transformations needed to save the data properly.
|
||||
|
||||
However, there are often additional constraints or transformations you want to impose on your data, beyond simply
|
||||
making sure that Cassandra won't complain when you try to insert it. To define additional validation on a model,
|
||||
extend the model's validation method:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class Member(Model):
|
||||
person_id = UUID(primary_key=True)
|
||||
name = Text(required=True)
|
||||
|
||||
def validate(self):
|
||||
super(Member, self).validate()
|
||||
if self.name == 'jon':
|
||||
raise ValidationError('no jon\'s allowed')
|
||||
|
||||
*Note*: while not required, the convention is to raise a ``ValidationError`` (``from cqlengine import ValidationError``)
|
||||
if validation fails
|
||||
|
||||
|
||||
Table Properties
|
||||
================
|
||||
|
||||
Each table can have its own set of configuration options.
|
||||
These can be specified on a model with the following attributes:
|
||||
|
||||
.. attribute:: Model.__bloom_filter_fp_chance
|
||||
|
||||
.. attribute:: Model.__caching__
|
||||
|
||||
.. attribute:: Model.__comment__
|
||||
|
||||
.. attribute:: Model.__dclocal_read_repair_chance__
|
||||
|
||||
.. attribute:: Model.__default_time_to_live__
|
||||
|
||||
.. attribute:: Model.__gc_grace_seconds__
|
||||
|
||||
.. attribute:: Model.__index_interval__
|
||||
|
||||
.. attribute:: Model.__memtable_flush_period_in_ms__
|
||||
|
||||
.. attribute:: Model.__populate_io_cache_on_flush__
|
||||
|
||||
.. attribute:: Model.__read_repair_chance__
|
||||
|
||||
.. attribute:: Model.__replicate_on_write__
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from cqlengine import CACHING_ROWS_ONLY, columns
|
||||
from cqlengine.models import Model
|
||||
|
||||
class User(Model):
|
||||
__caching__ = CACHING_ROWS_ONLY # cache only rows instead of keys only by default
|
||||
__gc_grace_seconds__ = 86400 # 1 day instead of the default 10 days
|
||||
|
||||
user_id = columns.UUID(primary_key=True)
|
||||
name = columns.Text()
|
||||
|
||||
Will produce the following CQL statement:
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
CREATE TABLE cqlengine.user (
|
||||
user_id uuid,
|
||||
name text,
|
||||
PRIMARY KEY (user_id)
|
||||
) WITH caching = 'rows_only'
|
||||
AND gc_grace_seconds = 86400;
|
||||
|
||||
See the `list of supported table properties for more information
|
||||
<http://www.datastax.com/documentation/cql/3.1/cql/cql_reference/tabProp.html>`_.
|
||||
|
||||
|
||||
Compaction Options
|
||||
==================
|
||||
|
||||
As of cqlengine 0.7 we've added support for specifying compaction options. cqlengine will only use your compaction options if you have a strategy set. When a table is synced, it will be altered to match the compaction options set on your table. This means that if you are changing settings manually they will be changed back on resync. Do not use the compaction settings of cqlengine if you want to manage your compaction settings manually.
|
||||
|
||||
cqlengine supports all compaction options as of Cassandra 1.2.8.
|
||||
|
||||
Available Options:
|
||||
|
||||
.. attribute:: Model.__compaction_bucket_high__
|
||||
|
||||
.. attribute:: Model.__compaction_bucket_low__
|
||||
|
||||
.. attribute:: Model.__compaction_max_compaction_threshold__
|
||||
|
||||
.. attribute:: Model.__compaction_min_compaction_threshold__
|
||||
|
||||
.. attribute:: Model.__compaction_min_sstable_size__
|
||||
|
||||
.. attribute:: Model.__compaction_sstable_size_in_mb__
|
||||
|
||||
.. attribute:: Model.__compaction_tombstone_compaction_interval__
|
||||
|
||||
.. attribute:: Model.__compaction_tombstone_threshold__
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class User(Model):
|
||||
__compaction__ = cqlengine.LeveledCompactionStrategy
|
||||
__compaction_sstable_size_in_mb__ = 64
|
||||
__compaction_tombstone_threshold__ = .2
|
||||
|
||||
user_id = columns.UUID(primary_key=True)
|
||||
name = columns.Text()
|
||||
|
||||
or for SizeTieredCompaction:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class TimeData(Model):
|
||||
__compaction__ = SizeTieredCompactionStrategy
|
||||
__compaction_bucket_low__ = .3
|
||||
__compaction_bucket_high__ = 2
|
||||
__compaction_min_threshold__ = 2
|
||||
__compaction_max_threshold__ = 64
|
||||
__compaction_tombstone_compaction_interval__ = 86400
|
||||
|
||||
Tables may use `LeveledCompactionStrategy` or `SizeTieredCompactionStrategy`. Both options are available in the top level cqlengine module. To reiterate, you will need to set your `__compaction__` option explicitly in order for cqlengine to handle any of your settings.
|
||||
|
||||
|
||||
Manipulating model instances as dictionaries
|
||||
============================================
|
||||
|
||||
As of cqlengine 0.12, we've added support for treating model instances like dictionaries. See below for examples.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class Person(Model):
|
||||
first_name = columns.Text()
|
||||
last_name = columns.Text()
|
||||
|
||||
kevin = Person.create(first_name="Kevin", last_name="Deldycke")
|
||||
dict(kevin) # returns {'first_name': 'Kevin', 'last_name': 'Deldycke'}
|
||||
kevin['first_name'] # returns 'Kevin'
|
||||
kevin.keys() # returns ['first_name', 'last_name']
|
||||
kevin.values() # returns ['Kevin', 'Deldycke']
|
||||
kevin.items() # returns [('first_name', 'Kevin'), ('last_name', 'Deldycke')]
|
||||
|
||||
kevin['first_name'] = 'KEVIN5000' # changes the models first name
|
||||
|
664
docs/topics/queryset.rst
Normal file
664
docs/topics/queryset.rst
Normal file
@ -0,0 +1,664 @@
|
||||
==============
|
||||
Making Queries
|
||||
==============
|
||||
|
||||
**Users of versions < 0.4, please read this post before upgrading:** `Breaking Changes`_
|
||||
|
||||
.. _Breaking Changes: https://groups.google.com/forum/?fromgroups#!topic/cqlengine-users/erkSNe1JwuU
|
||||
|
||||
.. module:: cqlengine.connection
|
||||
|
||||
.. module:: cqlengine.query
|
||||
|
||||
Retrieving objects
|
||||
==================
|
||||
Once you've populated Cassandra with data, you'll probably want to retrieve some of it. This is accomplished with QuerySet objects. This section will describe how to use QuerySet objects to retrieve the data you're looking for.
|
||||
|
||||
Retrieving all objects
|
||||
----------------------
|
||||
The simplest query you can make is to return all objects from a table.
|
||||
|
||||
This is accomplished with the ``.all()`` method, which returns a QuerySet of all objects in a table
|
||||
|
||||
Using the Person example model, we would get all Person objects like this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
all_objects = Person.objects.all()
|
||||
|
||||
.. _retrieving-objects-with-filters:
|
||||
|
||||
Retrieving objects with filters
|
||||
-------------------------------
|
||||
Typically, you'll want to query only a subset of the records in your database.
|
||||
|
||||
That can be accomplished with the QuerySet's ``.filter(\*\*)`` method.
|
||||
|
||||
For example, given the model definition:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class Automobile(Model):
|
||||
manufacturer = columns.Text(primary_key=True)
|
||||
year = columns.Integer(primary_key=True)
|
||||
model = columns.Text()
|
||||
price = columns.Decimal()
|
||||
|
||||
...and assuming the Automobile table contains a record of every car model manufactured in the last 20 years or so, we can retrieve only the cars made by a single manufacturer like this:
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
q = Automobile.objects.filter(manufacturer='Tesla')
|
||||
|
||||
You can also use the more convenient syntax:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
q = Automobile.objects(Automobile.manufacturer == 'Tesla')
|
||||
|
||||
We can then further filter our query with another call to **.filter**
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
q = q.filter(year=2012)
|
||||
|
||||
*Note: all queries involving any filtering MUST define either an '=' or an 'in' relation to either a primary key column, or an indexed column.*
|
||||
|
||||
Accessing objects in a QuerySet
|
||||
===============================
|
||||
|
||||
There are several methods for getting objects out of a queryset
|
||||
|
||||
* iterating over the queryset
|
||||
.. code-block:: python
|
||||
|
||||
for car in Automobile.objects.all():
|
||||
#...do something to the car instance
|
||||
pass
|
||||
|
||||
* list index
|
||||
.. code-block:: python
|
||||
|
||||
q = Automobile.objects.all()
|
||||
q[0] #returns the first result
|
||||
q[1] #returns the second result
|
||||
|
||||
|
||||
* list slicing
|
||||
.. code-block:: python
|
||||
|
||||
q = Automobile.objects.all()
|
||||
q[1:] #returns all results except the first
|
||||
q[1:9] #returns a slice of the results
|
||||
|
||||
*Note: CQL does not support specifying a start position in it's queries. Therefore, accessing elements using array indexing / slicing will load every result up to the index value requested*
|
||||
|
||||
* calling :attr:`get() <query.QuerySet.get>` on the queryset
|
||||
.. code-block:: python
|
||||
|
||||
q = Automobile.objects.filter(manufacturer='Tesla')
|
||||
q = q.filter(year=2012)
|
||||
car = q.get()
|
||||
|
||||
this returns the object matching the queryset
|
||||
|
||||
* calling :attr:`first() <query.QuerySet.first>` on the queryset
|
||||
.. code-block:: python
|
||||
|
||||
q = Automobile.objects.filter(manufacturer='Tesla')
|
||||
q = q.filter(year=2012)
|
||||
car = q.first()
|
||||
|
||||
this returns the first value in the queryset
|
||||
|
||||
.. _query-filtering-operators:
|
||||
|
||||
Filtering Operators
|
||||
===================
|
||||
|
||||
:attr:`Equal To <query.QueryOperator.EqualsOperator>`
|
||||
|
||||
The default filtering operator.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
q = Automobile.objects.filter(manufacturer='Tesla')
|
||||
q = q.filter(year=2012) #year == 2012
|
||||
|
||||
In addition to simple equal to queries, cqlengine also supports querying with other operators by appending a ``__<op>`` to the field name on the filtering call
|
||||
|
||||
:attr:`in (__in) <query.QueryOperator.InOperator>`
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
q = Automobile.objects.filter(manufacturer='Tesla')
|
||||
q = q.filter(year__in=[2011, 2012])
|
||||
|
||||
|
||||
:attr:`> (__gt) <query.QueryOperator.GreaterThanOperator>`
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
q = Automobile.objects.filter(manufacturer='Tesla')
|
||||
q = q.filter(year__gt=2010) # year > 2010
|
||||
|
||||
# or the nicer syntax
|
||||
|
||||
q.filter(Automobile.year > 2010)
|
||||
|
||||
:attr:`>= (__gte) <query.QueryOperator.GreaterThanOrEqualOperator>`
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
q = Automobile.objects.filter(manufacturer='Tesla')
|
||||
q = q.filter(year__gte=2010) # year >= 2010
|
||||
|
||||
# or the nicer syntax
|
||||
|
||||
q.filter(Automobile.year >= 2010)
|
||||
|
||||
:attr:`< (__lt) <query.QueryOperator.LessThanOperator>`
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
q = Automobile.objects.filter(manufacturer='Tesla')
|
||||
q = q.filter(year__lt=2012) # year < 2012
|
||||
|
||||
# or...
|
||||
|
||||
q.filter(Automobile.year < 2012)
|
||||
|
||||
:attr:`<= (__lte) <query.QueryOperator.LessThanOrEqualOperator>`
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
q = Automobile.objects.filter(manufacturer='Tesla')
|
||||
q = q.filter(year__lte=2012) # year <= 2012
|
||||
|
||||
q.filter(Automobile.year <= 2012)
|
||||
|
||||
|
||||
TimeUUID Functions
|
||||
==================
|
||||
|
||||
In addition to querying using regular values, there are two functions you can pass in when querying TimeUUID columns to help make filtering by them easier. Note that these functions don't actually return a value, but instruct the cql interpreter to use the functions in it's query.
|
||||
|
||||
.. class:: MinTimeUUID(datetime)
|
||||
|
||||
returns the minimum time uuid value possible for the given datetime
|
||||
|
||||
.. class:: MaxTimeUUID(datetime)
|
||||
|
||||
returns the maximum time uuid value possible for the given datetime
|
||||
|
||||
*Example*
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class DataStream(Model):
|
||||
time = cqlengine.TimeUUID(primary_key=True)
|
||||
data = cqlengine.Bytes()
|
||||
|
||||
min_time = datetime(1982, 1, 1)
|
||||
max_time = datetime(1982, 3, 9)
|
||||
|
||||
DataStream.filter(time__gt=cqlengine.MinTimeUUID(min_time), time__lt=cqlengine.MaxTimeUUID(max_time))
|
||||
|
||||
Token Function
|
||||
==============
|
||||
|
||||
Token functon may be used only on special, virtual column pk__token, representing token of partition key (it also works for composite partition keys).
|
||||
Cassandra orders returned items by value of partition key token, so using cqlengine.Token we can easy paginate through all table rows.
|
||||
|
||||
See http://cassandra.apache.org/doc/cql3/CQL.html#tokenFun
|
||||
|
||||
*Example*
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class Items(Model):
|
||||
id = cqlengine.Text(primary_key=True)
|
||||
data = cqlengine.Bytes()
|
||||
|
||||
query = Items.objects.all().limit(10)
|
||||
|
||||
first_page = list(query);
|
||||
last = first_page[-1]
|
||||
next_page = list(query.filter(pk__token__gt=cqlengine.Token(last.pk)))
|
||||
|
||||
QuerySets are immutable
|
||||
======================
|
||||
|
||||
When calling any method that changes a queryset, the method does not actually change the queryset object it's called on, but returns a new queryset object with the attributes of the original queryset, plus the attributes added in the method call.
|
||||
|
||||
*Example*
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
#this produces 3 different querysets
|
||||
#q does not change after it's initial definition
|
||||
q = Automobiles.objects.filter(year=2012)
|
||||
tesla2012 = q.filter(manufacturer='Tesla')
|
||||
honda2012 = q.filter(manufacturer='Honda')
|
||||
|
||||
Ordering QuerySets
|
||||
==================
|
||||
|
||||
Since Cassandra is essentially a distributed hash table on steroids, the order you get records back in will not be particularly predictable.
|
||||
|
||||
However, you can set a column to order on with the ``.order_by(column_name)`` method.
|
||||
|
||||
*Example*
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
#sort ascending
|
||||
q = Automobiles.objects.all().order_by('year')
|
||||
#sort descending
|
||||
q = Automobiles.objects.all().order_by('-year')
|
||||
|
||||
*Note: Cassandra only supports ordering on a clustering key. In other words, to support ordering results, your model must have more than one primary key, and you must order on a primary key, excluding the first one.*
|
||||
|
||||
*For instance, given our Automobile model, year is the only column we can order on.*
|
||||
|
||||
Values Lists
|
||||
============
|
||||
|
||||
There is a special QuerySet's method ``.values_list()`` - when called, QuerySet returns lists of values instead of model instances. It may significantly speedup things with lower memory footprint for large responses.
|
||||
Each tuple contains the value from the respective field passed into the ``values_list()`` call — so the first item is the first field, etc. For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
items = list(range(20))
|
||||
random.shuffle(items)
|
||||
for i in items:
|
||||
TestModel.create(id=1, clustering_key=i)
|
||||
|
||||
values = list(TestModel.objects.values_list('clustering_key', flat=True))
|
||||
# [19L, 18L, 17L, 16L, 15L, 14L, 13L, 12L, 11L, 10L, 9L, 8L, 7L, 6L, 5L, 4L, 3L, 2L, 1L, 0L]
|
||||
|
||||
|
||||
|
||||
Batch Queries
|
||||
=============
|
||||
|
||||
cqlengine now supports batch queries using the BatchQuery class. Batch queries can be started and stopped manually, or within a context manager. To add queries to the batch object, you just need to precede the create/save/delete call with a call to batch, and pass in the batch object.
|
||||
|
||||
|
||||
|
||||
Batch Query General Use Pattern
|
||||
-------------------------------
|
||||
|
||||
You can only create, update, and delete rows with a batch query, attempting to read rows out of the database with a batch query will fail.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from cqlengine import BatchQuery
|
||||
|
||||
#using a context manager
|
||||
with BatchQuery() as b:
|
||||
now = datetime.now()
|
||||
em1 = ExampleModel.batch(b).create(example_type=0, description="1", created_at=now)
|
||||
em2 = ExampleModel.batch(b).create(example_type=0, description="2", created_at=now)
|
||||
em3 = ExampleModel.batch(b).create(example_type=0, description="3", created_at=now)
|
||||
|
||||
# -- or --
|
||||
|
||||
#manually
|
||||
b = BatchQuery()
|
||||
now = datetime.now()
|
||||
em1 = ExampleModel.batch(b).create(example_type=0, description="1", created_at=now)
|
||||
em2 = ExampleModel.batch(b).create(example_type=0, description="2", created_at=now)
|
||||
em3 = ExampleModel.batch(b).create(example_type=0, description="3", created_at=now)
|
||||
b.execute()
|
||||
|
||||
# updating in a batch
|
||||
|
||||
b = BatchQuery()
|
||||
em1.description = "new description"
|
||||
em1.batch(b).save()
|
||||
em2.description = "another new description"
|
||||
em2.batch(b).save()
|
||||
b.execute()
|
||||
|
||||
# deleting in a batch
|
||||
b = BatchQuery()
|
||||
ExampleModel.objects(id=some_id).batch(b).delete()
|
||||
ExampleModel.objects(id=some_id2).batch(b).delete()
|
||||
b.execute()
|
||||
|
||||
|
||||
Typically you will not want the block to execute if an exception occurs inside the `with` block. However, in the case that this is desirable, it's achievable by using the following syntax:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
with BatchQuery(execute_on_exception=True) as b:
|
||||
LogEntry.batch(b).create(k=1, v=1)
|
||||
mystery_function() # exception thrown in here
|
||||
LogEntry.batch(b).create(k=1, v=2) # this code is never reached due to the exception, but anything leading up to here will execute in the batch.
|
||||
|
||||
If an exception is thrown somewhere in the block, any statements that have been added to the batch will still be executed. This is useful for some logging situations.
|
||||
|
||||
Batch Query Execution Callbacks
|
||||
-------------------------------
|
||||
|
||||
In order to allow secondary tasks to be chained to the end of batch, BatchQuery instances allow callbacks to be
|
||||
registered with the batch, to be executed immediately after the batch executes.
|
||||
|
||||
Multiple callbacks can be attached to same BatchQuery instance, they are executed in the same order that they
|
||||
are added to the batch.
|
||||
|
||||
The callbacks attached to a given batch instance are executed only if the batch executes. If the batch is used as a
|
||||
context manager and an exception is raised, the queued up callbacks will not be run.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def my_callback(*args, **kwargs):
|
||||
pass
|
||||
|
||||
batch = BatchQuery()
|
||||
|
||||
batch.add_callback(my_callback)
|
||||
batch.add_callback(my_callback, 'positional arg', named_arg='named arg value')
|
||||
|
||||
# if you need reference to the batch within the callback,
|
||||
# just trap it in the arguments to be passed to the callback:
|
||||
batch.add_callback(my_callback, cqlengine_batch=batch)
|
||||
|
||||
# once the batch executes...
|
||||
batch.execute()
|
||||
|
||||
# the effect of the above scheduled callbacks will be similar to
|
||||
my_callback()
|
||||
my_callback('positional arg', named_arg='named arg value')
|
||||
my_callback(cqlengine_batch=batch)
|
||||
|
||||
Failure in any of the callbacks does not affect the batch's execution, as the callbacks are started after the execution
|
||||
of the batch is complete.
|
||||
|
||||
Logged vs Unlogged Batches
|
||||
---------------------------
|
||||
By default, queries in cqlengine are LOGGED, which carries additional overhead from UNLOGGED. To explicitly state which batch type to use, simply:
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from cqlengine.query import BatchType
|
||||
with BatchQuery(batch_type=BatchType.Unlogged) as b:
|
||||
LogEntry.batch(b).create(k=1, v=1)
|
||||
LogEntry.batch(b).create(k=1, v=2)
|
||||
|
||||
|
||||
|
||||
QuerySet method reference
|
||||
=========================
|
||||
|
||||
.. class:: QuerySet
|
||||
|
||||
.. method:: all()
|
||||
|
||||
Returns a queryset matching all rows
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for user in User.objects().all():
|
||||
print(user)
|
||||
|
||||
|
||||
.. method:: batch(batch_object)
|
||||
|
||||
Sets the batch object to run the query on. Note that running a select query with a batch object will raise an exception
|
||||
|
||||
.. method:: consistency(consistency_setting)
|
||||
|
||||
Sets the consistency level for the operation. Options may be imported from the top level :attr:`cqlengine` package.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for user in User.objects(id=3).consistency(ONE):
|
||||
print(user)
|
||||
|
||||
|
||||
.. method:: count()
|
||||
|
||||
Returns the number of matching rows in your QuerySet
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
print(User.objects().count())
|
||||
|
||||
.. method:: filter(\*\*values)
|
||||
|
||||
:param values: See :ref:`retrieving-objects-with-filters`
|
||||
|
||||
Returns a QuerySet filtered on the keyword arguments
|
||||
|
||||
.. method:: get(\*\*values)
|
||||
|
||||
:param values: See :ref:`retrieving-objects-with-filters`
|
||||
|
||||
Returns a single object matching the QuerySet. If no objects are matched, a :attr:`~models.Model.DoesNotExist` exception is raised. If more than one object is found, a :attr:`~models.Model.MultipleObjectsReturned` exception is raised.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
user = User.get(id=1)
|
||||
|
||||
|
||||
.. method:: limit(num)
|
||||
|
||||
Limits the number of results returned by Cassandra.
|
||||
|
||||
*Note that CQL's default limit is 10,000, so all queries without a limit set explicitly will have an implicit limit of 10,000*
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for user in User.objects().limit(100):
|
||||
print(user)
|
||||
|
||||
.. method:: order_by(field_name)
|
||||
|
||||
:param field_name: the name of the field to order on. *Note: the field_name must be a clustering key*
|
||||
:type field_name: string
|
||||
|
||||
Sets the field to order on.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from uuid import uuid1,uuid4
|
||||
|
||||
class Comment(Model):
|
||||
photo_id = UUID(primary_key=True)
|
||||
comment_id = TimeUUID(primary_key=True, default=uuid1) # auto becomes clustering key
|
||||
comment = Text()
|
||||
|
||||
sync_table(Comment)
|
||||
|
||||
u = uuid4()
|
||||
for x in range(5):
|
||||
Comment.create(photo_id=u, comment="test %d" % x)
|
||||
|
||||
print("Normal")
|
||||
for comment in Comment.objects(photo_id=u):
|
||||
print comment.comment_id
|
||||
|
||||
print("Reversed")
|
||||
for comment in Comment.objects(photo_id=u).order_by("-comment_id"):
|
||||
print comment.comment_id
|
||||
|
||||
|
||||
.. method:: allow_filtering()
|
||||
|
||||
Enables the (usually) unwise practive of querying on a clustering key without also defining a partition key
|
||||
|
||||
.. method:: timestamp(timestamp_or_long_or_datetime)
|
||||
|
||||
Allows for custom timestamps to be saved with the record.
|
||||
|
||||
.. method:: ttl(ttl_in_seconds)
|
||||
|
||||
:param ttl_in_seconds: time in seconds in which the saved values should expire
|
||||
:type ttl_in_seconds: int
|
||||
|
||||
Sets the ttl to run the query query with. Note that running a select query with a ttl value will raise an exception
|
||||
|
||||
.. _blind_updates:
|
||||
|
||||
.. method:: update(**values)
|
||||
|
||||
Performs an update on the row selected by the queryset. Include values to update in the
|
||||
update like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
Model.objects(key=n).update(value='x')
|
||||
|
||||
Passing in updates for columns which are not part of the model will raise a ValidationError.
|
||||
Per column validation will be performed, but instance level validation will not
|
||||
(`Model.validate` is not called). This is sometimes referred to as a blind update.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class User(Model):
|
||||
id = Integer(primary_key=True)
|
||||
name = Text()
|
||||
|
||||
setup(["localhost"], "test")
|
||||
sync_table(User)
|
||||
|
||||
u = User.create(id=1, name="jon")
|
||||
|
||||
User.objects(id=1).update(name="Steve")
|
||||
|
||||
# sets name to null
|
||||
User.objects(id=1).update(name=None)
|
||||
|
||||
|
||||
The queryset update method also supports blindly adding and removing elements from container columns, without
|
||||
loading a model instance from Cassandra.
|
||||
|
||||
Using the syntax `.update(column_name={x, y, z})` will overwrite the contents of the container, like updating a
|
||||
non container column. However, adding `__<operation>` to the end of the keyword arg, makes the update call add
|
||||
or remove items from the collection, without overwriting then entire column.
|
||||
|
||||
|
||||
Given the model below, here are the operations that can be performed on the different container columns:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class Row(Model):
|
||||
row_id = columns.Integer(primary_key=True)
|
||||
set_column = columns.Set(Integer)
|
||||
list_column = columns.Set(Integer)
|
||||
map_column = columns.Set(Integer, Integer)
|
||||
|
||||
:class:`~cqlengine.columns.Set`
|
||||
|
||||
- `add`: adds the elements of the given set to the column
|
||||
- `remove`: removes the elements of the given set to the column
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# add elements to a set
|
||||
Row.objects(row_id=5).update(set_column__add={6})
|
||||
|
||||
# remove elements to a set
|
||||
Row.objects(row_id=5).update(set_column__remove={4})
|
||||
|
||||
:class:`~cqlengine.columns.List`
|
||||
|
||||
- `append`: appends the elements of the given list to the end of the column
|
||||
- `prepend`: prepends the elements of the given list to the beginning of the column
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# append items to a list
|
||||
Row.objects(row_id=5).update(list_column__append=[6, 7])
|
||||
|
||||
# prepend items to a list
|
||||
Row.objects(row_id=5).update(list_column__prepend=[1, 2])
|
||||
|
||||
|
||||
:class:`~cqlengine.columns.Map`
|
||||
|
||||
- `update`: adds the given keys/values to the columns, creating new entries if they didn't exist, and overwriting old ones if they did
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# add items to a map
|
||||
Row.objects(row_id=5).update(map_column__update={1: 2, 3: 4})
|
||||
|
||||
|
||||
Per Query Timeouts
|
||||
===================
|
||||
|
||||
By default all queries are executed with the timeout defined in `~cqlengine.connection.setup()`
|
||||
The examples below show how to specify a per-query timeout.
|
||||
A timeout is specified in seconds and can be an int, float or None.
|
||||
None means no timeout.
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class Row(Model):
|
||||
id = columns.Integer(primary_key=True)
|
||||
name = columns.Text()
|
||||
|
||||
|
||||
Fetch all objects with a timeout of 5 seconds
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
Row.objects().timeout(5).all()
|
||||
|
||||
Create a single row with a 50ms timeout
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
Row(id=1, name='Jon').timeout(0.05).create()
|
||||
|
||||
Delete a single row with no timeout
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
Row(id=1).timeout(None).delete()
|
||||
|
||||
Update a single row with no timeout
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
Row(id=1).timeout(None).update(name='Blake')
|
||||
|
||||
Batch query timeouts
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
with BatchQuery(timeout=10) as b:
|
||||
Row(id=1, name='Jon').create()
|
||||
|
||||
|
||||
NOTE: You cannot set both timeout and batch at the same time, batch will use the timeout defined in it's constructor.
|
||||
Setting the timeout on the model is meaningless and will raise an AssertionError.
|
||||
|
||||
|
||||
Named Tables
|
||||
===================
|
||||
|
||||
Named tables are a way of querying a table without creating an class. They're useful for querying system tables or exploring an unfamiliar database.
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from cqlengine.connection import setup
|
||||
setup("127.0.0.1", "cqlengine_test")
|
||||
|
||||
from cqlengine.named import NamedTable
|
||||
user = NamedTable("cqlengine_test", "user")
|
||||
user.objects()
|
||||
user.objects()[0]
|
||||
|
||||
# {u'pk': 1, u't': datetime.datetime(2014, 6, 26, 17, 10, 31, 774000)}
|
||||
|
22
docs/topics/related_projects.rst
Normal file
22
docs/topics/related_projects.rst
Normal file
@ -0,0 +1,22 @@
|
||||
==================================
|
||||
Related Projects
|
||||
==================================
|
||||
|
||||
Cassandra Native Driver
|
||||
=========================
|
||||
|
||||
- Docs: http://datastax.github.io/python-driver/api/index.html
|
||||
- Github: https://github.com/datastax/python-driver
|
||||
- Pypi: https://pypi.python.org/pypi/cassandra-driver/2.1.0
|
||||
|
||||
|
||||
Sphinx Contrib Module
|
||||
=========================
|
||||
|
||||
- Github https://github.com/dokai/sphinxcontrib-cqlengine
|
||||
|
||||
|
||||
Django Integration
|
||||
=========================
|
||||
|
||||
- Github https://cqlengine.readthedocs.org
|
67
docs/topics/third_party.rst
Normal file
67
docs/topics/third_party.rst
Normal file
@ -0,0 +1,67 @@
|
||||
========================
|
||||
Third party integrations
|
||||
========================
|
||||
|
||||
|
||||
Celery
|
||||
------
|
||||
|
||||
Here's how, in substance, CQLengine can be plugged to `Celery
|
||||
<http://celery.readthedocs.org/>`_:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from celery import Celery
|
||||
from celery.signals import worker_process_init, beat_init
|
||||
from cqlengine import connection
|
||||
from cqlengine.connection import (
|
||||
cluster as cql_cluster, session as cql_session)
|
||||
|
||||
def cassandra_init():
|
||||
""" Initialize a clean Cassandra connection. """
|
||||
if cql_cluster is not None:
|
||||
cql_cluster.shutdown()
|
||||
if cql_session is not None:
|
||||
cql_session.shutdown()
|
||||
connection.setup()
|
||||
|
||||
# Initialize worker context for both standard and periodic tasks.
|
||||
worker_process_init.connect(cassandra_init)
|
||||
beat_init.connect(cassandra_init)
|
||||
|
||||
app = Celery()
|
||||
|
||||
For more details, see `issue #237
|
||||
<https://github.com/cqlengine/cqlengine/issues/237>`_.
|
||||
|
||||
|
||||
uWSGI
|
||||
-----
|
||||
|
||||
This is the code required for proper connection handling of CQLengine for a
|
||||
`uWSGI <https://uwsgi-docs.readthedocs.org>`_-run application:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from cqlengine import connection
|
||||
from cqlengine.connection import (
|
||||
cluster as cql_cluster, session as cql_session)
|
||||
|
||||
try:
|
||||
from uwsgidecorators import postfork
|
||||
except ImportError:
|
||||
# We're not in a uWSGI context, no need to hook Cassandra session
|
||||
# initialization to the postfork event.
|
||||
pass
|
||||
else:
|
||||
@postfork
|
||||
def cassandra_init():
|
||||
""" Initialize a new Cassandra session in the context.
|
||||
|
||||
Ensures that a new session is returned for every new request.
|
||||
"""
|
||||
if cql_cluster is not None:
|
||||
cql_cluster.shutdown()
|
||||
if cql_session is not None:
|
||||
cql_session.shutdown()
|
||||
connection.setup()
|
66
manifests/default.pp
Normal file
66
manifests/default.pp
Normal file
@ -0,0 +1,66 @@
|
||||
# Basic virtualbox configuration
|
||||
Exec { path => "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" }
|
||||
|
||||
node basenode {
|
||||
package{["build-essential", "git-core", "vim"]:
|
||||
ensure => installed
|
||||
}
|
||||
}
|
||||
|
||||
class xfstools {
|
||||
package{['lvm2', 'xfsprogs']:
|
||||
ensure => installed
|
||||
}
|
||||
}
|
||||
class java {
|
||||
package {['openjdk-7-jre-headless']:
|
||||
ensure => installed
|
||||
}
|
||||
}
|
||||
|
||||
class cassandra {
|
||||
include xfstools
|
||||
include java
|
||||
|
||||
package {"wget":
|
||||
ensure => latest
|
||||
}
|
||||
|
||||
file {"/etc/init/cassandra.conf":
|
||||
source => "puppet:///modules/cassandra/cassandra.upstart",
|
||||
owner => root
|
||||
}
|
||||
|
||||
exec {"download-cassandra":
|
||||
cwd => "/tmp",
|
||||
command => "wget http://download.nextag.com/apache/cassandra/1.2.19/apache-cassandra-1.2.19-bin.tar.gz",
|
||||
creates => "/tmp/apache-cassandra-1.2.19-bin.tar.gz",
|
||||
require => [Package["wget"], File["/etc/init/cassandra.conf"]]
|
||||
}
|
||||
|
||||
exec {"install-cassandra":
|
||||
cwd => "/tmp",
|
||||
command => "tar -xzf apache-cassandra-1.2.19-bin.tar.gz; mv apache-cassandra-1.2.19 /usr/local/cassandra",
|
||||
require => Exec["download-cassandra"],
|
||||
creates => "/usr/local/cassandra/bin/cassandra"
|
||||
}
|
||||
|
||||
service {"cassandra":
|
||||
ensure => running,
|
||||
require => Exec["install-cassandra"]
|
||||
}
|
||||
}
|
||||
|
||||
node cassandraengine inherits basenode {
|
||||
include cassandra
|
||||
|
||||
package {["python-pip", "python-dev", "python-nose"]:
|
||||
ensure => installed
|
||||
}
|
||||
|
||||
exec {"install-requirements":
|
||||
cwd => "/vagrant",
|
||||
command => "pip install -r requirements-dev.txt",
|
||||
require => [Package["python-pip"], Package["python-dev"]]
|
||||
}
|
||||
}
|
18
modules/cassandra/files/cassandra.upstart
Normal file
18
modules/cassandra/files/cassandra.upstart
Normal file
@ -0,0 +1,18 @@
|
||||
# Cassandra Upstart
|
||||
#
|
||||
|
||||
description "Cassandra"
|
||||
|
||||
start on runlevel [2345]
|
||||
stop on runlevel [!2345]
|
||||
|
||||
respawn
|
||||
|
||||
exec /usr/local/cassandra/bin/cassandra -f
|
||||
|
||||
|
||||
|
||||
pre-stop script
|
||||
kill `cat /tmp/cassandra.pid`
|
||||
sleep 3
|
||||
end script
|
7
requirements-dev.txt
Normal file
7
requirements-dev.txt
Normal file
@ -0,0 +1,7 @@
|
||||
-r requirements.txt
|
||||
nose
|
||||
nose-progressive
|
||||
profilestats
|
||||
pycallgraph
|
||||
ipdbplugin==1.2
|
||||
ipdb==0.7
|
3
setup.py
3
setup.py
@ -205,7 +205,8 @@ def run_setup(extensions):
|
||||
url='http://github.com/datastax/python-driver',
|
||||
author='Tyler Hobbs',
|
||||
author_email='tyler@datastax.com',
|
||||
packages=['cassandra', 'cassandra.io'],
|
||||
packages=['cassandra', 'cassandra.io'], # TODO: add cqlengine after moving
|
||||
keywords='cassandra,cql,orm',
|
||||
include_package_data=True,
|
||||
install_requires=dependencies,
|
||||
tests_require=['nose', 'mock', 'PyYAML', 'pytz'],
|
||||
|
7
tox.ini
7
tox.ini
@ -46,3 +46,10 @@ deps = nose
|
||||
six
|
||||
commands = {envpython} setup.py build_ext --inplace
|
||||
nosetests --verbosity=2 tests/unit/
|
||||
|
||||
# TODO: integrate cqlengine tests
|
||||
#envlist=py27,py34
|
||||
#
|
||||
#[testenv]
|
||||
#deps= -rrequirements.txt
|
||||
#commands=bin/test.py --no-skip
|
||||
|
13
upgrading.txt
Normal file
13
upgrading.txt
Normal file
@ -0,0 +1,13 @@
|
||||
0.15 Upgrade to use Datastax Native Driver
|
||||
|
||||
We no longer raise cqlengine based OperationalError when connection fails. Now using the exception thrown in the native driver.
|
||||
|
||||
If you're calling setup() with the old thrift port in place, you will get connection errors. Either remove it (the default native port is assumed) or change to the default native port, 9042.
|
||||
|
||||
The cqlengine connection pool has been removed. Connections are now managed by the native driver. This should drastically reduce the socket overhead as the native driver can multiplex queries.
|
||||
|
||||
If you were previously manually using "ALL", "QUORUM", etc, to specificy consistency levels, you will need to migrate to the cqlengine.ALL, QUORUM, etc instead. If you had been using the module level constants before, nothing should need to change.
|
||||
|
||||
No longer accepting username & password as arguments to setup. Use the native driver's authentication instead. See http://datastax.github.io/python-driver/api/cassandra/auth.html
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user