cassandra_model 0.9.21 → 1.0.16

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: da8cdc27700ca95add22df7ece2de9e050a09642
4
- data.tar.gz: 7a9a39c9bd2bf63bb9dc03442461b5f860c95718
3
+ metadata.gz: 8caf227ae8b22d30047431ce0293f39a965c07aa
4
+ data.tar.gz: b03cf72cb34c9d9fa8f9e18ee4930f8e7574639a
5
5
  SHA512:
6
- metadata.gz: c7a028e42484dd36523c86934ef8b9d79045354d720159cc1ab75fadb8cd9e150f2921d40b58038c2ecdd54bff87a80a07ff2fedd83ff976952ce4e5a3b0a484
7
- data.tar.gz: 39dd8d3525696fc4d183a9b5f077d0cfb24f74da2d86d3c8eeba0db843d023b1257bb38d4c61f9f62da4d6c366769b8d5a722dd83e1eb53d9d0f8e1630a1928d
6
+ metadata.gz: e1d04f2b752b5509709bebfe0fe5e2c3ec79a57876ad4a246858e1ff2a3dc14aa27d342db54b90bf2e00a0e20c726b636511f2dd09b960c8ffbf7fc1fa76168a
7
+ data.tar.gz: 06b8f1d4f77529b16b0a530c0c981a2025313b20280c0627a71ef8c00ee6acc31ca0692ed89e8d99e746fe670c393ab67c8c427035b48fa8d1ae7037c60ad1c6
@@ -1,4 +1,4 @@
1
- Copyright 2014-2015 Thomas RM Rogers
1
+ Copyright 2014-2016 Thomas RM Rogers
2
2
 
3
3
  Licensed under the Apache License, Version 2.0 (the "License");
4
4
  you may not use this file except in compliance with the License.
@@ -10,4 +10,4 @@ Unless required by applicable law or agreed to in writing, software
10
10
  distributed under the License is distributed on an "AS IS" BASIS,
11
11
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  See the License for the specific language governing permissions and
13
- limitations under the License.
13
+ limitations under the License.
data/README.md CHANGED
@@ -155,7 +155,7 @@ There are a number of features in Cassandra Model that may have missing or incom
155
155
 
156
156
  ## Copyright
157
157
 
158
- Copyright 2014-2015 Thomas Rogers.
158
+ Copyright 2014-2016 Thomas Rogers.
159
159
 
160
160
  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
161
161
 
@@ -1,5 +1,5 @@
1
1
  #--
2
- # Copyright 2014-2015 Thomas RM Rogers
2
+ # Copyright 2014-2016 Thomas RM Rogers
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
5
5
  # you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@ require 'batch_reactor'
23
23
  require 'active_support/all'
24
24
  require 'active_support/core_ext/class/attribute_accessors'
25
25
 
26
+ require 'cassandra_model/session_compatibility'
26
27
  require 'cassandra_model/concurrency_helper'
27
28
  require 'cassandra_model/logging'
28
29
  require 'cassandra_model/global_callbacks'
@@ -39,9 +40,13 @@ require 'cassandra_model/table_definition'
39
40
  require 'cassandra_model/table_debug'
40
41
  require 'cassandra_model/table_redux'
41
42
  require 'cassandra_model/result_paginator'
43
+ require 'cassandra_model/result_limiter'
44
+ require 'cassandra_model/result_chunker'
45
+ require 'cassandra_model/result_reducer'
42
46
  require 'cassandra_model/query_result'
43
47
  require 'cassandra_model/query_builder'
44
48
  require 'cassandra_model/displayable_attributes'
49
+ require 'cassandra_model/scopes'
45
50
  require 'cassandra_model/record_debug'
46
51
  require 'cassandra_model/record'
47
52
  require 'cassandra_model/counter_record'
@@ -25,7 +25,7 @@ module CassandraModel
25
25
  def batch_callback(_)
26
26
  batch = @batch_klass.new
27
27
  yield batch
28
- @session.execute_async(batch).on_success { |result| batch.result = result }
28
+ @session.execute_async(batch).then { |result| batch.result = result }
29
29
  end
30
30
 
31
31
  end
@@ -1,7 +1,17 @@
1
1
  module CassandraModel
2
+ module CompositeCounterRecord
3
+ def increment_async!(counts)
4
+ futures = composite_rows.map { |record| record.internal_increment_async!(counts) }
5
+
6
+ futures << internal_increment_async!(counts)
7
+ Cassandra::Future.all(futures).then { self }
8
+ end
9
+ end
10
+
2
11
  module CompositeRecord
3
12
  def self.included(klass)
4
13
  klass.extend CompositeRecordStatic
14
+ klass.send(:include, CompositeCounterRecord) if klass < CounterRecord
5
15
  end
6
16
 
7
17
  def save_async(options = {})
@@ -44,7 +54,7 @@ module CassandraModel
44
54
 
45
55
  def internal_attributes
46
56
  internal_columns.inject({}) do |memo, column|
47
- memo.merge(column => attribute(column))
57
+ memo.merge!(column => attribute(column))
48
58
  end
49
59
  end
50
60
  end
@@ -1,30 +1,33 @@
1
1
  module CassandraModel
2
+ #noinspection ALL
2
3
  module CompositeRecordStatic
