hairtrigger 0.2.9 → 0.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +34 -4
- data/lib/hair_trigger/builder.rb +54 -7
- data/lib/hair_trigger/version.rb +1 -1
- data/spec/builder_spec.rb +40 -1
- metadata +2 -2
    
        data/README.md
    CHANGED
    
    | @@ -22,6 +22,10 @@ class AccountUser < ActiveRecord::Base | |
| 22 22 | 
             
              trigger.after(:insert) do
         | 
| 23 23 | 
             
                "UPDATE accounts SET user_count = user_count + 1 WHERE id = NEW.account_id;"
         | 
| 24 24 | 
             
              end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
              trigger.after(:update).of(:name) do
         | 
| 27 | 
            +
                "INSERT INTO user_changes(id, name) VALUES(NEW.id, NEW.name);"
         | 
| 28 | 
            +
              end
         | 
| 25 29 | 
             
            end
         | 
| 26 30 | 
             
            ```
         | 
| 27 31 |  | 
| @@ -39,7 +43,15 @@ CREATE TRIGGER account_users_after_insert_row_tr AFTER INSERT ON account_users | |
| 39 43 | 
             
            FOR EACH ROW
         | 
| 40 44 | 
             
            BEGIN
         | 
| 41 45 | 
             
                UPDATE accounts SET user_count = user_count + 1 WHERE id = NEW.account_id;
         | 
| 42 | 
            -
            END
         | 
| 46 | 
            +
            END;
         | 
| 47 | 
            +
             | 
| 48 | 
            +
            CREATE TRIGGER account_users_after_update_on_name_row_tr AFTER UPDATE ON account_users
         | 
| 49 | 
            +
            FOR EACH ROW
         | 
| 50 | 
            +
            BEGIN
         | 
| 51 | 
            +
                IF NEW.name <> OLD.name OR (NEW.name IS NULL) <> (OLD.name IS NULL) THEN
         | 
| 52 | 
            +
                    INSERT INTO user_changes(id, name) VALUES(NEW.id, NEW.name);
         | 
| 53 | 
            +
                END IF;
         | 
| 54 | 
            +
            END;
         | 
| 43 55 | 
             
            ```
         | 
| 44 56 |  | 
| 45 57 | 
             
            Note that these auto-generated `create_trigger` statements in the migration
         | 
| @@ -74,6 +86,10 @@ Shorthand for `timing(:after).events(*events)`. | |
| 74 86 | 
             
            #### where(conditions)
         | 
| 75 87 | 
             
            Optional, SQL snippet limiting when the trigger will fire. Supports delayed interpolation of variables.
         | 
| 76 88 |  | 
| 89 | 
            +
            #### of(*columns)
         | 
| 90 | 
            +
             | 
| 91 | 
            +
            Only fire the update trigger if at least one of the columns is specified in the statement. Platforms that support it use a native `OF` clause, others will have an inferred `IF ...` statement in the trigger body. Note the former will fire even if the column's value hasn't changed; the latter will not.
         | 
| 92 | 
            +
             | 
| 77 93 | 
             
            #### security(user)
         | 
| 78 94 | 
             
            Permissions/role to check when calling trigger. PostgreSQL supports `:invoker` (default) and `:definer`, MySQL supports `:definer` (default) and arbitrary users (syntax: `'user'@'host'`).
         | 
| 79 95 |  | 
| @@ -84,10 +100,24 @@ Required (but may be satisified by `before`/`after`). Possible values are `:befo | |
| 84 100 | 
             
            Required (but may be satisified by `before`/`after`). Possible values are `:insert`/`:update`/`:delete`/`:truncate`. MySQL/SQLite only support one action per trigger, and don't support `:truncate`.
         | 
| 85 101 |  | 
| 86 102 | 
             
            #### nowrap(flag = true)
         | 
| 87 | 
            -
            PostgreSQL | 
| 103 | 
            +
            PostgreSQL-specific option to prevent the trigger action from being wrapped in a `CREATE FUNCTION`. This is useful for executing existing triggers/functions directly, but is not compatible with the `security` setting nor can it be used with pre-9.0 PostgreSQL when supplying a `where` condition.
         | 
| 88 104 |  | 
| 89 105 | 
             
            Example: `trigger.after(:update).nowrap { "tsvector_update_trigger(...)" }`
         | 
| 90 106 |  | 
| 107 | 
            +
            #### declare
         | 
| 108 | 
            +
            PostgreSQL-specific option for declaring variables for use in the
         | 
| 109 | 
            +
            trigger function. Declarations should be separate by semicolons, e.g.
         | 
| 110 | 
            +
             | 
| 111 | 
            +
            ```ruby
         | 
