activerecord 1.13.2 → 1.14.0

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 (144) hide show
  1. data/CHANGELOG +452 -10
  2. data/RUNNING_UNIT_TESTS +1 -1
  3. data/lib/active_record.rb +5 -2
  4. data/lib/active_record/acts/list.rb +1 -1
  5. data/lib/active_record/acts/tree.rb +29 -25
  6. data/lib/active_record/aggregations.rb +3 -2
  7. data/lib/active_record/associations.rb +783 -337
  8. data/lib/active_record/associations/association_collection.rb +7 -12
  9. data/lib/active_record/associations/association_proxy.rb +62 -24
  10. data/lib/active_record/associations/belongs_to_association.rb +27 -46
  11. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +50 -0
  12. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +38 -38
  13. data/lib/active_record/associations/has_many_association.rb +61 -56
  14. data/lib/active_record/associations/has_many_through_association.rb +144 -0
  15. data/lib/active_record/associations/has_one_association.rb +22 -16
  16. data/lib/active_record/base.rb +482 -182
  17. data/lib/active_record/calculations.rb +225 -0
  18. data/lib/active_record/callbacks.rb +7 -7
  19. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +162 -47
  20. data/lib/active_record/connection_adapters/abstract/quoting.rb +1 -1
  21. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +2 -1
  22. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +21 -1
  23. data/lib/active_record/connection_adapters/abstract_adapter.rb +34 -2
  24. data/lib/active_record/connection_adapters/db2_adapter.rb +107 -61
  25. data/lib/active_record/connection_adapters/mysql_adapter.rb +29 -6
  26. data/lib/active_record/connection_adapters/openbase_adapter.rb +349 -0
  27. data/lib/active_record/connection_adapters/{oci_adapter.rb → oracle_adapter.rb} +125 -59
  28. data/lib/active_record/connection_adapters/postgresql_adapter.rb +24 -21
  29. data/lib/active_record/connection_adapters/sqlite_adapter.rb +47 -8
  30. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +36 -16
  31. data/lib/active_record/connection_adapters/sybase_adapter.rb +684 -0
  32. data/lib/active_record/fixtures.rb +42 -17
  33. data/lib/active_record/locking.rb +36 -15
  34. data/lib/active_record/migration.rb +111 -8
  35. data/lib/active_record/observer.rb +25 -1
  36. data/lib/active_record/reflection.rb +103 -41
  37. data/lib/active_record/schema.rb +2 -2
  38. data/lib/active_record/schema_dumper.rb +55 -18
  39. data/lib/active_record/timestamp.rb +6 -6
  40. data/lib/active_record/validations.rb +65 -40
  41. data/lib/active_record/vendor/db2.rb +10 -5
  42. data/lib/active_record/vendor/simple.rb +693 -702
  43. data/lib/active_record/version.rb +2 -2
  44. data/rakefile +4 -4
  45. data/test/aaa_create_tables_test.rb +25 -6
  46. data/test/abstract_unit.rb +39 -1
  47. data/test/adapter_test.rb +31 -4
  48. data/test/associations_cascaded_eager_loading_test.rb +106 -0
  49. data/test/associations_go_eager_test.rb +85 -16
  50. data/test/associations_join_model_test.rb +338 -0
  51. data/test/associations_test.rb +129 -50
  52. data/test/base_test.rb +204 -49
  53. data/test/binary_test.rb +1 -1
  54. data/test/calculations_test.rb +169 -0
  55. data/test/callbacks_test.rb +5 -23
  56. data/test/class_inheritable_attributes_test.rb +1 -1
  57. data/test/column_alias_test.rb +1 -1
  58. data/test/connections/native_mysql/connection.rb +1 -0
  59. data/test/connections/native_openbase/connection.rb +22 -0
  60. data/test/connections/{native_oci → native_oracle}/connection.rb +7 -9
  61. data/test/connections/native_sqlite/connection.rb +1 -1
  62. data/test/connections/native_sqlite3/connection.rb +1 -0
  63. data/test/connections/native_sqlite3/in_memory_connection.rb +1 -0
  64. data/test/connections/native_sybase/connection.rb +24 -0
  65. data/test/defaults_test.rb +18 -0
  66. data/test/deprecated_associations_test.rb +2 -2
  67. data/test/deprecated_finder_test.rb +0 -6
  68. data/test/finder_test.rb +26 -23
  69. data/test/fixtures/accounts.yml +10 -0
  70. data/test/fixtures/author.rb +31 -6
  71. data/test/fixtures/author_favorites.yml +4 -0
  72. data/test/fixtures/categories/special_categories.yml +9 -0
  73. data/test/fixtures/categories/subsubdir/arbitrary_filename.yml +4 -0
  74. data/test/fixtures/categories_posts.yml +4 -0
  75. data/test/fixtures/categorization.rb +5 -0
  76. data/test/fixtures/categorizations.yml +11 -0
  77. data/test/fixtures/category.rb +6 -0
  78. data/test/fixtures/company.rb +17 -5
  79. data/test/fixtures/company_in_module.rb +19 -5
  80. data/test/fixtures/db_definitions/db2.drop.sql +3 -0
  81. data/test/fixtures/db_definitions/db2.sql +121 -100
  82. data/test/fixtures/db_definitions/db22.sql +2 -2
  83. data/test/fixtures/db_definitions/firebird.drop.sql +4 -0
  84. data/test/fixtures/db_definitions/firebird.sql +26 -0
  85. data/test/fixtures/db_definitions/mysql.drop.sql +3 -0
  86. data/test/fixtures/db_definitions/mysql.sql +21 -1
  87. data/test/fixtures/db_definitions/openbase.drop.sql +2 -0
  88. data/test/fixtures/db_definitions/openbase.sql +282 -0
  89. data/test/fixtures/db_definitions/openbase2.drop.sql +2 -0
  90. data/test/fixtures/db_definitions/openbase2.sql +7 -0
  91. data/test/fixtures/db_definitions/{oci.drop.sql → oracle.drop.sql} +6 -0
  92. data/test/fixtures/db_definitions/{oci.sql → oracle.sql} +25 -4
  93. data/test/fixtures/db_definitions/{oci2.drop.sql → oracle2.drop.sql} +0 -0
  94. data/test/fixtures/db_definitions/{oci2.sql → oracle2.sql} +0 -0
  95. data/test/fixtures/db_definitions/postgresql.drop.sql +4 -0
  96. data/test/fixtures/db_definitions/postgresql.sql +22 -1
  97. data/test/fixtures/db_definitions/schema.rb +32 -0
  98. data/test/fixtures/db_definitions/sqlite.drop.sql +3 -0
  99. data/test/fixtures/db_definitions/sqlite.sql +18 -0
  100. data/test/fixtures/db_definitions/sqlserver.drop.sql +3 -0
  101. data/test/fixtures/db_definitions/sqlserver.sql +23 -3
  102. data/test/fixtures/db_definitions/sybase.drop.sql +31 -0
  103. data/test/fixtures/db_definitions/sybase.sql +204 -0
  104. data/test/fixtures/db_definitions/sybase2.drop.sql +4 -0
  105. data/test/fixtures/db_definitions/sybase2.sql +5 -0
  106. data/test/fixtures/developers.yml +6 -1
  107. data/test/fixtures/developers_projects.yml +4 -0
  108. data/test/fixtures/funny_jokes.yml +14 -0
  109. data/test/fixtures/joke.rb +6 -0
  110. data/test/fixtures/legacy_thing.rb +3 -0
  111. data/test/fixtures/legacy_things.yml +3 -0
  112. data/test/fixtures/mixin.rb +1 -1
  113. data/test/fixtures/person.rb +4 -1
  114. data/test/fixtures/post.rb +26 -1
  115. data/test/fixtures/project.rb +1 -0
  116. data/test/fixtures/reader.rb +4 -0
  117. data/test/fixtures/readers.yml +4 -0
  118. data/test/fixtures/reply.rb +2 -1
  119. data/test/fixtures/tag.rb +5 -0
  120. data/test/fixtures/tagging.rb +6 -0
  121. data/test/fixtures/taggings.yml +18 -0
  122. data/test/fixtures/tags.yml +7 -0
  123. data/test/fixtures/tasks.yml +2 -2
  124. data/test/fixtures/topic.rb +2 -2
  125. data/test/fixtures/topics.yml +1 -0
  126. data/test/fixtures_test.rb +47 -13
  127. data/test/inheritance_test.rb +2 -2
  128. data/test/locking_test.rb +15 -1
  129. data/test/method_scoping_test.rb +248 -13
  130. data/test/migration_test.rb +68 -11
  131. data/test/mixin_nested_set_test.rb +1 -1
  132. data/test/modules_test.rb +6 -1
  133. data/test/readonly_test.rb +1 -1
  134. data/test/reflection_test.rb +63 -9
  135. data/test/schema_dumper_test.rb +41 -0
  136. data/test/{synonym_test_oci.rb → synonym_test_oracle.rb} +1 -1
  137. data/test/threaded_connections_test.rb +10 -0
  138. data/test/unconnected_test.rb +12 -5
  139. data/test/validations_test.rb +197 -10
  140. metadata +295 -260
  141. data/test/fixtures/db_definitions/create_oracle_db.bat +0 -0
  142. data/test/fixtures/db_definitions/create_oracle_db.sh +0 -0
  143. data/test/fixtures/fixture_database.sqlite +0 -0
  144. data/test/fixtures/fixture_database_2.sqlite +0 -0
@@ -47,11 +47,11 @@ module ActiveRecord
47
47
  cols = columns('schema_info')
48
48
 
49
49
  info = info.map do |k,v|
50
- v = quote(v, cols.detect { |c| c.name == k.to_s })
50
+ v = Base.connection.quote(v, cols.detect { |c| c.name == k.to_s })
51
51
  "#{k} = #{v}"
52
52
  end
53
53
 
54
- update "UPDATE schema_info SET #{info.join(", ")}"
54
+ Base.connection.update "UPDATE #{Migrator.schema_info_table_name} SET #{info.join(", ")}"
55
55
  end
56
56
  end
57
57
  end
@@ -3,6 +3,12 @@ module ActiveRecord
3
3
  # output format (i.e., ActiveRecord::Schema).
4
4
  class SchemaDumper #:nodoc:
5
5
  private_class_method :new
6
+
7
+ # A list of tables which should not be dumped to the schema.
8
+ # Acceptable values are strings as well as regexp.
9
+ # This setting is only used if ActiveRecord::Base.schema_format == :ruby
10
+ cattr_accessor :ignore_tables
11
+ @@ignore_tables = []
6
12
 
7
13
  def self.dump(connection=ActiveRecord::Base.connection, stream=STDOUT)
8
14
  new(connection).dump(stream)
@@ -43,32 +49,63 @@ HEADER
43
49
 
44
50
  def tables(stream)
45
51
  @connection.tables.sort.each do |tbl|