3
- MUTEX = Mutex.new
4
-
5
4
  extend Forwardable
6
5
 
7
6
  def_delegator :table_config, :composite_defaults=
7
+
8
+ def self.extended(base)
9
+ base.instance_variable_set(:@mutex, Mutex.new)
10
+ end
8
11
 
9
12
  def partition_key
10
- table_data.composite_partition_key ||= internal_partition_key.map { |column| trimmed_column(column, /^rk_/, composite_pk_map) || column }
13
+ table_data.composite_partition_key ||= internal_partition_key.map { |column| trim_column!(column, /^rk_/, composite_pk_map) || column }
11
14
  end
12
15
 
13
16
  def clustering_columns
14
- table_data.composite_clustering_columns ||= internal_clustering_columns.map { |column| trimmed_column(column, /^ck_/, composite_ck_map) || column }
17
+ table_data.composite_clustering_columns ||= internal_clustering_columns.map { |column| trim_column!(column, /^ck_/, composite_ck_map) || column }
15
18
  end
16
19
 
17
20
  def primary_key
18
21
  table_data.composite_primary_key ||= (internal_partition_key + internal_clustering_columns).map do |column|
19
- trimmed_column(column, /^rk_/, composite_pk_map) ||
20
- trimmed_column(column, /^ck_/, composite_ck_map) ||
22
+ trim_column!(column, /^rk_/, composite_pk_map) ||
23
+ trim_column!(column, /^ck_/, composite_ck_map) ||
21
24
  column
22
25
  end.uniq
23
26
  end
24
27
 
25
28
  def columns
26
29
  unless table_data.composite_columns
27
- MUTEX.synchronize do
30
+ @mutex.synchronize do
28
31
  return table_data.composite_columns if table_data.composite_columns
29
32
 
30
33
  table_data.composite_pk_map = {}
@@ -37,6 +40,20 @@ module CassandraModel
37
40
 
38
41
  alias :ensure_attributes_accessible! :columns
39
42
 
43
+ def denormalized_column_map(input_columns)
44
+ internal_columns.inject({}) do |memo, column|
45
+ result_column = input_columns.find { |input_column| input_column == column }
46
+ unless result_column
47
+ trimmed_column = trimmed_column(column, /^rk_/) ||
48
+ trimmed_column(column, /^ck_/) ||
49
+ column
50
+ result_column = input_columns.find { |input_column| input_column == trimmed_column }
51
+ end
52
+ memo[column] = result_column if result_column
53
+ memo
54
+ end
55
+ end
56
+
40
57
  def composite_pk_map
41
58
  ensure_attributes_accessible! unless table_data.composite_columns
42
59
  table_data.composite_pk_map
@@ -122,22 +139,27 @@ module CassandraModel
122
139
 
123
140
  def composite_columns
124
141
  internal_columns.map do |column|
125
- trimmed_column(column, /^rk_/, table_data.composite_pk_map) ||
126
- trimmed_column(column, /^ck_/, table_data.composite_ck_map) ||
142
+ trim_column!(column, /^rk_/, table_data.composite_pk_map) ||
143
+ trim_column!(column, /^ck_/, table_data.composite_ck_map) ||
127
144
  column
128
145
  end.uniq
129
146
  end
130
147
 
131
- def trimmed_column(column, column_trim, map)
132
- column_str = column.to_s
133
- if column_str =~ column_trim
134
- column_str.gsub(column_trim, '').to_sym.tap do |result_column|
148
+ def trim_column!(column, column_trim, map)
149
+ trimmed_column = trimmed_column(column, column_trim)
150
+ if trimmed_column
151
+ trimmed_column.tap do |result_column|
135
152
  map[result_column] = column
136
153
  map[column] = result_column
137
154
  end
138
155
  end
139
156
  end
140
157
 
158
+ def trimmed_column(column, column_trim)
159
+ column_str = column.to_s
160
+ column_str.gsub(column_trim, '').to_sym if column_str =~ column_trim
161
+ end
162
+
141
163
  def select_clause(select)
142
164
  select = select_columns(select) if select
143
165
  super(select)
@@ -2,6 +2,20 @@ module CassandraModel
2
2
  class CounterRecord < Record
3
3
 
4
4
  def increment_async!(options)
5
+ internal_increment_async!(options)
6
+ end
7
+
8
+ def increment!(options)
9
+ increment_async!(options).get
10
+ end
11
+
12
+ protected
13
+
14
+ def internal_save_async(options = {})
15
+ raise NotImplementedError
16
+ end
17
+
18
+ def internal_increment_async!(options)
5
19
  counter_clause = counter_clause(options)
6
20
  row_key = internal_primary_key.values
7
21
  statement = increment_statement(counter_clause)
@@ -22,14 +36,6 @@ module CassandraModel
22
36
  end.then { self }
23
37
  end
24
38
 
25
- def increment!(options)
26
- increment_async!(options).get
27
- end
28
-
29
- def save_async
30
- raise NotImplementedError
31
- end
32
-
33
39
  private
34
40
 
35
41
  def internal_primary_key
@@ -5,6 +5,10 @@ module CassandraModel
5
5
  base.send(:include, CompositeRecord)
6
6
  end
7
7
 
8
+ def table_properties=(value)
9
+ @table_properties = value
10
+ end
11
+
8
12
  def model_data
