cassie 1.0.0.alpha.10 → 1.0.0.alpha.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|