46
- next if tbl == "schema_info"
52
+ next if ["schema_info", ignore_tables].flatten.any? do |ignored|
53
+ case ignored
54
+ when String: tbl == ignored
55
+ when Regexp: tbl =~ ignored
56
+ else
57
+ raise StandardError, 'ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values.'
58
+ end
59
+ end
47
60
  table(tbl, stream)
48
61
  end
49
62
  end
50
63
 
51
64
  def table(table, stream)
52
65
  columns = @connection.columns(table)
53
-
54
- stream.print " create_table #{table.inspect}"
55
- stream.print ", :id => false" if !columns.detect { |c| c.name == "id" }
56
- stream.print ", :force => true"
57
- stream.puts " do |t|"
58
-
59
- columns.each do |column|
60
- next if column.name == "id"
61
- stream.print " t.column #{column.name.inspect}, #{column.type.inspect}"
62
- stream.print ", :limit => #{column.limit.inspect}" if column.limit != @types[column.type][:limit]
63
- stream.print ", :default => #{column.default.inspect}" if !column.default.nil?
64
- stream.print ", :null => false" if !column.null
66
+ begin
67
+ tbl = StringIO.new
68
+
69
+ if @connection.respond_to?(:pk_and_sequence_for)
70
+ pk, pk_seq = @connection.pk_and_sequence_for(table)
71
+ end
72
+ pk ||= 'id'
73
+
74
+ tbl.print " create_table #{table.inspect}"
75
+ if columns.detect { |c| c.name == pk }
76
+ if pk != 'id'
77
+ tbl.print %Q(, :primary_key => "#{pk}")
78
+ end
79
+ else
80
+ tbl.print ", :id => false"
81
+ end
82
+ tbl.print ", :force => true"
83
+ tbl.puts " do |t|"
84
+
85
+ columns.each do |column|
86
+ raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" if @types[column.type].nil?
87
+ next if column.name == pk
88
+ tbl.print " t.column #{column.name.inspect}, #{column.type.inspect}"
89
+ tbl.print ", :limit => #{column.limit.inspect}" if column.limit != @types[column.type][:limit]
90
+ tbl.print ", :default => #{column.default.inspect}" if !column.default.nil?
91
+ tbl.print ", :null => false" if !column.null
92
+ tbl.puts
93
+ end
94
+
95
+ tbl.puts " end"
96
+ tbl.puts
97
+
98
+ indexes(table, tbl)
99
+
100
+ tbl.rewind
101
+ stream.print tbl.read
102
+ rescue => e
103
+ stream.puts "# Could not dump table #{table.inspect} because of following #{e.class}"
104
+ stream.puts "# #{e.message}"
65
105
  stream.puts
66
106
  end
67
-
68
- stream.puts " end"
69
- stream.puts
70
-
71
- indexes(table, stream)
107
+
108
+ stream
72
109
  end
73
110
 
74
111
  def indexes(table, stream)
@@ -4,8 +4,8 @@ module ActiveRecord
4
4
  # automatically included, so you don't need to do that manually.
5
5
  #
6
6
  # This behavior can be turned off by setting <tt>ActiveRecord::Base.record_timestamps = false</tt>.
7
- # This behavior can use GMT by setting <tt>ActiveRecord::Base.timestamps_gmt = true</tt>
8
- module Timestamp
7
+ # This behavior by default uses local time, but can use UTC by setting <tt>ActiveRecord::Base.default_timezone = :utc</tt>
8
+ module Timestamp
9
9
  def self.append_features(base) # :nodoc:
10
10
  super
11
11
 
@@ -16,14 +16,14 @@ module ActiveRecord
16
16
  alias_method :update_without_timestamps, :update
17
17
  alias_method :update, :update_with_timestamps
18
18
  end
19
- end
20
-
19
+ end
20
+
21
21
  def create_with_timestamps #:nodoc:
22
22
  if record_timestamps
23
23
  t = ( self.class.default_timezone == :utc ? Time.now.utc : Time.now )
24
24
  write_attribute('created_at', t) if respond_to?(:created_at) && created_at.nil?
25
25
  write_attribute('created_on', t) if respond_to?(:created_on) && created_on.nil?
26
-
26
+
27
27
  write_attribute('updated_at', t) if respond_to?(:updated_at)
28
28
  write_attribute('updated_on', t) if respond_to?(:updated_on)
29
29
  end
@@ -38,7 +38,7 @@ module ActiveRecord
38
38
  end
39
39
  update_without_timestamps
40
40
  end
41
- end
41
+ end
42
42
 
43
43
  class Base
44
44
  # Records the creation date and possibly time in created_on (date only) or created_at (date and time) and the update date and possibly
@@ -6,11 +6,11 @@ module ActiveRecord
6
6
  # rescue ActiveRecord::RecordInvalid => invalid
7
7
  # puts invalid.record.errors
8
8
  # end
9
- class RecordInvalid < ActiveRecordError
9
+ class RecordInvalid < ActiveRecordError #:nodoc:
10
10
  attr_reader :record
11
- def initialize(record, *args)
11
+ def initialize(record)
12
12
  @record = record
13
- super(*args)
13
+ super("Validation failed: #{@record.errors.full_messages.join(", ")}")
14
14
  end
15
15
  end
16
16
 
@@ -31,8 +31,8 @@ module ActiveRecord
31
31
  :accepted => "must be accepted",
32
32
  :empty => "can't be empty",
33
33
  :blank => "can't be blank",
34
- :too_long => "is too long (max is %d characters)",
35
- :too_short => "is too short (min is %d characters)",
34
+ :too_long => "is too long (maximum is %d characters)",
35
+ :too_short => "is too short (minimum is %d characters)",
36
36
  :wrong_length => "is the wrong length (should be %d characters)",
37
37
  :taken => "has already been taken",
38
38
  :not_a_number => "is not a number"
@@ -147,7 +147,7 @@ module ActiveRecord
147
147
  def empty?
148
148
  return @errors.empty?
149
149
  end
150
-
150
+
151
151
  # Removes all the errors that have been added.
152
152
  def clear
153
153
  @errors = {}
@@ -155,11 +155,14 @@ module ActiveRecord
155
155
 
156
156
  # Returns the total number of errors added. Two errors added to the same attribute will be counted as such
157
157
  # with this as well.
158
- def count
158
+ def size
159
159
  error_count = 0
160
160
  @errors.each_value { |attribute| error_count += attribute.length }
161
161
  error_count
162
162
  end
163
+
164
+ alias_method :count, :size
165
+ alias_method :length, :size
163
166
  end
164
167
 
165
168
 
@@ -213,6 +216,9 @@ module ActiveRecord
213
216
  alias_method :save_without_validation, :save
214
217
  alias_method :save, :save_with_validation
215
218
 
219
+ alias_method :save_without_validation!, :save!
220
+ alias_method :save!, :save_with_validation!
221
+
216
222
  alias_method :update_attribute_without_validation_skipping, :update_attribute
217
223
  alias_method :update_attribute, :update_attribute_with_validation_skipping
218
224
  end
@@ -361,8 +367,14 @@ module ActiveRecord
361
367
  end
362
368
  end
363
369
 
364
- # Validates that the specified attributes are not blank (as defined by Object#blank?). Happens by default on save.
370
+ # Validates that the specified attributes are not blank (as defined by Object#blank?). Happens by default on save. Example:
365
371
  #
372
+ # class Person < ActiveRecord::Base
373
+ # validates_presence_of :first_name
374
+ # end
375
+ #
376
+ # The first_name attribute must be in the object and it cannot be blank.
377
+ #
366
378
  # Configuration options:
367
379
  # * <tt>message</tt> - A custom error message (default is: "can't be blank")
368
380
  # * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
@@ -403,7 +415,7 @@ module ActiveRecord
403
415
  # * <tt>in</tt> - A synonym(or alias) for :within
404
416
  # * <tt>allow_nil</tt> - Attribute may be nil; skip validation.
405
417
  #
406
- # * <tt>too_long</tt> - The error message if the attribute goes over the maximum (default is: "is too long (max is %d characters)")
418
+ # * <tt>too_long</tt> - The error message if the attribute goes over the maximum (default is: "is too long (maximum is %d characters)")
407
419
  # * <tt>too_short</tt> - The error message if the attribute goes under the minimum (default is: "is too short (min is %d characters)")
408
420
  # * <tt>wrong_length</tt> - The error message if using the :is method and the attribute is the wrong size (default is: "is the wrong length (should be %d characters)")
409
421
  # * <tt>message</tt> - The error message to use for a :minimum, :maximum, or :is violation. An alias of the appropriate too_long/too_short/wrong_length message
@@ -436,31 +448,35 @@ module ActiveRecord
436
448
  option_value = options[range_options.first]
437
449
 
438
450
  case option
439
- when :within, :in
440
- raise ArgumentError, ":#{option} must be a Range" unless option_value.is_a?(Range)
441
-
442
- too_short = options[:too_short] % option_value.begin
443
- too_long = options[:too_long] % option_value.end
444
-
445
- validates_each(attrs, options) do |record, attr, value|
446
- if value.nil? or value.size < option_value.begin
447
- record.errors.add(attr, too_short)
448
- elsif value.size > option_value.end
449
- record.errors.add(attr, too_long)
451
+ when :within, :in
452
+ raise ArgumentError, ":#{option} must be a Range" unless option_value.is_a?(Range)
453
+
454
+ too_short = options[:too_short] % option_value.begin
455
+ too_long = options[:too_long] % option_value.end
456
+
457
+ validates_each(attrs, options) do |record, attr, value|
458
+ if value.nil? or value.split(//).size < option_value.begin
459
+ record.errors.add(attr, too_short)
460
+ elsif value.split(//).size > option_value.end
461
+ record.errors.add(attr, too_long)
462
+ end
450
463
  end
451
- end
452
- when :is, :minimum, :maximum
453
- raise ArgumentError, ":#{option} must be a nonnegative Integer" unless option_value.is_a?(Integer) and option_value >= 0
464
+ when :is, :minimum, :maximum
465
+ raise ArgumentError, ":#{option} must be a nonnegative Integer" unless option_value.is_a?(Integer) and option_value >= 0
454
466
 
455
- # Declare different validations per option.
456
- validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" }
457
- message_options = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long }
467
+ # Declare different validations per option.
468
+ validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" }
469
+ message_options = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long }
458
470
 
459
- message = (options[:message] || options[message_options[option]]) % option_value
471
+ message = (options[:message] || options[message_options[option]]) % option_value
460
472
 