9
13
  inquirer = DataInquirer.new
10
14
  data_set = DataSet.new
@@ -17,6 +21,13 @@ module CassandraModel
17
21
  end
18
22
 
19
23
  generate_composite_defaults_from_inquirer(inquirer)
24
+ columns
25
+ end
26
+
27
+ def serialized_column(column, serializer)
28
+ serialized_column = :"#{column}_data"
29
+ deferred_column column, on_load: ->(attributes) { serializer.load(attributes[serialized_column]) if attributes[serialized_column] },
30
+ on_save: ->(attributes, value) { attributes[serialized_column] = (serializer.dump(value) if value) }
20
31
  end
21
32
 
22
33
  private
@@ -37,9 +48,9 @@ module CassandraModel
37
48
  end
38
49
 
39
50
  def meta_table(table_name, inquirer, data_set)
40
- table_definition = CassandraModel::TableDefinition.from_data_model(table_name, inquirer, data_set)
51
+ table_definition = CassandraModel::TableDefinition.from_data_model(table_name, inquirer, data_set, @table_properties)
41
52
  CassandraModel::MetaTable.new(table_config.connection_name, table_definition)
42
53
  end
43
54
 
44
55
  end
45
- end
56
+ end
@@ -2,12 +2,13 @@ module CassandraModel
2
2
  class DataSet
3
3
  include TypeGuessing
4
4
 
5
- attr_reader :columns, :clustering_columns, :data_rotation
5
+ attr_reader :columns, :data_rotation, :clustering_order
6
6
 
7
7
  def initialize
8
8
  @columns = Hash.new { |hash, key| hash[key] = :text }
9
9
  @data_rotation = {}
10
10
  @clustering_columns = []
11
+ @clustering_order = {}
11
12
  end
12
13
 
13
14
  def knows_about(*columns)
@@ -34,7 +35,7 @@ module CassandraModel
34
35
 
35
36
  def is_defined_by(*columns)
36
37
  knows_about(*columns)
37
- @clustering_columns = columns
38
+ clustering_columns.concat(columns)
38
39
  end
39
40
 
40
41
  def change_type_of(column)
@@ -42,6 +43,16 @@ module CassandraModel
42
43
  ColumnType.new(column, self)
43
44
  end
44
45
 
46
+ def sorts(sorting)
47
+ unknown_column = sorting.keys.find { |column, _| !columns.include?(column) }
48
+ raise "Cannot sort unknown column #{unknown_column}" if unknown_column
49
+ clustering_order.merge!(sorting)
50
+ end
51
+
52
+ def clustering_columns
53
+ @clustering_columns ||= []
54
+ end
55
+
45
56
  private
46
57
 
47
58
  def count_column(column)
@@ -11,7 +11,7 @@ module CassandraModel
11
11
 
12
12
  def name
13
13
  @name ||= begin
14
- create_table
14
+ table
15
15
  name_in_cassandra
16
16
  end
17
17
  end
@@ -28,7 +28,7 @@ module CassandraModel
28
28
  private
29
29
 
30
30
  def table
31
- @table ||= create_table
31
+ keyspace.table(name_in_cassandra) || create_table
32
32
  end
33
33
 
34
34
  def keyspace
@@ -42,7 +42,13 @@ module CassandraModel
42
42
  sleep 0.100
43
43
  break if keyspace.table(name_in_cassandra)
44
44
  end
45
- keyspace.table(name_in_cassandra) or raise "Could not verify the creation of table #{name_in_cassandra}"
45
+
46
+ keyspace.table(name_in_cassandra).tap do |table|
47
+ unless table
48
+ descriptor.delete
49
+ raise "Could not verify the creation of table #{name_in_cassandra}"
50
+ end
51
+ end
46
52
  end
47
53
 
48
54
  def create_cassandra_table(descriptor)
@@ -1,15 +1,7 @@
1
1
  module CassandraModel
2
2
  class RawConnection
3
3
  def cluster
4
- @cluster ||= begin
5
- Cassandra::Mocks::Cluster.new.tap do |cluster|
6
- cluster.add_keyspace(config[:keyspace])
7
- end
8
- end
9
- end
10
-
11
- def session
12
- @session ||= Cassandra::Mocks::Session.new(config[:keyspace], cluster)
4
+ @cluster ||= Cassandra::Mocks::Cluster.new
13
5
  end
14
6
  end
15
7
  end
@@ -3,12 +3,13 @@ module CassandraModel
3
3
  include Enumerable
4
4
  extend Forwardable
5
5
 
6
- def_delegator :async, :each
6
+ EMPTY_OPTION = [].freeze
7
7
 
8
- def initialize(record_klass)
8
+ def initialize(record_klass, params = {}, options = {}, extra_options = {})
9
9
  @record_klass = record_klass
10
- @params = {}
11
- @options = {}
10
+ @params = params
11
+ @options = options
12
+ @extra_options = extra_options
12
13
  end
13
14
 
14
15
  def async
@@ -59,8 +60,7 @@ module CassandraModel
59
60
  end
60
61
 
61
62
  def check_exists
62
- @options.merge!(check_exists: true)
63
- self
63
+ new_instance(@params, @options.merge(check_exists: true), @extra_options)
64
64
  end
65
65
 
66
66
  def pluck(*columns)
@@ -73,12 +73,34 @@ module CassandraModel
73
73
  end
