friendly_postgres 0.4.3

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.
Files changed (129) hide show
  1. data/.document +2 -0
  2. data/.gitignore +26 -0
  3. data/APACHE-LICENSE +202 -0
  4. data/CHANGELOG.md +19 -0
  5. data/CONTRIBUTORS.md +7 -0
  6. data/LICENSE +20 -0
  7. data/README.md +265 -0
  8. data/Rakefile +68 -0
  9. data/TODO.md +5 -0
  10. data/VERSION +1 -0
  11. data/examples/friendly.yml +7 -0
  12. data/friendly.gemspec +232 -0
  13. data/lib/friendly.rb +54 -0
  14. data/lib/friendly/associations.rb +7 -0
  15. data/lib/friendly/associations/association.rb +34 -0
  16. data/lib/friendly/associations/set.rb +37 -0
  17. data/lib/friendly/attribute.rb +91 -0
  18. data/lib/friendly/boolean.rb +10 -0
  19. data/lib/friendly/cache.rb +24 -0
  20. data/lib/friendly/cache/by_id.rb +33 -0
  21. data/lib/friendly/config.rb +5 -0
  22. data/lib/friendly/data_store.rb +72 -0
  23. data/lib/friendly/document.rb +257 -0
  24. data/lib/friendly/document_table.rb +56 -0
  25. data/lib/friendly/index.rb +73 -0
  26. data/lib/friendly/memcached.rb +48 -0
  27. data/lib/friendly/named_scope.rb +17 -0
  28. data/lib/friendly/newrelic.rb +6 -0
  29. data/lib/friendly/query.rb +42 -0
  30. data/lib/friendly/scope.rb +100 -0
  31. data/lib/friendly/scope_proxy.rb +45 -0
  32. data/lib/friendly/sequel_monkey_patches.rb +34 -0
  33. data/lib/friendly/storage.rb +31 -0
  34. data/lib/friendly/storage_factory.rb +24 -0
  35. data/lib/friendly/storage_proxy.rb +103 -0
  36. data/lib/friendly/table.rb +15 -0
  37. data/lib/friendly/table_creator.rb +48 -0
  38. data/lib/friendly/time.rb +14 -0
  39. data/lib/friendly/translator.rb +32 -0
  40. data/lib/friendly/uuid.rb +148 -0
  41. data/rails/init.rb +3 -0
  42. data/spec/config.yml.example +7 -0
  43. data/spec/fakes/data_store_fake.rb +29 -0
  44. data/spec/fakes/database_fake.rb +12 -0
  45. data/spec/fakes/dataset_fake.rb +28 -0
  46. data/spec/fakes/document.rb +18 -0
  47. data/spec/fakes/serializer_fake.rb +12 -0
  48. data/spec/fakes/time_fake.rb +12 -0
  49. data/spec/integration/ad_hoc_scopes_spec.rb +42 -0
  50. data/spec/integration/basic_object_lifecycle_spec.rb +114 -0
  51. data/spec/integration/batch_insertion_spec.rb +29 -0
  52. data/spec/integration/convenience_api_spec.rb +25 -0
  53. data/spec/integration/count_spec.rb +12 -0
  54. data/spec/integration/default_value_spec.rb +15 -0
  55. data/spec/integration/find_via_cache_spec.rb +101 -0
  56. data/spec/integration/finder_spec.rb +64 -0
  57. data/spec/integration/has_many_spec.rb +18 -0
  58. data/spec/integration/index_spec.rb +57 -0
  59. data/spec/integration/named_scope_spec.rb +34 -0
  60. data/spec/integration/pagination_spec.rb +63 -0
  61. data/spec/integration/scope_chaining_spec.rb +22 -0
  62. data/spec/integration/table_creator_spec.rb +64 -0
  63. data/spec/integration/write_through_cache_spec.rb +53 -0
  64. data/spec/spec.opts +1 -0
  65. data/spec/spec_helper.rb +103 -0
  66. data/spec/unit/associations/association_spec.rb +57 -0
  67. data/spec/unit/associations/set_spec.rb +43 -0
  68. data/spec/unit/attribute_spec.rb +105 -0
  69. data/spec/unit/cache_by_id_spec.rb +102 -0
  70. data/spec/unit/cache_spec.rb +21 -0
  71. data/spec/unit/config_spec.rb +4 -0
  72. data/spec/unit/data_store_spec.rb +188 -0
  73. data/spec/unit/document_spec.rb +358 -0
  74. data/spec/unit/document_table_spec.rb +126 -0
  75. data/spec/unit/friendly_spec.rb +25 -0
  76. data/spec/unit/index_spec.rb +196 -0
  77. data/spec/unit/memcached_spec.rb +114 -0
  78. data/spec/unit/named_scope_spec.rb +16 -0
  79. data/spec/unit/query_spec.rb +104 -0
  80. data/spec/unit/scope_proxy_spec.rb +44 -0
  81. data/spec/unit/scope_spec.rb +113 -0
  82. data/spec/unit/storage_factory_spec.rb +59 -0
  83. data/spec/unit/storage_proxy_spec.rb +218 -0
  84. data/spec/unit/translator_spec.rb +96 -0
  85. data/website/index.html +210 -0
  86. data/website/scripts/clipboard.swf +0 -0
  87. data/website/scripts/shBrushAS3.js +61 -0
  88. data/website/scripts/shBrushBash.js +66 -0
  89. data/website/scripts/shBrushCSharp.js +67 -0
  90. data/website/scripts/shBrushColdFusion.js +102 -0
  91. data/website/scripts/shBrushCpp.js +99 -0
  92. data/website/scripts/shBrushCss.js +93 -0
  93. data/website/scripts/shBrushDelphi.js +57 -0
  94. data/website/scripts/shBrushDiff.js +43 -0
  95. data/website/scripts/shBrushErlang.js +54 -0
  96. data/website/scripts/shBrushGroovy.js +69 -0
  97. data/website/scripts/shBrushJScript.js +52 -0
  98. data/website/scripts/shBrushJava.js +59 -0
  99. data/website/scripts/shBrushJavaFX.js +60 -0
  100. data/website/scripts/shBrushPerl.js +74 -0
  101. data/website/scripts/shBrushPhp.js +91 -0
  102. data/website/scripts/shBrushPlain.js +35 -0
  103. data/website/scripts/shBrushPowerShell.js +76 -0
  104. data/website/scripts/shBrushPython.js +66 -0
  105. data/website/scripts/shBrushRuby.js +57 -0
  106. data/website/scripts/shBrushScala.js +53 -0
  107. data/website/scripts/shBrushSql.js +68 -0
  108. data/website/scripts/shBrushVb.js +58 -0
  109. data/website/scripts/shBrushXml.js +71 -0
  110. data/website/scripts/shCore.js +30 -0
  111. data/website/scripts/shLegacy.js +30 -0
  112. data/website/styles/friendly.css +103 -0
  113. data/website/styles/help.png +0 -0
  114. data/website/styles/ie.css +35 -0
  115. data/website/styles/magnifier.png +0 -0
  116. data/website/styles/page_white_code.png +0 -0
  117. data/website/styles/page_white_copy.png +0 -0
  118. data/website/styles/print.css +29 -0
  119. data/website/styles/printer.png +0 -0
  120. data/website/styles/screen.css +257 -0
  121. data/website/styles/shCore.css +330 -0
  122. data/website/styles/shThemeDefault.css +173 -0
  123. data/website/styles/shThemeDjango.css +176 -0
  124. data/website/styles/shThemeEclipse.css +190 -0
  125. data/website/styles/shThemeEmacs.css +175 -0
  126. data/website/styles/shThemeFadeToGrey.css +177 -0
  127. data/website/styles/shThemeMidnight.css +175 -0
  128. data/website/styles/shThemeRDark.css +175 -0
  129. metadata +302 -0
