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 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