cassie 1.0.0.alpha.10 → 1.0.0.alpha.15
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/cassie +29 -0
- data/lib/cassie/configuration/README.md +57 -0
- data/lib/cassie/configuration/core.rb +53 -0
- data/lib/cassie/configuration/generator.rb +55 -0
- data/lib/cassie/configuration/loading.rb +42 -0
- data/lib/cassie/configuration/templates/cassandra.yml +30 -0
- data/lib/cassie/configuration.rb +9 -0
- data/lib/cassie/connection.rb +47 -0
- data/lib/cassie/connection_handler/README.md +123 -0
- data/lib/cassie/connection_handler/cluster.rb +11 -0
- data/lib/cassie/connection_handler/sessions.rb +18 -0
- data/lib/cassie/connection_handler.rb +17 -0
- data/lib/cassie/queries/README.md +316 -0
- data/lib/cassie/queries/statement.rb +1 -0
- data/lib/cassie/query.rb +8 -19
- data/lib/cassie/testing/README.md +58 -0
- data/lib/cassie/testing/fake/query.rb +1 -1
- data/lib/cassie/testing/fake/session_methods.rb +1 -19
- data/lib/cassie/testing.rb +2 -1
- data/lib/cassie.rb +22 -1
- metadata +34 -5
- data/lib/cassie/queries/session.rb +0 -22
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 51aee51ca2bee455d530c72090b9f44c52f9bd76
|
4
|
+
data.tar.gz: 49525a14eb23b418fae95415c63ef6cfbe12eabb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9349821c15b7c90df9ebbdb43577477b3c0fd549c2d10c9bc29b20286e773b2ccd978fddc8f44e8a4634ccd3def35f326901d19a4a5aaeaa70caf7b91e875816
|
7
|
+
data.tar.gz: ad8dc12ebfc74ae8719919894939a952a0dbd31a2b2095af436d5c2b34d4e23322abd79e5a854321b3ec8a5f0b2592e257086933daef483cca3d89d8fd8f5877
|
data/bin/cassie
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#! ruby
|
2
|
+
require_relative '../lib/cassie/configuration/generator'
|
3
|
+
|
4
|
+
def color(message)
|
5
|
+
"\e[1;31m#{message}\e[0m"
|
6
|
+
end
|
7
|
+
|
8
|
+
def generate_config
|
9
|
+
opts = {}
|
10
|
+
if ARGV[1]
|
11
|
+
opts[:destination_path] = if ARGV[1][0] == "/"
|
12
|
+
# cassie configuration:generate /usr/var/my_config_dir/cassandra_db.yml
|
13
|
+
ARGV[1]
|
14
|
+
else
|
15
|
+
# cassie configuration:generate my_config_dir/cassandra_db.yml
|
16
|
+
File.join(Dir.pwd, ARGV[1])
|
17
|
+
end
|
18
|
+
end
|
19
|
+
opts[:app_name] = ARGV[2] if ARGV[2]
|
20
|
+
|
21
|
+
Cassie::Configuration::Generator.new(opts).save
|
22
|
+
end
|
23
|
+
|
24
|
+
case ARGV[0]
|
25
|
+
when "configuration:generate"
|
26
|
+
generate_config
|
27
|
+
else
|
28
|
+
puts color("`#{ARGV[0]}` is not a supported command. Did you mean `cassie configuration:generate`?")
|
29
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# Cassie Configuration
|
2
|
+
|
3
|
+
Cassie provides cluster configuration storage and retrieval.
|
4
|
+
|
5
|
+
`Cassie` extends `Configuration::Core`, providing functionality to act as a configuration handler.
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
Cassie.env
|
9
|
+
=> "development"
|
10
|
+
|
11
|
+
Cassie.configurations
|
12
|
+
=> {"development"=>{"hosts"=>["127.0.0.1"], "port"=>9042, "reconnection_policy"=>nil, "keyspace"=>"my_app_development"}, "test"=>{"hosts"=>["127.0.0.1"], "port"=>9042, "idle_timeout"=>"nil", "keyspace"=>"my_app_test"}, "production"=>{"hosts"=>["cass1.my_app.biz", "cass2.my_app.biz", "cass3.my_app.biz"], "port"=>9042, "keyspace"=>"my_app_production"}}
|
13
|
+
|
14
|
+
Cassie.configuration
|
15
|
+
=> {"hosts"=>["127.0.0.1"], "port"=>9042, "reconnection_policy"=>nil, "keyspace"=>"my_app_development"}
|
16
|
+
|
17
|
+
Cassie.keyspace
|
18
|
+
=> "my_app_development"
|
19
|
+
```
|
20
|
+
|
21
|
+
The env supports loading from the environment, by default, as follows:
|
22
|
+
```
|
23
|
+
ENV["CASSANDRA_ENV"] || ENV["RACK_ENV"] || "development"
|
24
|
+
```
|
25
|
+
It may also explicitly be set via `Cassie.env=`.
|
26
|
+
|
27
|
+
#### Usage
|
28
|
+
|
29
|
+
Cassie also acts as a connection handler. It uses the above configuration functionality to instantiate a `Cassandra::Cluster` using the desired configuration and connect `Cassandra::Sessions`.
|
30
|
+
|
31
|
+
See the [Connection README](./lib/cassie/connection_hanlder/README.md#readme) for more on features and usage.]
|
32
|
+
|
33
|
+
#### Advanced / Manual Usage
|
34
|
+
|
35
|
+
A YAML backend is provided by default. Run `cassie configuration:generate` to generate the configuration file. The default location for these cluster configurations is `config/cassandra.yml`. This is configurable.
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
$ cassie configuration:generate cassandra_clusters.yml
|
39
|
+
$ irb
|
40
|
+
irb(main):001:0> require 'cassie'
|
41
|
+
=> true
|
42
|
+
irb(main):002:0> Cassie.paths
|
43
|
+
=> {"cluster_configurations"=>"config/cassandra.yml"}
|
44
|
+
irb(main):003:0> Cassie.paths["cluster_configurations"] = 'cassandra_clusters.yml'
|
45
|
+
=> "cassandra_clusters.yml"
|
46
|
+
irb(main):004:0> Cassie.configurations
|
47
|
+
=> {"development"=>{"hosts"=>["127.0.0.1"], "port"=>9042, "reconnection_policy"=>nil, "keyspace"=>"my_app_development"}, "test"=>{"hosts"=>["127.0.0.1"], "port"=>9042, "idle_timeout"=>"nil", "keyspace"=>"my_app_test"}, "production"=>{"hosts"=>["cass1.my_app.biz", "cass2.my_app.biz", "cass3.my_app.biz"], "port"=>9042, "keyspace"=>"my_app_production"}}
|
48
|
+
irb(main):005:0>
|
49
|
+
```
|
50
|
+
|
51
|
+
`configurations`, `env`, `configuration` and `keyspace` may be set explicitly as well.
|
52
|
+
|
53
|
+
```
|
54
|
+
Cassie.configuration = {"hosts"=>["localhost"], "port"=>9042, "keyspace"=> 'my_default_keyspace'}
|
55
|
+
```
|
56
|
+
|
57
|
+
> *Note:* Setting the `configuration` explicitly naturally means that `configurations` and `env` will no longer have functional meaning.
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require_relative 'loading'
|
2
|
+
|
3
|
+
module Cassie::Configuration
|
4
|
+
#TODO: proper rdoc
|
5
|
+
# Extend a class with Core to enable configuration management
|
6
|
+
module Core
|
7
|
+
include Loading
|
8
|
+
|
9
|
+
attr_writer :keyspace
|
10
|
+
|
11
|
+
def self.extended(extender)
|
12
|
+
extender.paths["cluster_configurations"] = "config/cassandra.yml"
|
13
|
+
end
|
14
|
+
|
15
|
+
def env
|
16
|
+
@env ||= ActiveSupport::StringInquirer.new(ENV["CASSANDRA_ENV"] || ENV["RACK_ENV"] || "development")
|
17
|
+
end
|
18
|
+
|
19
|
+
def env=(val)
|
20
|
+
@env = ActiveSupport::StringInquirer.new(val)
|
21
|
+
end
|
22
|
+
|
23
|
+
def paths
|
24
|
+
@paths ||= {}.with_indifferent_access
|
25
|
+
end
|
26
|
+
|
27
|
+
def configurations
|
28
|
+
@configurations ||= cluster_configurations
|
29
|
+
end
|
30
|
+
|
31
|
+
def configurations=(val)
|
32
|
+
if val && defined?(@configuration)
|
33
|
+
puts "WARNING:\n `#{self}.configuration` as been set explicitly. Setting `configurations` will have no effect."
|
34
|
+
end
|
35
|
+
@configurations = val
|
36
|
+
end
|
37
|
+
|
38
|
+
def configuration
|
39
|
+
return @configuration if defined?(@configuration)
|
40
|
+
configurations[env]
|
41
|
+
end
|
42
|
+
|
43
|
+
def configuration=(val)
|
44
|
+
@configuration = val
|
45
|
+
end
|
46
|
+
|
47
|
+
def keyspace
|
48
|
+
return @keyspace if defined?(@keyspace)
|
49
|
+
@keyspace = configuration[:keyspace]
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require "erb"
|
2
|
+
|
3
|
+
module Cassie
|
4
|
+
module Configuration
|
5
|
+
class Generator
|
6
|
+
include ERB::Util
|
7
|
+
attr_accessor :app_name,
|
8
|
+
:template_path,
|
9
|
+
:destination_path
|
10
|
+
|
11
|
+
def initialize(opts={})
|
12
|
+
@app_name = opts.fetch(:app_name, default_app_name)
|
13
|
+
@template_path = opts.fetch(:template_path, default_template_path)
|
14
|
+
@destination_path = opts.fetch(:destination_path, default_destination_path)
|
15
|
+
end
|
16
|
+
|
17
|
+
def render
|
18
|
+
ERB.new(template).result(binding)
|
19
|
+
end
|
20
|
+
|
21
|
+
def save
|
22
|
+
File.open(destination_path, "w+") do |f|
|
23
|
+
f.write(render)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
|
29
|
+
def template
|
30
|
+
File.new(template_path).read
|
31
|
+
end
|
32
|
+
|
33
|
+
def default_app_name
|
34
|
+
"my_app"
|
35
|
+
end
|
36
|
+
|
37
|
+
def default_template_path
|
38
|
+
File.expand_path("../templates/cassandra.yml", __FILE__)
|
39
|
+
end
|
40
|
+
|
41
|
+
def default_destination_path
|
42
|
+
Dir.mkdir(config_dir) unless File.directory?(config_dir)
|
43
|
+
File.join(root, "config/cassandra.yml")
|
44
|
+
end
|
45
|
+
|
46
|
+
def config_dir
|
47
|
+
File.join(root, "config")
|
48
|
+
end
|
49
|
+
|
50
|
+
def root
|
51
|
+
Dir.pwd
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Cassie::Configuration
|
2
|
+
module Loading
|
3
|
+
|
4
|
+
def cluster_configurations
|
5
|
+
path = paths["cluster_configurations"]
|
6
|
+
|
7
|
+
file = begin
|
8
|
+
File.new(path)
|
9
|
+
rescue StandardError
|
10
|
+
raise MissingClusterConfigurations.new(path)
|
11
|
+
end
|
12
|
+
|
13
|
+
require "yaml"
|
14
|
+
require "erb"
|
15
|
+
|
16
|
+
hash = YAML.load(ERB.new(file.read).result) || {}
|
17
|
+
hash.with_indifferent_access
|
18
|
+
rescue StandardError => e
|
19
|
+
raise e, "Cannot load Cassandra cluster configurations:\n#{e.message}", e.backtrace
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class MissingClusterConfigurations < StandardError
|
24
|
+
attr_reader :path
|
25
|
+
|
26
|
+
def initialize(path)
|
27
|
+
@path = path
|
28
|
+
super(build_message)
|
29
|
+
end
|
30
|
+
|
31
|
+
def build_message
|
32
|
+
msg = "Could not load cassandra cluster configurations. "
|
33
|
+
msg += "No cluster configurations exists at #{path}.\n"
|
34
|
+
msg += generation_instructions
|
35
|
+
msg += ", or configure the correct path via Cassie::Configuration.paths['cluster_configurations'] = <path>."
|
36
|
+
end
|
37
|
+
|
38
|
+
def generation_instructions
|
39
|
+
"Generate #{path} by running `cassie configuration:generate` or `cassie configuration:generate <relative or absolute path>.yml`"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# Generated and used by Cassie::Configuration.
|
2
|
+
#
|
3
|
+
# Per-enviornment options are passed to `cassandra-driver` during
|
4
|
+
# cluster creation and used to determine default keyspace for session creation.
|
5
|
+
# See valid options and values for cluster configuration at:
|
6
|
+
# http://datastax.github.io/ruby-driver/api/#cluster-class_method
|
7
|
+
|
8
|
+
development:
|
9
|
+
hosts:
|
10
|
+
- 127.0.0.1
|
11
|
+
port: 9042
|
12
|
+
reconnection_policy: <%%= Cassandra::Reconnection::Policies::Exponential.new(0.5, 60, 2) %>
|
13
|
+
keyspace: <%=app_name%>_development
|
14
|
+
|
15
|
+
test:
|
16
|
+
hosts:
|
17
|
+
- 127.0.0.1
|
18
|
+
port: 9042
|
19
|
+
idle_timeout: nil
|
20
|
+
keyspace: <%=app_name%>_test
|
21
|
+
|
22
|
+
production:
|
23
|
+
hosts:
|
24
|
+
- cass1.<%=app_name%>.biz
|
25
|
+
- cass2.<%=app_name%>.biz
|
26
|
+
- cass3.<%=app_name%>.biz
|
27
|
+
port: 9042
|
28
|
+
# username: 'cassandra_web_server_user'
|
29
|
+
# password: 'cassandra_web_server_password'
|
30
|
+
keyspace: <%=app_name%>_production
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Cassie
|
2
|
+
#TODO: proper rdoc
|
3
|
+
# include to give #session and #keyspace
|
4
|
+
# convenience methods
|
5
|
+
module Connection
|
6
|
+
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
attr_writer :keyspace
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
def keyspace(val=NilClass)
|
15
|
+
# support DSL style
|
16
|
+
# class Foo
|
17
|
+
# include Cassie::Connection
|
18
|
+
# keyspace :foo
|
19
|
+
# end
|
20
|
+
if val == NilClass
|
21
|
+
# regular getter behavior
|
22
|
+
return @keyspace if defined?(@keyspace)
|
23
|
+
# fall back to global default when not
|
24
|
+
# defined for class
|
25
|
+
Cassie.keyspace
|
26
|
+
else
|
27
|
+
# DSL style set
|
28
|
+
self.keyspace = val
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def keyspace=(val)
|
33
|
+
#support Class.keyspace = :foo
|
34
|
+
@keyspace = val
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def keyspace
|
39
|
+
return @keyspace if defined?(@keyspace)
|
40
|
+
self.class.keyspace
|
41
|
+
end
|
42
|
+
|
43
|
+
def session
|
44
|
+
Cassie.session(keyspace)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# Cassie Connection Handling
|
2
|
+
|
3
|
+
Cassie provides cluster and session connection handling that adheres to `cassandra-driver` best practices:
|
4
|
+
* Maintains 1 `Cassandra::Cluster` instance
|
5
|
+
* Maintains 1 `Cassandra::Session` per keyspace (or less)
|
6
|
+
|
7
|
+
Cassie also provides a `Connection` module to allow easy integration of connection handling into application classes.
|
8
|
+
|
9
|
+
|
10
|
+
#### Core functionality
|
11
|
+
|
12
|
+
`Cassie` extends `ConnectionHandler`, providing functionality to act as a connection handler using its `configuration` and `keyspace` attributes.
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
Cassie.cluster
|
16
|
+
=> #<Cassandra::Cluster:0x3fec245dceb0> # <= cluster instance configured according to `Cassie::configuration`
|
17
|
+
|
18
|
+
Cassie.keyspace
|
19
|
+
=> "default_keyspace"
|
20
|
+
|
21
|
+
Cassie.session
|
22
|
+
=> #<Cassandra::Session:0x3fec24b13668>
|
23
|
+
|
24
|
+
Cassie.session(nil)
|
25
|
+
=> #<Cassandra::Session:0x3fec24b339b8>
|
26
|
+
|
27
|
+
Cassie.session('my_other_keyspace')
|
28
|
+
=> #<Cassandra::Session:0x3fec24b558a8>
|
29
|
+
|
30
|
+
Cassie.sessions
|
31
|
+
=> {
|
32
|
+
"default_keyspace"=>#<Cassandra::Session:0x3fec24b13668>,
|
33
|
+
""=>#<Cassandra::Session:0x3fec24b13668>,
|
34
|
+
"my_other_keyspace" >#<Cassandra::Session:0x3fec24b558a8>
|
35
|
+
}
|
36
|
+
|
37
|
+
# Future session retrieval reuses previously connected sessions
|
38
|
+
Cassie.session
|
39
|
+
=> #<Cassandra::Session:0x3fec24b13668>
|
40
|
+
|
41
|
+
Cassie.sessions
|
42
|
+
=> {
|
43
|
+
"default_keyspace"=>#<Cassandra::Session:0x3fec24b13668>,
|
44
|
+
""=>#<Cassandra::Session:0x3fec24b13668>,
|
45
|
+
"my_other_keyspace" >#<Cassandra::Session:0x3fec24b558a8>
|
46
|
+
}
|
47
|
+
```
|
48
|
+
|
49
|
+
|
50
|
+
#### Mixin functionality
|
51
|
+
|
52
|
+
Including `Connection` gives convenience accessors that allow overriding and fallback behavior.
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
class HelpfulCounter
|
56
|
+
include Cassie::Connection
|
57
|
+
|
58
|
+
def user_count
|
59
|
+
session.execute('SELECT count(*) FROM users WHERE id = ?;').rows.first['count']
|
60
|
+
end
|
61
|
+
end
|
62
|
+
```
|
63
|
+
|
64
|
+
Ignoring the likely irresponsible example query used -- The object falls back to using the `Cassie::keyspace` value by default.
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
Cassie.keyspace
|
68
|
+
=> "default_keyspace"
|
69
|
+
|
70
|
+
object = HelpfulCounter.new
|
71
|
+
|
72
|
+
object.keyspace
|
73
|
+
=> "default_keyspace"
|
74
|
+
|
75
|
+
object.user_count
|
76
|
+
=> 302525
|
77
|
+
```
|
78
|
+
|
79
|
+
The keyspace can be set at the class level.
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
class Analytics::HelpfulCounter
|
83
|
+
include Cassie::Connection
|
84
|
+
|
85
|
+
keyspace :analytics_keyspace
|
86
|
+
|
87
|
+
def user_count
|
88
|
+
session.execute('SELECT count(*) FROM users WHERE id = ?;').rows.first['count']
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
Cassie.keyspace
|
93
|
+
=> "default_keyspace"
|
94
|
+
|
95
|
+
object = Analytics::HelpfulCounter.new
|
96
|
+
|
97
|
+
object.keyspace
|
98
|
+
=> "analytics_keyspace"
|
99
|
+
|
100
|
+
object.user_count
|
101
|
+
=> 300715
|
102
|
+
|
103
|
+
```
|
104
|
+
|
105
|
+
Or at the object level
|
106
|
+
|
107
|
+
```
|
108
|
+
Cassie.keyspace
|
109
|
+
=> "default_keyspace"
|
110
|
+
|
111
|
+
object = HelpfulCounter.new
|
112
|
+
|
113
|
+
object.keyspace
|
114
|
+
=> "default_keyspace"
|
115
|
+
|
116
|
+
object.user_count
|
117
|
+
=> 302525
|
118
|
+
|
119
|
+
object.keyspace = "analytics_keyspace"
|
120
|
+
=> "analytics_keyspace"
|
121
|
+
|
122
|
+
object.user_count
|
123
|
+
=> 300715
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Cassie::ConnectionHandler
|
2
|
+
module Sessions
|
3
|
+
|
4
|
+
def sessions
|
5
|
+
@sessions ||= {}
|
6
|
+
end
|
7
|
+
|
8
|
+
def session(_keyspace=self.keyspace)
|
9
|
+
sessions[_keyspace] || connect(_keyspace)
|
10
|
+
end
|
11
|
+
|
12
|
+
protected
|
13
|
+
|
14
|
+
def connect(_keyspace)
|
15
|
+
@sessions[_keyspace] = cluster.connect(_keyspace)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Cassie
|
2
|
+
#TODO: proper rdoc
|
3
|
+
# Assumes module responds to `configuration`, `keyspace`, and `cluster`
|
4
|
+
module ConnectionHandler
|
5
|
+
require_relative 'connection_handler/cluster'
|
6
|
+
require_relative 'connection_handler/sessions'
|
7
|
+
|
8
|
+
include Cluster
|
9
|
+
include Sessions
|
10
|
+
|
11
|
+
def self.extended(extender)
|
12
|
+
#TODO: raise if extender doesn't
|
13
|
+
# respond to configuration
|
14
|
+
# and keyspace
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,316 @@
|
|
1
|
+
# Cassie Queries
|
2
|
+
|
3
|
+
`cassie` query classes provide query interface that is
|
4
|
+
|
5
|
+
* Easy to use
|
6
|
+
* Easy to understand (and thus maintain)
|
7
|
+
* Easy to test
|
8
|
+
* Works well with the data mapper design pattern
|
9
|
+
|
10
|
+
### Usage
|
11
|
+
|
12
|
+
What you might expect to see:
|
13
|
+
|
14
|
+
```
|
15
|
+
Cassie.insert(:users_by_username,
|
16
|
+
"id = #{some_id}",
|
17
|
+
username: some_username)
|
18
|
+
```
|
19
|
+
|
20
|
+
Queries defined on the fly like this tend to create debt for an application in the long term. They:
|
21
|
+
* create gaps in test coverage
|
22
|
+
* resist documentation
|
23
|
+
* resist refactoring
|
24
|
+
|
25
|
+
Your application queries represent behavior, `cassie` queries are structured to help you create query classes that are reusable, testable and maintainable, so you can sleep better at night.
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
# Some PORO user model
|
29
|
+
user = User.new(username: username)
|
30
|
+
|
31
|
+
MyInsertionQuery.new.insert(user)
|
32
|
+
```
|
33
|
+
<pre><b>
|
34
|
+
(1.2ms) INSERT INTO users_by_username (id, username) VALUES (?, ?); [["uuid()", "eprothro"]]
|
35
|
+
</b></pre>
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
class MyInsertionQuery < Cassie::Query
|
39
|
+
|
40
|
+
insert :users_by_username do |u|
|
41
|
+
u.id,
|
42
|
+
u.username
|
43
|
+
end
|
44
|
+
|
45
|
+
def id
|
46
|
+
"uuid()"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
CQL algebra is less complex than with SQL. So, rather than introducing a query abstraction layer (e.g. something like [arel](https://github.com/rails/arel)), `cassie` queries provide a lightweight CQL DSL to codify your CQL queries.
|
52
|
+
|
53
|
+
```sql
|
54
|
+
SELECT *
|
55
|
+
FROM posts_by_author_category
|
56
|
+
WHERE author_id = ?
|
57
|
+
AND category = ?
|
58
|
+
LIMIT 30;
|
59
|
+
```
|
60
|
+
```ruby
|
61
|
+
select :posts_by_author_category
|
62
|
+
where :author_id, :eq
|
63
|
+
where :category, :eq
|
64
|
+
limit 30
|
65
|
+
```
|
66
|
+
|
67
|
+
This maintains the clarity of your CQL, allowing you to be expressive, but still use additional features without having get crazy with string manipulation.
|
68
|
+
|
69
|
+
#### Dynamic term values
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
select :posts_by_author
|
73
|
+
|
74
|
+
where :user_id, :eq
|
75
|
+
```
|
76
|
+
|
77
|
+
Defining a CQL relation in a cassie query (the "where") creates a setter and getter for that relation. This allows the term value to be set for a particular query instance.
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
query.user_id = 123
|
81
|
+
query.fetch
|
82
|
+
=> [#<Struct user_id=123, id="some post id">]
|
83
|
+
```
|
84
|
+
|
85
|
+
<pre><b>
|
86
|
+
(2.9ms) SELECT * FROM posts_by_author WHERE user_id = ? LIMIT 1; [[123]]
|
87
|
+
</b></pre>
|
88
|
+
|
89
|
+
These methods are plain old attr_accessors, and may be overriden
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
select :posts_by_author
|
93
|
+
|
94
|
+
where :user_id, :eq
|
95
|
+
|
96
|
+
def author=(user)
|
97
|
+
@user_id = user.id
|
98
|
+
end
|
99
|
+
```
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
query.author = User.new(id: 123)
|
103
|
+
query.fetch
|
104
|
+
=> [#<Struct user_id=123, id="some post id">]
|
105
|
+
```
|
106
|
+
|
107
|
+
<pre><b>
|
108
|
+
(2.9ms) SELECT * FROM posts_by_author WHERE user_id = ? LIMIT 1; [[123]]
|
109
|
+
</b></pre>
|
110
|
+
|
111
|
+
A specific name can be provided for the setter/getter:
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
select :posts_by_author
|
115
|
+
|
116
|
+
where :user_id, :eq, value: :author_id
|
117
|
+
```
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
query.author_id = 123
|
121
|
+
query.fetch
|
122
|
+
=> [#<Struct user_id=123, id="some post id">]
|
123
|
+
```
|
124
|
+
|
125
|
+
<pre><b>
|
126
|
+
(2.9ms) SELECT * FROM posts_by_author WHERE user_id = ? LIMIT 1; [[123]]
|
127
|
+
</b></pre>
|
128
|
+
|
129
|
+
#### Conditional relations
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
select :posts_by_author_category
|
133
|
+
|
134
|
+
where :author_id, :eq
|
135
|
+
where :category, :eq, if: "category.present?"
|
136
|
+
```
|
137
|
+
|
138
|
+
or
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
select :posts_by_author_category
|
142
|
+
|
143
|
+
where :author_id, :eq
|
144
|
+
where :category, :eq, if: :filter_by_category?
|
145
|
+
|
146
|
+
def filter_by_category?
|
147
|
+
#true or false, as makes sense for your query
|
148
|
+
end
|
149
|
+
```
|
150
|
+
|
151
|
+
#### Object Mapping
|
152
|
+
For Selection Queries, resources are returned as structs by default for manipulation using accessor methods.
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
UsersByUsernameQuery.new.fetch(username: "eprothro")
|
156
|
+
=> [#<Struct id=:123, username=:eprothro>]
|
157
|
+
|
158
|
+
UsersByUsernameQuery.new.find(username: "eprothro").username
|
159
|
+
=> "eprothro"
|
160
|
+
```
|
161
|
+
|
162
|
+
Override `build_resource` to construct more useful objects
|
163
|
+
|
164
|
+
```
|
165
|
+
class UsersByUsernameQuery < Cassie::Query
|
166
|
+
|
167
|
+
select :users_by_username
|
168
|
+
|
169
|
+
where :username, :eq
|
170
|
+
|
171
|
+
def build_resource(row)
|
172
|
+
User.new(row)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
```
|
176
|
+
|
177
|
+
```ruby
|
178
|
+
UsersByUsernameQuery.new.find(username: "eprothro")
|
179
|
+
=> #<User:0x007fedec219cd8 @id=123, @username="eprothro">
|
180
|
+
```
|
181
|
+
|
182
|
+
For Data Modification Queries (`insert`, `update`, `delete`), mapping binding values from an object is supported.
|
183
|
+
|
184
|
+
```ruby
|
185
|
+
class UpdateUserQuery < Cassandra::Query
|
186
|
+
|
187
|
+
update :users_by_id do |q|
|
188
|
+
q.set :phone
|
189
|
+
q.set :email
|
190
|
+
q.set :address
|
191
|
+
q.set :username
|
192
|
+
end
|
193
|
+
|
194
|
+
where :id, :eq
|
195
|
+
|
196
|
+
map_from :user
|
197
|
+
```
|
198
|
+
|
199
|
+
Allowing you to pass an object to the modification method, and binding values will be retrieved from the object
|
200
|
+
|
201
|
+
```ruby
|
202
|
+
user
|
203
|
+
=> #<User:0x007ff8895ce660 @id=6539, @phone="+15555555555", @email="etp@example.com", @address=nil, @username= "etp">
|
204
|
+
UpdateUserQuery.new.update(user)
|
205
|
+
```
|
206
|
+
|
207
|
+
<pre><b>
|
208
|
+
(1.2ms) UPDATE users_by_id (phone, email, address, username) VALUES (?, ?, ?, ?) WHERE id = ?; [["+15555555555", "etp@example.com", nil, "etp", 6539]]
|
209
|
+
</b></pre>
|
210
|
+
|
211
|
+
#### Cursored paging
|
212
|
+
|
213
|
+
Read about [cursored pagination](https://www.google.com/webhp?q=cursored%20paging#safe=off&q=cursor+paging) if unfamiliar with concept and how it optimizes paging through frequently updated data sets and I/O bandwidth.
|
214
|
+
|
215
|
+
```ruby
|
216
|
+
class MyPagedQuery < Cassie::Query
|
217
|
+
|
218
|
+
select :events_by_user
|
219
|
+
|
220
|
+
where :user_id, :eq
|
221
|
+
|
222
|
+
max_cursor :event_id
|
223
|
+
since_cursor :event_id
|
224
|
+
end
|
225
|
+
```
|
226
|
+
|
227
|
+
```ruby
|
228
|
+
# Imagine a set of id's 100 decreasing to 1
|
229
|
+
# where the client already has 1-50 in memory.
|
230
|
+
|
231
|
+
q = MyPagedQuery.new(page_size: 25, user: current_user)
|
232
|
+
|
233
|
+
# fetch 100 - 76
|
234
|
+
page_1 = q.fetch(max_event_id: nil, since_event_id: 50)
|
235
|
+
q.next_max_event_id
|
236
|
+
# => 75
|
237
|
+
|
238
|
+
# fetch 75 - 51
|
239
|
+
page_2 = q.fetch(max_event_id: q.next_max_event_id, since_event_id: 50)
|
240
|
+
q.next_max_id
|
241
|
+
# => nil
|
242
|
+
```
|
243
|
+
|
244
|
+
The `cursor_by` helper can be used as shorthand for defining these relations for which you wish to use cursors.
|
245
|
+
```ruby
|
246
|
+
class MyPagedQuery < Cassie::Query
|
247
|
+
|
248
|
+
select :events_by_user
|
249
|
+
|
250
|
+
where :user_id, :eq
|
251
|
+
|
252
|
+
cursor_by :event_id
|
253
|
+
end
|
254
|
+
```
|
255
|
+
|
256
|
+
#### Prepared statements
|
257
|
+
|
258
|
+
A `Cassie::Query` will use prepared statements by default, cacheing prepared statements across all Cassie::Query objects, keyed by the bound CQL string.
|
259
|
+
|
260
|
+
|
261
|
+
To not use prepared statements for a particular query, disable the `.prepare` class option.
|
262
|
+
|
263
|
+
```ruby
|
264
|
+
class MySpecialQuery < Cassie::Query
|
265
|
+
|
266
|
+
select :users_by_some_value do
|
267
|
+
where :bucket
|
268
|
+
where :some_value, :in
|
269
|
+
end
|
270
|
+
|
271
|
+
# the length of `some_values` that will be passed in
|
272
|
+
# is highly variable, so we don't want to incur the
|
273
|
+
# cost of preparing a statement for each unique length
|
274
|
+
self.prepare = false
|
275
|
+
end
|
276
|
+
```
|
277
|
+
|
278
|
+
```ruby
|
279
|
+
query = MySpecialQuery.new
|
280
|
+
|
281
|
+
# will not prepare statement
|
282
|
+
set_1 = query.fetch([1, 2, 3])
|
283
|
+
# will not prepare statement
|
284
|
+
set_2 = query.fetch([7, 8, 9, 10, 11, 12])
|
285
|
+
```
|
286
|
+
|
287
|
+
#### Unbound statements
|
288
|
+
|
289
|
+
Cassie Query features are built around bound statements. However, we've tried to keep a simple ruby design in place to make custom behavior easier. If you want to override the assumption of bound statements, simply override `#statement`, returnign something that a `Cassandra::Session` can execute.
|
290
|
+
|
291
|
+
```ruby
|
292
|
+
class NotSureWhyIWouldDoThisButHereItIsQuery < Cassie::Query
|
293
|
+
def statement
|
294
|
+
"SELECT * FROM users WHERE id IN (1,2,3);"
|
295
|
+
end
|
296
|
+
end
|
297
|
+
```
|
298
|
+
|
299
|
+
#### Logging
|
300
|
+
|
301
|
+
Set the log level to debug to log execution details.
|
302
|
+
|
303
|
+
```ruby
|
304
|
+
Cassie::Queries::Logging.logger.level = Logger::DEBUG
|
305
|
+
```
|
306
|
+
|
307
|
+
```ruby
|
308
|
+
SelectUserByUsernameQuery.new('some_user').execute
|
309
|
+
(2.9ms) SELECT * FROM users_by_username WHERE username = ? LIMIT 1; [["some_user"]]
|
310
|
+
```
|
311
|
+
|
312
|
+
Logs to STDOUT by default. Set any log stream you wish.
|
313
|
+
|
314
|
+
```ruby
|
315
|
+
Cassie::Queries::Logging.logger = my_app.config.logger
|
316
|
+
```
|
data/lib/cassie/query.rb
CHANGED
@@ -1,23 +1,12 @@
|
|
1
1
|
module Cassie
|
2
|
-
|
3
|
-
|
4
|
-
# * string extensions
|
5
|
-
# * notification pub/sub
|
6
|
-
# * log formatting
|
7
|
-
#
|
8
|
-
# We require/autoload extensions only as needed,
|
9
|
-
# this base require has almost no overhead
|
10
|
-
#
|
11
|
-
# http://guides.rubyonrails.org/active_support_core_extensions.html
|
12
|
-
require 'active_support'
|
13
|
-
require 'cassandra'
|
14
|
-
require_relative 'queries/session'
|
15
|
-
require_relative 'queries/statement'
|
16
|
-
require_relative 'queries/instrumentation'
|
17
|
-
require_relative 'queries/logging'
|
18
|
-
|
2
|
+
module Queries
|
3
|
+
end
|
19
4
|
class Query
|
20
|
-
|
5
|
+
require_relative 'queries/statement'
|
6
|
+
require_relative 'queries/instrumentation'
|
7
|
+
require_relative 'queries/logging'
|
8
|
+
|
9
|
+
include Cassie::Connection
|
21
10
|
include Queries::Statement
|
22
11
|
include Queries::Instrumentation
|
23
12
|
include Queries::Logging
|
@@ -30,4 +19,4 @@ module Cassie
|
|
30
19
|
super()
|
31
20
|
end
|
32
21
|
end
|
33
|
-
end
|
22
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# Cassie Test Harnessing
|
2
|
+
|
3
|
+
We're all trying to avoid overly integrated tests. When it comes to the persistance layer adapter, that can be tough. Cassie provides a simple test harness to allow you to stub out the `cassie-driver` layer when it makes sense to do so.
|
4
|
+
|
5
|
+
### Usage
|
6
|
+
Extend a `Cassie::Query` class or object with `Cassie::Testing::Fake::Query` to stub out calls to the `cassandra-driver` (and thus actual persistance layer) in a way that still allows calls to `execute` to occur.
|
7
|
+
|
8
|
+
Stubbing an object will only apply to that object, not other objects created from that class.
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
some_query = SomeQuery.new
|
12
|
+
some_query.extend(Cassie::Testing::Fake::Query)
|
13
|
+
some_query.session
|
14
|
+
=> #<Cassie::Testing::Fake::Session::Session:0x007fd03e29a688>
|
15
|
+
|
16
|
+
another_query = SomeQuery.new
|
17
|
+
another_query.session
|
18
|
+
# => this is not a fake session
|
19
|
+
```
|
20
|
+
|
21
|
+
Stubbing a class will apply to all objects of that class.
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
SomeQuery.include(Cassie::Testing::Fake::Query)
|
25
|
+
SomeQuery.new.session
|
26
|
+
=> #<Cassie::Testing::Fake::Session::Session:0x007fd03e29a688>
|
27
|
+
SomeQuery.new.session
|
28
|
+
=> #<Cassie::Testing::Fake::Session::Session:0x007fd03e3577a8>
|
29
|
+
```
|
30
|
+
|
31
|
+
If you're testing query extensions you have created, it may be more DRY to use a `Cassie::FakeQuery`, which is simply a child of `Cassie::Query` that has already extended `Cassie::Testing::Fake::Query`.
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
class TestQuery < Cassie::FakeQuery
|
35
|
+
end
|
36
|
+
TestQuery.new.session
|
37
|
+
=> #<Cassie::Testing::Fake::Session::Session:0x007fd03e29a688>
|
38
|
+
```
|
39
|
+
|
40
|
+
As shown above, query fakes uses a fake session, which provides a few useful features in additon to allowing mock execution:
|
41
|
+
|
42
|
+
##### Accessing the last statement executed
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
some_query.execute
|
46
|
+
|
47
|
+
some_query.session.last_statement
|
48
|
+
=> #<Cassandra::Statements::Simple:0x3ffde09930b8 @cql="SELECT * FROM users LIMIT 1;" @params=[]>
|
49
|
+
```
|
50
|
+
|
51
|
+
##### Mocking rows returned in result from query execution
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
some_query.session.rows = [{id: 1, username: "eprothro"}]
|
55
|
+
|
56
|
+
some_query.fetch
|
57
|
+
=> [#<Struct id=1, username="eprothro">]
|
58
|
+
```
|
@@ -2,26 +2,8 @@ require_relative 'session'
|
|
2
2
|
|
3
3
|
module Cassie::Testing::Fake
|
4
4
|
module SessionMethods
|
5
|
-
def self.extended(extender)
|
6
|
-
return if extender.class === Class
|
7
5
|
|
8
|
-
|
9
|
-
# memoize the fake session in metaclass for this object
|
10
|
-
# as we don't want to change behavior of _every_ object
|
11
|
-
# instantiated from the class, only _this_ object
|
12
|
-
extender.class.define_singleton_method(:session) do
|
13
|
-
@session ||= Cassie::Testing::Fake::Session.new
|
14
|
-
end
|
15
|
-
|
16
|
-
# overwrite definition from extension
|
17
|
-
# to delegate to class definition to
|
18
|
-
# minimize difference from vanilla Query
|
19
|
-
def extender.session
|
20
|
-
self.class.session
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
def session
|
6
|
+
def session(_keyspace=self.keyspace)
|
25
7
|
@session ||= Cassie::Testing::Fake::Session.new
|
26
8
|
end
|
27
9
|
end
|
data/lib/cassie/testing.rb
CHANGED
data/lib/cassie.rb
CHANGED
@@ -1 +1,22 @@
|
|
1
|
-
|
1
|
+
# Active Support used for
|
2
|
+
# * include convenience via ActiveSupport::Concern
|
3
|
+
# * string extensions
|
4
|
+
# * notification pub/sub
|
5
|
+
# * log formatting
|
6
|
+
#
|
7
|
+
# We require/autoload extensions only as needed,
|
8
|
+
# this base require has almost no overhead
|
9
|
+
#
|
10
|
+
# http://guides.rubyonrails.org/active_support_core_extensions.html
|
11
|
+
require 'active_support'
|
12
|
+
require 'cassandra'
|
13
|
+
|
14
|
+
module Cassie
|
15
|
+
require_relative 'cassie/configuration'
|
16
|
+
require_relative 'cassie/connection_handler'
|
17
|
+
require_relative 'cassie/connection'
|
18
|
+
require_relative 'cassie/query'
|
19
|
+
|
20
|
+
extend Configuration::Core
|
21
|
+
extend ConnectionHandler
|
22
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cassie
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.0.alpha.
|
4
|
+
version: 1.0.0.alpha.15
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Evan Prothro
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-03-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: cassandra-driver
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '4.2'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.3'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.3'
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: rspec
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -99,17 +113,30 @@ description: Cassie provides database configration, versioned migrations, effici
|
|
99
113
|
provided by the official `cassandra-driver` through lightweight and easy to use
|
100
114
|
interfaces.
|
101
115
|
email: evan.prothro@gmail.com
|
102
|
-
executables:
|
116
|
+
executables:
|
117
|
+
- cassie
|
103
118
|
extensions: []
|
104
119
|
extra_rdoc_files: []
|
105
120
|
files:
|
121
|
+
- bin/cassie
|
106
122
|
- lib/cassie.rb
|
123
|
+
- lib/cassie/configuration.rb
|
124
|
+
- lib/cassie/configuration/README.md
|
125
|
+
- lib/cassie/configuration/core.rb
|
126
|
+
- lib/cassie/configuration/generator.rb
|
127
|
+
- lib/cassie/configuration/loading.rb
|
128
|
+
- lib/cassie/configuration/templates/cassandra.yml
|
129
|
+
- lib/cassie/connection.rb
|
130
|
+
- lib/cassie/connection_handler.rb
|
131
|
+
- lib/cassie/connection_handler/README.md
|
132
|
+
- lib/cassie/connection_handler/cluster.rb
|
133
|
+
- lib/cassie/connection_handler/sessions.rb
|
134
|
+
- lib/cassie/queries/README.md
|
107
135
|
- lib/cassie/queries/instrumentation.rb
|
108
136
|
- lib/cassie/queries/logging.rb
|
109
137
|
- lib/cassie/queries/logging/cql_execution_event.rb
|
110
138
|
- lib/cassie/queries/logging/logger.rb
|
111
139
|
- lib/cassie/queries/logging/subscription.rb
|
112
|
-
- lib/cassie/queries/session.rb
|
113
140
|
- lib/cassie/queries/statement.rb
|
114
141
|
- lib/cassie/queries/statement/assignment.rb
|
115
142
|
- lib/cassie/queries/statement/assignments.rb
|
@@ -133,6 +160,7 @@ files:
|
|
133
160
|
- lib/cassie/queries/statement/updating.rb
|
134
161
|
- lib/cassie/query.rb
|
135
162
|
- lib/cassie/testing.rb
|
163
|
+
- lib/cassie/testing/README.md
|
136
164
|
- lib/cassie/testing/fake/execution_info.rb
|
137
165
|
- lib/cassie/testing/fake/prepared_statement.rb
|
138
166
|
- lib/cassie/testing/fake/query.rb
|
@@ -159,8 +187,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
159
187
|
version: 1.3.1
|
160
188
|
requirements: []
|
161
189
|
rubyforge_project:
|
162
|
-
rubygems_version: 2.5.
|
190
|
+
rubygems_version: 2.5.2
|
163
191
|
signing_key:
|
164
192
|
specification_version: 4
|
165
193
|
summary: Apache Cassandra application support
|
166
194
|
test_files: []
|
195
|
+
has_rdoc:
|
@@ -1,22 +0,0 @@
|
|
1
|
-
module Cassie::Queries
|
2
|
-
module Session
|
3
|
-
extend ::ActiveSupport::Concern
|
4
|
-
|
5
|
-
module ClassMethods
|
6
|
-
def session
|
7
|
-
# until cassie-configuration exists,
|
8
|
-
# we're relying on the client to
|
9
|
-
# supply the session
|
10
|
-
if defined?(super)
|
11
|
-
super
|
12
|
-
else
|
13
|
-
raise "Oops! Cassie::Queries doesn't manage a Cassandra session for you, yet. You must provide a .session class method that returns a valid session."
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
def session
|
19
|
-
self.class.session
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|