@@ -0,0 +1,91 @@
1
+ module Friendly
2
+ class Attribute
3
+ class << self
4
+ def register_type(type, sql_type, &block)
5
+ sql_types[type.name] = sql_type
6
+ converters[type] = block
7
+ end
8
+
9
+ def deregister_type(type)
10
+ sql_types.delete(type.name)
11
+ converters.delete(type)
12
+ end
13
+
14
+ def sql_type(type)
15
+ sql_types[type.name]
16
+ end
17
+
18
+ def sql_types
19
+ @sql_types ||= {}
20
+ end
21
+
22
+ def converters
23
+ @converters ||= {}
24
+ end
25
+
26
+ def custom_type?(klass)
27
+ !sql_type(klass).nil?
28
+ end
29
+ end
30
+
31
+ converters[Integer] = lambda { |s| s.to_i }
32
+ converters[String] = lambda { |s| s.to_s }
33
+
34
+ attr_reader :klass, :name, :type, :default_value
35
+
36
+ def initialize(klass, name, type, options = {})
37
+ @klass = klass
38
+ @name = name
39
+ @type = type
40
+ @default_value = options[:default]
41
+ build_accessors
42
+ end
43
+
44
+ def typecast(value)
45
+ !type || value.is_a?(type) ? value : convert(value)
46
+ end
47
+
48
+ def convert(value)
49
+ assert_converter_exists(value)
50
+ converters[type].call(value)
51
+ end
52
+
53
+ def default
54
+ if !default_value.nil?
55
+ default_value
56
+ elsif type.respond_to?(:new)
57
+ type.new
58
+ else
59
+ nil
60
+ end
61
+ end
62
+
63
+ protected
64
+ def build_accessors
65
+ n = name
66
+ klass.class_eval do
67
+ eval <<-__END__
68
+ def #{n}=(value)
69
+ @#{n} = self.class.attributes[:#{n}].typecast(value)
70
+ end
71
+
72
+ def #{n}
73
+ @#{n} ||= self.class.attributes[:#{n}].default
74
+ end
75
+ __END__
76
+ end
77
+ end
78
+
79
+ def assert_converter_exists(value)
80
+ unless converters.has_key?(type)
81
+ msg = "Can't convert #{value} to #{type}.
82
+ Add a custom converter to Friendly::Attribute::CONVERTERS."
83
+ raise NoConverterExists, msg
84
+ end
85
+ end
86
+
87
+ def converters
88
+ self.class.converters
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,10 @@
1
+ require 'friendly/attribute'
2
+
3
+ module Friendly
4
+ # placeholder that represents a boolean
5
+ # since ruby has no boolean superclass
6
+ module Boolean
7
+ end
8
+ end
9
+
10
+ Friendly::Attribute.register_type(Friendly::Boolean, 'boolean') { |s| s }
@@ -0,0 +1,24 @@
1
+ require 'friendly/storage'
2
+
3
+ module Friendly
4
+ class Cache < Storage
5
+ class << self
6
+ def cache_for(klass, fields, options)
7
+ unless fields == [:id]
8
+ raise NotSupported, "Caching is only possible by id at the moment."
9
+ end
10
+
11
+ ByID.new(klass, fields, options)
12
+ end
13
+ end
14
+
15
+ attr_reader :klass, :fields, :cache, :version
16
+
17
+ def initialize(klass, fields, options = {}, cache = Friendly.cache)
18
+ @klass = klass
19
+ @fields = fields
20
+ @cache = cache
21
+ @version = options[:version] || 0
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,33 @@
1
+ module Friendly
2
+ class Cache
3
+ class ByID < Cache
4
+ def store(document)
5
+ cache.set(cache_key(document.id), document)
6
+ end
7
+ alias_method :create, :store
8
+ alias_method :update, :store
9
+
10
+ def destroy(document)
11
+ cache.delete(cache_key(document.id))
12
+ end
13
+
14
+ def first(query, &block)
15
+ cache.get(cache_key(query.conditions[:id]), &block)
16
+ end
17
+
18
+ def all(query, &block)
19
+ keys = query.conditions[:id].map { |k| cache_key(k) }
20
+ cache.multiget(keys, &block).values
21
+ end
22
+
23
+ def satisfies?(query)
24
+ query.conditions.keys == [:id]
25
+ end
26
+
27
+ protected
28
+ def cache_key(id)
29
+ [klass.name, version, id.to_guid].join("/")
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,5 @@
1
+ module Friendly
2
+ class Config < Struct.new(:repository)
3
+ end
4
+ end
5
+
@@ -0,0 +1,72 @@
1
+ module Friendly
2
+ class DataStore
3
+ attr_reader :database
4
+
5
+ def initialize(database)
6
+ @database = database
7
+ end
8
+
9
+ def insert(persistable, attributes)
10
+ batch? ? batch_insert(persistable, attributes) :
11
+ immediate_insert(persistable, attributes)
12
+ end
13
+
14
+ def all(persistable, query)
15
+ filtered = dataset(persistable).where(query.conditions)
16
+ if query.limit || query.offset
17
+ filtered = filtered.limit(query.limit, query.offset)
18
+ end
19
+ filtered = filtered.order(query.order) if query.order
20
+ filtered.map
21
+ end
22
+
23
+ def first(persistable, query)
24
+ dataset(persistable).first(query.conditions)
25
+ end
26
+
27
+ def update(persistable, id, attributes)
28
+ dataset(persistable).where(:id => id).update(attributes)
29
+ end
30
+
31
+ def delete(persistable, id)
32
+ dataset(persistable).where(:id => id).delete
33
+ end
34
+
35
+ def count(persistable, query)
36
+ dataset(persistable).where(query.conditions).count
37
+ end
38
+
39
+ def start_batch
40
+ Thread.current[:friendly_batch] = Hash.new { |h, k| h[k] = [] }
41
+ end
42
+
43
+ def reset_batch
44
+ Thread.current[:friendly_batch] = nil
45
+ end
46
+
47
+ def flush_batch
48
+ batch = Thread.current[:friendly_batch]
49
+ batch.keys.each do |k|
50
+ database.from(k).multi_insert(batch[k], :commit_every => 1000)
51
+ end
52
+ reset_batch
53
+ end
54
+
55
+ protected
56
+ def dataset(persistable)
57
+ database.from(persistable.table_name)
58
+ end
59
+
60
+ def immediate_insert(persistable, attributes)
61
+ dataset(persistable).insert(attributes)
62
+ end
63
+
64
+ def batch_insert(persistable, attributes)
65
+ Thread.current[:friendly_batch][persistable.table_name] << attributes
66
+ end
67
+
68
+ def batch?
69
+ Thread.current[:friendly_batch]
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,257 @@
1
+ require 'active_support/inflector'
2
+ require 'friendly/associations'
3
+
4
+ module Friendly
5
+ module Document
6
+ class << self
7
+ attr_writer :documents
8
+
9
+ def included(klass)
10
+ documents << klass
11
+ klass.class_eval do
12
+ extend ClassMethods
13
+ attribute :id, UUID
14
+ attribute :created_at, Time
15
+ attribute :updated_at, Time
16
+ end
17
+ end
18
+
19
+ def documents
20
+ @documents ||= []
21
+ end
22
+
23
+ def create_tables!
24
+ documents.each { |d| d.create_tables! }
25
+ end
26
+ end
27
+
28
+ module ClassMethods
29
+ attr_writer :storage_proxy, :query_klass,
30
+ :table_name, :collection_klass,
31
+ :scope_proxy, :association_set
32
+
33
+ def create_tables!
34
+ storage_proxy.create_tables!
35
+ end
36
+
37
+ def attribute(name, type = nil, options = {})
38
+ attributes[name] = Attribute.new(self, name, type, options)
39
+ end
40
+
41
+ def storage_proxy
42
+ @storage_proxy ||= StorageProxy.new(self)
43
+ end
44
+
45
+ def query_klass
46
+ @query_klass ||= Query
47
+ end
48
+
49
+ def collection_klass
50
+ @collection_klass ||= WillPaginate::Collection
51
+ end
52
+
53
+ def indexes(*args)
54
+ storage_proxy.add(args)
55
+ end
56
+
57
+ def caches_by(*fields)
58
+ options = fields.last.is_a?(Hash) ? fields.pop : {}
59
+ storage_proxy.cache(fields, options)
60
+ end
61
+
62
+ def attributes
63
+ @attributes ||= {}
64
+ end
65
+
66
+ def first(query)
67
+ storage_proxy.first(query(query))
68
+ end
69
+
70
+ def all(query)
71
+ storage_proxy.all(query(query))
72
+ end
73
+
74
+ def find(id)
75
+ doc = first(:id => id)
76
+ raise RecordNotFound, "Couldn't find #{name}/#{id}" if doc.nil?
77
+ doc
78
+ end
79
+
80
+ def count(conditions)
81
+ storage_proxy.count(query(conditions))
82
+ end
83
+
84
+ def paginate(conditions)
85
+ query = query(conditions)
86
+ count = count(query)
87
+ collection = collection_klass.new(query.page, query.per_page, count)
88
+ collection.replace(all(query))
89
+ end
90
+
91
+ def create(attributes = {})
92
+ doc = new(attributes)
93
+ doc.save
94
+ doc
95
+ end
96
+
97
+ def table_name
98
+ @table_name ||= name.pluralize.underscore
99
+ end
100
+
101
+ def scope_proxy
102
+ @scope_proxy ||= ScopeProxy.new(self)
103
+ end
104
+
105
+ # Add a named scope to this Document.
106
+ #
107
+ # e.g.
108
+ #
109
+ # class Post
110
+ # indexes :created_at
111
+ # named_scope :recent, :order! => :created_at.desc
112
+ # end
113
+ #
114
+ # Then, you can access the recent posts with:
115
+ #
116
+ # Post.recent.all
117
+ # ...or...
118
+ # Post.recent.first
119
+ #
120
+ # Both #all and #first also take additional parameters:
121
+ #
122
+ # Post.recent.all(:author_id => @author.id)
123
+ #
124
+ # Scopes are also chainable. See the README or Friendly::Scope docs for details.
125
+ #
126
+ # @param [Symbol] name the name of the scope.
127
+ # @param [Hash] parameters the query that this named scope will perform.
128
+ #
129
+ def named_scope(name, parameters)
130
+ scope_proxy.add_named(name, parameters)
131
+ end
132
+
133
+ # Returns boolean based on whether the Document has a scope by a particular name.
134
+ #
135
+ # @param [Symbol] name The name of the scope in question.
136
+ #
137
+ def has_named_scope?(name)
138
+ scope_proxy.has_named_scope?(name)
139
+ end
140
+
141
+ # Create an ad hoc scope on this Document.
142
+ #
143
+ # e.g.
144
+ #
145
+ # scope = Post.scope(:order! => :created_at)
146
+ # scope.all # => [#<Post>, #<Post>]
147
+ #
148
+ # @param [Hash] parameters the query parameters to create the scope with.
149
+ #
150
+ def scope(parameters)
151
+ scope_proxy.ad_hoc(parameters)
152
+ end
153
+
154
+ def association_set
155
+ @association_set ||= Associations::Set.new(self)
156
+ end
157
+
158
+ # Add a has_many association.
159
+ #
160
+ # e.g.
161
+ #
162
+ # class Post
163
+ # attribute :user_id, Friendly::UUID
164
+ # indexes :user_id
165
+ # end
166
+ #
167
+ # class User
168
+ # has_many :posts
169
+ # end
170
+ #
171
+ # @user = User.create
172
+ # @post = @user.posts.create
173
+ # @user.posts.all == [@post] # => true
174
+ #
175
+ # _Note: Make sure that the target model is indexed on the foreign key. If it isn't, querying the association will raise Friendly::MissingIndex._
176
+ #
177
+ # Friendly defaults the foreign key to class_name_id just like ActiveRecord.
178
+ # It also converts the name of the association to the name of the target class just like ActiveRecord does.
179
+ #
180
+ # The biggest difference in semantics between Friendly's has_many and active_record's is that Friendly's just returns a Friendly::Scope object. If you want all the associated objects, you have to call #all to get them. You can also use any other Friendly::Scope method.
181
+ #
182
+ # @param [Symbol] name The name of the association and plural name of the target class.
183
+ # @option options [String] :class_name The name of the target class of this association if it is different than the name would imply.
184
+ # @option options [Symbol] :foreign_key Override the foreign key.
185
+ #
186
+ def has_many(name, options = {})
187
+ association_set.add(name, options)
188
+ end
189
+
190
+ protected
191
+ def query(conditions)
192
+ conditions.is_a?(Query) ? conditions : query_klass.new(conditions)
193
+ end
194
+ end
195
+
196
+ def initialize(opts = {})
197
+ self.attributes = opts
198
+ end
199
+
200
+ def attributes=(attrs)
201
+ assert_no_duplicate_keys(attrs)
202
+ attrs.each { |name, value| send("#{name}=", value) }
203
+ end
204
+
205
+ def save
206
+ new_record? ? storage_proxy.create(self) : storage_proxy.update(self)
207
+ end
208
+
209
+ def update_attributes(attributes)
210
+ self.attributes = attributes
211
+ save
212
+ end
213
+
214
+ def destroy
215
+ storage_proxy.destroy(self)
216
+ end
217
+
218
+ def to_hash
219
+ Hash[*self.class.attributes.keys.map { |n| [n, send(n)] }.flatten]
220
+ end
221
+
222
+ def table_name
223
+ self.class.table_name
224
+ end
225
+
226
+ def new_record?
227
+ new_record
228
+ end
229
+
230
+ def new_record
231
+ @new_record = true if @new_record.nil?
232
+ @new_record
233
+ end
234
+
235
+ def new_record=(value)
236
+ @new_record = value
237
+ end
238
+
239
+ def storage_proxy
240
+ self.class.storage_proxy
241
+ end
242
+
243
+ def ==(comparison_object)
244
+ comparison_object.equal?(self) ||
245
+ (comparison_object.is_a?(self.class) &&
246
+ !comparison_object.new_record? &&
247
+ comparison_object.id == id)
248
+ end
249
+
250
+ protected
251
+ def assert_no_duplicate_keys(hash)
252
+ if hash.keys.map { |k| k.to_s }.uniq.length < hash.keys.length
253
+ raise ArgumentError, "Duplicate keys: #{hash.inspect}"
254
+ end
255
+ end
256
+ end
257
+ end