extendi-cassandra_object 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.travis.yml +23 -0
  4. data/CHANGELOG +0 -0
  5. data/Gemfile +17 -0
  6. data/LICENSE +13 -0
  7. data/MIT-LICENSE +20 -0
  8. data/README.md +177 -0
  9. data/Rakefile +12 -0
  10. data/extendi-cassandra_object.gemspec +26 -0
  11. data/lib/cassandra_object.rb +73 -0
  12. data/lib/cassandra_object/adapters/abstract_adapter.rb +61 -0
  13. data/lib/cassandra_object/adapters/cassandra_adapter.rb +269 -0
  14. data/lib/cassandra_object/adapters/cassandra_schemaless_adapter.rb +306 -0
  15. data/lib/cassandra_object/attribute_methods.rb +96 -0
  16. data/lib/cassandra_object/attribute_methods/definition.rb +22 -0
  17. data/lib/cassandra_object/attribute_methods/dirty.rb +36 -0
  18. data/lib/cassandra_object/attribute_methods/primary_key.rb +25 -0
  19. data/lib/cassandra_object/attribute_methods/typecasting.rb +59 -0
  20. data/lib/cassandra_object/base.rb +33 -0
  21. data/lib/cassandra_object/base_schema.rb +11 -0
  22. data/lib/cassandra_object/base_schemaless.rb +11 -0
  23. data/lib/cassandra_object/base_schemaless_dynamic.rb +11 -0
  24. data/lib/cassandra_object/belongs_to.rb +63 -0
  25. data/lib/cassandra_object/belongs_to/association.rb +49 -0
  26. data/lib/cassandra_object/belongs_to/builder.rb +40 -0
  27. data/lib/cassandra_object/belongs_to/reflection.rb +30 -0
  28. data/lib/cassandra_object/callbacks.rb +29 -0
  29. data/lib/cassandra_object/core.rb +63 -0
  30. data/lib/cassandra_object/errors.rb +10 -0
  31. data/lib/cassandra_object/identity.rb +26 -0
  32. data/lib/cassandra_object/inspect.rb +25 -0
  33. data/lib/cassandra_object/log_subscriber.rb +44 -0
  34. data/lib/cassandra_object/model.rb +60 -0
  35. data/lib/cassandra_object/persistence.rb +203 -0
  36. data/lib/cassandra_object/railtie.rb +33 -0
  37. data/lib/cassandra_object/railties/controller_runtime.rb +45 -0
  38. data/lib/cassandra_object/schema.rb +83 -0
  39. data/lib/cassandra_object/schemaless.rb +83 -0
  40. data/lib/cassandra_object/scope.rb +86 -0
  41. data/lib/cassandra_object/scope/finder_methods.rb +54 -0
  42. data/lib/cassandra_object/scope/query_methods.rb +69 -0
  43. data/lib/cassandra_object/scoping.rb +27 -0
  44. data/lib/cassandra_object/serialization.rb +6 -0
  45. data/lib/cassandra_object/tasks/ks.rake +54 -0
  46. data/lib/cassandra_object/timestamps.rb +19 -0
  47. data/lib/cassandra_object/type.rb +16 -0
  48. data/lib/cassandra_object/types.rb +8 -0
  49. data/lib/cassandra_object/types/array_type.rb +16 -0
  50. data/lib/cassandra_object/types/base_type.rb +26 -0
  51. data/lib/cassandra_object/types/boolean_type.rb +20 -0
  52. data/lib/cassandra_object/types/date_type.rb +22 -0
  53. data/lib/cassandra_object/types/float_type.rb +16 -0
  54. data/lib/cassandra_object/types/integer_type.rb +20 -0
  55. data/lib/cassandra_object/types/json_type.rb +13 -0
  56. data/lib/cassandra_object/types/string_type.rb +19 -0
  57. data/lib/cassandra_object/types/time_type.rb +16 -0
  58. data/lib/cassandra_object/types/type_helper.rb +39 -0
  59. data/lib/cassandra_object/validations.rb +44 -0
  60. data/test/support/cassandra.rb +63 -0
  61. data/test/support/issue.rb +12 -0
  62. data/test/support/issue_dynamic.rb +12 -0
  63. data/test/support/issue_schema.rb +17 -0
  64. data/test/support/issue_schema_child.rb +17 -0
  65. data/test/support/issue_schema_father.rb +13 -0
  66. data/test/test_helper.rb +41 -0
  67. data/test/unit/active_model_test.rb +18 -0
  68. data/test/unit/adapters/adapter_test.rb +6 -0
  69. data/test/unit/attribute_methods/definition_test.rb +13 -0
  70. data/test/unit/attribute_methods/dirty_test.rb +72 -0
  71. data/test/unit/attribute_methods/primary_key_test.rb +26 -0
  72. data/test/unit/attribute_methods/typecasting_test.rb +119 -0
  73. data/test/unit/attribute_methods_test.rb +51 -0
  74. data/test/unit/base_test.rb +20 -0
  75. data/test/unit/belongs_to/reflection_test.rb +12 -0
  76. data/test/unit/belongs_to_test.rb +63 -0
  77. data/test/unit/callbacks_test.rb +46 -0
  78. data/test/unit/connection_test.rb +6 -0
  79. data/test/unit/connections/connections_test.rb +55 -0
  80. data/test/unit/core_test.rb +55 -0
  81. data/test/unit/identity_test.rb +26 -0
  82. data/test/unit/inspect_test.rb +26 -0
  83. data/test/unit/log_subscriber_test.rb +25 -0
  84. data/test/unit/persistence_schema_test.rb +156 -0
  85. data/test/unit/persistence_test.rb +266 -0
  86. data/test/unit/railties/controller_runtime_test.rb +48 -0
  87. data/test/unit/schema/tasks_test.rb +32 -0
  88. data/test/unit/schema_test.rb +115 -0
  89. data/test/unit/schemaless_test.rb +100 -0
  90. data/test/unit/scope/finder_methods_test.rb +117 -0
  91. data/test/unit/scope/query_methods_test.rb +32 -0
  92. data/test/unit/scoping_test.rb +7 -0
  93. data/test/unit/timestamps_test.rb +27 -0
  94. data/test/unit/types/array_type_test.rb +17 -0
  95. data/test/unit/types/base_type_test.rb +19 -0
  96. data/test/unit/types/boolean_type_test.rb +24 -0
  97. data/test/unit/types/date_type_test.rb +15 -0
  98. data/test/unit/types/float_type_test.rb +17 -0
  99. data/test/unit/types/integer_type_test.rb +19 -0
  100. data/test/unit/types/json_type_test.rb +23 -0
  101. data/test/unit/types/string_type_test.rb +25 -0
  102. data/test/unit/types/time_type_test.rb +14 -0
  103. data/test/unit/validations_test.rb +27 -0
  104. metadata +202 -0
