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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fec7a911c85e20f50813efaa558b5d0874d22f81
4
- data.tar.gz: 9b96499ef12e8544d238af49185afdf38eb762e1
3
+ metadata.gz: 51aee51ca2bee455d530c72090b9f44c52f9bd76
4
+ data.tar.gz: 49525a14eb23b418fae95415c63ef6cfbe12eabb
5
5
  SHA512:
6
- metadata.gz: 5940e76a95f2414c6268d474edea614199c4313e17d0902e3be82a16d4641e9fd2e94aa0cdb22d33825dffe7bf5e5f5ce13c70a159c8154e70c6bec944c3e1a8
7
- data.tar.gz: 6a774ce27b9934ac5023b2abd081c88a3b737f92833328866fe3ea0ee3a45fac728ebc06e521e29f06fb9a8417bbc831dde021a72ace61d9475fb0a72308c828
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,9 @@
1
+ require 'active_support/core_ext/hash/indifferent_access'
2
+
3
+ module Cassie
4
+ module Configuration
5
+ require_relative 'configuration/generator'
6
+ require_relative 'configuration/core'
7
+
8
+ end
9
+ end
@@ -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,11 @@
1
+ module Cassie::ConnectionHandler
2
+ module Cluster
3
+
4
+ def cluster
5
+ # Cassandra::cluster parses suppored
6
+ # options from the passed hash, no need
7
+ # to validate/transform ourselves yet
8
+ @cluster ||= Cassandra.cluster(configuration)
9
+ end
10
+ end
11
+ end
@@ -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
+ ```
@@ -1,4 +1,5 @@
1
1
  require 'active_support/core_ext/string/filters'
2
+ require 'active_support/hash_with_indifferent_access'
2
3
  require_relative 'statement/preparation'
3
4
  require_relative 'statement/callbacks'
4
5
  require_relative 'statement/limiting'
data/lib/cassie/query.rb CHANGED
@@ -1,23 +1,12 @@
1
1
  module Cassie
2
- # Active Support used for
3
- # * include convenience via ActiveSupport::Concern
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
- include Queries::Session
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
+ ```
@@ -5,6 +5,6 @@ module Cassie
5
5
  end
6
6
 
7
7
  class FakeQuery < Cassie::Query
8
- extend Cassie::Testing::Fake::SessionMethods
8
+ include Cassie::Testing::Fake::SessionMethods
9
9
  end
10
10
  end
@@ -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
- # object has been extended (as opposed to class)
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
@@ -1,5 +1,6 @@
1
1
  module Cassie
2
2
  module Testing
3
3
  require_relative 'testing/fake/query'
4
+
4
5
  end
5
- end
6
+ end
data/lib/cassie.rb CHANGED
@@ -1 +1,22 @@
1
- require_relative 'cassie/query'
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.10
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-01-20 00:00:00.000000000 Z
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.1
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