lore 0.4.8 → 0.9.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.
Files changed (111) hide show
  1. data/Manifest.txt +16 -7
  2. data/README.rdoc +91 -0
  3. data/benchmark/benchmark.sql +11 -0
  4. data/benchmark/results.txt +28 -0
  5. data/benchmark/select.rb +352 -0
  6. data/lib/lore.rb +22 -8
  7. data/lib/lore/adapters/context.rb +64 -0
  8. data/lib/lore/adapters/postgres-pr.rb +6 -0
  9. data/lib/lore/adapters/postgres-pr/connection.rb +93 -0
  10. data/lib/lore/adapters/postgres-pr/result.rb +63 -0
  11. data/lib/lore/{types.rb → adapters/postgres-pr/types.rb} +36 -0
  12. data/lib/lore/adapters/postgres.rb +24 -0
  13. data/lib/lore/adapters/postgres/connection.rb +81 -0
  14. data/lib/lore/adapters/postgres/result.rb +82 -0
  15. data/lib/lore/adapters/postgres/types.rb +91 -0
  16. data/lib/lore/bits.rb +18 -0
  17. data/lib/lore/cache/abstract_entity_cache.rb +2 -1
  18. data/lib/lore/cache/cacheable.rb +12 -177
  19. data/lib/lore/cache/memcache_entity_cache.rb +89 -0
  20. data/lib/lore/cache/memory_entity_cache.rb +77 -0
  21. data/lib/lore/cache/mmap_entity_cache.rb +2 -2
  22. data/lib/lore/cache/mmap_entity_cache_bork.rb +86 -0
  23. data/lib/lore/clause.rb +107 -35
  24. data/lib/lore/{exception → exceptions}/ambiguous_attribute.rb +2 -2
  25. data/lib/lore/{exception → exceptions}/cache_exception.rb +1 -1
  26. data/lib/lore/exceptions/database_exception.rb +16 -0
  27. data/lib/lore/{exception/invalid_parameter.rb → exceptions/invalid_field.rb} +7 -4
  28. data/lib/lore/exceptions/unknown_type.rb +18 -0
  29. data/lib/lore/exceptions/validation_failure.rb +71 -0
  30. data/lib/lore/gui/form_generator.rb +109 -60
  31. data/lib/lore/gui/lore_model_select_field.rb +1 -0
  32. data/lib/lore/migration.rb +84 -25
  33. data/lib/lore/model.rb +3 -18
  34. data/lib/lore/{aspect.rb → model/aspect.rb} +0 -0
  35. data/lib/lore/model/associations.rb +225 -0
  36. data/lib/lore/model/attribute_settings.rb +233 -0
  37. data/lib/lore/model/filters.rb +34 -0
  38. data/lib/lore/model/mockable.rb +62 -0
  39. data/lib/lore/{model_factory.rb → model/model_factory.rb} +68 -39
  40. data/lib/lore/model/model_instance.rb +382 -0
  41. data/lib/lore/{model_shortcuts.rb → model/model_shortcuts.rb} +7 -0
  42. data/lib/lore/model/polymorphic.rb +53 -0
  43. data/lib/lore/model/prepare.rb +97 -0
  44. data/lib/lore/model/table_accessor.rb +1016 -0
  45. data/lib/lore/query.rb +71 -0
  46. data/lib/lore/query_shortcuts.rb +43 -11
  47. data/lib/lore/strategies/table_delete.rb +115 -0
  48. data/lib/lore/strategies/table_insert.rb +146 -0
  49. data/lib/lore/strategies/table_select.rb +299 -0
  50. data/lib/lore/strategies/table_update.rb +155 -0
  51. data/lib/lore/validation/parameter_validator.rb +85 -26
  52. data/lib/lore/validation/type_validator.rb +34 -78
  53. data/{custom_models.rb → lore-0.9.2.gem} +0 -0
  54. data/lore.gemspec +26 -17
  55. data/spec/clause.rb +37 -0
  56. data/spec/fixtures/blank_models.rb +37 -0
  57. data/{test/model.rb → spec/fixtures/models.rb} +64 -41
  58. data/spec/fixtures/polymorphic_models.rb +68 -0
  59. data/spec/model_associations.rb +86 -0
  60. data/spec/model_create.rb +47 -0
  61. data/spec/model_definition.rb +151 -0
  62. data/spec/model_delete.rb +31 -0
  63. data/spec/model_inheritance.rb +50 -0
  64. data/spec/model_polymorphic.rb +85 -0
  65. data/spec/model_select.rb +101 -0
  66. data/spec/model_select_eager.rb +42 -0
  67. data/spec/model_union_select.rb +33 -0
  68. data/spec/model_update.rb +45 -0
  69. data/spec/model_validation.rb +20 -0
  70. data/spec/spec_db.sql +808 -0
  71. data/spec/spec_env.rb +19 -0
  72. data/spec/spec_helpers.rb +77 -0
  73. metadata +93 -82
  74. data/lib/lore/README.txt +0 -84
  75. data/lib/lore/behaviours/lockable.rb +0 -55
  76. data/lib/lore/behaviours/movable.rb +0 -72
  77. data/lib/lore/behaviours/paginated.rb +0 -31
  78. data/lib/lore/behaviours/versioned.rb +0 -36
  79. data/lib/lore/connection.rb +0 -152
  80. data/lib/lore/exception/invalid_klass_parameters.rb +0 -63
  81. data/lib/lore/exception/unknown_typecode.rb +0 -19
  82. data/lib/lore/result.rb +0 -119
  83. data/lib/lore/symbol.rb +0 -58
  84. data/lib/lore/table_accessor.rb +0 -1790
  85. data/lib/lore/table_deleter.rb +0 -116
  86. data/lib/lore/table_inserter.rb +0 -170
  87. data/lib/lore/table_instance.rb +0 -389
  88. data/lib/lore/table_selector.rb +0 -285
  89. data/lib/lore/table_updater.rb +0 -157
  90. data/lib/lore/validation.rb +0 -65
  91. data/lib/lore/validation/message.rb +0 -60
  92. data/lib/lore/validation/reason.rb +0 -52
  93. data/lore_test.log +0 -2366
  94. data/test/README +0 -31
  95. data/test/custom_models.rb +0 -18
  96. data/test/env.rb +0 -5
  97. data/test/prepare.rb +0 -37
  98. data/test/tc_aspect.rb +0 -58
  99. data/test/tc_cache.rb +0 -83
  100. data/test/tc_clause.rb +0 -104
  101. data/test/tc_deep_inheritance.rb +0 -49
  102. data/test/tc_factory.rb +0 -57
  103. data/test/tc_filter.rb +0 -37
  104. data/test/tc_form.rb +0 -32
  105. data/test/tc_model.rb +0 -140
  106. data/test/tc_prepare.rb +0 -44
  107. data/test/tc_refined_query.rb +0 -88
  108. data/test/tc_table_accessor.rb +0 -267
  109. data/test/tc_thread.rb +0 -100
  110. data/test/test_db.sql +0 -400
  111. data/test/test_lore.rb +0 -50