@@ -0,0 +1,27 @@
1
+ module CassandraObject
2
+ module Scoping
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ singleton_class.class_eval do
7
+ delegate :find, :find_by_id, :find_all_in_batches, :first, to: :scope
8
+ delegate :select, :where, :where_ids, to: :scope
9
+ delegate :cql_response, :columns, :limit, :per_page, to: :scope
10
+ end
11
+ end
12
+
13
+ module ClassMethods
14
+ def scope
15
+ self.current_scope ||= Scope.new(self)
16
+ end
17
+
18
+ def current_scope
19
+ Thread.current["#{self}_current_scope"]
20
+ end
21
+
22
+ def current_scope=(new_scope)
23
+ Thread.current["#{self}_current_scope"] = new_scope
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,6 @@
1
+ module CassandraObject
2
+ module Serialization
3
+ extend ActiveSupport::Concern
4
+ include ActiveModel::Serializers::JSON
5
+ end
6
+ end
@@ -0,0 +1,54 @@
1
+ ks_namespace = namespace :ks do
2
+ desc 'Create the keyspace in config/cassandra.yml for the current environment'
3
+ task create: :environment do
4
+ begin
5
+ CassandraObject::Schema.create_keyspace CassandraObject::Base.config[:keyspace], CassandraObject::Base.config[:keyspace_options]
6
+ rescue Exception => e
7
+ if e.message =~ /conflicts/
8
+ p "Keyspace #{CassandraObject::Base.config[:keyspace]} already exists"
9
+ else
10
+ raise e
11
+ end
12
+ end
13
+ end
14
+
15
+ task drop: :environment do
16
+ begin
17
+ CassandraObject::Schema.drop_keyspace CassandraObject::Base.config[:keyspace]
18
+ rescue Exception => e
19
+ if e.message =~ /non existing keyspace/
20
+ p "Keyspace #{CassandraObject::Base.config[:keyspace]} does not exist"
21
+ else
22
+ raise e
23
+ end
24
+ end
25
+ end
26
+
27
+ task reset: [:drop, :setup]
28
+
29
+ task setup: [:create, :_load]
30
+
31
+ namespace :structure do
32
+ task dump: :environment do
33
+ filename = ENV['SCHEMA'] || "#{Rails.root}/ks/structure.cql"
34
+ File.open(filename, "w:utf-8") do |file|
35
+ CassandraObject::Schema.dump(file)
36
+ end
37
+ end
38
+
39
+ task load: :environment do
40
+ filename = ENV['SCHEMA'] || "#{Rails.root}/ks/structure.cql"
41
+ File.open(filename) do |file|
42
+ CassandraObject::Schema.load(file)
43
+ end
44
+ end
45
+ end
46
+
47
+ task :_dump do
48
+ ks_namespace["structure:dump"].invoke
49
+ end
50
+
51
+ task :_load do
52
+ ks_namespace["structure:load"].invoke
53
+ end
54
+ end
@@ -0,0 +1,19 @@
1
+ module CassandraObject
2
+ module Timestamps
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ attribute :created_at, type: :time
7
+ attribute :updated_at, type: :time
8
+
9
+ before_create do
10
+ self.created_at ||= Time.current
11
+ self.updated_at ||= Time.current
12
+ end
13
+
14
+ before_update if: :changed? do
15
+ self.updated_at = Time.current
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ module CassandraObject
2
+ class Type
3
+ cattr_accessor :attribute_types
4
+ self.attribute_types = {}.with_indifferent_access
5
+
6
+ class << self
7
+ def register(name, coder)
8
+ attribute_types[name] = coder
9
+ end
10
+
11
+ def get_coder(name)
12
+ attribute_types[name]
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,8 @@
1
+ CassandraObject::Type.register(:array, CassandraObject::Types::ArrayType)
2
+ CassandraObject::Type.register(:boolean, CassandraObject::Types::BooleanType)
3
+ CassandraObject::Type.register(:date, CassandraObject::Types::DateType)
4
+ CassandraObject::Type.register(:float, CassandraObject::Types::FloatType)
5
+ CassandraObject::Type.register(:integer, CassandraObject::Types::IntegerType)
6
+ CassandraObject::Type.register(:json, CassandraObject::Types::JsonType)
7
+ CassandraObject::Type.register(:time, CassandraObject::Types::TimeType)
8
+ CassandraObject::Type.register(:string, CassandraObject::Types::StringType)
@@ -0,0 +1,16 @@
1
+ module CassandraObject
2
+ module Types
3
+ class ArrayType < BaseType
4
+ def encode(array)
5
+ raise ArgumentError.new("#{array.inspect} is not an Array") unless array.kind_of?(Array)
6
+ array.to_a.to_json
7
+ end
8
+
9
+ def decode(str)
10
+ return nil if str.blank?
11
+
12
+ ActiveSupport::JSON.decode(str)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,26 @@
1
+ module CassandraObject
2
+ module Types
3
+ class BaseType
4
+ attr_accessor :options
5
+ def initialize(options = {})
6
+ @options = options
7
+ end
8
+
9
+ def default
10
+ options[:default].duplicable? ? options[:default].dup : options[:default]
11
+ end
12
+
13
+ def encode(value)
14
+ value.to_s
15
+ end
16
+
17
+ def decode(str)
18
+ str
19
+ end
20
+
21
+ def typecast(value)
22
+ value
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,20 @@
1
+ module CassandraObject
2
+ module Types
3
+ class BooleanType < BaseType
4
+ TRUE_VALS = [true, 'true', '1']
5
+ FALSE_VALS = [false, 'false', '0', '', nil]
6
+ VALID_VALS = TRUE_VALS + FALSE_VALS
7
+
8
+ def encode(bool)
9
+ unless VALID_VALS.include?(bool)
10
+ raise ArgumentError.new("#{bool.inspect} is not a Boolean")
11
+ end
12
+ TRUE_VALS.include?(bool) ? '1' : '0'
13
+ end
14
+
15
+ def decode(str)
16
+ TRUE_VALS.include?(str)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,22 @@
1
+ module CassandraObject
2
+ module Types
3
+ class DateType < BaseType
4
+ FORMAT = '%Y-%m-%d'
5
+ REGEX = /\A\d{4}-\d{2}-\d{2}\Z/
6
+
7
+ def encode(value)
8
+ raise ArgumentError.new("#{value.inspect} is not a Date") unless value.kind_of?(Date)
9
+ value.strftime(FORMAT)
10
+ end
11
+
12
+ def decode(str)
13
+ return nil if str.empty?
14
+ Date.parse(str)
15
+ end
16
+
17
+ def typecast(value)
18
+ value.to_date
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,16 @@
1
+ module CassandraObject
2
+ module Types
3
+ class FloatType < BaseType
4
+ REGEX = /\A[-+]?\d+(\.\d+)?\Z/
5
+ def encode(float)
6
+ raise ArgumentError.new("#{float.inspect} is not a Float") unless float.kind_of?(Float)
7
+ float.to_s
8
+ end
9
+
10
+ def decode(str)
11
+ return nil if str.empty?
12
+ str.to_f
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,20 @@
1
+ module CassandraObject
2
+ module Types
3
+ class IntegerType < BaseType
4
+ REGEX = /\A[-+]?\d+\Z/
5
+ def encode(int)
6
+ raise ArgumentError.new("#{int.inspect} is not an Integer.") unless int.kind_of?(Integer)
7
+ int.to_s
8
+ end
9
+
10
+ def decode(str)
11
+ return nil if str.empty?
12
+ str.to_i
13
+ end
14
+
15
+ def typecast(value)
16
+ value.to_i
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ module CassandraObject
2
+ module Types
3
+ class JsonType < BaseType
4
+ def encode(hash)
5
+ ActiveSupport::JSON.encode(hash)
6
+ end
7
+
8
+ def decode(str)
9
+ ActiveSupport::JSON.decode(str)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ module CassandraObject
2
+ module Types
3
+ class StringType < BaseType
4
+ def encode(str)
5
+ raise ArgumentError.new("#{str.inspect} is not a String") unless str.kind_of?(String)
6
+
7
+ unless str.encoding == Encoding::UTF_8
8
+ (str.frozen? ? str.dup : str).force_encoding('UTF-8')
9
+ else
10
+ str
11
+ end
12
+ end
13
+
14
+ def typecast(value)
15
+ value.to_s
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ module CassandraObject
2
+ module Types
3
+ class TimeType < BaseType
4
+ def encode(time)
5
+ raise ArgumentError.new("#{time.inspect} is not a Time") unless time.kind_of?(Time)
6
+ time.utc.xmlschema(6)
7
+ end
8
+
9
+ def decode(str)
10
+ Time.parse(str).utc.in_time_zone if str
11
+ rescue
12
+
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,39 @@
1
+ module CassandraObject
2
+ module Types
3
+ class TypeHelper
4
+
5
+ def self.guess_type(object)
6
+ case object
7
+ when ::String then Cassandra::Types.varchar
8
+ when ::Fixnum then Cassandra::Types.int
9
+ when ::Integer then Cassandra::Types.int
10
+ when ::Float then Cassandra::Types.float
11
+ when ::Bignum then Cassandra::Types.varint
12
+ when ::BigDecimal then Cassandra::Types.decimal
13
+ when ::TrueClass then Cassandra::Types.boolean
14
+ when ::FalseClass then Cassandra::Types.boolean
15
+ when ::NilClass then Cassandra::Types.bigint
16
+ # when Uuid then Cassandra::Types.uuid
17
+ # when TimeUuid then Cassandra::Types.timeuuid
18
+ when ::IPAddr then Cassandra::Types.inet
19
+ when ::Time then Cassandra::Types.timestamp
20
+ when ::Hash
21
+ pair = object.first
22
+ Types.map(guess_type(pair[0]), guess_type(pair[1]))
23
+ when ::Array then Types.list(guess_type(object.first))
24
+ when ::Set then Types.set(guess_type(object.first))
25
+ # when Tuple::Strict then Types.tuple(*object.types)
26
+ # when Tuple then Types.tuple(*object.map {|v| guess_type(v)})
27
+ # when UDT::Strict
28
+ # Types.udt(object.keyspace, object.name, object.types)
29
+ # when UDT
30
+ # Types.udt('unknown', 'unknown', object.map {|k, v| [k, guess_type(v)]})
31
+ when Cassandra::CustomData then object.class.type
32
+ else
33
+ raise ::ArgumentError, "Unable to guess the type of the argument: #{object.inspect}"
34
+ end
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,44 @@
1
+ module CassandraObject
2
+ class RecordInvalid < StandardError
3
+ attr_reader :record
4
+ def initialize(record)
5
+ @record = record
6
+ super("Invalid record: #{@record.errors.full_messages.to_sentence}")
7
+ end
8
+ end
9
+
10
+ module Validations
11
+ extend ActiveSupport::Concern
12
+ include ActiveModel::Validations
13
+
14
+ included do
15
+ define_callbacks :validate, scope: :name
16
+ end
17
+
18
+ module ClassMethods
19
+ def create!(attributes = {})
20
+ new(attributes).tap do |object|
21
+ object.save!
22
+ end
23
+ end
24
+ end
25
+
26
+ def save(options={})
27
+ if perform_validations(options)
28
+ super
29
+ true
30
+ else
31
+ false
32
+ end
33
+ end
34
+
35
+ def save!
36
+ save || raise(RecordInvalid.new(self))
37
+ end
38
+
39
+ protected
40
+ def perform_validations(options={})
41
+ (options[:validate] != false) ? valid? : true
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,63 @@
1
+ Bundler.require :cassandra
2
+
3
+ CassandraObject::Base.config = {
4
+ keyspace: 'cassandra_object_test',
5
+ hosts: ['127.0.0.1'],
6
+ compression: :lz4,
7
+ connect_timeout: 1,
8
+ timeout: 30,
9
+ consistency: :quorum,
10
+ protocol_version: 3,
11
+ page_size: 10000,
12
+ trace: true,
13
+ connections_per_local_node: 4,
14
+ schema_refresh_delay: 0.1,
15
+ schema_refresh_timeout: 0.1,
16
+ # connections_per_remote_node: nil,
17
+ # logger: Logger.new($stderr)
18
+ }
19
+
20
+ begin
21
+ CassandraObject::Schema.drop_keyspace 'cassandra_object_test', true
22
+ CassandraObject::Schema.drop_keyspace 'blah', true
23
+ rescue Exception => e
24
+ puts e.message
25
+ end
26
+
27
+ sleep 1
28
+ CassandraObject::Schema.create_keyspace 'cassandra_object_test'
29
+ CassandraObject::Schemaless.create_column_family 'Issues'
30
+ CassandraObject::Schema.create_column_family 'IssueSchemas', {attributes: 'id text, title text, description text, field float, intero int, created_at timestamp, updated_at timestamp, PRIMARY KEY (id)', options: {}}
31
+ CassandraObject::Schemaless.create_column_family 'IssueDynamics'
32
+ CassandraObject::Schema.create_column_family 'IssueSchemaFathers', {attributes: 'id text, title text, field float, created_at timestamp, updated_at timestamp, PRIMARY KEY (id)', options: {}}
33
+ CassandraObject::Schema.create_column_family 'IssueSchemaChildren', {attributes: 'id text, title text, description text, field float, created_at timestamp, updated_at timestamp, issue_schema_father_id text, PRIMARY KEY (id)', options: {}}
34
+ CassandraObject::BaseSchemaless.adapter.consistency = :quorum
35
+ CassandraObject::BaseSchemalessDynamic.adapter.consistency = :quorum
36
+ CassandraObject::BaseSchema.adapter.consistency = :quorum
37
+
38
+ CassandraObject::Base.class_eval do
39
+ class_attribute :created_records
40
+ self.created_records = []
41
+
42
+ after_create do
43
+ created_records << self
44
+ end
45
+
46
+ def self.delete_after_test
47
+ # created_records.reject(&:destroyed?).each(&:destroy)
48
+ Issue.delete_all
49
+ IssueSchema.delete_all
50
+ IssueDynamic.delete_all
51
+ created_records.clear
52
+ end
53
+ end
54
+
55
+ module ActiveSupport
56
+ class TestCase
57
+ teardown do
58
+ if CassandraObject::Base.created_records.any?
59
+ CassandraObject::Base.delete_after_test
60
+ end
61
+ end
62
+ end
63
+ end