461
- validates_each(attrs, options) do |record, attr, value|
462
- record.errors.add(attr, message) unless !value.nil? and value.size.method(validity_checks[option])[option_value]
463
- end
473
+ validates_each(attrs, options) do |record, attr, value|
474
+ if value.kind_of?(String)
475
+ record.errors.add(attr, message) unless !value.nil? and value.split(//).size.method(validity_checks[option])[option_value]
476
+ else
477
+ record.errors.add(attr, message) unless !value.nil? and value.size.method(validity_checks[option])[option_value]
478
+ end
479
+ end
464
480
  end
465
481
  end
466
482
 
@@ -471,7 +487,14 @@ module ActiveRecord
471
487
  # can be named "davidhh".
472
488
  #
473
489
  # class Person < ActiveRecord::Base
474
- # validates_uniqueness_of :user_name, :scope => "account_id"
490
+ # validates_uniqueness_of :user_name, :scope => :account_id
491
+ # end
492
+ #
493
+ # It can also validate whether the value of the specified attributes are unique based on multiple scope parameters. For example,
494
+ # making sure that a teacher can only be on the schedule once per semester for a particular class.
495
+ #
496
+ # class TeacherSchedule < ActiveRecord::Base
497
+ # validates_uniqueness_of :teacher_id, :scope => [:semester_id, :class_id]
475
498
  # end
476
499
  #
477
500
  # When the record is created, a check is performed to make sure that no record exists in the database with the given value for the specified
@@ -479,24 +502,27 @@ module ActiveRecord
479
502
  #
480
503
  # Configuration options:
481
504
  # * <tt>message</tt> - Specifies a custom error message (default is: "has already been taken")
482
- # * <tt>scope</tt> - Ensures that the uniqueness is restricted to a condition of "scope = record.scope"
505
+ # * <tt>scope</tt> - One or more columns by which to limit the scope of the uniquness constraint.
483
506
  # * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
484
507
  # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
485
508
  # method, proc or string should return or evaluate to a true or false value.
509
+
486
510
  def validates_uniqueness_of(*attr_names)
487
511
  configuration = { :message => ActiveRecord::Errors.default_error_messages[:taken] }
488
512
  configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
489
513
 
490
514
  validates_each(attr_names,configuration) do |record, attr_name, value|
491
- condition_sql = "#{attr_name} #{attribute_condition(value)}"
515
+ condition_sql = "#{record.class.table_name}.#{attr_name} #{attribute_condition(value)}"
492
516
  condition_params = [value]
493
517
  if scope = configuration[:scope]
494
- scope_value = record.send(scope)
495
- condition_sql << " AND #{scope} #{attribute_condition(scope_value)}"
496
- condition_params << scope_value
518
+ Array(scope).map do |scope_item|
519
+ scope_value = record.send(scope_item)
520
+ condition_sql << " AND #{record.class.table_name}.#{scope_item} #{attribute_condition(scope_value)}"
521
+ condition_params << scope_value
522
+ end
497
523
  end
498
524
  unless record.new_record?
499
- condition_sql << " AND #{record.class.primary_key} <> ?"
525
+ condition_sql << " AND #{record.class.table_name}.#{record.class.primary_key} <> ?"
500
526
  condition_params << record.send(:id)
501
527
  end
502
528
  if record.class.find(:first, :conditions => [condition_sql, *condition_params])
@@ -696,7 +722,6 @@ module ActiveRecord
696
722
  def save_with_validation(perform_validation = true)
697
723
  if perform_validation && valid? || !perform_validation
698
724
  save_without_validation
699
- true
700
725
  else
701
726
  false
702
727
  end
@@ -704,9 +729,9 @@ module ActiveRecord
704
729
 
705
730
  # Attempts to save the record just like Base#save but will raise a RecordInvalid exception instead of returning false
706
731
  # if the record is not valid.
707
- def save!
732
+ def save_with_validation!
708
733
  if valid?
709
- save(false)
734
+ save_without_validation!
710
735
  else
711
736
  raise RecordInvalid.new(self)
712
737
  end
@@ -66,7 +66,7 @@ module DB2
66
66
  end
67
67
 
68
68
  def connect(server_name, user_name = '', auth = '')
69
- check_rc(SQLConnect(@handle, server_name, user_name, auth))
69
+ check_rc(SQLConnect(@handle, server_name, user_name.to_s, auth.to_s))
70
70
  end
71
71
 
72
72
  def set_connect_attr(attr, value)
@@ -110,13 +110,18 @@ module DB2
110
110
  check_rc(rc)
111
111
  end
112
112
 
113
- def columns(table_name)
114
- check_rc(SQLColumns(@handle, "", "%", table_name, "%"))
113
+ def columns(table_name, schema_name = '%')
114
+ check_rc(SQLColumns(@handle, '', schema_name.upcase, table_name.upcase, '%'))
115
115
  fetch_all
116
116
  end
117
117
 
118
- def tables
119
- check_rc(SQLTables(@handle, "", "%", "%", "TABLE"))
118
+ def tables(schema_name = '%')
119
+ check_rc(SQLTables(@handle, '', schema_name.upcase, '%', 'TABLE'))
120
+ fetch_all
121
+ end
122
+
123
+ def indexes(table_name, schema_name = '')
124
+ check_rc(SQLStatistics(@handle, '', schema_name.upcase, table_name.upcase, SQL_INDEX_ALL, SQL_ENSURE))
120
125
  fetch_all
121
126
  end
122
127
 
@@ -1,702 +1,693 @@
1
- # :title: Transaction::Simple
2
- #
3
- # == Licence
4
- #
5
- # Permission is hereby granted, free of charge, to any person obtaining a copy
6
- # of this software and associated documentation files (the "Software"), to
7
- # deal in the Software without restriction, including without limitation the
8
- # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
9
- # sell copies of the Software, and to permit persons to whom the Software is
10
- # furnished to do so, subject to the following conditions:
11
- #
12
- # The above copyright notice and this permission notice shall be included in
13
- # all copies or substantial portions of the Software.
14
- #
15
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
- # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21
- # IN THE SOFTWARE.
22
- #--
23
- # Transaction::Simple
24
- # Simple object transaction support for Ruby
25
- # Version 1.11
26
- #
27
- # Copyright (c) 2003 Austin Ziegler
28
- #
29
- # $Id: simple.rb,v 1.2 2004/08/20 13:56:37 webster132 Exp $
30
- #
31
- # ==========================================================================
32
- # Revision History ::
33
- # YYYY.MM.DD Change ID Developer
34
- # Description
35
- # --------------------------------------------------------------------------
36
- # 2003.07.29 Austin Ziegler
37
- # Added debugging capabilities and VERSION string.
38
- # 2003.08.21 Austin Ziegler
39
- # Added named transactions.
40
- #
41
- # ==========================================================================
42
- #++
43
- require 'thread'
44
-
45
- # The "Transaction" namespace can be used for additional transactional
46
- # support objects and modules.
47
- module Transaction
48
-
49
- # A standard exception for transactional errors.
50
- class TransactionError < StandardError; end
51
- # A standard exception for transactional errors involving the acquisition
52
- # of locks for Transaction::Simple::ThreadSafe.
53
- class TransactionThreadError < StandardError; end
54
-
55
- # = Transaction::Simple for Ruby
56
- # Simple object transaction support for Ruby
57
- #
58
- # == Introduction
59
- #
60
- # Transaction::Simple provides a generic way to add active transactional
61
- # support to objects. The transaction methods added by this module will
62
- # work with most objects, excluding those that cannot be <i>Marshal</i>ed
63
- # (bindings, procedure objects, IO instances, or singleton objects).
64
- #
65
- # The transactions supported by Transaction::Simple are not backed
66
- # transactions; that is, they have nothing to do with any sort of data
67
- # store. They are "live" transactions occurring in memory and in the
68
- # object itself. This is to allow "test" changes to be made to an object
69
- # before making the changes permanent.
70
- #
71
- # Transaction::Simple can handle an "infinite" number of transactional
72
- # levels (limited only by memory). If I open two transactions, commit the
73
- # first, but abort the second, the object will revert to the original
74
- # version.
75
- #
76
- # Transaction::Simple supports "named" transactions, so that multiple
77
- # levels of transactions can be committed, aborted, or rewound by
78
- # referring to the appropriate name of the transaction. Names may be any
79
- # object *except* +nil+.
80
- #
81
- # Copyright:: Copyright 2003 by Austin Ziegler
82
- # Version:: 1.1
83
- # Licence:: MIT-Style
84
- #
85
- # Thanks to David Black for help with the initial concept that led to this
86
- # library.
87
- #
88
- # == Usage
89
- # include 'transaction/simple'
90
- #
91
- # v = "Hello, you." # => "Hello, you."
92
- # v.extend(Transaction::Simple) # => "Hello, you."
93
- #
94
- # v.start_transaction # => ... (a Marshal string)
95
- # v.transaction_open? # => true
96
- # v.gsub!(/you/, "world") # => "Hello, world."
97
- #
98
- # v.rewind_transaction # => "Hello, you."
99
- # v.transaction_open? # => true
100
- #
101
- # v.gsub!(/you/, "HAL") # => "Hello, HAL."
102
- # v.abort_transaction # => "Hello, you."
103
- # v.transaction_open? # => false
104
- #
105
- # v.start_transaction # => ... (a Marshal string)
106
- # v.start_transaction # => ... (a Marshal string)
107
- #
108
- # v.transaction_open? # => true
109
- # v.gsub!(/you/, "HAL") # => "Hello, HAL."
110
- #
111
- # v.commit_transaction # => "Hello, HAL."
112
- # v.transaction_open? # => true
113
- # v.abort_transaction # => "Hello, you."
114
- # v.transaction_open? # => false
115
- #
116
- # == Named Transaction Usage
117
- # v = "Hello, you." # => "Hello, you."
118
- # v.extend(Transaction::Simple) # => "Hello, you."
119
- #
120
- # v.start_transaction(:first) # => ... (a Marshal string)
121
- # v.transaction_open? # => true
122
- # v.transaction_open?(:first) # => true
123
- # v.transaction_open?(:second) # => false
124
- # v.gsub!(/you/, "world") # => "Hello, world."
125
- #
126
- # v.start_transaction(:second) # => ... (a Marshal string)
127
- # v.gsub!(/world/, "HAL") # => "Hello, HAL."
128
- # v.rewind_transaction(:first) # => "Hello, you."
129
- # v.transaction_open? # => true
130
- # v.transaction_open?(:first) # => true
131
- # v.transaction_open?(:second) # => false
132
- #
133
- # v.gsub!(/you/, "world") # => "Hello, world."
134
- # v.start_transaction(:second) # => ... (a Marshal string)
135
- # v.gsub!(/world/, "HAL") # => "Hello, HAL."
136
- # v.transaction_name # => :second
137
- # v.abort_transaction(:first) # => "Hello, you."
138
- # v.transaction_open? # => false
139
- #
140
- # v.start_transaction(:first) # => ... (a Marshal string)
141
- # v.gsub!(/you/, "world") # => "Hello, world."
142
- # v.start_transaction(:second) # => ... (a Marshal string)
143
- # v.gsub!(/world/, "HAL") # => "Hello, HAL."
144
- #
145
- # v.commit_transaction(:first) # => "Hello, HAL."
146
- # v.transaction_open? # => false
147
- #
148
- # == Contraindications
149
- #
150
- # While Transaction::Simple is very useful, it has some severe limitations
151
- # that must be understood. Transaction::Simple:
152
- #
153
- # * uses Marshal. Thus, any object which cannot be <i>Marshal</i>ed cannot
154
- # use Transaction::Simple.
155
- # * does not manage resources. Resources external to the object and its
156
- # instance variables are not managed at all. However, all instance
157
- # variables and objects "belonging" to those instance variables are
158
- # managed. If there are object reference counts to be handled,
159
- # Transaction::Simple will probably cause problems.
160
- # * is not inherently thread-safe. In the ACID ("atomic, consistent,
161
- # isolated, durable") test, Transaction::Simple provides CD, but it is
162
- # up to the user of Transaction::Simple to provide isolation and
163
- # atomicity. Transactions should be considered "critical sections" in
164
- # multi-threaded applications. If thread safety and atomicity is
165
- # absolutely required, use Transaction::Simple::ThreadSafe, which uses a
166
- # Mutex object to synchronize the accesses on the object during the
167
- # transactional operations.
168
- # * does not necessarily maintain Object#__id__ values on rewind or abort.
169
- # This may change for future versions that will be Ruby 1.8 or better
170
- # *only*. Certain objects that support #replace will maintain
171
- # Object#__id__.
172
- # * Can be a memory hog if you use many levels of transactions on many
173
- # objects.
174
- #
175
- module Simple
176
- VERSION = '1.1.1.0';
177
-
178
- # Sets the Transaction::Simple debug object. It must respond to #<<.
179
- # Sets the transaction debug object. Debugging will be performed
180
- # automatically if there's a debug object. The generic transaction error
181
- # class.
182
- def self.debug_io=(io)
183
- raise TransactionError, "Transaction Error: the transaction debug object must respond to #<<" unless io.respond_to?(:<<)
184
- @tdi = io
185
- end
186
-
187
- # Returns the Transaction::Simple debug object. It must respond to #<<.
188
- def self.debug_io
189
- @tdi
190
- end
191
-
192
- # If +name+ is +nil+ (default), then returns +true+ if there is
193
- # currently a transaction open.
194
- #
195
- # If +name+ is specified, then returns +true+ if there is currently a
196
- # transaction that responds to +name+ open.
197
- def transaction_open?(name = nil)
198
- if name.nil?
199
- Transaction::Simple.debug_io << "Transaction [#{(@__transaction_checkpoint__.nil?) ? 'closed' : 'open'}]\n" unless Transaction::Simple.debug_io.nil?
200
- return (not @__transaction_checkpoint__.nil?)
201
- else
202
- Transaction::Simple.debug_io << "Transaction(#{name.inspect}) [#{(@__transaction_checkpoint__.nil?) ? 'closed' : 'open'}]\n" unless Transaction::Simple.debug_io.nil?
203
- return ((not @__transaction_checkpoint__.nil?) and @__transaction_names__.include?(name))
204
- end
205
- end
206
-
207
- # Returns the current name of the transaction. Transactions not
208
- # explicitly named are named +nil+.
209
- def transaction_name
210
- raise TransactionError, "Transaction Error: No transaction open." if @__transaction_checkpoint__.nil?
211
- Transaction::Simple.debug_io << "#{'|' * @__transaction_level__} Transaction Name: #{@__transaction_names__[-1].inspect}\n" unless Transaction::Simple.debug_io.nil?
212
- @__transaction_names__[-1]
213
- end
214
-
215
- # Starts a transaction. Stores the current object state. If a
216
- # transaction name is specified, the transaction will be named.
217
- # Transaction names must be unique. Transaction names of +nil+ will be
218
- # treated as unnamed transactions.
219
- def start_transaction(name = nil)
220
- @__transaction_level__ ||= 0
221
- @__transaction_names__ ||= []
222
-
223
- if name.nil?
224
- @__transaction_names__ << nil
225
- s = ""
226
- else
227
- raise TransactionError, "Transaction Error: Named transactions must be unique." if @__transaction_names__.include?(name)
228
- @__transaction_names__ << name
229
- s = "(#{name.inspect})"
230
- end
231
-
232
- @__transaction_level__ += 1
233
-
234
- Transaction::Simple.debug_io << "#{'>' * @__transaction_level__} Start Transaction#{s}\n" unless Transaction::Simple.debug_io.nil?
235
-
236
- @__transaction_checkpoint__ = Marshal.dump(self)
237
- end
238
-
239
- # Rewinds the transaction. If +name+ is specified, then the intervening
240
- # transactions will be aborted and the named transaction will be
241
- # rewound. Otherwise, only the current transaction is rewound.
242
- def rewind_transaction(name = nil)
243
- raise TransactionError, "Transaction Error: Cannot rewind. There is no current transaction." if @__transaction_checkpoint__.nil?
244
- if name.nil?
245
- __rewind_this_transaction
246
- s = ""
247
- else
248
- raise TransactionError, "Transaction Error: Cannot rewind to transaction #{name.inspect} because it does not exist." unless @__transaction_names__.include?(name)
249
- s = "(#{name})"
250
-
251
- while @__transaction_names__[-1] != name
252
- @__transaction_checkpoint__ = __rewind_this_transaction
253
- Transaction::Simple.debug_io << "#{'|' * @__transaction_level__} Rewind Transaction#{s}\n" unless Transaction::Simple.debug_io.nil?
254
- @__transaction_level__ -= 1
255
- @__transaction_names__.pop
256
- end
257
- __rewind_this_transaction
258
- end
259
- Transaction::Simple.debug_io << "#{'|' * @__transaction_level__} Rewind Transaction#{s}\n" unless Transaction::Simple.debug_io.nil?
260
- self
261
- end
262
-
263
- # Aborts the transaction. Resets the object state to what it was before
264
- # the transaction was started and closes the transaction. If +name+ is
265
- # specified, then the intervening transactions and the named transaction
266
- # will be aborted. Otherwise, only the current transaction is aborted.
267
- def abort_transaction(name = nil)
268
- raise TransactionError, "Transaction Error: Cannot abort. There is no current transaction." if @__transaction_checkpoint__.nil?
269
- if name.nil?
270
- __abort_transaction(name)
271
- else
272
- raise TransactionError, "Transaction Error: Cannot abort nonexistant transaction #{name.inspect}." unless @__transaction_names__.include?(name)
273
-
274
- __abort_transaction(name) while @__transaction_names__.include?(name)
275
- end
276
- self
277
- end
278
-
279
- # If +name+ is +nil+ (default), the current transaction level is closed
280
- # out and the changes are committed.
281
- #
282
- # If +name+ is specified and +name+ is in the list of named
283
- # transactions, then all transactions are closed and committed until the
284
- # named transaction is reached.
285
- def commit_transaction(name = nil)
286
- raise TransactionError, "Transaction Error: Cannot commit. There is no current transaction." if @__transaction_checkpoint__.nil?
287
-
288
- if name.nil?
289
- s = ""
290
- __commit_transaction
291
- Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} Commit Transaction#{s}\n" unless Transaction::Simple.debug_io.nil?
292
- else
293
- raise TransactionError, "Transaction Error: Cannot commit nonexistant transaction #{name.inspect}." unless @__transaction_names__.include?(name)
294
- s = "(#{name})"
295
-
296
- while @__transaction_names__[-1] != name
297
- Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} Commit Transaction#{s}\n" unless Transaction::Simple.debug_io.nil?
298
- __commit_transaction
299
- end
300
- Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} Commit Transaction#{s}\n" unless Transaction::Simple.debug_io.nil?
301
- __commit_transaction
302
- end
303
- self
304
- end
305
-
306
- # Alternative method for calling the transaction methods. An optional
307
- # name can be specified for named transaction support.
308
- #
309
- # #transaction(:start):: #start_transaction
310
- # #transaction(:rewind):: #rewind_transaction
311
- # #transaction(:abort):: #abort_transaction
312
- # #transaction(:commit):: #commit_transaction
313
- # #transaction(:name):: #transaction_name
314
- # #transaction:: #transaction_open?
315
- def transaction(action = nil, name = nil)
316
- case action
317
- when :start
318
- start_transaction(name)
319
- when :rewind
320
- rewind_transaction(name)
321
- when :abort
322
- abort_transaction(name)
323
- when :commit
324
- commit_transaction(name)
325
- when :name
326
- transaction_name
327
- when nil
328
- transaction_open?(name)
329
- end
330
- end
331
-
332
- def __abort_transaction(name = nil) #:nodoc:
333
- @__transaction_checkpoint__ = __rewind_this_transaction
334
-
335
- if name.nil?
336
- s = ""
337
- else
338
- s = "(#{name.inspect})"
339
- end
340
-
341
- Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} Abort Transaction#{s}\n" unless Transaction::Simple.debug_io.nil?
342
- @__transaction_level__ -= 1
343
- @__transaction_names__.pop
344
- if @__transaction_level__ < 1
345
- @__transaction_level__ = 0
346
- @__transaction_names__ = []
347
- end
348
- end
349
-
350
- TRANSACTION_CHECKPOINT = "@__transaction_checkpoint__" #:nodoc:
351
- SKIP_TRANSACTION_VARS = [TRANSACTION_CHECKPOINT, "@__transaction_level__"] #:nodoc:
352
-
353
- def __rewind_this_transaction #:nodoc:
354
- r = Marshal.restore(@__transaction_checkpoint__)
355
-
356
- begin
357
- self.replace(r) if respond_to?(:replace)
358
- rescue
359
- nil
360
- end
361
-
362
- r.instance_variables.each do |i|
363
- next if SKIP_TRANSACTION_VARS.include?(i)
364
- if respond_to?(:instance_variable_get)
365
- instance_variable_set(i, r.instance_variable_get(i))
366
- else
367
- instance_eval(%q|#{i} = r.instance_eval("#{i}")|)
368
- end
369
- end
370
-
371
- if respond_to?(:instance_variable_get)
372
- return r.instance_variable_get(TRANSACTION_CHECKPOINT)
373
- else
374
- return r.instance_eval(TRANSACTION_CHECKPOINT)
375
- end
376
- end
377
-
378
- def __commit_transaction #:nodoc:
379
- if respond_to?(:instance_variable_get)
380
- @__transaction_checkpoint__ = Marshal.restore(@__transaction_checkpoint__).instance_variable_get(TRANSACTION_CHECKPOINT)
381
- else
382
- @__transaction_checkpoint__ = Marshal.restore(@__transaction_checkpoint__).instance_eval(TRANSACTION_CHECKPOINT)
383
- end
384
-
385
- @__transaction_level__ -= 1
386
- @__transaction_names__.pop
387
- if @__transaction_level__ < 1
388
- @__transaction_level__ = 0
389
- @__transaction_names__ = []
390
- end
391
- end
392
-
393
- private :__abort_transaction, :__rewind_this_transaction, :__commit_transaction
394
-
395
- # = Transaction::Simple::ThreadSafe
396
- # Thread-safe simple object transaction support for Ruby.
397
- # Transaction::Simple::ThreadSafe is used in the same way as
398
- # Transaction::Simple. Transaction::Simple::ThreadSafe uses a Mutex
399
- # object to ensure atomicity at the cost of performance in threaded
400
- # applications.
401
- #
402
- # Transaction::Simple::ThreadSafe will not wait to obtain a lock; if the
403
- # lock cannot be obtained immediately, a
404
- # Transaction::TransactionThreadError will be raised.
405
- #
406
- # Thanks to Mauricio Fern�ndez for help with getting this part working.
407
- module ThreadSafe
408
- VERSION = '1.1.1.0';
409
-
410
- include Transaction::Simple
411
-
412
- SKIP_TRANSACTION_VARS = Transaction::Simple::SKIP_TRANSACTION_VARS.dup #:nodoc:
413
- SKIP_TRANSACTION_VARS << "@__transaction_mutex__"
414
-
415
- Transaction::Simple.instance_methods(false) do |meth|
416
- next if meth == "transaction"
417
- arg = "(name = nil)" unless meth == "transaction_name"
418
- module_eval <<-EOS
419
- def #{meth}#{arg}
420
- if (@__transaction_mutex__ ||= Mutex.new).try_lock
421
- result = super
422
- @__transaction_mutex__.unlock
423
- return result
424
- else
425
- raise TransactionThreadError, "Transaction Error: Cannot obtain lock for ##{meth}"
426
- end
427
- ensure
428
- @__transaction_mutex__.unlock
429
- end
430
- EOS
431
- end
432
- end
433
- end
434
- end
435
-
436
- if $0 == __FILE__
437
- require 'test/unit'
438
-
439
- class Test__Transaction_Simple < Test::Unit::TestCase #:nodoc:
440
- VALUE = "Now is the time for all good men to come to the aid of their country."
441
-
442
- def setup
443
- @value = VALUE.dup
444
- @value.extend(Transaction::Simple)
445
- end
446
-
447
- def test_extended
448
- assert_respond_to(@value, :start_transaction)
449
- end
450
-
451
- def test_started
452
- assert_equal(false, @value.transaction_open?)
453
- assert_nothing_raised { @value.start_transaction }
454
- assert_equal(true, @value.transaction_open?)
455
- end
456
-
457
- def test_rewind
458
- assert_equal(false, @value.transaction_open?)
459
- assert_raises(Transaction::TransactionError) { @value.rewind_transaction }
460
- assert_nothing_raised { @value.start_transaction }
461
- assert_equal(true, @value.transaction_open?)
462
- assert_nothing_raised { @value.gsub!(/men/, 'women') }
463
- assert_not_equal(VALUE, @value)
464
- assert_nothing_raised { @value.rewind_transaction }
465
- assert_equal(true, @value.transaction_open?)
466
- assert_equal(VALUE, @value)
467
- end
468
-
469
- def test_abort
470
- assert_equal(false, @value.transaction_open?)
471
- assert_raises(Transaction::TransactionError) { @value.abort_transaction }
472
- assert_nothing_raised { @value.start_transaction }
473
- assert_equal(true, @value.transaction_open?)
474
- assert_nothing_raised { @value.gsub!(/men/, 'women') }
475
- assert_not_equal(VALUE, @value)
476
- assert_nothing_raised { @value.abort_transaction }
477
- assert_equal(false, @value.transaction_open?)
478
- assert_equal(VALUE, @value)
479
- end
480
-
481
- def test_commit
482
- assert_equal(false, @value.transaction_open?)
483
- assert_raises(Transaction::TransactionError) { @value.commit_transaction }
484
- assert_nothing_raised { @value.start_transaction }
485
- assert_equal(true, @value.transaction_open?)
486
- assert_nothing_raised { @value.gsub!(/men/, 'women') }
487
- assert_not_equal(VALUE, @value)
488
- assert_equal(true, @value.transaction_open?)
489
- assert_nothing_raised { @value.commit_transaction }
490
- assert_equal(false, @value.transaction_open?)
491
- assert_not_equal(VALUE, @value)
492
- end
493
-
494
- def test_multilevel
495
- assert_equal(false, @value.transaction_open?)
496
- assert_nothing_raised { @value.start_transaction }
497
- assert_equal(true, @value.transaction_open?)
498
- assert_nothing_raised { @value.gsub!(/men/, 'women') }
499
- assert_equal(VALUE.gsub(/men/, 'women'), @value)
500
- assert_equal(true, @value.transaction_open?)
501
- assert_nothing_raised { @value.start_transaction }
502
- assert_nothing_raised { @value.gsub!(/country/, 'nation-state') }
503
- assert_nothing_raised { @value.commit_transaction }
504
- assert_equal(VALUE.gsub(/men/, 'women').gsub(/country/, 'nation-state'), @value)
505
- assert_equal(true, @value.transaction_open?)
506
- assert_nothing_raised { @value.abort_transaction }
507
- assert_equal(VALUE, @value)
508
- end
509
-
510
- def test_multilevel_named
511
- assert_equal(false, @value.transaction_open?)
512
- assert_raises(Transaction::TransactionError) { @value.transaction_name }
513
- assert_nothing_raised { @value.start_transaction(:first) } # 1
514
- assert_raises(Transaction::TransactionError) { @value.start_transaction(:first) }
515
- assert_equal(true, @value.transaction_open?)
516
- assert_equal(true, @value.transaction_open?(:first))
517
- assert_equal(:first, @value.transaction_name)
518
- assert_nothing_raised { @value.start_transaction } # 2
519
- assert_not_equal(:first, @value.transaction_name)
520
- assert_equal(nil, @value.transaction_name)
521
- assert_raises(Transaction::TransactionError) { @value.abort_transaction(:second) }
522
- assert_nothing_raised { @value.abort_transaction(:first) }
523
- assert_equal(false, @value.transaction_open?)
524
- assert_nothing_raised do
525
- @value.start_transaction(:first)
526
- @value.gsub!(/men/, 'women')
527
- @value.start_transaction(:second)
528
- @value.gsub!(/women/, 'people')
529
- @value.start_transaction
530
- @value.gsub!(/people/, 'sentients')
531
- end
532
- assert_nothing_raised { @value.abort_transaction(:second) }
533
- assert_equal(true, @value.transaction_open?(:first))
534
- assert_equal(VALUE.gsub(/men/, 'women'), @value)
535
- assert_nothing_raised do
536
- @value.start_transaction(:second)
537
- @value.gsub!(/women/, 'people')
538
- @value.start_transaction
539
- @value.gsub!(/people/, 'sentients')
540
- end
541
- assert_raises(Transaction::TransactionError) { @value.rewind_transaction(:foo) }
542
- assert_nothing_raised { @value.rewind_transaction(:second) }
543
- assert_equal(VALUE.gsub(/men/, 'women'), @value)
544
- assert_nothing_raised do
545
- @value.gsub!(/women/, 'people')
546
- @value.start_transaction
547
- @value.gsub!(/people/, 'sentients')
548
- end
549
- assert_raises(Transaction::TransactionError) { @value.commit_transaction(:foo) }
550
- assert_nothing_raised { @value.commit_transaction(:first) }
551
- assert_equal(VALUE.gsub(/men/, 'sentients'), @value)
552
- assert_equal(false, @value.transaction_open?)
553
- end
554
-
555
- def test_array
556
- assert_nothing_raised do
557
- @orig = ["first", "second", "third"]
558
- @value = ["first", "second", "third"]
559
- @value.extend(Transaction::Simple)
560
- end
561
- assert_equal(@orig, @value)
562
- assert_nothing_raised { @value.start_transaction }
563
- assert_equal(true, @value.transaction_open?)
564
- assert_nothing_raised { @value[1].gsub!(/second/, "fourth") }
565
- assert_not_equal(@orig, @value)
566
- assert_nothing_raised { @value.abort_transaction }
567
- assert_equal(@orig, @value)
568
- end
569
- end
570
-
571
- class Test__Transaction_Simple_ThreadSafe < Test::Unit::TestCase #:nodoc:
572
- VALUE = "Now is the time for all good men to come to the aid of their country."
573
-
574
- def setup
575
- @value = VALUE.dup
576
- @value.extend(Transaction::Simple::ThreadSafe)
577
- end
578
-
579
- def test_extended
580
- assert_respond_to(@value, :start_transaction)
581
- end
582
-
583
- def test_started
584
- assert_equal(false, @value.transaction_open?)
585
- assert_nothing_raised { @value.start_transaction }
586
- assert_equal(true, @value.transaction_open?)
587
- end
588
-
589
- def test_rewind
590
- assert_equal(false, @value.transaction_open?)
591
- assert_raises(Transaction::TransactionError) { @value.rewind_transaction }
592
- assert_nothing_raised { @value.start_transaction }
593
- assert_equal(true, @value.transaction_open?)
594
- assert_nothing_raised { @value.gsub!(/men/, 'women') }
595
- assert_not_equal(VALUE, @value)
596
- assert_nothing_raised { @value.rewind_transaction }
597
- assert_equal(true, @value.transaction_open?)
598
- assert_equal(VALUE, @value)
599
- end
600
-
601
- def test_abort
602
- assert_equal(false, @value.transaction_open?)
603
- assert_raises(Transaction::TransactionError) { @value.abort_transaction }
604
- assert_nothing_raised { @value.start_transaction }
605
- assert_equal(true, @value.transaction_open?)
606
- assert_nothing_raised { @value.gsub!(/men/, 'women') }
607
- assert_not_equal(VALUE, @value)
608
- assert_nothing_raised { @value.abort_transaction }
609
- assert_equal(false, @value.transaction_open?)
610
- assert_equal(VALUE, @value)
611
- end
612
-
613
- def test_commit
614
- assert_equal(false, @value.transaction_open?)
615
- assert_raises(Transaction::TransactionError) { @value.commit_transaction }
616
- assert_nothing_raised { @value.start_transaction }
617
- assert_equal(true, @value.transaction_open?)
618
- assert_nothing_raised { @value.gsub!(/men/, 'women') }
619
- assert_not_equal(VALUE, @value)
620
- assert_equal(true, @value.transaction_open?)
621
- assert_nothing_raised { @value.commit_transaction }
622
- assert_equal(false, @value.transaction_open?)
623
- assert_not_equal(VALUE, @value)
624
- end
625
-
626
- def test_multilevel
627
- assert_equal(false, @value.transaction_open?)
628
- assert_nothing_raised { @value.start_transaction }
629
- assert_equal(true, @value.transaction_open?)
630
- assert_nothing_raised { @value.gsub!(/men/, 'women') }
631
- assert_equal(VALUE.gsub(/men/, 'women'), @value)
632
- assert_equal(true, @value.transaction_open?)
633
- assert_nothing_raised { @value.start_transaction }
634
- assert_nothing_raised { @value.gsub!(/country/, 'nation-state') }
635
- assert_nothing_raised { @value.commit_transaction }
636
- assert_equal(VALUE.gsub(/men/, 'women').gsub(/country/, 'nation-state'), @value)
637
- assert_equal(true, @value.transaction_open?)
638
- assert_nothing_raised { @value.abort_transaction }
639
- assert_equal(VALUE, @value)
640
- end
641
-
642
- def test_multilevel_named
643
- assert_equal(false, @value.transaction_open?)
644
- assert_raises(Transaction::TransactionError) { @value.transaction_name }
645
- assert_nothing_raised { @value.start_transaction(:first) } # 1
646
- assert_raises(Transaction::TransactionError) { @value.start_transaction(:first) }
647
- assert_equal(true, @value.transaction_open?)
648
- assert_equal(true, @value.transaction_open?(:first))
649
- assert_equal(:first, @value.transaction_name)
650
- assert_nothing_raised { @value.start_transaction } # 2
651
- assert_not_equal(:first, @value.transaction_name)
652
- assert_equal(nil, @value.transaction_name)
653
- assert_raises(Transaction::TransactionError) { @value.abort_transaction(:second) }
654
- assert_nothing_raised { @value.abort_transaction(:first) }
655
- assert_equal(false, @value.transaction_open?)
656
- assert_nothing_raised do
657
- @value.start_transaction(:first)
658
- @value.gsub!(/men/, 'women')
659
- @value.start_transaction(:second)
660
- @value.gsub!(/women/, 'people')
661
- @value.start_transaction
662
- @value.gsub!(/people/, 'sentients')
663
- end
664
- assert_nothing_raised { @value.abort_transaction(:second) }
665
- assert_equal(true, @value.transaction_open?(:first))
666
- assert_equal(VALUE.gsub(/men/, 'women'), @value)
667
- assert_nothing_raised do
668
- @value.start_transaction(:second)
669
- @value.gsub!(/women/, 'people')
670
- @value.start_transaction
671
- @value.gsub!(/people/, 'sentients')
672
- end
673
- assert_raises(Transaction::TransactionError) { @value.rewind_transaction(:foo) }
674
- assert_nothing_raised { @value.rewind_transaction(:second) }
675
- assert_equal(VALUE.gsub(/men/, 'women'), @value)
676
- assert_nothing_raised do
677
- @value.gsub!(/women/, 'people')
678
- @value.start_transaction
679
- @value.gsub!(/people/, 'sentients')
680
- end
681
- assert_raises(Transaction::TransactionError) { @value.commit_transaction(:foo) }
682
- assert_nothing_raised { @value.commit_transaction(:first) }
683
- assert_equal(VALUE.gsub(/men/, 'sentients'), @value)
684
- assert_equal(false, @value.transaction_open?)
685
- end
686
-
687
- def test_array
688
- assert_nothing_raised do
689
- @orig = ["first", "second", "third"]
690
- @value = ["first", "second", "third"]
691
- @value.extend(Transaction::Simple::ThreadSafe)
692
- end
693
- assert_equal(@orig, @value)
694
- assert_nothing_raised { @value.start_transaction }
695
- assert_equal(true, @value.transaction_open?)
696
- assert_nothing_raised { @value[1].gsub!(/second/, "fourth") }
697
- assert_not_equal(@orig, @value)
698
- assert_nothing_raised { @value.abort_transaction }
699
- assert_equal(@orig, @value)
700
- end
701
- end
702
- end
1
+ # :title: Transaction::Simple -- Active Object Transaction Support for Ruby
2
+ # :main: Transaction::Simple
3
+ #
4
+ # == Licence
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a
7
+ # copy of this software and associated documentation files (the "Software"),
8
+ # to deal in the Software without restriction, including without limitation
9
+ # the rights to use, copy, modify, merge, publish, distribute, sublicense,
10
+ # and/or sell copies of the Software, and to permit persons to whom the
11
+ # Software is furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in
14
+ # all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
19
+ # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
22
+ # DEALINGS IN THE SOFTWARE.
23
+ #--
24
+ # Transaction::Simple
25
+ # Simple object transaction support for Ruby
26
+ # Version 1.3.0
27
+ #
28
+ # Copyright (c) 2003 - 2005 Austin Ziegler
29
+ #
30
+ # $Id: simple.rb,v 1.5 2005/05/05 16:16:49 austin Exp $
31
+ #++
32
+ # The "Transaction" namespace can be used for additional transaction
33
+ # support objects and modules.
34
+ module Transaction
35
+ # A standard exception for transaction errors.
36
+ class TransactionError < StandardError; end
37
+ # The TransactionAborted exception is used to indicate when a
38
+ # transaction has been aborted in the block form.
39
+ class TransactionAborted < Exception; end
40
+ # The TransactionCommitted exception is used to indicate when a
41
+ # transaction has been committed in the block form.
42
+ class TransactionCommitted < Exception; end
43
+
44
+ te = "Transaction Error: %s"
45
+
46
+ Messages = {
47
+ :bad_debug_object =>
48
+ te % "the transaction debug object must respond to #<<.",
49
+ :unique_names =>
50
+ te % "named transactions must be unique.",
51
+ :no_transaction_open =>
52
+ te % "no transaction open.",
53
+ :cannot_rewind_no_transaction =>
54
+ te % "cannot rewind; there is no current transaction.",
55
+ :cannot_rewind_named_transaction =>
56
+ te % "cannot rewind to transaction %s because it does not exist.",
57
+ :cannot_rewind_transaction_before_block =>
58
+ te % "cannot rewind a transaction started before the execution block.",
59
+ :cannot_abort_no_transaction =>
60
+ te % "cannot abort; there is no current transaction.",
61
+ :cannot_abort_transaction_before_block =>
62
+ te % "cannot abort a transaction started before the execution block.",
63
+ :cannot_abort_named_transaction =>
64
+ te % "cannot abort nonexistant transaction %s.",
65
+ :cannot_commit_no_transaction =>
66
+ te % "cannot commit; there is no current transaction.",
67
+ :cannot_commit_transaction_before_block =>
68
+ te % "cannot commit a transaction started before the execution block.",
69
+ :cannot_commit_named_transaction =>
70
+ te % "cannot commit nonexistant transaction %s.",
71
+ :cannot_start_empty_block_transaction =>
72
+ te % "cannot start a block transaction with no objects.",
73
+ :cannot_obtain_transaction_lock =>
74
+ te % "cannot obtain transaction lock for #%s.",
75
+ }
76
+
77
+ # = Transaction::Simple for Ruby
78
+ # Simple object transaction support for Ruby
79
+ #
80
+ # == Introduction
81
+ # Transaction::Simple provides a generic way to add active transaction
82
+ # support to objects. The transaction methods added by this module will
83
+ # work with most objects, excluding those that cannot be
84
+ # <i>Marshal</i>ed (bindings, procedure objects, IO instances, or
85
+ # singleton objects).
86
+ #
87
+ # The transactions supported by Transaction::Simple are not backed
88
+ # transactions; they are not associated with any sort of data store.
89
+ # They are "live" transactions occurring in memory and in the object
90
+ # itself. This is to allow "test" changes to be made to an object
91
+ # before making the changes permanent.
92
+ #
93
+ # Transaction::Simple can handle an "infinite" number of transaction
94
+ # levels (limited only by memory). If I open two transactions, commit
95
+ # the second, but abort the first, the object will revert to the
96
+ # original version.
97
+ #
98
+ # Transaction::Simple supports "named" transactions, so that multiple
99
+ # levels of transactions can be committed, aborted, or rewound by
100
+ # referring to the appropriate name of the transaction. Names may be any
101
+ # object *except* +nil+. As with Hash keys, String names will be
102
+ # duplicated and frozen before using.
103
+ #
104
+ # Copyright:: Copyright � 2003 - 2005 by Austin Ziegler
105
+ # Version:: 1.3.0
106
+ # Licence:: MIT-Style
107
+ #
108
+ # Thanks to David Black for help with the initial concept that led to
109
+ # this library.
110
+ #
111
+ # == Usage
112
+ # include 'transaction/simple'
113
+ #
114
+ # v = "Hello, you." # -> "Hello, you."
115
+ # v.extend(Transaction::Simple) # -> "Hello, you."
116
+ #
117
+ # v.start_transaction # -> ... (a Marshal string)
118
+ # v.transaction_open? # -> true
119
+ # v.gsub!(/you/, "world") # -> "Hello, world."
120
+ #
121
+ # v.rewind_transaction # -> "Hello, you."
122
+ # v.transaction_open? # -> true
123
+ #
124
+ # v.gsub!(/you/, "HAL") # -> "Hello, HAL."
125
+ # v.abort_transaction # -> "Hello, you."
126
+ # v.transaction_open? # -> false
127
+ #
128
+ # v.start_transaction # -> ... (a Marshal string)
129
+ # v.start_transaction # -> ... (a Marshal string)
130
+ #
131
+ # v.transaction_open? # -> true
132
+ # v.gsub!(/you/, "HAL") # -> "Hello, HAL."
133
+ #
134
+ # v.commit_transaction # -> "Hello, HAL."
135
+ # v.transaction_open? # -> true
136
+ # v.abort_transaction # -> "Hello, you."
137
+ # v.transaction_open? # -> false
138
+ #
139
+ # == Named Transaction Usage
140
+ # v = "Hello, you." # -> "Hello, you."
141
+ # v.extend(Transaction::Simple) # -> "Hello, you."
142
+ #
143
+ # v.start_transaction(:first) # -> ... (a Marshal string)
144
+ # v.transaction_open? # -> true
145
+ # v.transaction_open?(:first) # -> true
146
+ # v.transaction_open?(:second) # -> false
147
+ # v.gsub!(/you/, "world") # -> "Hello, world."
148
+ #
149
+ # v.start_transaction(:second) # -> ... (a Marshal string)
150
+ # v.gsub!(/world/, "HAL") # -> "Hello, HAL."
151
+ # v.rewind_transaction(:first) # -> "Hello, you."
152
+ # v.transaction_open? # -> true
153
+ # v.transaction_open?(:first) # -> true
154
+ # v.transaction_open?(:second) # -> false
155
+ #
156
+ # v.gsub!(/you/, "world") # -> "Hello, world."
157
+ # v.start_transaction(:second) # -> ... (a Marshal string)
158
+ # v.gsub!(/world/, "HAL") # -> "Hello, HAL."
159
+ # v.transaction_name # -> :second
160
+ # v.abort_transaction(:first) # -> "Hello, you."
161
+ # v.transaction_open? # -> false
162
+ #
163
+ # v.start_transaction(:first) # -> ... (a Marshal string)
164
+ # v.gsub!(/you/, "world") # -> "Hello, world."
165
+ # v.start_transaction(:second) # -> ... (a Marshal string)
166
+ # v.gsub!(/world/, "HAL") # -> "Hello, HAL."
167
+ #
168
+ # v.commit_transaction(:first) # -> "Hello, HAL."
169
+ # v.transaction_open? # -> false
170
+ #
171
+ # == Block Usage
172
+ # v = "Hello, you." # -> "Hello, you."
173
+ # Transaction::Simple.start(v) do |tv|
174
+ # # v has been extended with Transaction::Simple and an unnamed
175
+ # # transaction has been started.
176
+ # tv.transaction_open? # -> true
177
+ # tv.gsub!(/you/, "world") # -> "Hello, world."
178
+ #
179
+ # tv.rewind_transaction # -> "Hello, you."
180
+ # tv.transaction_open? # -> true
181
+ #
182
+ # tv.gsub!(/you/, "HAL") # -> "Hello, HAL."
183
+ # # The following breaks out of the transaction block after
184
+ # # aborting the transaction.
185
+ # tv.abort_transaction # -> "Hello, you."
186
+ # end
187
+ # # v still has Transaction::Simple applied from here on out.
188
+ # v.transaction_open? # -> false
189
+ #
190
+ # Transaction::Simple.start(v) do |tv|
191
+ # tv.start_transaction # -> ... (a Marshal string)
192
+ #
193
+ # tv.transaction_open? # -> true
194
+ # tv.gsub!(/you/, "HAL") # -> "Hello, HAL."
195
+ #
196
+ # # If #commit_transaction were called without having started a
197
+ # # second transaction, then it would break out of the transaction
198
+ # # block after committing the transaction.
199
+ # tv.commit_transaction # -> "Hello, HAL."
200
+ # tv.transaction_open? # -> true
201
+ # tv.abort_transaction # -> "Hello, you."
202
+ # end
203
+ # v.transaction_open? # -> false
204
+ #
205
+ # == Named Transaction Usage
206
+ # v = "Hello, you." # -> "Hello, you."
207
+ # v.extend(Transaction::Simple) # -> "Hello, you."
208
+ #
209
+ # v.start_transaction(:first) # -> ... (a Marshal string)
210
+ # v.transaction_open? # -> true
211
+ # v.transaction_open?(:first) # -> true
212
+ # v.transaction_open?(:second) # -> false
213
+ # v.gsub!(/you/, "world") # -> "Hello, world."
214
+ #
215
+ # v.start_transaction(:second) # -> ... (a Marshal string)
216
+ # v.gsub!(/world/, "HAL") # -> "Hello, HAL."
217
+ # v.rewind_transaction(:first) # -> "Hello, you."
218
+ # v.transaction_open? # -> true
219
+ # v.transaction_open?(:first) # -> true
220
+ # v.transaction_open?(:second) # -> false
221
+ #
222
+ # v.gsub!(/you/, "world") # -> "Hello, world."
223
+ # v.start_transaction(:second) # -> ... (a Marshal string)
224
+ # v.gsub!(/world/, "HAL") # -> "Hello, HAL."
225
+ # v.transaction_name # -> :second
226
+ # v.abort_transaction(:first) # -> "Hello, you."
227
+ # v.transaction_open? # -> false
228
+ #
229
+ # v.start_transaction(:first) # -> ... (a Marshal string)
230
+ # v.gsub!(/you/, "world") # -> "Hello, world."
231
+ # v.start_transaction(:second) # -> ... (a Marshal string)
232
+ # v.gsub!(/world/, "HAL") # -> "Hello, HAL."
233
+ #
234
+ # v.commit_transaction(:first) # -> "Hello, HAL."
235
+ # v.transaction_open? # -> false
236
+ #
237
+ # == Thread Safety
238
+ # Threadsafe version of Transaction::Simple and
239
+ # Transaction::Simple::Group exist; these are loaded from
240
+ # 'transaction/simple/threadsafe' and
241
+ # 'transaction/simple/threadsafe/group', respectively, and are
242
+ # represented in Ruby code as Transaction::Simple::ThreadSafe and
243
+ # Transaction::Simple::ThreadSafe::Group, respectively.
244
+ #
245
+ # == Contraindications
246
+ # While Transaction::Simple is very useful, it has some severe
247
+ # limitations that must be understood. Transaction::Simple:
248
+ #
249
+ # * uses Marshal. Thus, any object which cannot be <i>Marshal</i>ed
250
+ # cannot use Transaction::Simple. In my experience, this affects
251
+ # singleton objects more often than any other object. It may be that
252
+ # Ruby 2.0 will solve this problem.
253
+ # * does not manage resources. Resources external to the object and its
254
+ # instance variables are not managed at all. However, all instance
255
+ # variables and objects "belonging" to those instance variables are
256
+ # managed. If there are object reference counts to be handled,
257
+ # Transaction::Simple will probably cause problems.
258
+ # * is not inherently thread-safe. In the ACID ("atomic, consistent,
259
+ # isolated, durable") test, Transaction::Simple provides CD, but it is
260
+ # up to the user of Transaction::Simple to provide isolation and
261
+ # atomicity. Transactions should be considered "critical sections" in
262
+ # multi-threaded applications. If thread safety and atomicity is
263
+ # absolutely required, use Transaction::Simple::ThreadSafe, which uses
264
+ # a Mutex object to synchronize the accesses on the object during the
265
+ # transaction operations.
266
+ # * does not necessarily maintain Object#__id__ values on rewind or
267
+ # abort. This may change for future versions that will be Ruby 1.8 or
268
+ # better *only*. Certain objects that support #replace will maintain
269
+ # Object#__id__.
270
+ # * Can be a memory hog if you use many levels of transactions on many
271
+ # objects.
272
+ #
273
+ module Simple
274
+ TRANSACTION_SIMPLE_VERSION = '1.3.0'
275
+
276
+ # Sets the Transaction::Simple debug object. It must respond to #<<.
277
+ # Sets the transaction debug object. Debugging will be performed
278
+ # automatically if there's a debug object. The generic transaction
279
+ # error class.
280
+ def self.debug_io=(io)
281
+ if io.nil?
282
+ @tdi = nil
283
+ @debugging = false
284
+ else
285
+ unless io.respond_to?(:<<)
286
+ raise TransactionError, Messages[:bad_debug_object]
287
+ end
288
+ @tdi = io
289
+ @debugging = true
290
+ end
291
+ end
292
+
293
+ # Returns +true+ if we are debugging.
294
+ def self.debugging?
295
+ @debugging
296
+ end
297
+
298
+ # Returns the Transaction::Simple debug object. It must respond to
299
+ # #<<.
300
+ def self.debug_io
301
+ @tdi ||= ""
302
+ @tdi
303
+ end
304
+
305
+ # If +name+ is +nil+ (default), then returns +true+ if there is
306
+ # currently a transaction open.
307
+ #
308
+ # If +name+ is specified, then returns +true+ if there is currently a
309
+ # transaction that responds to +name+ open.
310
+ def transaction_open?(name = nil)
311
+ if name.nil?
312
+ if Transaction::Simple.debugging?
313
+ Transaction::Simple.debug_io << "Transaction " <<
314
+ "[#{(@__transaction_checkpoint__.nil?) ? 'closed' : 'open'}]\n"
315
+ end
316
+ return (not @__transaction_checkpoint__.nil?)
317
+ else
318
+ if Transaction::Simple.debugging?
319
+ Transaction::Simple.debug_io << "Transaction(#{name.inspect}) " <<
320
+ "[#{(@__transaction_checkpoint__.nil?) ? 'closed' : 'open'}]\n"
321
+ end
322
+ return ((not @__transaction_checkpoint__.nil?) and @__transaction_names__.include?(name))
323
+ end
324
+ end
325
+
326
+ # Returns the current name of the transaction. Transactions not
327
+ # explicitly named are named +nil+.
328
+ def transaction_name
329
+ if @__transaction_checkpoint__.nil?
330
+ raise TransactionError, Messages[:no_transaction_open]
331
+ end
332
+ if Transaction::Simple.debugging?
333
+ Transaction::Simple.debug_io << "#{'|' * @__transaction_level__} " <<
334
+ "Transaction Name: #{@__transaction_names__[-1].inspect}\n"
335
+ end
336
+ if @__transaction_names__[-1].kind_of?(String)
337
+ @__transaction_names__[-1].dup
338
+ else
339
+ @__transaction_names__[-1]
340
+ end
341
+ end
342
+
343
+ # Starts a transaction. Stores the current object state. If a
344
+ # transaction name is specified, the transaction will be named.
345
+ # Transaction names must be unique. Transaction names of +nil+ will be
346
+ # treated as unnamed transactions.
347
+ def start_transaction(name = nil)
348
+ @__transaction_level__ ||= 0
349
+ @__transaction_names__ ||= []
350
+
351
+ if name.nil?
352
+ @__transaction_names__ << nil
353
+ ss = "" if Transaction::Simple.debugging?
354
+ else
355
+ if @__transaction_names__.include?(name)
356
+ raise TransactionError, Messages[:unique_names]
357
+ end
358
+ name = name.dup.freeze if name.kind_of?(String)
359
+ @__transaction_names__ << name
360
+ ss = "(#{name.inspect})" if Transaction::Simple.debugging?
361
+ end
362
+
363
+ @__transaction_level__ += 1
364
+
365
+ if Transaction::Simple.debugging?
366
+ Transaction::Simple.debug_io << "#{'>' * @__transaction_level__} " <<
367
+ "Start Transaction#{ss}\n"
368
+ end
369
+
370
+ @__transaction_checkpoint__ = Marshal.dump(self)
371
+ end
372
+
373
+ # Rewinds the transaction. If +name+ is specified, then the
374
+ # intervening transactions will be aborted and the named transaction
375
+ # will be rewound. Otherwise, only the current transaction is rewound.
376
+ def rewind_transaction(name = nil)
377
+ if @__transaction_checkpoint__.nil?
378
+ raise TransactionError, Messages[:cannot_rewind_no_transaction]
379
+ end
380
+
381
+ # Check to see if we are trying to rewind a transaction that is
382
+ # outside of the current transaction block.
383
+ if @__transaction_block__ and name
384
+ nix = @__transaction_names__.index(name) + 1
385
+ if nix < @__transaction_block__
386
+ raise TransactionError, Messages[:cannot_rewind_transaction_before_block]
387
+ end
388
+ end
389
+
390
+ if name.nil?
391
+ __rewind_this_transaction
392
+ ss = "" if Transaction::Simple.debugging?
393
+ else
394
+ unless @__transaction_names__.include?(name)
395
+ raise TransactionError, Messages[:cannot_rewind_named_transaction] % name.inspect
396
+ end
397
+ ss = "(#{name})" if Transaction::Simple.debugging?
398
+
399
+ while @__transaction_names__[-1] != name
400
+ @__transaction_checkpoint__ = __rewind_this_transaction
401
+ if Transaction::Simple.debugging?
402
+ Transaction::Simple.debug_io << "#{'|' * @__transaction_level__} " <<
403
+ "Rewind Transaction#{ss}\n"
404
+ end
405
+ @__transaction_level__ -= 1
406
+ @__transaction_names__.pop
407
+ end
408
+ __rewind_this_transaction
409
+ end
410
+ if Transaction::Simple.debugging?
411
+ Transaction::Simple.debug_io << "#{'|' * @__transaction_level__} " <<
412
+ "Rewind Transaction#{ss}\n"
413
+ end
414
+ self
415
+ end
416
+
417
+ # Aborts the transaction. Resets the object state to what it was
418
+ # before the transaction was started and closes the transaction. If
419
+ # +name+ is specified, then the intervening transactions and the named
420
+ # transaction will be aborted. Otherwise, only the current transaction
421
+ # is aborted.
422
+ #
423
+ # If the current or named transaction has been started by a block
424
+ # (Transaction::Simple.start), then the execution of the block will be
425
+ # halted with +break+ +self+.
426
+ def abort_transaction(name = nil)
427
+ if @__transaction_checkpoint__.nil?
428
+ raise TransactionError, Messages[:cannot_abort_no_transaction]
429
+ end
430
+
431
+ # Check to see if we are trying to abort a transaction that is
432
+ # outside of the current transaction block. Otherwise, raise
433
+ # TransactionAborted if they are the same.
434
+ if @__transaction_block__ and name
435
+ nix = @__transaction_names__.index(name) + 1
436
+ if nix < @__transaction_block__
437
+ raise TransactionError, Messages[:cannot_abort_transaction_before_block]
438
+ end
439
+
440
+ raise TransactionAborted if @__transaction_block__ == nix
441
+ end
442
+
443
+ raise TransactionAborted if @__transaction_block__ == @__transaction_level__
444
+
445
+ if name.nil?
446
+ __abort_transaction(name)
447
+ else
448
+ unless @__transaction_names__.include?(name)
449
+ raise TransactionError, Messages[:cannot_abort_named_transaction] % name.inspect
450
+ end
451
+ __abort_transaction(name) while @__transaction_names__.include?(name)
452
+ end
453
+ self
454
+ end
455
+
456
+ # If +name+ is +nil+ (default), the current transaction level is
457
+ # closed out and the changes are committed.
458
+ #
459
+ # If +name+ is specified and +name+ is in the list of named
460
+ # transactions, then all transactions are closed and committed until
461
+ # the named transaction is reached.
462
+ def commit_transaction(name = nil)
463
+ if @__transaction_checkpoint__.nil?
464
+ raise TransactionError, Messages[:cannot_commit_no_transaction]
465
+ end
466
+ @__transaction_block__ ||= nil
467
+
468
+ # Check to see if we are trying to commit a transaction that is
469
+ # outside of the current transaction block. Otherwise, raise
470
+ # TransactionCommitted if they are the same.
471
+ if @__transaction_block__ and name
472
+ nix = @__transaction_names__.index(name) + 1
473
+ if nix < @__transaction_block__
474
+ raise TransactionError, Messages[:cannot_commit_transaction_before_block]
475
+ end
476
+
477
+ raise TransactionCommitted if @__transaction_block__ == nix
478
+ end
479
+
480
+ raise TransactionCommitted if @__transaction_block__ == @__transaction_level__
481
+
482
+ if name.nil?
483
+ ss = "" if Transaction::Simple.debugging?
484
+ __commit_transaction
485
+ if Transaction::Simple.debugging?
486
+ Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} " <<
487
+ "Commit Transaction#{ss}\n"
488
+ end
489
+ else
490
+ unless @__transaction_names__.include?(name)
491
+ raise TransactionError, Messages[:cannot_commit_named_transaction] % name.inspect
492
+ end
493
+ ss = "(#{name})" if Transaction::Simple.debugging?
494
+
495
+ while @__transaction_names__[-1] != name
496
+ if Transaction::Simple.debugging?
497
+ Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} " <<
498
+ "Commit Transaction#{ss}\n"
499
+ end
500
+ __commit_transaction
501
+ end
502
+ if Transaction::Simple.debugging?
503
+ Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} " <<
504
+ "Commit Transaction#{ss}\n"
505
+ end
506
+ __commit_transaction
507
+ end
508
+
509
+ self
510
+ end
511
+
512
+ # Alternative method for calling the transaction methods. An optional
513
+ # name can be specified for named transaction support.
514
+ #
515
+ # #transaction(:start):: #start_transaction
516
+ # #transaction(:rewind):: #rewind_transaction
517
+ # #transaction(:abort):: #abort_transaction
518
+ # #transaction(:commit):: #commit_transaction
519
+ # #transaction(:name):: #transaction_name
520
+ # #transaction:: #transaction_open?
521
+ def transaction(action = nil, name = nil)
522
+ case action
523
+ when :start
524
+ start_transaction(name)
525
+ when :rewind
526
+ rewind_transaction(name)
527
+ when :abort
528
+ abort_transaction(name)
529
+ when :commit
530
+ commit_transaction(name)
531
+ when :name
532
+ transaction_name
533
+ when nil
534
+ transaction_open?(name)
535
+ end
536
+ end
537
+
538
+ # Allows specific variables to be excluded from transaction support.
539
+ # Must be done after extending the object but before starting the
540
+ # first transaction on the object.
541
+ #
542
+ # vv.transaction_exclusions << "@io"
543
+ def transaction_exclusions
544
+ @transaction_exclusions ||= []
545
+ end
546
+
547
+ class << self
548
+ def __common_start(name, vars, &block)
549
+ if vars.empty?
550
+ raise TransactionError, Messages[:cannot_start_empty_block_transaction]
551
+ end
552
+
553
+ if block
554
+ begin
555
+ vlevel = {}
556
+
557
+ vars.each do |vv|
558
+ vv.extend(Transaction::Simple)
559
+ vv.start_transaction(name)
560
+ vlevel[vv.__id__] = vv.instance_variable_get(:@__transaction_level__)
561
+ vv.instance_variable_set(:@__transaction_block__, vlevel[vv.__id__])
562
+ end
563
+
564
+ yield(*vars)
565
+ rescue TransactionAborted
566
+ vars.each do |vv|
567
+ if name.nil? and vv.transaction_open?
568
+ loop do
569
+ tlevel = vv.instance_variable_get(:@__transaction_level__) || -1
570
+ vv.instance_variable_set(:@__transaction_block__, -1)
571
+ break if tlevel < vlevel[vv.__id__]
572
+ vv.abort_transaction if vv.transaction_open?
573
+ end
574
+ elsif vv.transaction_open?(name)
575
+ vv.instance_variable_set(:@__transaction_block__, -1)
576
+ vv.abort_transaction(name)
577
+ end
578
+ end
579
+ rescue TransactionCommitted
580
+ nil
581
+ ensure
582
+ vars.each do |vv|
583
+ if name.nil? and vv.transaction_open?
584
+ loop do
585
+ tlevel = vv.instance_variable_get(:@__transaction_level__) || -1
586
+ break if tlevel < vlevel[vv.__id__]
587
+ vv.instance_variable_set(:@__transaction_block__, -1)
588
+ vv.commit_transaction if vv.transaction_open?
589
+ end
590
+ elsif vv.transaction_open?(name)
591
+ vv.instance_variable_set(:@__transaction_block__, -1)
592
+ vv.commit_transaction(name)
593
+ end
594
+ end
595
+ end
596
+ else
597
+ vars.each do |vv|
598
+ vv.extend(Transaction::Simple)
599
+ vv.start_transaction(name)
600
+ end
601
+ end
602
+ end
603
+ private :__common_start
604
+
605
+ def start_named(name, *vars, &block)
606
+ __common_start(name, vars, &block)
607
+ end
608
+
609
+ def start(*vars, &block)
610
+ __common_start(nil, vars, &block)
611
+ end
612
+ end
613
+
614
+ def __abort_transaction(name = nil) #:nodoc:
615
+ @__transaction_checkpoint__ = __rewind_this_transaction
616
+
617
+ if name.nil?
618
+ ss = "" if Transaction::Simple.debugging?
619
+ else
620
+ ss = "(#{name.inspect})" if Transaction::Simple.debugging?
621
+ end
622
+
623
+ if Transaction::Simple.debugging?
624
+ Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} " <<
625
+ "Abort Transaction#{ss}\n"
626
+ end
627
+ @__transaction_level__ -= 1
628
+ @__transaction_names__.pop
629
+ if @__transaction_level__ < 1
630
+ @__transaction_level__ = 0
631
+ @__transaction_names__ = []
632
+ end
633
+ end
634
+
635
+ TRANSACTION_CHECKPOINT = "@__transaction_checkpoint__" #:nodoc:
636
+ SKIP_TRANSACTION_VARS = [TRANSACTION_CHECKPOINT, "@__transaction_level__"] #:nodoc:
637
+
638
+ def __rewind_this_transaction #:nodoc:
639
+ rr = Marshal.restore(@__transaction_checkpoint__)
640
+
641
+ begin
642
+ self.replace(rr) if respond_to?(:replace)
643
+ rescue
644
+ nil
645
+ end
646
+
647
+ rr.instance_variables.each do |vv|
648
+ next if SKIP_TRANSACTION_VARS.include?(vv)
649
+ next if self.transaction_exclusions.include?(vv)
650
+ if respond_to?(:instance_variable_get)
651
+ instance_variable_set(vv, rr.instance_variable_get(vv))
652
+ else
653
+ instance_eval(%q|#{vv} = rr.instance_eval("#{vv}")|)
654
+ end
655
+ end
656
+
657
+ new_ivar = instance_variables - rr.instance_variables - SKIP_TRANSACTION_VARS
658
+ new_ivar.each do |vv|
659
+ if respond_to?(:instance_variable_set)
660
+ instance_variable_set(vv, nil)
661
+ else
662
+ instance_eval(%q|#{vv} = nil|)
663
+ end
664
+ end
665
+
666
+ if respond_to?(:instance_variable_get)
667
+ rr.instance_variable_get(TRANSACTION_CHECKPOINT)
668
+ else
669
+ rr.instance_eval(TRANSACTION_CHECKPOINT)
670
+ end
671
+ end
672
+
673
+ def __commit_transaction #:nodoc:
674
+ if respond_to?(:instance_variable_get)
675
+ @__transaction_checkpoint__ = Marshal.restore(@__transaction_checkpoint__).instance_variable_get(TRANSACTION_CHECKPOINT)
676
+ else
677
+ @__transaction_checkpoint__ = Marshal.restore(@__transaction_checkpoint__).instance_eval(TRANSACTION_CHECKPOINT)
678
+ end
679
+
680
+ @__transaction_level__ -= 1
681
+ @__transaction_names__.pop
682
+
683
+ if @__transaction_level__ < 1
684
+ @__transaction_level__ = 0
685
+ @__transaction_names__ = []
686
+ end
687
+ end
688
+
689
+ private :__abort_transaction
690
+ private :__rewind_this_transaction
691
+ private :__commit_transaction
692
+ end
693
+ end