dm-is-schemaless 0.10.2
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.
- data/LICENSE +20 -0
- data/README.rdoc +48 -0
- data/Rakefile +67 -0
- data/TODO +0 -0
- data/VERSION +1 -0
- data/benchmarks/dynamic_methods.rb +143 -0
- data/benchmarks/operator_test.rb +22 -0
- data/dm-is-schemaless.gemspec +67 -0
- data/lib/dm-is-schemaless.rb +8 -0
- data/lib/dm-is-schemaless/is/index.rb +47 -0
- data/lib/dm-is-schemaless/is/schemaless.rb +123 -0
- data/lib/dm-is-schemaless/is/version.rb +7 -0
- data/spec/integration/schemaless_spec.rb +128 -0
- data/spec/models.rb +25 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +35 -0
- data/tasks/install.rb +13 -0
- data/tasks/spec.rb +25 -0
- metadata +95 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 John Doe
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
= dm-is-schemaless
|
2
|
+
|
3
|
+
Inspired by http://bret.appspot.com/entry/how-friendfeed-uses-mysql I wanted to build a way to do that seamless. Hence this plugin.
|
4
|
+
|
5
|
+
= Basics
|
6
|
+
|
7
|
+
It's pretty straight forward. The bare minimum to use it is:
|
8
|
+
|
9
|
+
class Message
|
10
|
+
include DataMapper::Resource
|
11
|
+
|
12
|
+
is :schemaless
|
13
|
+
|
14
|
+
# The following properties will be defined automatically
|
15
|
+
# property :added_id, DataMapper::Types::Serial, :key => false
|
16
|
+
# property :id, DataMapper::Types::UUID, :unique => true, :nullable => false, :index => true
|
17
|
+
# property :updated, DataMapper::Types::EpochTime, :key => true, :index => true
|
18
|
+
# property :body, DataMapper::Types::Json
|
19
|
+
end
|
20
|
+
|
21
|
+
Away you go! By default it creates keys and a few other fields. It adds a bit of method missing magic so any property you want automatically has name, name=, and name?. You should use these instead of accessing the body hash directly in order to keep nil indexes from being setup.
|
22
|
+
|
23
|
+
= Indexes
|
24
|
+
|
25
|
+
Declaring indexes. Just use the class level index_on method and supply a symbol. This will create the association and a table called <property>Index. It also creates an update hook to monitor the record when its save and handle creating/updating/destroying the index record.
|
26
|
+
|
27
|
+
class Message
|
28
|
+
include DataMapper::Resource
|
29
|
+
|
30
|
+
is :schemaless
|
31
|
+
|
32
|
+
index_on :email
|
33
|
+
end
|
34
|
+
|
35
|
+
= Querying
|
36
|
+
|
37
|
+
This is handled for you automatically. After you create an index on a property whenever you use that in a query it will transform the query to look it up on the index table instead. So internally here's what happens.
|
38
|
+
|
39
|
+
# original query
|
40
|
+
Message.all(:email => 'test@gmail.com')
|
41
|
+
# transformed query
|
42
|
+
Message.all('email_index.email' => 'test@gmail.com')
|
43
|
+
|
44
|
+
This will also still support all of DM's query operators.
|
45
|
+
|
46
|
+
Props to Dan Kubb for all his awesome work on DM and helping fix/refine this code.
|
47
|
+
|
48
|
+
File all bugs as issues on the project http://github.com/BrianTheCoder/dm-is-schemaless
|
data/Rakefile
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "dm-is-schemaless"
|
8
|
+
gem.summary = %Q{An implementation of friendfeed's schemaless store for rbdms'}
|
9
|
+
gem.description = %Q{A plugin that allows you to easily treat an rdbms like a schemaless store, perfect for something like heroku *wink, wink*}
|
10
|
+
gem.email = "wbsmith83@gmail.com"
|
11
|
+
gem.homepage = "http://github.com/BrianTheCoder/dm-is-schemales"
|
12
|
+
gem.authors = ["brianthecoder"]
|
13
|
+
gem.files.include %w(lib/dm-is-schemaless.rb lib/dm-is-schemaless/is/index.rb lib/dm-is-schemaless/is/schemaless.rb lib/dm-is-schemaless/is/version.rb)
|
14
|
+
gem.add_development_dependency "yard"
|
15
|
+
gem.add_dependency "dm-types"
|
16
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
17
|
+
end
|
18
|
+
Jeweler::GemcutterTasks.new
|
19
|
+
rescue LoadError
|
20
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
21
|
+
end
|
22
|
+
|
23
|
+
require 'rake/testtask'
|
24
|
+
Rake::TestTask.new(:test) do |test|
|
25
|
+
test.libs << 'lib' << 'test'
|
26
|
+
test.pattern = 'test/**/*_test.rb'
|
27
|
+
test.verbose = true
|
28
|
+
end
|
29
|
+
|
30
|
+
begin
|
31
|
+
require 'rcov/rcovtask'
|
32
|
+
Rcov::RcovTask.new do |test|
|
33
|
+
test.libs << 'test'
|
34
|
+
test.pattern = 'test/**/*_test.rb'
|
35
|
+
test.verbose = true
|
36
|
+
end
|
37
|
+
rescue LoadError
|
38
|
+
task :rcov do
|
39
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
task :test => :check_dependencies
|
44
|
+
|
45
|
+
begin
|
46
|
+
require 'reek/rake_task'
|
47
|
+
Reek::RakeTask.new do |t|
|
48
|
+
t.fail_on_error = true
|
49
|
+
t.verbose = false
|
50
|
+
t.source_files = 'lib/**/*.rb'
|
51
|
+
end
|
52
|
+
rescue LoadError
|
53
|
+
task :reek do
|
54
|
+
abort "Reek is not available. In order to run reek, you must: sudo gem install reek"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
task :default => :test
|
59
|
+
|
60
|
+
begin
|
61
|
+
require 'yard'
|
62
|
+
YARD::Rake::YardocTask.new
|
63
|
+
rescue LoadError
|
64
|
+
task :yardoc do
|
65
|
+
abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
|
66
|
+
end
|
67
|
+
end
|
data/TODO
ADDED
File without changes
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.10.2
|
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'benchmark'
|
3
|
+
require 'extlib'
|
4
|
+
require 'randexp'
|
5
|
+
require 'dm-core'
|
6
|
+
require 'dm-types'
|
7
|
+
|
8
|
+
PROPS = 10.of{ /\w+/.gen }
|
9
|
+
|
10
|
+
class SimpleDataMapper
|
11
|
+
include DataMapper::Resource
|
12
|
+
|
13
|
+
property :id, Serial
|
14
|
+
property :body, DataMapper::Types::Json, :default => {}
|
15
|
+
end
|
16
|
+
|
17
|
+
class Missing < SimpleDataMapper
|
18
|
+
def method_missing(method_symbol, *args)
|
19
|
+
method_name = method_symbol.to_s
|
20
|
+
case method_name[-1..-1]
|
21
|
+
when "="
|
22
|
+
val = args.first
|
23
|
+
prop = method_name[0..-2]
|
24
|
+
if val.blank? && body.has_key?(prop)
|
25
|
+
body.delete(prop)
|
26
|
+
else
|
27
|
+
body[prop] = args.first
|
28
|
+
end
|
29
|
+
when "?"
|
30
|
+
body[method_name[0..-2]] == true
|
31
|
+
else
|
32
|
+
# Returns nil on failure so forms will work
|
33
|
+
body[method_name]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class DefineMethod < SimpleDataMapper
|
39
|
+
private
|
40
|
+
|
41
|
+
def method_missing(method_symbol, *args)
|
42
|
+
method_name = method_symbol.to_s
|
43
|
+
|
44
|
+
method = case method_name[-1, -1]
|
45
|
+
when '?' then define_bool(method_symbol, method_name[0..-2])
|
46
|
+
when '=' then define_setter(method_symbol, method_name[0..-2])
|
47
|
+
else define_getter(method_symbol)
|
48
|
+
end
|
49
|
+
|
50
|
+
method.call(*args)
|
51
|
+
end
|
52
|
+
|
53
|
+
def define_bool(method_symbol, property_name)
|
54
|
+
self.class.send(:define_method, method_symbol) do
|
55
|
+
body[property_name].blank?
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def define_setter(method_symbol, property_name)
|
60
|
+
self.class.send(:define_method, method_symbol) do |value|
|
61
|
+
if value.blank?
|
62
|
+
body.delete(property_name)
|
63
|
+
else
|
64
|
+
body[property_name] = value
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def define_getter(property_name)
|
70
|
+
self.class.send(:define_method, property_name) do
|
71
|
+
body[property_name]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
class InstanceEvalMethod < SimpleDataMapper
|
77
|
+
private
|
78
|
+
|
79
|
+
def method_missing(method_symbol, *args)
|
80
|
+
method_name = method_symbol.to_s
|
81
|
+
case method_name[-1..-1]
|
82
|
+
when "="
|
83
|
+
define_setter(method_name, method_name[0..-2], args.first)
|
84
|
+
when "?"
|
85
|
+
define_bool(method_name, method_name[0..-2])
|
86
|
+
else
|
87
|
+
define_getter(method_name)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def define_getter(prop)
|
92
|
+
instance_eval <<-RUBY, __FILE__, __LINE__ + 1
|
93
|
+
def #{prop}
|
94
|
+
body["#{prop}"]
|
95
|
+
end
|
96
|
+
RUBY
|
97
|
+
send(prop)
|
98
|
+
end
|
99
|
+
|
100
|
+
def define_setter(method, prop, value)
|
101
|
+
instance_eval <<-RUBY, __FILE__, __LINE__ + 1
|
102
|
+
def #{method}(val)
|
103
|
+
if val.blank? && body.has_key?("#{prop}")
|
104
|
+
body.delete("#{prop}")
|
105
|
+
else
|
106
|
+
body["#{prop}"] = val
|
107
|
+
end
|
108
|
+
end
|
109
|
+
RUBY
|
110
|
+
send(method, value)
|
111
|
+
end
|
112
|
+
|
113
|
+
def define_bool(method, prop)
|
114
|
+
method_body = lambda{ body[prop].blank? }
|
115
|
+
instance_eval <<-RUBY, __FILE__, __LINE__ + 1
|
116
|
+
def #{method}
|
117
|
+
body["#{prop}"].blank?
|
118
|
+
end
|
119
|
+
RUBY
|
120
|
+
send(method)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def bench(klass, n = 20000)
|
125
|
+
extensions = ['','=','?']
|
126
|
+
n.times do
|
127
|
+
model = klass.new
|
128
|
+
ext = extensions.pick
|
129
|
+
method = "#{PROPS.pick}#{ext}"
|
130
|
+
case ext
|
131
|
+
when '='
|
132
|
+
model.send(method, /\w+/.gen)
|
133
|
+
else
|
134
|
+
model.send(method)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
Benchmark.bm(30) do |x|
|
140
|
+
x.report('method missing'){ bench(Missing) }
|
141
|
+
x.report('method missing w/define_method'){ bench(DefineMethod) }
|
142
|
+
x.report('method missing w/instance_eval'){ bench(InstanceEvalMethod) }
|
143
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'benchmark'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'dm-core'
|
4
|
+
|
5
|
+
def respond_to_bench(n = 100000)
|
6
|
+
fields = [ :test, :test.gt ]
|
7
|
+
n.times do
|
8
|
+
fields[rand(fields.size)].respond_to?(:target)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def is_a_bench(n = 100000)
|
13
|
+
fields = [ :test, :test.gt ]
|
14
|
+
n.times do
|
15
|
+
fields[rand(fields.size)].is_a?(DataMapper::Query::Operator)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
Benchmark.bm(10) do |x|
|
20
|
+
x.report('respond_to?'){ respond_to_bench }
|
21
|
+
x.report('is_a?'){ is_a_bench }
|
22
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{dm-is-schemaless}
|
8
|
+
s.version = "0.10.2"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["brianthecoder"]
|
12
|
+
s.date = %q{2010-02-27}
|
13
|
+
s.description = %q{A plugin that allows you to easily treat an rdbms like a schemaless store, perfect for something like heroku *wink, wink*}
|
14
|
+
s.email = %q{wbsmith83@gmail.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README.rdoc",
|
18
|
+
"TODO"
|
19
|
+
]
|
20
|
+
s.files = [
|
21
|
+
"LICENSE",
|
22
|
+
"README.rdoc",
|
23
|
+
"Rakefile",
|
24
|
+
"TODO",
|
25
|
+
"VERSION",
|
26
|
+
"benchmarks/dynamic_methods.rb",
|
27
|
+
"benchmarks/operator_test.rb",
|
28
|
+
"dm-is-schemaless.gemspec",
|
29
|
+
"lib/dm-is-schemaless.rb",
|
30
|
+
"lib/dm-is-schemaless/is/index.rb",
|
31
|
+
"lib/dm-is-schemaless/is/schemaless.rb",
|
32
|
+
"lib/dm-is-schemaless/is/version.rb",
|
33
|
+
"spec/integration/schemaless_spec.rb",
|
34
|
+
"spec/models.rb",
|
35
|
+
"spec/spec.opts",
|
36
|
+
"spec/spec_helper.rb",
|
37
|
+
"tasks/install.rb",
|
38
|
+
"tasks/spec.rb"
|
39
|
+
]
|
40
|
+
s.homepage = %q{http://github.com/BrianTheCoder/dm-is-schemales}
|
41
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
42
|
+
s.require_paths = ["lib"]
|
43
|
+
s.rubygems_version = %q{1.3.5}
|
44
|
+
s.summary = %q{An implementation of friendfeed's schemaless store for rbdms'}
|
45
|
+
s.test_files = [
|
46
|
+
"spec/integration/schemaless_spec.rb",
|
47
|
+
"spec/models.rb",
|
48
|
+
"spec/spec_helper.rb"
|
49
|
+
]
|
50
|
+
|
51
|
+
if s.respond_to? :specification_version then
|
52
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
53
|
+
s.specification_version = 3
|
54
|
+
|
55
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
56
|
+
s.add_development_dependency(%q<yard>, [">= 0"])
|
57
|
+
s.add_runtime_dependency(%q<dm-types>, [">= 0"])
|
58
|
+
else
|
59
|
+
s.add_dependency(%q<yard>, [">= 0"])
|
60
|
+
s.add_dependency(%q<dm-types>, [">= 0"])
|
61
|
+
end
|
62
|
+
else
|
63
|
+
s.add_dependency(%q<yard>, [">= 0"])
|
64
|
+
s.add_dependency(%q<dm-types>, [">= 0"])
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module Is
|
3
|
+
module Schemaless
|
4
|
+
class Index
|
5
|
+
attr_accessor :storage_name, :parent, :assoc_name, :model
|
6
|
+
|
7
|
+
class IndexingError < StandardError; end
|
8
|
+
|
9
|
+
def initialize(resource,field, opts)
|
10
|
+
name = "#{field.to_s.camel_case}Index"
|
11
|
+
@storage_name = Extlib::Inflection.tableize(name)
|
12
|
+
@parent = :"#{resource.to_s.snake_case}"
|
13
|
+
@model = build_resource(name, field, resource)
|
14
|
+
update_field_callbacks(resource, field)
|
15
|
+
end
|
16
|
+
|
17
|
+
def update_field_callbacks(model, field)
|
18
|
+
self.assoc_name = @model.to_s.snake_case
|
19
|
+
model.has 1, assoc_name.to_sym
|
20
|
+
model.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
21
|
+
def update_#{field}_index
|
22
|
+
if body.key?('#{field}')
|
23
|
+
self.#{assoc_name} ||= #{@model}.new
|
24
|
+
#{assoc_name}.#{field} = body['#{field}']
|
25
|
+
elsif #{assoc_name} && #{assoc_name}.destroy
|
26
|
+
self.#{assoc_name} = nil
|
27
|
+
else
|
28
|
+
end
|
29
|
+
end
|
30
|
+
RUBY
|
31
|
+
model.before :save, :"update_#{field}_index"
|
32
|
+
end
|
33
|
+
|
34
|
+
def build_resource(name, field, parent_resource)
|
35
|
+
klass = Object.const_set(name, Class.new)
|
36
|
+
klass.send(:include, DataMapper::Resource)
|
37
|
+
klass.property field.to_sym, String, :key => true
|
38
|
+
parent_resource.key.each do |prop|
|
39
|
+
klass.property :"#{@parent}_#{prop.name}", prop.type, :key => true
|
40
|
+
end
|
41
|
+
klass.belongs_to @parent, :parent_key => parent_resource.key.map{|k| k.name }
|
42
|
+
klass
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module Is
|
3
|
+
module Schemaless
|
4
|
+
##
|
5
|
+
# fired when your plugin gets included into Resource
|
6
|
+
#
|
7
|
+
def self.included(base)
|
8
|
+
end
|
9
|
+
##
|
10
|
+
# Methods that should be included in DataMapper::Model.
|
11
|
+
# Normally this should just be your generator, so that the namespace
|
12
|
+
# does not get cluttered. ClassMethods and InstanceMethods gets added
|
13
|
+
# in the specific resources when you fire is :example
|
14
|
+
##
|
15
|
+
|
16
|
+
def is_schemaless(options = {})
|
17
|
+
# Add class-methods
|
18
|
+
extend DataMapper::Is::Schemaless::ClassMethods
|
19
|
+
# Add instance-methods
|
20
|
+
include DataMapper::Is::Schemaless::InstanceMethods
|
21
|
+
class_inheritable_accessor(:indexes)
|
22
|
+
self.indexes ||= {}
|
23
|
+
|
24
|
+
property :added_id, DataMapper::Types::Serial, :key => false
|
25
|
+
property :type, DataMapper::Types::Discriminator
|
26
|
+
property :id, DataMapper::Types::UUID, :unique => true,
|
27
|
+
:required => true,
|
28
|
+
:index => true,
|
29
|
+
:default => Proc.new{ Guid.new.to_s }
|
30
|
+
property :updated, DataMapper::Types::EpochTime, :key => true,
|
31
|
+
:index => true,
|
32
|
+
:default => Proc.new{ Time.now }
|
33
|
+
property :body, DataMapper::Types::Json, :default => {}
|
34
|
+
|
35
|
+
# before :save, :update_indexes
|
36
|
+
end
|
37
|
+
|
38
|
+
module ClassMethods
|
39
|
+
def storage_name(repository_name = default_repository_name); 'entities' end
|
40
|
+
|
41
|
+
def index_on(field, opts = {})
|
42
|
+
indexes[field] = Index.new(self, field, opts)
|
43
|
+
end
|
44
|
+
|
45
|
+
def all(query = {})
|
46
|
+
super transform_query(query)
|
47
|
+
end
|
48
|
+
|
49
|
+
def first(query = {})
|
50
|
+
super transform_query(query)
|
51
|
+
end
|
52
|
+
|
53
|
+
def last(query = {})
|
54
|
+
super transform_query(query)
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def transform_query(query)
|
60
|
+
find_indexes(query).each do |(field, value)|
|
61
|
+
name = field.respond_to?(:target) ? field.target : field
|
62
|
+
rewritten_field = "#{indexes[name].assoc_name}.#{name}"
|
63
|
+
rewritten_field << ".#{key.operator}" if field.respond_to?(:operator)
|
64
|
+
query[rewritten_field] = value
|
65
|
+
query.delete(key)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def find_indexes(query)
|
70
|
+
query.select do |key, value|
|
71
|
+
indexes.has_key?(key.respond_to?(:target) ? key.target : key)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
module InstanceMethods
|
77
|
+
def initialize(args = {})
|
78
|
+
super({})
|
79
|
+
self.body = args
|
80
|
+
end
|
81
|
+
|
82
|
+
def method_missing(method_symbol, *args)
|
83
|
+
method_name = method_symbol.to_s
|
84
|
+
if %w(? =).include?(method_name[-1,1])
|
85
|
+
method = method_name[0..-2]
|
86
|
+
operator = method_name[-1,1]
|
87
|
+
if operator == '='
|
88
|
+
set_value(method, args.first)
|
89
|
+
elsif operator == '?'
|
90
|
+
!body[method].blank?
|
91
|
+
end
|
92
|
+
else
|
93
|
+
body[method_name]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def set_value(method, val)
|
98
|
+
if val.blank?
|
99
|
+
body.delete(method)
|
100
|
+
else
|
101
|
+
body[method] = val
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def update_indexes
|
106
|
+
self.class.indexes.each do |field, index|
|
107
|
+
field = field.to_s
|
108
|
+
assoc = if send(index.assoc_name).blank?
|
109
|
+
self.send(:"#{index.assoc_name}=", index.model.new)
|
110
|
+
else
|
111
|
+
self.send(index.assoc_name)
|
112
|
+
end
|
113
|
+
if body.key?(field)
|
114
|
+
assoc[field] = body[field]
|
115
|
+
elsif assoc && assoc.destroy
|
116
|
+
self.send(:"#{index.assoc_name}=", nil)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end # Schemaless
|
122
|
+
end # Is
|
123
|
+
end # DataMapper
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
if HAS_SQLITE3 || HAS_MYSQL || HAS_POSTGRES
|
4
|
+
require 'pathname'
|
5
|
+
require Pathname(__FILE__).dirname.expand_path.parent + 'spec_helper'
|
6
|
+
|
7
|
+
describe 'DataMapper::Is::Schemaless' do
|
8
|
+
|
9
|
+
before :each do
|
10
|
+
DataMapper.auto_migrate!
|
11
|
+
@message = Message.new
|
12
|
+
@photo = Photo.new
|
13
|
+
end
|
14
|
+
|
15
|
+
describe 'common table' do
|
16
|
+
it 'should set each models table to entities' do
|
17
|
+
Message.storage_name.should == "entities"
|
18
|
+
Photo.storage_name.should == "entities"
|
19
|
+
Photo.storage_name.should == Message.storage_name
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe 'structure' do
|
24
|
+
{
|
25
|
+
:added_id => DataMapper::Types::Serial,
|
26
|
+
:id => DataMapper::Types::UUID,
|
27
|
+
:updated => DataMapper::Types::EpochTime,
|
28
|
+
:body => DataMapper::Types::Json
|
29
|
+
}.each do |k, v|
|
30
|
+
it "has the property #{k}" do
|
31
|
+
Message.properties[k].should_not be_nil
|
32
|
+
end
|
33
|
+
|
34
|
+
it "has the property #{k} of type #{v}" do
|
35
|
+
Message.properties[k].type.should == v
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe 'index tables' do
|
41
|
+
it 'should have empty indexes if none are created' do
|
42
|
+
@photo.indexes.should be_empty
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'should add the index to the list' do
|
46
|
+
Message.indexes.should have_key(:email)
|
47
|
+
@message.indexes.should have_key(:email)
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'should create a table named ModelProperty' do
|
51
|
+
defined?(EmailIndex).should == "constant"
|
52
|
+
end
|
53
|
+
|
54
|
+
{
|
55
|
+
:email => String,
|
56
|
+
:message_updated => DataMapper::Types::EpochTime
|
57
|
+
}.each do |k, v|
|
58
|
+
it "has the property #{k}" do
|
59
|
+
EmailIndex.properties[k].should_not be_nil
|
60
|
+
end
|
61
|
+
|
62
|
+
it "has the property #{k} of type #{v}" do
|
63
|
+
EmailIndex.properties[k].type.should == v
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'should define a has n relationship on the model' do
|
68
|
+
Message.relationships[:email_index].should_not be_nil
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'should define a belongs_to relationship on the index table' do
|
72
|
+
EmailIndex.relationships[:message].should_not be_nil
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe 'model_type field' do
|
77
|
+
it 'adds it to date on save' do
|
78
|
+
@message.save
|
79
|
+
@msg = Message.first
|
80
|
+
@msg.body.should have_key("model_type")
|
81
|
+
@msg.model_type.should == "Message"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe 'update the index' do
|
86
|
+
before :each do
|
87
|
+
@message.email = Faker::Internet.free_email
|
88
|
+
p 'save 1'
|
89
|
+
@message.save
|
90
|
+
@message.reload
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'should create a new record on save' do
|
94
|
+
@message.reload
|
95
|
+
@message.email_index.should_not be_nil
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'should destroy the index if the value becomes nil' do
|
99
|
+
@message.email = nil
|
100
|
+
p 'message'
|
101
|
+
p @message
|
102
|
+
@message.save
|
103
|
+
p 'saved'
|
104
|
+
@message.email_index.should be_nil
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'should update the index when the value is changed' do
|
108
|
+
email = Faker::Internet.free_email
|
109
|
+
@message.email = email
|
110
|
+
p 'message'
|
111
|
+
p @message
|
112
|
+
@message.save
|
113
|
+
p 'saved'
|
114
|
+
@message.email_index.email.should == email
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
describe 'querying' do
|
119
|
+
it 'should look in the index tables if the property is indexed' do
|
120
|
+
email = Faker::Internet.free_email
|
121
|
+
@message.email = email
|
122
|
+
@message.save
|
123
|
+
queried = Message.first(:email => email)
|
124
|
+
queried.id.should == @message.id
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
data/spec/models.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'dm-sweatshop'
|
2
|
+
require 'faker'
|
3
|
+
|
4
|
+
class Message
|
5
|
+
include DataMapper::Resource
|
6
|
+
|
7
|
+
is :schemaless
|
8
|
+
|
9
|
+
index_on :email
|
10
|
+
end
|
11
|
+
|
12
|
+
class Photo
|
13
|
+
include DataMapper::Resource
|
14
|
+
|
15
|
+
is :schemaless
|
16
|
+
end
|
17
|
+
|
18
|
+
Message.fixture{{
|
19
|
+
:username => Faker::Internet.user_name,
|
20
|
+
:email => Faker::Internet.free_email,
|
21
|
+
:body => Faker::Lorem.paragraph(10),
|
22
|
+
:city => Faker::Address.city,
|
23
|
+
:us_state_abbr => Faker::Address.us_state_abbr,
|
24
|
+
:post_code => Faker::Address.zip_code
|
25
|
+
}}
|
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
# use local dm-core if running from a typical dev checkout.
|
5
|
+
lib = File.join('..', '..', 'dm-core', 'lib')
|
6
|
+
$LOAD_PATH.unshift(lib) if File.directory?(lib)
|
7
|
+
require 'dm-core'
|
8
|
+
require 'faker'
|
9
|
+
|
10
|
+
# Support running specs with 'rake spec' and 'spec'
|
11
|
+
$LOAD_PATH.unshift('lib') unless $LOAD_PATH.include?('lib')
|
12
|
+
|
13
|
+
require 'dm-is-schemaless'
|
14
|
+
load File.join(File.dirname(__FILE__), 'models.rb')
|
15
|
+
|
16
|
+
def load_driver(name, default_uri)
|
17
|
+
return false if ENV['ADAPTER'] != name.to_s
|
18
|
+
|
19
|
+
begin
|
20
|
+
DataMapper.setup(name, ENV["#{name.to_s.upcase}_SPEC_URI"] || default_uri)
|
21
|
+
DataMapper::Repository.adapters[:default] = DataMapper::Repository.adapters[name]
|
22
|
+
true
|
23
|
+
rescue LoadError => e
|
24
|
+
warn "Could not load do_#{name}: #{e}"
|
25
|
+
false
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
ENV['ADAPTER'] ||= 'postgres'
|
30
|
+
|
31
|
+
HAS_SQLITE3 = load_driver(:sqlite3, 'sqlite3::memory:')
|
32
|
+
HAS_MYSQL = load_driver(:mysql, 'mysql://localhost/schemaless_test')
|
33
|
+
HAS_POSTGRES = load_driver(:postgres, 'postgres://postgres@localhost/schemaless_test')
|
34
|
+
|
35
|
+
# DataObjects::Postgres.logger = Logger.new(STDOUT)
|
data/tasks/install.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
def sudo_gem(cmd)
|
2
|
+
sh "#{SUDO} #{RUBY} -S gem #{cmd}", :verbose => false
|
3
|
+
end
|
4
|
+
|
5
|
+
desc "Install #{GEM_NAME} #{GEM_VERSION}"
|
6
|
+
task :install => [ :package ] do
|
7
|
+
sudo_gem "install pkg/#{GEM_NAME}-#{GEM_VERSION} --no-update-sources"
|
8
|
+
end
|
9
|
+
|
10
|
+
desc "Uninstall #{GEM_NAME} #{GEM_VERSION}"
|
11
|
+
task :uninstall => [ :clobber ] do
|
12
|
+
sudo_gem "uninstall #{GEM_NAME} -v#{GEM_VERSION} -Ix"
|
13
|
+
end
|
data/tasks/spec.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
begin
|
2
|
+
require 'spec/rake/spectask'
|
3
|
+
|
4
|
+
task :default => [ :spec ]
|
5
|
+
|
6
|
+
desc 'Run specifications'
|
7
|
+
Spec::Rake::SpecTask.new(:spec) do |t|
|
8
|
+
t.spec_opts << '--options' << 'spec/spec.opts' if File.exists?('spec/spec.opts')
|
9
|
+
t.libs << 'lib' << 'spec' # needed for CI rake spec task, duplicated in spec_helper
|
10
|
+
|
11
|
+
begin
|
12
|
+
require 'rcov'
|
13
|
+
t.rcov = JRUBY ? false : (ENV.has_key?('NO_RCOV') ? ENV['NO_RCOV'] != 'true' : true)
|
14
|
+
t.rcov_opts << '--exclude' << 'spec'
|
15
|
+
t.rcov_opts << '--text-summary'
|
16
|
+
t.rcov_opts << '--sort' << 'coverage' << '--sort-reverse'
|
17
|
+
rescue LoadError
|
18
|
+
# rcov not installed
|
19
|
+
rescue SyntaxError
|
20
|
+
# rcov syntax invalid
|
21
|
+
end
|
22
|
+
end
|
23
|
+
rescue LoadError
|
24
|
+
# rspec not installed
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dm-is-schemaless
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.10.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- brianthecoder
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-02-27 00:00:00 -06:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: yard
|
17
|
+
type: :development
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: dm-types
|
27
|
+
type: :runtime
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: "0"
|
34
|
+
version:
|
35
|
+
description: A plugin that allows you to easily treat an rdbms like a schemaless store, perfect for something like heroku *wink, wink*
|
36
|
+
email: wbsmith83@gmail.com
|
37
|
+
executables: []
|
38
|
+
|
39
|
+
extensions: []
|
40
|
+
|
41
|
+
extra_rdoc_files:
|
42
|
+
- LICENSE
|
43
|
+
- README.rdoc
|
44
|
+
- TODO
|
45
|
+
files:
|
46
|
+
- LICENSE
|
47
|
+
- README.rdoc
|
48
|
+
- Rakefile
|
49
|
+
- TODO
|
50
|
+
- VERSION
|
51
|
+
- benchmarks/dynamic_methods.rb
|
52
|
+
- benchmarks/operator_test.rb
|
53
|
+
- dm-is-schemaless.gemspec
|
54
|
+
- lib/dm-is-schemaless.rb
|
55
|
+
- lib/dm-is-schemaless/is/index.rb
|
56
|
+
- lib/dm-is-schemaless/is/schemaless.rb
|
57
|
+
- lib/dm-is-schemaless/is/version.rb
|
58
|
+
- spec/integration/schemaless_spec.rb
|
59
|
+
- spec/models.rb
|
60
|
+
- spec/spec.opts
|
61
|
+
- spec/spec_helper.rb
|
62
|
+
- tasks/install.rb
|
63
|
+
- tasks/spec.rb
|
64
|
+
has_rdoc: true
|
65
|
+
homepage: http://github.com/BrianTheCoder/dm-is-schemales
|
66
|
+
licenses: []
|
67
|
+
|
68
|
+
post_install_message:
|
69
|
+
rdoc_options:
|
70
|
+
- --charset=UTF-8
|
71
|
+
require_paths:
|
72
|
+
- lib
|
73
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: "0"
|
78
|
+
version:
|
79
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: "0"
|
84
|
+
version:
|
85
|
+
requirements: []
|
86
|
+
|
87
|
+
rubyforge_project:
|
88
|
+
rubygems_version: 1.3.5
|
89
|
+
signing_key:
|
90
|
+
specification_version: 3
|
91
|
+
summary: An implementation of friendfeed's schemaless store for rbdms'
|
92
|
+
test_files:
|
93
|
+
- spec/integration/schemaless_spec.rb
|
94
|
+
- spec/models.rb
|
95
|
+
- spec/spec_helper.rb
|