| 112 | 
            +
            trigger.after(:insert).declare("user_type text; status text") do
         | 
| 113 | 
            +
              <<-SQL
         | 
| 114 | 
            +
                IF (NEW.account_id = 1 OR NEW.email LIKE '%company.com') THEN
         | 
| 115 | 
            +
                  user_type := 'employee';
         | 
| 116 | 
            +
                ELSIF ...
         | 
| 117 | 
            +
              SQL
         | 
| 118 | 
            +
            end
         | 
| 119 | 
            +
            ```
         | 
| 120 | 
            +
             | 
| 91 121 | 
             
            #### all
         | 
| 92 122 | 
             
            Noop, useful for trigger groups (see below).
         | 
| 93 123 |  | 
| @@ -103,10 +133,10 @@ trigger.after(:update) do |t| | |
| 103 133 | 
             
              t.all do # every row
         | 
| 104 134 | 
             
                # some sql
         | 
| 105 135 | 
             
              end
         | 
| 106 | 
            -
              t. | 
| 136 | 
            +
              t.of("foo") do
         | 
| 107 137 | 
             
                # some more sql
         | 
| 108 138 | 
             
              end
         | 
| 109 | 
            -
              t.where("OLD.bar != NEW.bar") do
         | 
| 139 | 
            +
              t.where("OLD.bar != NEW.bar AND NEW.bar != 'lol'") do
         | 
| 110 140 | 
             
                # some other sql
         | 
| 111 141 | 
             
              end
         | 
| 112 142 | 
             
            end
         | 
    
        data/lib/hair_trigger/builder.rb
    CHANGED
    
    | @@ -77,6 +77,14 @@ module HairTrigger | |
| 77 77 | 
             
                  options[:nowrap] = flag
         | 
| 78 78 | 
             
                end
         | 
| 79 79 |  | 
| 80 | 
            +
                def of(*columns)
         | 
| 81 | 
            +
                  options[:of] = columns
         | 
| 82 | 
            +
                end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                def declare(declarations)
         | 
| 85 | 
            +
                  options[:declarations] = declarations
         | 
| 86 | 
            +
                end
         | 
| 87 | 
            +
             | 
| 80 88 | 
             
                # noop, just a way you can pass a block within a trigger group
         | 
| 81 89 | 
             
                def all
         | 
| 82 90 | 
             
                end
         | 
| @@ -150,7 +158,7 @@ module HairTrigger | |
| 150 158 | 
             
                    METHOD
         | 
| 151 159 | 
             
                  end
         | 
| 152 160 | 
             
                end
         | 
| 153 | 
            -
                chainable_methods :name, :on, :for_each, :before, :after, :where, :security, :timing, :events, :all, :nowrap
         | 
| 161 | 
            +
                chainable_methods :name, :on, :for_each, :before, :after, :where, :security, :timing, :events, :all, :nowrap, :of, :declare
         | 
| 154 162 |  | 
| 155 163 | 
             
                def create_grouped_trigger?
         | 
| 156 164 | 
             
                  adapter_name == :mysql
         | 
| @@ -158,7 +166,7 @@ module HairTrigger | |
| 158 166 |  | 
| 159 167 | 
             
                def prepare!
         | 
| 160 168 | 
             
                  @triggers.each(&:prepare!) if @triggers
         | 
| 161 | 
            -
                   | 
| 169 | 
            +
                  prepare_where!
         | 
| 162 170 | 
             
                  if @actions
         | 
| 163 171 | 
             
                    @prepared_actions = @actions.is_a?(Hash) ?
         | 
| 164 172 | 
             
                      @actions.inject({}){ |hash, (key, value)| hash[key] = interpolate(value).rstrip; hash } :
         | 
| @@ -167,6 +175,20 @@ module HairTrigger | |
| 167 175 | 
             
                  all_names # ensure (component) trigger names are all cached
         | 
| 168 176 | 
             
                end
         | 
| 169 177 |  | 
| 178 | 
            +
                def prepare_where!
         | 
| 179 | 
            +
                  parts = []
         | 
| 180 | 
            +
                  parts << @explicit_where = options[:where] = interpolate(options[:where]) if options[:where]
         | 
| 181 | 
            +
                  parts << options[:of].map{ |col| change_clause(col) }.join(" OR ") if options[:of] && !supports_of?
         | 
| 182 | 
            +
                  if parts.present?
         | 
| 183 | 
            +
                    parts.map!{ |part| "(" + part + ")" } if parts.size > 1
         | 
| 184 | 
            +
                    @prepared_where = parts.join(" AND ")
         | 
| 185 | 
            +
                  end
         | 
| 186 | 
            +
                end
         | 
| 187 | 
            +
             | 
| 188 | 
            +
                def change_clause(column)
         | 
| 189 | 
            +
                  "NEW.#{column} <> OLD.#{column} OR (NEW.#{column} IS NULL) <> (OLD.#{column} IS NULL)"
         | 
| 190 | 
            +
                end
         | 
| 191 | 
            +
             | 
| 170 192 | 
             
                def validate!(direction = :down)
         | 
| 171 193 | 
             
                  @errors.each do |(error, *adapters)|
         | 
| 172 194 | 
             
                    raise GenerationError, error if adapters.include?(adapter_name)
         | 
| @@ -259,7 +281,7 @@ module HairTrigger | |
| 259 281 | 
             
                end
         | 
| 260 282 |  | 
| 261 283 | 
             
                def components
         | 
| 262 | 
            -
                  [ | 
| 284 | 
            +
                  [@options, @prepared_actions, @explicit_where, @triggers, @compatibility]
         | 
| 263 285 | 
             
                end
         | 
| 264 286 |  | 
| 265 287 | 
             
                def errors
         | 
| @@ -296,6 +318,7 @@ module HairTrigger | |
| 296 318 | 
             
                end
         | 
| 297 319 |  | 
| 298 320 | 
             
                def maybe_execute(&block)
         | 
| 321 | 
            +
                  raise DeclarationError, "of may only be specified on update triggers" if options[:of] && options[:events] != ["UPDATE"]
         | 
| 299 322 | 
             
                  if block.arity > 0 # we're creating a trigger group, so set up some stuff and pass the buck
         | 
| 300 323 | 
             
                    @errors << ["trigger group must specify timing and event(s) for mysql", :mysql] unless options[:timing] && options[:events]
         | 
| 301 324 | 
             
                    @errors << ["nested trigger groups are not supported for mysql", :mysql] if @trigger_group
         | 
| @@ -340,12 +363,34 @@ module HairTrigger | |
| 340 363 | 
             
                  [options[:table],
         | 
| 341 364 | 
             
                   options[:timing],
         | 
| 342 365 | 
             
                   options[:events],
         | 
| 366 | 
            +
                   of_clause(false),
         | 
| 343 367 | 
             
                   options[:for_each],
         | 
| 344 | 
            -
                    | 
| 368 | 
            +
                   @explicit_where ? 'when_' + @explicit_where : nil
         | 
| 345 369 | 
             
                  ].flatten.compact.
         | 
| 346 370 | 
             
                  join("_").downcase.gsub(/[^a-z0-9_]/, '_').gsub(/_+/, '_')[0, 60] + "_tr"
         | 
| 347 371 | 
             
                end
         | 
| 348 372 |  | 
| 373 | 
            +
                def of_clause(check_support = true)
         | 
| 374 | 
            +
                  "OF " + options[:of].join(", ") + " " if options[:of] && (!check_support || supports_of?)
         | 
| 375 | 
            +
                end
         | 
| 376 | 
            +
             | 
| 377 | 
            +
                def declarations
         | 
| 378 | 
            +
                  return unless declarations = options[:declarations]
         | 
| 379 | 
            +
                  declarations = declarations.strip.split(/;/).map(&:strip).join(";\n")
         | 
| 380 | 
            +
                  "\nDECLARE\n" + normalize(declarations.sub(/;?\n?\z/, ';'), 1).rstrip
         | 
| 381 | 
            +
                end
         | 
| 382 | 
            +
             | 
| 383 | 
            +
                def supports_of?
         | 
| 384 | 
            +
                  case adapter_name
         | 
| 385 | 
            +
                  when :sqlite
         | 
| 386 | 
            +
                    true
         | 
| 387 | 
            +
                  when :postgresql, :postgis
         | 
| 388 | 
            +
                    db_version >= 90000
         | 
| 389 | 
            +
                  else
         | 
| 390 | 
            +
                    false
         | 
| 391 | 
            +
                  end
         | 
| 392 | 
            +
                end
         | 
| 393 | 
            +
             | 
| 349 394 | 
             
                def generate_drop_trigger
         | 
| 350 395 | 
             
                  case adapter_name
         | 
| 351 396 | 
             
                    when :sqlite, :mysql
         | 
| @@ -359,7 +404,7 @@ module HairTrigger | |
| 359 404 |  | 
| 360 405 | 
             
                def generate_trigger_sqlite
         | 
| 361 406 | 
             
                  <<-SQL
         | 
| 362 | 
            -
            CREATE TRIGGER #{prepared_name} #{options[:timing]} #{options[:events].first} ON #{options[:table]}
         | 
| 407 | 
            +
            CREATE TRIGGER #{prepared_name} #{options[:timing]} #{options[:events].first} #{of_clause}ON #{options[:table]}
         | 
| 363 408 | 
             
            FOR EACH #{options[:for_each]}#{prepared_where ? " WHEN " + prepared_where : ''}
         | 
| 364 409 | 
             
            BEGIN
         | 
| 365 410 | 
             
            #{normalize(raw_actions, 1).rstrip}
         | 
| @@ -370,8 +415,10 @@ END; | |
| 370 415 | 
             
                def generate_trigger_postgresql
         | 
| 371 416 | 
             
                  raise GenerationError, "truncate triggers are only supported on postgres 8.4 and greater" if db_version < 80400 && options[:events].include?('TRUNCATE')
         | 
| 372 417 | 
             
                  raise GenerationError, "FOR EACH ROW triggers may not be triggered by truncate events" if options[:for_each] == 'ROW' && options[:events].include?('TRUNCATE')
         | 
| 418 | 
            +
                  raise GenerationError, "declare cannot be used in conjunction with nowrap" if options[:nowrap] && options[:declare]
         | 
| 373 419 | 
             
                  raise GenerationError, "security cannot be used in conjunction with nowrap" if options[:nowrap] && options[:security]
         | 
| 374 420 | 
             
                  raise GenerationError, "where can only be used in conjunction with nowrap on postgres 9.0 and greater" if options[:nowrap] && prepared_where && db_version < 90000
         | 
| 421 | 
            +
                  raise GenerationError, "of can only be used in conjunction with nowrap on postgres 9.1 and greater" if options[:nowrap] && options[:of] && db_version < 91000
         | 
| 375 422 |  | 
| 376 423 | 
             
                  sql = ''
         | 
| 377 424 |  | 
| @@ -381,7 +428,7 @@ END; | |
| 381 428 | 
             
                    security = options[:security] if options[:security] && options[:security] != :invoker
         | 
| 382 429 | 
             
                    sql << <<-SQL
         | 
| 383 430 | 
             
            CREATE FUNCTION #{prepared_name}()
         | 
| 384 | 
            -
            RETURNS TRIGGER AS  | 
| 431 | 
            +
            RETURNS TRIGGER AS $$#{declarations}
         | 
| 385 432 | 
             
            BEGIN
         | 
| 386 433 | 
             
                    SQL
         | 
| 387 434 | 
             
                    if prepared_where && db_version < 90000
         | 
| @@ -410,7 +457,7 @@ $$ LANGUAGE plpgsql#{security ? " SECURITY #{security.to_s.upcase}" : ""}; | |
| 410 457 | 
             
                  end
         | 
| 411 458 |  | 
| 412 459 | 
             
                  [sql, <<-SQL]
         | 
| 413 | 
            -
            CREATE TRIGGER #{prepared_name} #{options[:timing]} #{options[:events].join(" OR ")} ON #{options[:table]}
         | 
| 460 | 
            +
            CREATE TRIGGER #{prepared_name} #{options[:timing]} #{options[:events].join(" OR ")} #{of_clause}ON #{options[:table]}
         | 
| 414 461 | 
             
            FOR EACH #{options[:for_each]}#{prepared_where && db_version >= 90000 ? " WHEN (" + prepared_where + ')': ''} EXECUTE PROCEDURE #{trigger_action};
         | 
| 415 462 | 
             
                  SQL
         | 
| 416 463 | 
             
                end
         | 
    
        data/lib/hair_trigger/version.rb
    CHANGED
    
    
    
        data/spec/builder_spec.rb
    CHANGED
    
    | @@ -60,6 +60,14 @@ describe "builder" do | |
| 60 60 | 
             
                end
         | 
| 61 61 | 
             
              end
         | 
| 62 62 |  | 
| 63 | 
            +
              describe "`of' columns" do
         | 