@@ -6,7 +6,14 @@ module Lore
6
6
  def html_escape_values_of(*attributes)
7
7
  attributes.each { |attrib|
8
8
  add_input_filter(attrib) { |a|
9
+ a = a.to_s
9
10
  a.gsub("'",''')
11
+ a.gsub("\"",'"')
12
+ }
13
+ add_output_filter(attrib) { |a|
14
+ a = a.to_s
15
+ a.gsub("'",''')
16
+ a.gsub("\"",'"')
10
17
  }
11
18
  }
12
19
  end
@@ -0,0 +1,53 @@
1
+
2
+ require('lore')
3
+
4
+ module Lore
5
+
6
+ class Lore::Table_Accessor
7
+ end
8
+
9
+ module Polymorphic_Class_Methods
10
+
11
+ def is_polymorphic(key=:concrete_model)
12
+ @polymorphic_attribute = key.to_sym
13
+ @polymorphic_attribute_index = get_fields_flat.index(key.to_sym)
14
+ @is_polymorphic = true
15
+ end
16
+
17
+ def is_polymorphic?
18
+ (@is_polymorphic == true)
19
+ end
20
+
21
+ def select_polymorphic(clause=nil, &block)
22
+ base_entities = select(clause, &block)
23
+ base_entities.map { |e|
24
+ cmodel = e.get_concrete_model
25
+ if cmodel then
26
+ e = cmodel.load(get_fields[table_name].first => e.pkey())
27
+ else
28
+ e = false
29
+ end
30
+ e
31
+ }
32
+ end
33
+
34
+ def polymorphic_attribute
35
+ @polymorphic_attribute
36
+ end
37
+ def polymorphic_attribute_index
38
+ @polymorphic_attribute_index
39
+ end
40
+
41
+ end
42
+
43
+ module Polymorphic_Instance_Methods
44
+
45
+ def get_concrete_model
46
+ concrete_model_name = self.__send__(self.class.polymorphic_attribute)
47
+ return unless concrete_model_name
48
+ eval(concrete_model_name)
49
+ end
50
+
51
+ end
52
+
53
+ end
@@ -0,0 +1,97 @@
1
+
2
+ module Lore
3
+
4
+ module Prepare
5
+
6
+ # Defines prepared statements like
7
+ # The_Model.by_id(id), The_Model.latest_entries(id, amount) etc.
8
+ def define_default_preps
9
+ return
10
+ unless @@prepared_statements[:default_preps] then
11
+ pkey_attrib_name = table_name + '.' << @primary_keys[table_name].first.to_s
12
+ prepare(:_by_id, Lore::Type.integer) { |e|
13
+ e.where(self.__send__(pkey_attrib_name.to_s.split('.')[-1].intern) == Lore::Clause.new('$1'))
14
+ e.limit(1)
15
+ }
16
+ prepare(:_latest, Lore::Type.integer) { |e|
17
+ e.where(true)
18
+ e.order_by(pkey_attrib_name, :desc)
19
+ e.limit(Lore::Clause.new('$1'))
20
+ }
21
+ @@prepared_statements[:default_preps] = true
22
+ end
23
+ end
24
+
25
+ end
26
+
27
+ # Model extension for prepared statements
28
+ module Prepare_Class_Methods
29
+
30
+ @@prepared_statements = Hash.new
31
+
32
+ def by_id(entity_id)
33
+ begin
34
+ return _by_id(entity_id).first
35
+ rescue ::Exception => excep
36
+ if @@prepared_statements[:default_preps] then
37
+ raise excep
38
+ else
39
+ raise ::Exception.new(excep.message + ' (call define_default_preps first?)')
40
+ end
41
+ end
42
+ end
43
+
44
+ # Prepares a query for execution. This offers four advantages:
45
+ # - The query doesn't have to be interpreted by the DB every time
46
+ # - The query call is available via direct method call.
47
+ # - DB validates against types, thus preventing SQL injection
48
+ # - It doesn't Lore require to compose the query string again. This
49
+ # effects the most significant performance gain (Up to 60% execution
50
+ # time in some benchmarks)
51
+ # Usage:
52
+ #
53
+ # Article.prepare(:by_name_and_date, Lore::Type::Integer, Lore::Type::Date) { |a,fields|
54
+ # a.where((Article.article_id == fields[0] &
55
+ # (Article.date == fields[1]))
56
+ # }
57
+ # Article.by_name_and_date('Merry Christmas', '20081224')
58
+ #
59
+ # From the PostgreSQL 7.4 Manual:
60
+ #
61
+ # "In some situations, the query plan produced by for a prepared statement may be
62
+ # inferior to the plan produced if the statement were submitted and executed normally.
63
+ # This is because when the statement is planned and the planner attempts to determine
64
+ # the optimal query plan, the actual values of any parameters specified in the
65
+ # statement are unavailable. PostgreSQL collects statistics on the distribution of
66
+ # data in the table, and can use constant values in a statement to make guesses about
67
+ # the likely result of executing the statement. Since this data is unavailable when
68
+ # planning prepared statements with parameters, the chosen plan may be suboptimal. To
69
+ # examine the query plan PostgreSQL has chosen for a prepared statement, use
70
+ # EXPLAIN EXECUTE. "
71
+ #
72
+ def prepare(plan_name, *args, &block)
73
+ # {{{
74
+ # log('PREPARE: TRYING CLASS METHOD ' << plan_name.to_s)
75
+ if !@@prepared_statements[plan_name] then
76
+ Table_Select.new(self).prepare(plan_name, args, &block)
77
+
78
+ # log('PREPARE: CREATE CLASS METHOD ' << plan_name.to_s)
79
+ instance_eval("
80
+ def #{plan_name.to_s}(*args)
81
+ execute_prepared(:#{plan_name}, args)
82
+ end")
83
+ @@prepared_statements[plan_name] = true
84
+ # log('PREPARE: CREATED CLASS METHOD ' << plan_name.to_s)
85
+ end
86
+ end # }}}
87
+
88
+ def execute_prepared(plan_name, *args)
89
+ plan_name = "#{table_name.gsub('.','_')}__#{plan_name.to_s}"
90
+ Table_Select.new(self).select_prepared(plan_name, args)
91
+ end
92
+
93
+
94
+
95
+ end
96
+
97
+ end
@@ -0,0 +1,1016 @@
1
+
2
+ require('logger')
3
+ require('rubygems')
4
+ require('lore')
5
+ require('lore/clause')
6
+ require('lore/model/aspect')
7
+ require('lore/model/associations')
8
+ require('lore/model/attribute_settings')
9
+ require('lore/model/filters')
10
+ require('lore/query')
11
+
12
+ require('lore/strategies/table_select')
13
+ require('lore/strategies/table_insert')
14
+ require('lore/strategies/table_update')
15
+ require('lore/strategies/table_delete')
16
+
17
+ require('lore/query_shortcuts')
18
+ require('lore/model/model_shortcuts')
19
+
20
+ require('lore/model/model_instance')
21
+ require('lore/model/polymorphic')
22
+ require('lore/model/prepare')
23
+ require('lore/model/mockable')
24
+ require('lore/cache/cacheable')
25
+
26
+ module Lore
27
+
28
+ class Table_Accessor
29
+ include Model_Instance
30
+ extend Aspect
31
+ extend Prepare
32
+ extend Query_Shortcuts
33
+ extend Model_Shortcuts
34
+ extend Polymorphic_Class_Methods
35
+ include Polymorphic_Instance_Methods
36
+ extend Prepare_Class_Methods
37
+ include Prepare
38
+ extend Cache::Cacheable
39
+
40
+ @@logger = Lore.logger
41
+
42
+ def self.log(message, level=:debug)
43
+ @@logger.debug(message)
44
+ end
45
+ def log(message, level=:debug)
46
+ @@logger.debug(message)
47
+ end
48
+
49
+ # Set in load_attribute_fields()
50
+ @__filters__ = false
51
+ @__associations__ = false
52
+ @__attributes__ = false
53
+
54
+ def self.__filters__
55
+ @__filters__
56
+ end
57
+ def self.__associations__
58
+ @__associations__
59
+ end
60
+ def self.__attributes__
61
+ @__attributes__
62
+ end
63
+
64
+ def self.__select_strategy__
65
+ @select_strategy
66
+ end
67
+ def self.__insert_strategy__
68
+ @insert_strategy
69
+ end
70
+ def self.__update_strategy__
71
+ @update_strategy
72
+ end
73
+ def self.__delete_strategy__
74
+ @delete_strategy
75
+ end
76
+
77
+ # Holds own table name, defaults to self.class if not given.
78
+ # If a schema has been set by
79
+ # table :table_name, :schema_name
80
+ # table_name is schema_name.table_name
81
+ @table_name = String.new
82
+
83
+ @labels = Array.new
84
+
85
+ public
86
+
87
+ ##########################################################################
88
+ # Constructor is usually wrapped by e.g. self.load or
89
+ # Model_Instancemarshal_load.
90
+ # Constructor just accepts a value hash, and returns a Table_Accessor
91
+ # instance holding it.
92
+ # Note that this method is operating on a Table_Accessor instance, not
93
+ # on class Table_Accessor itself.
94
+ def initialize(values, joined_models=[], cache=nil)
95
+ # {{{
96
+ # Note:
97
+ # 90% of additional time used on a query compared to
98
+ # plain, unprocessed SQL queries is consumed here - so
99
+ # be efficient!
100
+
101
+ @loaded_from_cache = (cache == :cached)
102
+ @joined_models = joined_models
103
+
104
+ if @loaded_from_cache then
105
+ @attribute_values_flat = values
106
+ else
107
+ @attribute_values_flat = {}
108
+ field_index = 0
109
+ models = [ self.class ]
110
+ models |= joined_models
111
+ models.each { |model|
112
+ tables = model.all_table_names
113
+ fields = model.get_fields_flat
114
+ # Increment over all fields, do not use each_with_index
115
+ fields.each { |field|
116
+ # First value set has precedence
117
+ @attribute_values_flat[field] ||= values[field_index]
118
+ field_index += 1
119
+ }
120
+ }
121
+ # Applying filter to *all* attribute values, including derived attributes.
122
+ # This way, an output filter can be added in a derived type that does not
123
+ # exist in a base type.
124
+
125
+ return if self.class.output_filters_disabled?
126
+
127
+ output_filters = self.class.__filters__.output_filters
128
+ @attribute_values_flat.each_pair { |attribute, value|
129
+ filter = output_filters[attribute]
130
+ @attribute_values_flat[attribute] = filter.call(value) if filter
131
+ }
132
+ end
133
+ end
134
+
135
+ # To be used indirectly via Model.polymorphic_select
136
+ # Parameter 'values' contains values for all fields returned from a
137
+ # polmymorphic select query.
138
+ def self.new_polymorphic(values, joined_models=[])
139
+ # {{{
140
+ # Parameter 'values' contains values for all fields returned from a
141
+ # polmymorphic select query. It thus contains (empty) fields that are
142
+ # not relevant for this model instance.
143
+ # Those have to be filtered out by resolving field indices and their
144
+ # offsets.
145
+ #
146
+ # Model.new expects values in following order:
147
+ #
148
+ # [ own fields, base klass fields, aggregeate klass fields, custom join fields ]
149
+ #
150
+ # But polymorphic selects return:
151
+ #
152
+ # [ polymorphic base klass fields, own fields, other base klass fields ... ]
153
+ #
154
+ # So value array has to be transformed accordingly, which is rather
155
+ # complicated.
156
+ #
157
+ fields = get_fields_flat
158
+ concrete_model_index = 0
159
+ concrete_model_name = values[polymorphic_attribute_index]
160
+ concrete_model = eval(concrete_model_name)
161
+
162
+ # We need to know where to inject values of the polymorphic
163
+ # base model:
164
+ concrete_base_joins = concrete_model.__associations__.joins
165
+ concrete_base_klasses = concrete_model.__associations__.base_klasses
166
+ inject_index = concrete_model.__attributes__.num_own_fields
167
+ # Be sure to iterate just like in
168
+ # Table_Select.build_joined_query strategy!
169
+ concrete_base_joins.each_pair { |table, foreign_base_tables|
170
+ # Ignore base tables - their fields are added automatically
171
+ # via __attributes__.num_fields below.
172
+ base_model = concrete_base_klasses[table].first
173
+ break if base_model == self # Offset ends with own fields
174
+ inject_index += base_model.__attributes__.num_fields
175
+ }
176
+
177
+ # Amount of polymorphic fields defined in this model
178
+ polymorphic_num_fields = @__attributes__.num_fields()
179
+ # Field offset should point to index where concrete
180
+ # fields begin.
181
+ field_offset = 0
182
+ @__associations__.concrete_models.each { |cm|
183
+ break if cm == concrete_model
184
+ concrete_model_index += 1
185
+ field_offset += (cm.__attributes__.num_fields - polymorphic_num_fields)
186
+ }
187
+
188
+ field_offset_end = (field_offset + concrete_model.__attributes__.num_fields)
189
+ basic_values = values[0...polymorphic_num_fields]
190
+ concrete_values = values[(polymorphic_num_fields + field_offset)...field_offset_end]
191
+
192
+ # Basic values from polymorphic base model have to be injected
193
+ # into concrete values at inject_index.
194
+ instance_values = concrete_values[0...inject_index]
195
+ instance_values += basic_values # Inject happens here
196
+ instance_values += concrete_values[inject_index..-1]
197
+
198
+ concrete_model.new(instance_values, joined_models)
199
+ end # }}}
200
+
201
+ def self.disable_output_filters
202
+ @output_filters_disabled = true
203
+ end
204
+ def self.enable_output_filters
205
+ @output_filters_disabled = false
206
+ end
207
+
208
+ def self.output_filters_disabled?
209
+ @output_filters_disabled || false
210
+ end
211
+
212
+ def attribute_values_by_table
213
+ return @attribute_values if @attribute_values
214
+
215
+ # Note that attributes might have been shadowed
216
+ # in @attribute_values flat, as first attribute
217
+ # takes precedence in casse two tables with common
218
+ # field name have been joined.
219
+ values = @attribute_values_flat
220
+
221
+ @attribute_values = Hash.new
222
+ field_index = 0
223
+
224
+ models = [ self.class ]
225
+ models |= @joined_models
226
+ models.each { |model|
227
+ tables = model.all_table_names
228
+ fields = model.get_fields
229
+ tables.each { |table|
230
+ map = {}
231
+ fields[table].each { |field_name|
232
+ map[field_name] = values[field_index]
233
+ field_index += 1
234
+ }
235
+ @attribute_values[table] = map
236
+ }
237
+ }
238
+
239
+ # Applying filter to *all* attribute values, including derived attributes.
240
+ # This way, an output filter can be added in a derived type that does not
241
+ # exist in a base type.
242
+ output_filters = self.class.__filters__.output_filters
243
+
244
+ @attribute_values.values.map { |v|
245
+ v.each_pair { |attribute, value|
246
+ filter = output_filters[attribute]
247
+ value = filter.call(value) if filter
248
+ value
249
+ }
250
+ }
251
+
252
+ @attribute_values_flat = {}
253
+ @attribute_values.values.each { |map| @attribute_values_flat.update map }
254
+
255
+ return @attribute_values
256
+ end # }}}
257
+
258
+
259
+
260
+ # Simulates inheritance: Delegate missing methods to parent Table_Accessor.
261
+ def self.method_missing(meth)
262
+ # {{{
263
+ if @is_a_klasses then
264
+ @is_a_klasses.each_pair { |foreign_key, k|
265
+ return (k.__send__(meth.to_s)) if k.respond_to? meth
266
+ }
267
+ end
268
+ raise ::Exception.new('Undefined method '<< meth.to_s << ' for ' << self.to_s)
269
+ end # }}}
270
+
271
+ # Inspect method
272
+ def self.inspect
273
+ # {{{
274
+ 'Lore::Table_Accessor: ' << self.to_s
275
+ end # }}}
276
+
277
+ # Recursively gets primary keys from parent, if own
278
+ # primary keys don't have been set, yet:
279
+ # Returns all derived primary keys WITHOUT OWN PKEYS.
280
+ def self.get_primary_keys
281
+ # {{{
282
+ @__attributes__.primary_keys
283
+ end # }}}
284
+
285
+ # Return primary key names of own table only,
286
+ # i.e. skipping inherited ones.
287
+ def self.get_own_primary_keys
288
+ # {{{
289
+ if !@own_primary_keys then
290
+ @own_primary_keys = @__attributes__.primary_keys[@table_name].uniq
291
+ end
292
+ @own_primary_keys
293
+ end # }}}
294
+
295
+ def self.key_array()
296
+ # {{{
297
+ # TODO: Use __attributes__ here
298
+ keys = Array.new
299
+ get_primary_keys.each_pair { |table, attribs|
300
+ attribs.each { |attrib|
301
+ keys.push attrib
302
+ }
303
+ }
304
+ return keys
305
+ end # }}}
306
+
307
+ # Recursively gets sequences from parent, if own
308
+ # sequences don't have been set, yet:
309
+ def self.get_sequences
310
+ # {{{
311
+
312
+ if @sequences.nil? then
313
+ if @is_a_klasses.nil? then
314
+ return Hash.new
315
+ else
316
+ seq_map = Hash.new
317
+ @is_a_klasses.each_pair { |foreign_key,k|
318
+ seq_map[k.table_name] = k.get_sequences
319
+ }
320
+ return seq_map
321
+ end
322
+ else
323
+ return @sequences
324
+ end
325
+
326
+ end # }}}
327
+
328
+ def self.set_sequences(arg) # :nodoc:
329
+ # {{{
330
+ @sequences = arg
331
+ end # }}}
332
+
333
+ # Returns base table of this model as String.
334
+ def self.table_name
335
+ @table_name
336
+ end
337
+
338
+ # Returns all (i.e. including joined) tables as
339
+ # Array of Strings, ordered by join order.
340
+ def self.all_table_names
341
+ # {{{
342
+ return @table_names if @table_names
343
+ @table_names = [@table_name]
344
+ @table_names += @__associations__.joins.keys_flat
345
+ return @table_names
346
+ end # }}}
347
+
348
+ # Returns all attribute fields as Hash of Array of Strings,
349
+ # in the same order as defined in the table, mapped by
350
+ # table names.
351
+ # Example: (Article < Content)
352
+ #
353
+ # Article.get_fields
354
+ # -->
355
+ # {
356
+ # 'public.content' => [ :content_id, :title, :date ],
357
+ # 'public.article' => [ :author, :lead_in ]
358
+ # }
359
+ #
360
+ # Also see get_fields_flat.
361
+ #
362
+ def self.get_fields
363
+ @fields ||= @__attributes__.fields
364
+ @fields
365
+ end
366
+ # Returns all attribute fields as Hash of Array of Strings,
367
+ # in the same order as defined in the table, mapped by
368
+ # table names.
369
+ # Example: (Article < Content)
370
+ #
371
+ # Article.get_fields_flat
372
+ # -->
373
+ # [ :content_id, :title, :date, :author, :lead_in
374
+ #
375
+ # Also see example in documentation to get_fields.
376
+ #
377
+ def self.get_fields_flat
378
+ @fields_flat ||= @__attributes__.fields_flat
379
+ @fields_flat
380
+ end
381
+
382
+ protected
383
+
384
+ # If this model is not to be located in a projects default context,
385
+ # you can tell Cuba which context to use via
386
+ #
387
+ # context :other_context_name
388
+ #
389
+ def self.context(context_name)
390
+ # {{{
391
+ @context = context_name
392
+ end
393
+ def self.get_context
394
+ @context
395
+ end
396
+ # }}}
397
+
398
+ # Define the base table of this model.
399
+ # Usage:
400
+ #
401
+ # class Content < Lore::Model
402
+ # table :content, :public $ table name is 'public.content'
403
+ # ...
404
+ # end
405
+ #
406
+ def self.table(model_table, model_schema=nil)
407
+ # {{{
408
+ model_table = "#{model_schema}.#{model_table}" if model_schema
409
+ @table_name = model_table
410
+ init_model(); # Table name is all we need to bootstrap a model
411
+ end # }}}
412
+
413
+ # Usage:
414
+ #
415
+ # primary_key :some_field, :some_field_sequence_name
416
+ # primary_key :some_other_field, :some_other_field_sequence_name
417
+ #
418
+ def self.primary_key(*prim_key)
419
+ # {{{
420
+ @__associations__.add_primary_key(prim_key.at(0), prim_key.at(1))
421
+ @__attributes__.add_primary_key(prim_key.at(0), prim_key.at(1))
422
+ end # }}}
423
+
424
+ # Define this model as derived from another model class,
425
+ # which realized model inheritance.
426
+ # Note that ruby only allows only single inheritance itself,
427
+ # but is_a may be used for multiple inheritance, too.
428
+ # Usage:
429
+ #
430
+ # class Article < Content
431
+ # table :article, :public
432
+ # is_a Content, :content_id
433
+ # is_a Asset, :asset_id
434
+ # end
435
+ #
436
+ # Effects:
437
+ # Creating an Article record will also create referenced
438
+ # Content and Assed records.
439
+ # Selecting an Article entity will automatically join
440
+ # Content and Asset records.
441
+ # Filters and hooks from Content and Asset are inherited.
442
+ #
443
+ # Article.is_a?(Content) --> true
444
+ # Article.is_a?(Asset) --> true(!)
445
+ #
446
+ def self.is_a(*args)
447
+ # {{{
448
+ parent = args.at(0)
449
+
450
+ @__filters__.inherit(parent)
451
+ @__attributes__.add_base_model(parent)
452
+ @__associations__.add_base_model(parent, args[1..-1])
453
+
454
+ define_entity_access_methods(parent, args[1..-1])
455
+ end # }}}
456
+
457
+ # Usage in derived classes:
458
+ # aggregates Other::Module::Other_Klass
459
+ # aggregates Another::Module::Another_Klass
460
+ #
461
+ # Effects:
462
+ # Performs eager join on given model on every select on this model.
463
+ # Unlike is_a, If foo.aggregates bar then creating/deleting a foo will not
464
+ # create/delete bar instance, but loading a foo will aggregate bar
465
+ # automatically, like is_a.
466
+ def self.aggregates(*args)
467
+ # {{{
468
+ parent = args.at(0)
469
+
470
+ @__filters__.inherit(parent)
471
+ @__attributes__.add_aggregate_model(parent)
472
+ @__associations__.add_aggregate_model(parent, args[1..-1])
473
+
474
+ define_entity_access_methods(parent, args[1..-1])
475
+ end # }}}
476
+
477
+
478
+ class << self
479
+ alias org_is_a? is_a?
480
+ def is_a?(model)
481
+ org_is_a?(model) || @__associations__ && @__associations__.has_joined_model?(model)
482
+ end
483
+ end
484
+
485
+ # Usage:
486
+ #
487
+ # has_a Other_Model, :foreign_key_1 <, foreign_key_2, ... >
488
+ #
489
+ # note that foreign_keys are fields in _this_ table,
490
+ # not the foreign one.
491
+ #
492
+ def self.has_a(*args)
493
+ # {{{
494
+ @__associations__.add_has_a(args.at(0), args[1..-1])
495
+ define_entity_access_methods(args.at(0), args[1..-1])
496
+ end # }}}
497
+
498
+ # Usage:
499
+ #
500
+ # has_n Other_Model, :foreign_key_1 <, foreign_key_2, ... >
501
+ #
502
+ # note that foreign_keys are fields in _this_ table,
503
+ # not the foreign one.
504
+ #
505
+ def self.has_n(other, *args)
506
+ # {{{
507
+ @__associations__.add_has_n(other, *args)
508
+ define_entities_access_methods(args.at(0), args[1..-1])
509
+ end # }}}
510
+
511
+ # Usage:
512
+ #
513
+ # maps Model_A => [ :foreign_keys, ... ], Model_B => [ foreign_keys, ... ]
514
+ #
515
+ # note that foreign_keys are fields in _this_ table,
516
+ # not the foreign one.
517
+ #
518
+ def self.maps(*accessors)
519
+ # {{{
520
+ @__associations__.add_mapping(*accessors)
521
+ end # }}}
522
+
523
+ def self.validates(attrib, constraints)
524
+ # {{{
525
+ @__attributes__.add_constraints(attrib, constraints)
526
+ end # }}}
527
+
528
+ def self.add_input_filter(attrib, &block)
529
+ @__filters__.add_input_filter(attrib, &block)
530
+ end
531
+
532
+ def self.add_output_filter(attrib, &block)
533
+ @__filters__.add_output_filter(attrib, &block)
534
+ end
535
+
536
+ # Demands a value to be set for create and update procedures.
537
+ def self.expects(attrib_name, klass=nil)
538
+ # {{{
539
+ @__attributes__.set_required(attrib_name)
540
+ end # }}}
541
+
542
+ def self.explicit(*args)
543
+ Aurita.log { "Model.explicit is deprecated (called for #{self.to_s}" }
544
+ end
545
+
546
+ def self.hide_attribute(*args)
547
+ Aurita.log { "Model.hidden_attribute is deprecated (called for #{self.to_s}.
548
+ Use Model.hidden instead" }
549
+ end
550
+
551
+ # Define an attribute as hidden.
552
+ # Especially needed for form generation (hidden fields)
553
+ def self.hides(attrib_name)
554
+ # {{{
555
+ @__attributes__.add_hidden(attrib_name)
556
+ end
557
+
558
+ def self.get_hidden()
559
+ return @hidden_attributes if @hidden_attributes
560
+ Hash.new
561
+ end # }}}
562
+
563
+ def self.belongs_to(*args)
564
+ # {{{
565
+ @__associations__.add_belongs_to(args.at(0), args[1..-1])
566
+ end # }}}
567
+
568
+ # Define attribute to use as label for instances of a Table_Accessor,
569
+ # e.g. for select boxes.
570
+ def self.use_label(*attribs)
571
+ # {{{
572
+ @labels = attribs.map { |e|
573
+
574
+ e = e.to_s # also removes Array wraps like [:attrib_name]
575
+
576
+ if((e.kind_of? Clause) or (e.include?('.'))) then
577
+ e = e.to_s
578
+ else
579
+ e = @table_name + '.' << e.to_s
580
+ end
581
+ }
582
+ @label = attribs[0]
583
+ end
584
+ # Returns full name of attribute set to use as label e.g. for
585
+ # select boxes.
586
+ def self.get_label
587
+ @labels[0]
588
+ end
589
+
590
+ def self.get_labels
591
+ @labels
592
+ end
593
+
594
+ # }}}
595
+
596
+ public
597
+
598
+ # Returns full attribute name of given attribute
599
+ def self.[](attribute_name)
600
+ # {{{
601
+ return "#{@table_name}.#{attribute_name}"
602
+ end # }}}
603
+
604
+
605
+ def self.update(&block)
606
+ # {{{
607
+ query_string = @update_strategy.block_update(&block)
608
+ end # def }}}
609
+
610
+ def self.select(clause=nil, &block)
611
+ # {{{
612
+ if(!clause.nil? && !clause.to_s.include?('*,')) then
613
+ query_string = @select_strategy.select_query(clause.to_s, &block)
614
+ return Clause.new(query_string[:query])
615
+ end
616
+ return Select_Query.new(self, clause.to_s, &block)
617
+ end # }}}
618
+
619
+ def self.select_query(clause=nil, &block)
620
+ query_string = @select_strategy.select_query(clause.to_s, &block)
621
+ end
622
+
623
+ def self.polymorphic_select(clause=nil, &block)
624
+ # {{{
625
+ if(!clause.nil? && !clause.to_s.include?('*,')) then
626
+ query_string = @select_strategy.select_query(clause.to_s, nil, true, &block)
627
+ return Clause.new(query_string[:query])
628
+ end
629
+ return Select_Query.new(self, clause.to_s, true, &block)
630
+ end # }}}
631
+
632
+ def self.polymorphic_select_query(clause=nil, &block)
633
+ query_string = @select_strategy.select_query(clause.to_s, nil, true, &block)
634
+ end
635
+
636
+ # Same as select, but returns scalar value.
637
+ def self.select_value(what, &block)
638
+ # {{{
639
+ db_result = @select_strategy.select(what, &block)
640
+ row = db_result.get_row
641
+ return row.first if row.first
642
+ return {}
643
+ end # }}}
644
+
645
+ def self.select_values(what, &block)
646
+ # {{{
647
+ @select_strategy.select(what, &block).get_rows
648
+ end # }}}
649
+
650
+ # Same as select, but returns scalar value.
651
+ def self.polymorphic_select_value(what, &block)
652
+ # {{{
653
+ db_result = @select_strategy.select(what, true, &block)
654
+ row = db_result.get_row
655
+ return row.first if row.first
656
+ return {}
657
+ end # }}}
658
+
659
+ def self.polymorphic_select_values(what, &block)
660
+ # {{{
661
+ @select_strategy.select(what, true, &block).get_rows
662
+ end # }}}
663
+
664
+ # Wrap explicit select. Example:
665
+ # SomeModule::SomeAccessor.explicit_insert({
666
+ # table_name_A =>
667
+ # {'some_field'=>'2',
668
+ # 'other_field'=>'3'},
669
+ # table_name_A =>
670
+ # {'another_field'=>'5'}
671
+ # })
672
+ # Note that field in 'field'=>'value' is exactly the
673
+ # field name in the table (e.g. table_name_A) it holds.
674
+ def self.explicit_insert(keys)
675
+ # {{{
676
+ @insert_strategy.perform_insert(keys)
677
+ end # }}}
678
+
679
+ # Wrap default select. Example:
680
+ # SomeModule::SomeAccessor.insert({
681
+ # 'some_field'=>'2',
682
+ # 'other_field'=>'3',
683
+ # 'another_field'=>'5'
684
+ # })
685
+ # Note that field in 'field'=>'value' is exactly the
686
+ # field name in the table it holds.
687
+ # Table_Accessor.insert basically resolves an explicit
688
+ # hash and passes it to Table_Accessor.explicit_insert.
689
+ def self.insert(keys)
690
+ # {{{
691
+ # Sequence values only are known after insert operation,
692
+ # so we have to retreive the complete key_hash back from
693
+ # Table_Inserter.perform_insert:
694
+ key_hash = @insert_strategy.perform_insert(keys)
695
+ # key_hash has been extended by sequence_values now, so
696
+ # we return it:
697
+ key_hash
698
+ end # }}}
699
+
700
+ def self.distribute_attrib_values(attrib_values)
701
+ # {{{
702
+ values = {}
703
+ # Predefine
704
+ attrib_name_array = []
705
+ # distribute attrib names to tables:
706
+ @__attributes__.fields().each_pair { |table, attribs|
707
+ table_values = {}
708
+ attrib_name_array = []
709
+
710
+ attrib_values.each_pair { |attrib_name, attrib_value|
711
+ attrib_name_array = attrib_name.split('.') unless attrib_name.instance_of?(Symbol)
712
+ attrib_name_array ||= []
713
+ attrib_short_name = false
714
+ if attrib_name_array.at(2)
715
+ attrib_short_name = attrib_name_array.at(2)
716
+ else
717
+ attrib_name = attrib_name.to_sym
718
+ end
719
+
720
+ if attribs.include? attrib_name then
721
+ table_values[attrib_name] = attrib_value
722
+ elsif attrib_short_name &&
723
+ attribs.include?(attrib_short_name) &&
724
+ table == "#{attrib_name_array.at(0)}.#{attrib_name_array.at(1)}" then
725
+ table_values[attrib_name_array.at(2)] = attrib_value
726
+ end
727
+ }
728
+ values[table] = table_values
729
+ }
730
+ values
731
+ end # }}}
732
+
733
+ # Returns a new Table_Accessor instance by inserting given attribute
734
+ # values into db and returning an instance for further operations.
735
+ def self.create(attrib_values={})
736
+ # {{{
737
+ attrib_values[:concrete_model] = self.to_s
738
+ before_create(attrib_values)
739
+
740
+ input_filters = @__filters__.input_filters
741
+ attrib_key = ''
742
+ attrib_name = ''
743
+
744
+ attrib_values.each_pair { |attrib_name, attrib_value|
745
+ if attrib_name.instance_of? Symbol then
746
+ attrib_key = attrib_name
747
+ else
748
+ attrib_key = attrib_name.split('.')[-1].to_sym
749
+ end
750
+
751
+ if (input_filters && input_filters[attrib_key]) then
752
+ attrib_values[attrib_name] = input_filters[attrib_key].call(attrib_value)
753
+ end
754
+ }
755
+ after_filters(attrib_values)
756
+
757
+ values = distribute_attrib_values(attrib_values)
758
+
759
+ before_validation(values)
760
+ if @__associations__.polymorphics then
761
+ Lore.logger.debug { "Polymorphic create on #{self.to_s}" }
762
+ @__associations__.polymorphics.each_pair { |table, model_field|
763
+ values[table][model_field] = self.to_s
764
+ }
765
+ end
766
+ Lore::Validation::Parameter_Validator.validate(self, values)
767
+
768
+ before_insert(attrib_values)
769
+
770
+ # retreive all final attrib values after insert: (this way, also
771
+ # sequence values are resolved):
772
+
773
+ attrib_values = @insert_strategy.perform_insert(values)
774
+
775
+ # This would be a double check, as self.load already filters
776
+ # non-primary key attributes
777
+ select_keys = Hash.new
778
+ @__associations__.primary_keys[table_name].each { |key|
779
+ select_keys["#{table_name}.#{key}"] = attrib_values[table_name][key]
780
+ }
781
+
782
+ obj = self.load(select_keys)
783
+ raise ::Exception.new("Could not load created instance of #{self.to_s}: #{select_keys.inspect}") unless obj
784
+ after_create(obj)
785
+
786
+ return obj
787
+
788
+ end # }}}
789
+
790
+ # Return new Table_Accessor instance by loading an existing entry from
791
+ # table if present, or false if no entry has been found.
792
+ # Accepts any combination of :primary_key => 'value'
793
+ # Also allows inherited primary keys.
794
+ def self.load(keys)
795
+ # {{{
796
+ before_load(keys)
797
+
798
+ select_keys = {}
799
+ value = false
800
+ @__associations__.primary_keys.each_pair { |table, pkeys|
801
+ pkeys.each { |attrib_name|
802
+ full_attrib_name = "#{table}.#{attrib_name}"
803
+ value = keys[full_attrib_name] # The more explicit, the better.
804
+ value ||= keys[attrib_name.to_sym] # Symbols are supposed to be most frequently used
805
+ value ||= keys[attrib_name.to_s]
806
+ select_keys[full_attrib_name] = value unless value.nil?
807
+ }
808
+ }
809
+
810
+ return false if select_keys.empty?
811
+
812
+ # We have to perform a select here instead of returning
813
+ # the instance with given attribute values, as this is the
814
+ # only way to retreive attribute values set in the DB via
815
+ # default values, triggers, etc.
816
+ c = Clause.new
817
+ select_keys.each_pair { |k,v|
818
+ c & (Clause.new(k.to_s.dup) == v.to_s)
819
+ }
820
+
821
+ instance = self.select { |inst|
822
+ inst.where(c)
823
+ inst.limit(1)
824
+ }.first
825
+
826
+ return false unless instance
827
+ return instance
828
+
829
+ end # }}}
830
+
831
+ # Load an instance by only providing primary key values.
832
+ # This is useful for polymorphic treatment of models, as
833
+ # you don't have to know about primary key names.
834
+ #
835
+ # Example:
836
+ #
837
+ # The_Model.get(123)
838
+ #
839
+ # Resolves to
840
+ #
841
+ # The_Model.load(:the_model_id => 123)
842
+ #
843
+ # If there is more than one primary key attribute,
844
+ # key values have to be provided in the same order they
845
+ # are specified in the database.
846
+ #
847
+ def self.get(*key_values)
848
+ # {{{
849
+ pkeys = {}
850
+ get_primary_keys[table_name].uniq.each_with_index { |pkey, idx|
851
+ pkeys[pkey] = key_values.at(idx)
852
+ }
853
+ load(pkeys)
854
+ end # }}}
855
+
856
+ # Delete this object and use Table_Deleter to delete its entity tuple
857
+ # from database.
858
+ def self.delete(value_keys=nil, &block)
859
+ # {{{
860
+ if value_keys then
861
+ before_delete(value_keys)
862
+ @delete_strategy.perform_delete(value_keys)
863
+ after_delete(value_keys)
864
+ else
865
+ @delete_strategy.block_delete(&block)
866
+ end
867
+ end # }}}
868
+
869
+ private
870
+
871
+ # Send a hollow query to db, thus only field names are returned.
872
+ # This works (and has to work) on empty tables, too.
873
+ #
874
+ # For every attribute, a class method with the attribute's name is defined
875
+ # in order to retreive the absolute field name (schema.table.field) by calling
876
+ # Table_Accessor.field
877
+ def self.init_model()
878
+ # {{{
879
+ Lore::Context.enter(@context) unless @context.nil?
880
+ begin
881
+ fields_result = Lore::Connection.perform("SELECT * FROM #{@table_name} WHERE false")
882
+ ensure
883
+ Lore::Context.leave unless @context.nil?
884
+ end
885
+ @__attributes__ = Attribute_Settings.new(self,
886
+ fields_result.field_names(),
887
+ fields_result.field_types())
888
+
889
+ @__associations__ = Associations.new(self)
890
+
891
+ @__filters__ = Filters.new(self)
892
+ @__attributes__.types.each_pair { |table, attributes|
893
+ attributes.each_pair { |field,type|
894
+ input_filter = Lore::Type_Filters.in[type]
895
+ output_filter = Lore::Type_Filters.out[type]
896
+ @__filters__.add_input_filter(field, &input_filter) if input_filter
897
+ @__filters__.add_output_filter(field, &output_filter) if output_filter
898
+ }
899
+ }
900
+
901
+ @select_strategy = Table_Select.new(self)
902
+ @insert_strategy = Table_Insert.new(self)
903
+ @delete_strategy = Table_Delete.new(self)
904
+ @update_strategy = Table_Update.new(self)
905
+
906
+ define_attribute_clause_methods()
907
+ end # }}}
908
+
909
+ def self.define_attribute_clause_methods
910
+ # {{{
911
+ if !@context.nil? then
912
+ Lore::Context.enter(@context)
913
+ context_switched = true
914
+ end
915
+ if @__attributes__[@table_name] then
916
+ @__attributes__[@table_name].each { |attribute|
917
+ # Only define methods on own attributes or on
918
+ # attributes only a parent Table_Accessor provides:
919
+ attribute_type = @__attributes__.types[@table_name][attribute]
920
+ method =
921
+ "def self.#{attribute}()
922
+ Clause.new('#{@table_name}.#{attribute}', '', '', { :add_types => [ #{attribute_type}], :add_name => '' })
923
+ end"
924
+ class_eval(method)
925
+
926
+ "def #{attribute}()
927
+ attr(:#{attribute})
928
+ end"
929
+ class_eval(method)
930
+
931
+ method =
932
+ "def set_#{attribute}(value)
933
+ set_attribute_value(:#{attribute.to_s}, value)
934
+ end"
935
+ class_eval(method)
936
+
937
+ method =
938
+ "def #{attribute}=(value)
939
+ set_attribute_value(:#{attribute.to_s}, value)
940
+ end"
941
+ class_eval(method)
942
+ }
943
+ end
944
+ Lore::Context.leave if context_switched
945
+
946
+ end # }}}
947
+
948
+ # Meta-programs class instance methods for accessing types
949
+ # associated via has_a.
950
+ def self.define_entity_access_methods(accessor, foreign_keys, type_name=nil)
951
+ # {{{
952
+ type_name = accessor.to_s.split('::').at(-1).downcase unless type_name
953
+
954
+ define_method(type_name) {
955
+
956
+ has_a_keys = Hash.new
957
+ foreign_keys.each { |foreign_key|
958
+ # self.<foreign_key> will return corresponding value
959
+ # no matter if this klass or a super klass is holding it
960
+ has_a_keys[accessor[foreign_key]] = self.__send__ foreign_key
961
+ }
962
+ return accessor.load(has_a_keys)
963
+ }
964
+
965
+ define_method("set_#{type_name}") { |other|
966
+ foreign_keys.each { |foreign_key|
967
+ # other.<foreign_key> will return corresponding value
968
+ # no matter if this klass or a super klass is holding it
969
+ self[foreign_key] = other.pkey
970
+ }
971
+ }
972
+
973
+ define_method("#{type_name}=") { |other|
974
+ self.__send__("set_#{type_name}", other)
975
+ }
976
+
977
+ define_method("set_#{type_name}!") { |other|
978
+ self.__send__("set_#{type_name}", other)
979
+ self.__send__("commit")
980
+ }
981
+ end # }}}
982
+
983
+ # Meta-programs class instance methods for accessing types
984
+ # associated via has_n.
985
+ def self.define_entities_access_methods(accessor, values)
986
+ # {{{
987
+ type_name = accessor.to_s.split('::').at(-1).downcase unless type_name
988
+
989
+ define_method('add_'+type_name) { |values|
990
+ values.update(get_primary_key_values)
991
+ accessor.create(values)
992
+ }
993
+ define_method(type_name+'_list') {
994
+ foreign_key_values = Hash.new
995
+
996
+ accessor.get_foreign_keys[self.table_name].each { |key|
997
+ foreign_key_values[key] = get_attribute_value[key]
998
+ }
999
+ accessor.all_with(foreign_key_values)
1000
+ }
1001
+
1002
+ define_method("remove_#{type_name}") { |*keys|
1003
+ # For usage:
1004
+ # Car.remove_wheel(wheel_obj)
1005
+ #
1006
+ if keys.length == 1 && keys.first.respond_to?(:pkey) then
1007
+ keys = [ keys.first.pkey ]
1008
+ end
1009
+ }
1010
+
1011
+ end # }}}
1012
+
1013
+
1014
+ end # class
1015
+
1016
+ end # module Lore