74
74
 
75
75
  def each_slice(slice_size = nil, &block)
76
+ raise NotImplementedError if @extra_options[:cluster]
76
77
  paginate(slice_size).async.each_slice(&block)
77
78
  end
78
79
 
80
+ def each(&block)
81
+ if @extra_options[:cluster]
82
+ enum = ResultChunker.new(async, @extra_options[:cluster])
83
+ enum = if @extra_options[:cluster_limit]
84
+ ResultLimiter.new(enum, @extra_options[:cluster_limit])
85
+ else
86
+ enum
87
+ end
88
+ block_given? ? enum.each(&block) : enum
89
+ else
90
+ async.each(&block)
91
+ end
92
+ end
93
+
94
+ def cluster(*columns)
95
+ new_instance(@params, @options, @extra_options.merge(cluster: columns))
96
+ end
97
+
98
+ def cluster_except(*columns)
99
+ cluster(*(@record_klass.primary_key - columns))
100
+ end
101
+
79
102
  def where(params)
80
- @params.merge!(params)
81
- self
103
+ new_instance(@params.merge(params.symbolize_keys), @options, @extra_options)
82
104
  end
83
105
 
84
106
  def select(*columns)
@@ -90,33 +112,55 @@ module CassandraModel
90
112
  end
91
113
 
92
114
  def limit(limit)
93
- @options[:limit] = limit
94
- self
115
+ if @extra_options[:cluster]
116
+ new_instance(@params, @options, @extra_options.merge(cluster_limit: limit))
117
+ else
118
+ new_instance(@params, @options.merge(limit: limit), @extra_options)
119
+ end
95
120
  end
96
121
 
97
122
  def trace(trace)
98
- @options[:trace] = trace
99
- self
123
+ new_instance(@params, @options.merge(trace: trace), @extra_options)
100
124
  end
101
125
 
102
126
  def paginate(page_size)
103
- @options[:page_size] = page_size
104
- self
127
+ new_instance(@params, @options.merge(page_size: page_size), @extra_options)
128
+ end
129
+
130
+ def ==(rhs)
131
+ rhs.is_a?(QueryBuilder) &&
132
+ rhs.record_klass == record_klass &&
133
+ rhs.params == params &&
134
+ rhs.options == options &&
135
+ rhs.extra_options == extra_options
136
+ end
137
+
138
+ def method_missing(method, *args)
139
+ scope = record_klass.scopes[method]
140
+ scope ? instance_exec(*args, &scope) : super
105
141
  end
106
142
 
143
+ protected
144
+
145
+ attr_reader :record_klass, :params, :options, :extra_options
146
+
107
147
  private
108
148
 
149
+ def new_instance(params, options, extra_options)
150
+ self.class.new(record_klass, params, options, extra_options)
151
+ end
152
+
109
153
  def append_option(columns, option)
110
- @options[option] ||= []
154
+ new_option = (@options[option] || EMPTY_OPTION).dup
111
155
  if columns.first.is_a?(Hash)
112
156
  columns = columns.first.map do |column, direction|
113
- {column => direction}
157
+ {column.to_sym => direction}
114
158
  end
115
- @options[option].concat(columns)
159
+ new_option.concat(columns)
116
160
  else
117
- @options[option].concat(columns)
161
+ new_option.concat(columns.map(&:to_sym))
118
162
  end
119
- self
163
+ new_instance(@params, @options.merge(option => new_option), @extra_options)
120
164
  end
121
165
 
122
166
  def pluck_values(columns, result)
@@ -1,10 +1,7 @@
1
1
  module CassandraModel
2
+ #noinspection RubyTooManyInstanceVariablesInspection
2
3
  class RawConnection
3
- CLUSTER_MUTEX = Mutex.new
4
- SESSION_MUTEX = Mutex.new
5
- CONFIG_MUTEX = Mutex.new
6
- STATEMENT_MUTEX = Mutex.new
7
- REACTOR_MUTEX = Mutex.new
4
+ extend Forwardable
8
5
 