| 64 | 
            +
                it "should be disallowed for non-update triggers" do
         | 
| 65 | 
            +
                  lambda {
         | 
| 66 | 
            +
                    builder.on(:foos).after(:insert).of(:bar, :baz){ "BAR" }
         | 
| 67 | 
            +
                  }.should raise_error /of may only be specified on update triggers/
         | 
| 68 | 
            +
                end
         | 
| 69 | 
            +
              end
         | 
| 70 | 
            +
             | 
| 63 71 | 
             
              describe "groups" do
         | 
| 64 72 | 
             
                it "should allow chained methods" do
         | 
| 65 73 | 
             
                  triggers = builder.on(:foos){ |t|
         | 
| @@ -143,6 +151,16 @@ describe "builder" do | |
| 143 151 | 
             
                    grep(/DEFINER = 'user'@'host'/).size.should eql(1)
         | 
| 144 152 | 
             
                end
         | 
| 145 153 |  | 
| 154 | 
            +
                it "should infer `if' conditionals from `of' columns" do
         | 
| 155 | 
            +
                  builder.on(:foos).after(:update).of(:bar){ "BAZ" }.generate.join("\n").
         | 
| 156 | 
            +
                    should include("IF NEW.bar <> OLD.bar OR (NEW.bar IS NULL) <> (OLD.bar IS NULL) THEN")
         | 
| 157 | 
            +
                end
         | 
| 158 | 
            +
             | 
| 159 | 
            +
                it "should merge `where` and `of` into an `if` conditional" do
         | 
| 160 | 
            +
                  builder.on(:foos).after(:update).of(:bar).where("lol"){ "BAZ" }.generate.join("\n").
         | 
| 161 | 
            +
                    should include("IF (lol) AND (NEW.bar <> OLD.bar OR (NEW.bar IS NULL) <> (OLD.bar IS NULL)) THEN")
         | 
| 162 | 
            +
                end
         | 
| 163 | 
            +
             | 
| 146 164 | 
             
                it "should reject :invoker security" do
         | 
| 147 165 | 
             
                  lambda {
         | 
| 148 166 | 
             
                    builder.on(:foos).after(:update).security(:invoker){ "FOO" }.generate
         | 
| @@ -170,7 +188,7 @@ describe "builder" do | |
| 170 188 |  | 
| 171 189 | 
             
              context "postgresql" do
         | 
| 172 190 | 
             
                before(:each) do
         | 
| 173 | 
            -
                  @adapter = MockAdapter.new("postgresql", :postgresql_version =>  | 
| 191 | 
            +
                  @adapter = MockAdapter.new("postgresql", :postgresql_version => 94000)
         | 
| 174 192 | 
             
                end
         | 
| 175 193 |  | 
| 176 194 | 
             
                it "should create multiple triggers for a group" do
         | 
| @@ -201,6 +219,11 @@ describe "builder" do | |
| 201 219 | 
             
                  trigger.warnings.first.first.should =~ /trigger group has an explicit name/
         | 
| 202 220 | 
             
                end
         | 
| 203 221 |  | 
| 222 | 
            +
                it "should accept `of' columns" do
         | 
| 223 | 
            +
                  trigger = builder.on(:foos).after(:update).of(:bar, :baz){ "BAR" }
         | 
| 224 | 
            +
                  trigger.generate.grep(/AFTER UPDATE OF bar, baz/).size.should eql(1)
         | 
| 225 | 
            +
                end
         | 
| 226 | 
            +
             | 
| 204 227 | 
             
                it "should accept security" do
         | 
| 205 228 | 
             
                  builder.on(:foos).after(:update).security(:invoker){ "FOO" }.generate.
         | 
| 206 229 | 
             
                    grep(/SECURITY/).size.should eql(0) # default, so we don't include it
         | 
| @@ -253,6 +276,11 @@ describe "builder" do | |
| 253 276 | 
             
                  }.should raise_error
         | 
| 254 277 | 
             
                end
         | 
| 255 278 |  | 
| 279 | 
            +
                it "should allow variable declarations" do
         | 
| 280 | 
            +
                  builder.on(:foos).after(:insert).declare("foo INT"){ "FOO" }.generate.join("\n").
         | 
| 281 | 
            +
                    should match(/DECLARE\s*foo INT;\s*BEGIN\s*FOO/)
         | 
| 282 | 
            +
                end
         | 
| 283 | 
            +
             | 
| 256 284 | 
             
                context "legacy" do
         | 
| 257 285 | 
             
                  it "should reject truncate pre-8.4" do
         | 
| 258 286 | 
             
                    @adapter = MockAdapter.new("postgresql", :postgresql_version => 80300)
         | 
| @@ -273,6 +301,12 @@ describe "builder" do | |
| 273 301 | 
             
                      builder.on(:foos).after(:insert).where("BAR").nowrap{ "FOO" }.generate
         | 
| 274 302 | 
             
                    }.should raise_error
         | 
| 275 303 | 
             
                  end
         | 
| 304 | 
            +
             | 
| 305 | 
            +
                  it "should infer `if' conditionals from `of' columns on pre-9.0" do
         | 
| 306 | 
            +
                    @adapter = MockAdapter.new("postgresql", :postgresql_version => 80400)
         | 
| 307 | 
            +
                    builder.on(:foos).after(:update).of(:bar){ "BAZ" }.generate.join("\n").
         | 
| 308 | 
            +
                      should include("IF NEW.bar <> OLD.bar OR (NEW.bar IS NULL) <> (OLD.bar IS NULL) THEN")
         | 
| 309 | 
            +
                  end
         | 
| 276 310 | 
             
                end
         | 
| 277 311 | 
             
              end
         | 
| 278 312 |  | 
| @@ -309,6 +343,11 @@ describe "builder" do | |
| 309 343 | 
             
                  trigger.warnings.first.first.should =~ /trigger group has an explicit name/
         | 
| 310 344 | 
             
                end
         | 
| 311 345 |  | 
| 346 | 
            +
                it "should accept `of' columns" do
         | 
| 347 | 
            +
                  trigger = builder.on(:foos).after(:update).of(:bar, :baz){ "BAR" }
         | 
| 348 | 
            +
                  trigger.generate.grep(/AFTER UPDATE OF bar, baz/).size.should eql(1)
         | 
| 349 | 
            +
                end
         | 
| 350 | 
            +
             | 
| 312 351 | 
             
                it "should reject security" do
         | 
| 313 352 | 
             
                  lambda {
         | 
| 314 353 | 
             
                    builder.on(:foos).after(:update).security(:definer){ "FOO" }.generate
         | 
    
        metadata
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: hairtrigger
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0.2. | 
| 4 | 
            +
              version: 0.2.10
         | 
| 5 5 | 
             
              prerelease: 
         | 
| 6 6 | 
             
            platform: ruby
         | 
| 7 7 | 
             
            authors:
         | 
| @@ -9,7 +9,7 @@ authors: | |
| 9 9 | 
             
            autorequire: 
         | 
| 10 10 | 
             
            bindir: bin
         | 
| 11 11 | 
             
            cert_chain: []
         | 
| 12 | 
            -
            date: 2014- | 
| 12 | 
            +
            date: 2014-06-24 00:00:00.000000000 Z
         | 
| 13 13 | 
             
            dependencies:
         | 
| 14 14 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 15 15 | 
             
              name: activerecord
         |