9
6
  DEFAULT_CONFIGURATION = {
10
7
  hosts: %w(localhost),
@@ -21,34 +18,46 @@ module CassandraModel
21
18
 
22
19
  include ConcurrencyHelper
23
20
 
21
+ def_delegator :@executor, :value, :executor
22
+ def_delegator :@futures_factory, :value, :futures_factory
23
+
24
24
  def initialize(config_name = nil)
25
25
  @config_name = config_name
26
- @statement_cache = {}
26
+ @statement_cache = Concurrent::Map.new
27
+
28
+ @cluster_mutex = Mutex.new
29
+ @session_mutex = Mutex.new
30
+ @config_mutex = Mutex.new
31
+ @reactor_mutex = Mutex.new
32
+
33
+ @executor = Concurrent::Delay.new { Concurrent::CachedThreadPool.new }
34
+ @futures_factory = Concurrent::Delay.new { Cassandra::Future::Factory.new(executor) }
27
35
  end
28
36
 
29
37
  def config=(value)
30
- CONFIG_MUTEX.synchronize { @config = DEFAULT_CONFIGURATION.merge(value) }
38
+ @config_mutex.synchronize { @config = DEFAULT_CONFIGURATION.merge(value) }
31
39
  end
32
40
 
33
41
  def config
34
- safe_getset_variable(CONFIG_MUTEX, :@config) { load_config }
42
+ safe_getset_variable(@config_mutex, :@config) { load_config }
35
43
  end
36
44
 
37
45
  def cluster
38
- safe_getset_variable(CLUSTER_MUTEX, :@cluster) do
46
+ safe_getset_variable(@cluster_mutex, :@cluster) do
39
47
  connection_configuration = config.slice(:hosts,
40
48
  :compression,
41
49
  :consistency,
42
50
  :connection_timeout, :timeout,
43
51
  :username, :password,
44
52
  :address_resolution)
45
- connection_configuration.merge!(logger: Logging.logger)
53
+ connection_configuration[:logger] = Logging.logger
54
+ connection_configuration[:futures_factory] = futures_factory
46
55
  Cassandra.cluster(connection_configuration)
47
56
  end
48
57
  end
49
58
 
50
59
  def session
51
- safe_getset_variable(SESSION_MUTEX, :@session) { cluster.connect(config[:keyspace]) }
60
+ safe_getset_variable(@session_mutex, :@session) { cluster.connect(config[:keyspace]) }
52
61
  end
53
62
 
54
63
  def keyspace
@@ -68,14 +77,12 @@ module CassandraModel
68
77
  end
69
78
 
70
79
  def statement(query)
71
- statement_cache[query] || begin
72
- STATEMENT_MUTEX.synchronize { statement_cache[query] ||= session.prepare(query) }
73
- end
80
+ statement_cache.fetch_or_store(query) { session.prepare(query) }
74
81
  end
75
82
 
76
83
  def shutdown
77
84
  @shutdown = true
78
- REACTOR_MUTEX.synchronize do
85
+ @reactor_mutex.synchronize do
79
86
  @unlogged_reactor.stop.get if @unlogged_reactor
80
87
  @unlogged_reactor = nil
81
88
 
@@ -85,11 +92,11 @@ module CassandraModel
85
92
  @counter_reactor.stop.get if @counter_reactor
86
93
  @counter_reactor = nil
87
94
  end
88
- SESSION_MUTEX.synchronize do
95
+ @session_mutex.synchronize do
89
96
  @session.close if @session
90
97
  @session = nil
91
98
  end
92
- CLUSTER_MUTEX.synchronize do
99
+ @cluster_mutex.synchronize do
93
100
  @cluster.close if @cluster
94
101
  @cluster = nil
95
102
  end
@@ -122,7 +129,7 @@ module CassandraModel
122
129
  end
123
130
 
124
131
  def reactor(name, type)
125
- safe_getset_variable(REACTOR_MUTEX, name) do
132
+ safe_getset_variable(@reactor_mutex, name) do
126
133
  BatchReactor.new(cluster, session, type, config[:batch_reactor] || {}).tap do |reactor|
127
134
  reactor.start.get
128
135
  end
@@ -3,6 +3,7 @@ require_relative 'meta_columns'
3
3
 
4
4
  module CassandraModel
5
5
  class Record
6
+ extend Scopes
6
7
  extend QueryHelper
7
8
  include MetaColumns
8
9
  include DisplayableAttributes
@@ -34,6 +35,7 @@ module CassandraModel
34
35
  :connection_name,
35
36
 
36
37
  :write_consistency,
38
+ :serial_consistency,
37
39
  :read_consistency,
38
40
 
39
41
  :before_save_callbacks,
@@ -109,7 +111,9 @@ module CassandraModel
109
111
  alias :to_s :inspect
110
112
 
111
113
  def ==(rhs)
112
- rhs.respond_to?(:attributes) && @attributes == rhs.attributes
114
+ rhs.respond_to?(:attributes) && columns.all? do |column|
115
+ attributes[column] == rhs.attributes[column]
116
+ end
113
117
  end
114
118
 
115
119
  private
@@ -228,6 +232,7 @@ module CassandraModel
228
232
  def write_query_options(options = {})
229
233
  {}.tap do |new_option|
230
234
  new_option[:consistency] = write_consistency if write_consistency
235
+ new_option[:serial_consistency] = serial_consistency if serial_consistency
231
236
  new_option[:trace] = true if options[:trace]
232
237
  end
233
238
  end
@@ -236,6 +241,10 @@ module CassandraModel
236
241
  self.class.write_consistency
237
242
  end
238
243
 
244
+ def serial_consistency
245
+ self.class.serial_consistency
246
+ end
247
+
239
248
  def column_values
240
249
  attributes = internal_attributes
241
250
  internal_columns.map { |column| attributes[column] }
@@ -355,7 +364,7 @@ module CassandraModel
355
364
  def_delegator :table, :primary_key, :internal_primary_key
356
365
  def_delegator :table, :name, :table_name
357
366
  def_delegator :table, :columns, :internal_columns
358
- def_delegators :table_config, :write_consistency, :read_consistency, :write_consistency=, :read_consistency=
367
+ def_delegators :table_config, :write_consistency, :serial_consistency, :read_consistency, :write_consistency=, :serial_consistency=, :read_consistency=
359
368
 
360
369
  alias :partition_key :internal_partition_key
361
370
  alias :clustering_columns :internal_clustering_columns
@@ -394,12 +403,21 @@ module CassandraModel
394
403
  end
395
404
  end
396
405
 
406
+ def denormalized_column_map(input_columns)
407
+ (columns & input_columns).inject({}) { |memo, column| memo.merge!(column => column) }
408
+ end
409
+
410
+ def composite_defaults
411
+ []
412
+ end
413
+
397
414
  def query_for_save(options = {})
398
415
  existence_clause = options[:check_exists] && ' IF NOT EXISTS'
416
+ ttl_clause = options[:ttl] && " USING TTL #{options[:ttl]}"
399
417
  column_names = internal_columns.join(', ')
400
418
  column_sanitizers = (%w(?) * internal_columns.size).join(', ')
401
419
  save_query = "INSERT INTO #{table_name} (#{column_names}) VALUES (#{column_sanitizers})"
402
- "#{save_query}#{existence_clause}"
420
+ "#{save_query}#{existence_clause}#{ttl_clause}"
403
421
  end
404
422
 
405
423
  def query_for_delete
@@ -0,0 +1,29 @@
1
+ module CassandraModel
2
+ class ResultChunker
3
+ include Enumerable
4
+
5
+ def initialize(enum, cluster)
6
+ @enum = enum
7
+ @cluster = cluster
8
+ end
9
+
10
+ def each(&block)
11
+ enum.chunk do |value|
12
+ value.attributes.values_at(*cluster)
13
+ end.each(&block)
14
+ end
15
+
16
+ alias :get :to_a
17
+
18
+ def ==(rhs)
19
+ rhs.is_a?(ResultChunker) &&
20
+ enum == rhs.enum &&
21
+ cluster == rhs.cluster
22
+ end
23
+
24
+ protected
25
+
26
+ attr_reader :enum, :cluster
27
+
28
+ end
29
+ end
@@ -0,0 +1,31 @@
1
+ module CassandraModel
2
+ class ResultLimiter
3
+ include Enumerable
4
+
5
+ def initialize(enum, limit)
6
+ @enum = enum
7
+ @limit = limit
8
+ end
9
+
10
+ def each
11
+ return to_enum(:each) unless block_given?
12
+
13
+ @enum.each.with_index do |value, index|
14
+ break if index >= @limit
15
+ yield value
16
+ end
17
+ end
18
+
19
+ alias :get :to_a
20
+
21
+ def ==(rhs)
22
+ rhs.is_a?(ResultLimiter) &&
23
+ enum == rhs.enum &&
24
+ limit == rhs.limit
25
+ end
26
+
27
+ protected
28
+
29
+ attr_reader :enum, :limit
30
+ end
31
+ end
@@ -13,25 +13,44 @@ module CassandraModel
13
13
  each_slice { |slice| slice.each(&block) }
14
14
  end
15
15
 
16
- def each_slice
16
+ def each_slice(&block)
17
17
  return to_enum(:each_slice) unless block_given?
18
18
 
19
19
  current_page = @page
20
- loop do
21
- page_results = current_page.get
22
- modified_results = page_results.map { |result| @callback.call(result, page_results.execution_info) }
23
- break if page_results.empty?
24
- if page_results.last_page?
25
- yield modified_results
26
- break
27
- else
28
- current_page = page_results.next_page_async
29
- yield modified_results
30
- end
20
+ while current_page
21
+ current_page = iterate_page(current_page, &block)
31
22
  end
32
23
  end
33
24
 
34
25
  alias :get :to_a
35
26
 
27
+ private
28
+
29
+ def iterate_page(current_page, &block)
30
+ page_results = current_page.get
31
+ unless page_results.empty?
32
+ next_page(page_results, &block)
33
+ end
34
+ end
35
+
36
+ def next_page(page_results, &block)
37
+ if page_results.last_page?
38
+ modify_and_yield_page_results(page_results, &block)
39
+ nil
40
+ else
41
+ current_page = page_results.next_page_async
42
+ modify_and_yield_page_results(page_results, &block)
43
+ current_page
44
+ end
45
+ end
46
+
47
+ def modify_and_yield_page_results(page_results)
48
+ yield modified_page_results(page_results)
49
+ end
50
+
51
+ def modified_page_results(page_results)
52
+ page_results.map { |result| @callback[result, page_results.execution_info] }
53
+ end
54
+
36
55
  end
37
56
  end
@@ -0,0 +1,49 @@
1
+ module CassandraModel
2
+ class ResultReducer
3
+ include Enumerable
4
+
5
+ def initialize(enum, filter_keys)
6
+ @enum = enum
7
+ @filter_keys = filter_keys
8
+ end
9
+
10
+ def each(&block)
11
+ return to_enum(:each) unless block_given?
12
+
13
+ @enum.each do |_, rows|
14
+ if @filter_keys.one?
15
+ yield rows.first
16
+ elsif @filter_keys.any?
17
+ filter_results(rows, &block)
18
+ else
19
+ rows.each(&block)
20
+ end
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def filter_results(rows)
27
+ prev_filter = []
28
+
29
+ rows.each.with_index do |row, index|
30
+ break if index >= @filter_keys.length
31
+ next_filter = row_filter(row, index)
32
+ break unless next_filter == prev_filter
33
+ prev_filter = next_filter << row_filter_key(index, row)
34
+ yield row
35
+ end
36
+ end
37
+
38
+ def row_filter(row, filter_length)
39
+ filter_length.times.map do |index|
40
+ row_filter_key(index, row)
41
+ end
42
+ end
43
+
44
+ def row_filter_key(index, row)
45
+ row.attributes[@filter_keys[index]]
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,14 @@
1
+ module CassandraModel
2
+ module Scopes
3
+ attr_reader :scopes
4
+
5
+ def scope(name, callback)
6
+ define_singleton_method(name, &callback)
7
+ scopes[name] = callback
8
+ end
9
+
10
+ def scopes
11
+ @scopes ||= {}
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,20 @@
1
+ # only install this hack if our driver is incompatible with the old interface
2
+ if Gem::Version.new(Gem.loaded_specs['cassandra-driver'].version) >= Gem::Version.new('2')
3
+ module Cassandra
4
+ class Session
5
+
6
+ alias :__execute_async :execute_async
7
+
8
+ def execute_async(statement, *args)
9
+ options = args.last.is_a?(Hash) ? args.pop : {}
10
+ options = options.merge(arguments: args) unless options[:arguments]
11
+ __execute_async(statement, options)
12
+ end
13
+
14
+ def execute(statement, *args)
15
+ execute_async(statement, *args).get
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -2,28 +2,72 @@ module CassandraModel
2
2
  class TableDefinition
3
3
  attr_reader :name
4
4
 
5
- def self.from_data_model(table_name, inquirer, data_set)
6
- partition_key = inquirer_partition_key(inquirer)
7
- if inquirer.shard_column
8
- if inquirer.shard_column.is_a?(Hash)
9
- column_name, type = inquirer.shard_column.first
10
- partition_key.merge!(:"rk_#{column_name}" => type)
5
+ class << self
6
+
7
+ def from_data_model(table_name, inquirer, data_set, properties)
8
+ partition_key = inquirer_partition_key(inquirer)
9
+ if inquirer.shard_column
10
+ if inquirer.shard_column.is_a?(Hash)
11
+ column_name, type = inquirer.shard_column.first
12
+ partition_key.merge!(:"rk_#{column_name}" => type)
13
+ else
14
+ partition_key.merge!(:"rk_#{inquirer.shard_column}" => :int)
15
+ end
16
+ end
17
+ clustering_columns = table_set_clustering_columns(data_set)
18
+ remaining_columns = table_set_remaining_columns(data_set)
19
+
20
+ updated_properties = table_properties_from_data_set(data_set, properties)
21
+
22
+ new(name: table_name, partition_key: partition_key,
23
+ clustering_columns: clustering_columns,
24
+ remaining_columns: remaining_columns,
25
+ properties: updated_properties)
26
+ end
27
+
28
+ private
29
+
30
+ def table_properties_from_data_set(data_set, properties)
31
+ if data_set.clustering_order.present?
32
+ updated_clustering_order = data_set.clustering_order.inject({}) do |memo, (column, order)|
33
+ memo.merge!(:"ck_#{column}" => order)
34
+ end
35
+ properties.merge(clustering_order: updated_clustering_order)
11
36
  else
12
- partition_key.merge!(:"rk_#{inquirer.shard_column}" => :int)
37
+ properties
38
+ end
39
+ end
40
+
41
+ def table_set_remaining_columns(data_set)
42
+ data_set.columns.except(*data_set.clustering_columns)
43
+ end
44
+
45
+ def table_set_clustering_columns(data_set)
46
+ data_set.clustering_columns.inject({}) do |memo, column|
47
+ memo.merge!(:"ck_#{column}" => data_set.columns[column])
48
+ end
49
+ end
50
+
51
+ def inquirer_partition_key(inquirer)
52
+ inquirer.partition_key.inject({}) do |memo, (key, value)|
53
+ memo.merge!(:"rk_#{key}" => value)
13
54
  end
14
55
  end
15
- clustering_columns = table_set_clustering_columns(data_set)
16
- remaining_columns = table_set_remaining_columns(data_set)
17
- new(name: table_name, partition_key: partition_key,
18
- clustering_columns: clustering_columns,
19
- remaining_columns: remaining_columns)
56
+
20
57
  end
21
58
 
59
+ attr_reader :table_id, :name_in_cassandra
60
+
22
61
  def initialize(options)
23
62
  @partition_key = options[:partition_key].keys
24
63
  @clustering_columns = options[:clustering_columns].keys
25
64
  @name = options[:name]
26
65
  @columns = options[:partition_key].merge(options[:clustering_columns].merge(options[:remaining_columns]))
66
+ @table_id = generate_table_id
67
+ @name_in_cassandra = "#{name}_#{table_id}"
68
+ @properties = (options[:properties] || {}).select do |name, _|
69
+ [:compaction, :clustering_order].include?(name)
70
+ end
27
71
  end
28
72
 
29
73
  def to_cql(options = {})
@@ -31,15 +75,18 @@ module CassandraModel
31
75
  exists = if options[:check_exists]
32
76
  'IF NOT EXISTS '
33
77
  end
34
- "CREATE TABLE #{exists}#{table_name} (#{columns}, PRIMARY KEY #{primary_key})"
35
- end
36
-
37
- def table_id
38
- Digest::MD5.hexdigest(columns)
39
- end
40
-
41
- def name_in_cassandra
42
- "#{name}_#{table_id}"
78
+ properties = if @properties.present?
79
+ property_values = @properties.map do |property, definition|
80
+ case property
81
+ when :compaction
82
+ "COMPACTION = #{to_property_string(definition)}"
83
+ when :clustering_order
84
+ "CLUSTERING ORDER BY #{to_clustering_order_string(definition)}"
85
+ end
86
+ end * ' AND '
87
+ " WITH #{property_values}"
88
+ end
89
+ "CREATE TABLE #{exists}#{table_name} (#{columns}, PRIMARY KEY #{primary_key})#{properties}"
43
90
  end
44
91
 
45
92
  def ==(rhs)
@@ -48,20 +95,16 @@ module CassandraModel
48
95
 
49
96
  private
50
97
 
51
- def self.table_set_remaining_columns(data_set)
52
- data_set.columns.except(*data_set.clustering_columns)
98
+ def to_property_string(property)
99
+ "{#{property.map { |key, value| "'#{key}': '#{value}'" } * ', '}}"
53
100
  end
54
101
 
55
- def self.table_set_clustering_columns(data_set)
56
- data_set.clustering_columns.inject({}) do |memo, column|
57
- memo.merge!(:"ck_#{column}" => data_set.columns[column])
58
- end
102
+ def to_clustering_order_string(clustering_order)
103
+ "(#{clustering_order.map { |column, order| "#{column} #{order.upcase}" } * ', '})"
59
104
  end
60
105
 
61
- def self.inquirer_partition_key(inquirer)
62
- inquirer.partition_key.inject({}) do |memo, (key, value)|
63
- memo.merge!(:"rk_#{key}" => value)
64
- end
106
+ def generate_table_id
107
+ Digest::MD5.hexdigest(columns)
65
108
  end
66
109
 
67
110
  def columns
@@ -1,6 +1,8 @@
1
1
  module CassandraModel
2
2
  class TableDescriptor < Record
3
3
 
4
+ self.serial_consistency = :serial
5
+
4
6
  class << self
5
7
  def create_async(table_definition)
6
8
  super(table_descriptor(table_definition), check_exists: true)
@@ -46,4 +48,4 @@ module CassandraModel
46
48
  end
47
49
 
48
50
  end
49
- end
51
+ end
metadata CHANGED
@@ -1,29 +1,35 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cassandra_model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.21
4
+ version: 1.0.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas RM Rogers
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-01-14 00:00:00.000000000 Z
11
+ date: 2016-05-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cassandra-driver
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ~>
17
+ - - '>='
18
18
  - !ruby/object:Gem::Version
19
19
  version: '1.1'
20
+ - - <=
21
+ - !ruby/object:Gem::Version
22
+ version: 2.0.1
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
23
26
  requirements:
24
- - - ~>
27
+ - - '>='
25
28
  - !ruby/object:Gem::Version
26
29
  version: '1.1'
30
+ - - <=
31
+ - !ruby/object:Gem::Version
32
+ version: 2.0.1
27
33
  - !ruby/object:Gem::Dependency
28
34
  name: activesupport
29
35
  requirement: !ruby/object:Gem::Requirement
@@ -54,18 +60,38 @@ dependencies:
54
60
  version: 0.0.1
55
61
  - !ruby/object:Gem::Dependency
56
62
  name: thomas_utils
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - '>='
66
+ - !ruby/object:Gem::Version
67
+ version: 0.1.19
68
+ - - <=
69
+ - !ruby/object:Gem::Version
70
+ version: '0.3'
71
+ type: :runtime
72
+ prerelease: false
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - '>='
76
+ - !ruby/object:Gem::Version
77
+ version: 0.1.19
78
+ - - <=
79
+ - !ruby/object:Gem::Version
80
+ version: '0.3'
81
+ - !ruby/object:Gem::Dependency
82
+ name: concurrent-ruby
57
83
  requirement: !ruby/object:Gem::Requirement
58
84
  requirements:
59
85
  - - ~>
60
86
  - !ruby/object:Gem::Version
61
- version: 0.1.13
87
+ version: 1.0.0
62
88
  type: :runtime
63
89
  prerelease: false
64
90
  version_requirements: !ruby/object:Gem::Requirement
65
91
  requirements:
66
92
  - - ~>
67
93
  - !ruby/object:Gem::Version
68
- version: 0.1.13
94
+ version: 1.0.0
69
95
  description: |-
70
96
  Cassandra data modelling framework for Ruby that makes
71
97
  data modelling for Cassandra tables easy, fast, and stable
@@ -99,8 +125,13 @@ files:
99
125
  - lib/cassandra_model/raw_connection.rb
100
126
  - lib/cassandra_model/record.rb
101
127
  - lib/cassandra_model/record_debug.rb
128
+ - lib/cassandra_model/result_chunker.rb
129
+ - lib/cassandra_model/result_limiter.rb
102
130
  - lib/cassandra_model/result_paginator.rb
131
+ - lib/cassandra_model/result_reducer.rb
103
132
  - lib/cassandra_model/rotating_table.rb
133
+ - lib/cassandra_model/scopes.rb
134
+ - lib/cassandra_model/session_compatibility.rb
104
135
  - lib/cassandra_model/single_token_batch.rb
105
136
  - lib/cassandra_model/single_token_counter_batch.rb
106
137
  - lib/cassandra_model/single_token_logged_batch.rb