[ARVADOS] created: 1.2.0-71-ga18fe6288

Git user git at public.curoverse.com
Thu Oct 4 15:31:38 EDT 2018


        at  a18fe628853e2042bb104088dd586cb8f41adcef (commit)


commit a18fe628853e2042bb104088dd586cb8f41adcef
Author: Lucas Di Pentima <ldipentima at veritasgenetics.com>
Date:   Thu Oct 4 15:54:18 2018 -0300

    13561: Avoid permission links to be attached to past collection versions.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <ldipentima at veritasgenetics.com>

diff --git a/services/api/app/models/link.rb b/services/api/app/models/link.rb
index dc961667b..bf21cf4b6 100644
--- a/services/api/app/models/link.rb
+++ b/services/api/app/models/link.rb
@@ -48,8 +48,12 @@ class Link < ArvadosModel
     # Administrators can grant permissions
     return true if current_user.is_admin
 
-    # All users can grant permissions on objects they own or can manage
     head_obj = ArvadosModel.find_by_uuid(head_uuid)
+
+    # No permission links can be pointed to past collection versions
+    return false if head_obj.is_a?(Collection) && head_obj.current_version_uuid != head_uuid
+
+    # All users can grant permissions on objects they own or can manage
     return true if current_user.can?(manage: head_obj)
 
     # Default = deny.
diff --git a/services/api/test/fixtures/collections.yml b/services/api/test/fixtures/collections.yml
index 2bc362a4c..62bb644c0 100644
--- a/services/api/test/fixtures/collections.yml
+++ b/services/api/test/fixtures/collections.yml
@@ -94,6 +94,21 @@ w_a_z_file:
   updated_at: 2015-02-09T10:53:38Z
   manifest_text: ". 4c6c2c0ac8aa0696edd7316a3be5ca3c+5 0:5:w\\040\\141\\040z\n"
   name: "\"w a z\" file"
+  version: 2
+
+w_a_z_file_version_1:
+  uuid: zzzzz-4zz18-25k12570yk1ver1
+  current_version_uuid: zzzzz-4zz18-25k12570yk134b3
+  portable_data_hash: 8706aadd12a0ebc07d74cae88762ba9e+56
+  owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+  created_at: 2015-02-09T10:53:38Z
+  modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+  modified_by_user_uuid: zzzzz-tpzed-d9tiejq69daie8f
+  modified_at: 2015-02-09T10:53:38Z
+  updated_at: 2015-02-09T10:53:38Z
+  manifest_text: ". 4c6c2c0ac8aa0696edd7316a3be5ca3c+5 0:5:w\\040\\141\\040z\n"
+  name: "waz file"
+  version: 1
 
 multilevel_collection_1:
   uuid: zzzzz-4zz18-pyw8yp9g3pr7irn
diff --git a/services/api/test/unit/link_test.rb b/services/api/test/unit/link_test.rb
index cba5d20cb..00f3cc291 100644
--- a/services/api/test/unit/link_test.rb
+++ b/services/api/test/unit/link_test.rb
@@ -80,4 +80,9 @@ class LinkTest < ActiveSupport::TestCase
   test "link granting project permissions to unreadable user is invalid" do
     refute new_active_link_valid?(tail_uuid: users(:admin).uuid)
   end
+
+  test "permission link can't exist on past collection versions" do
+    refute new_active_link_valid?(tail_uuid: groups(:public).uuid,
+                                  head_uuid: collections(:w_a_z_file_version_1).uuid)
+  end
 end

commit 762e0ab6ac28f783c5d0c9fdec438f13418f9ad6
Author: Lucas Di Pentima <ldipentima at veritasgenetics.com>
Date:   Thu Oct 4 15:22:31 2018 -0300

    13561: Expand index API to include past versions.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <ldipentima at veritasgenetics.com>

diff --git a/services/api/app/controllers/application_controller.rb b/services/api/app/controllers/application_controller.rb
index 05b39c1ae..a6718dd4d 100644
--- a/services/api/app/controllers/application_controller.rb
+++ b/services/api/app/controllers/application_controller.rb
@@ -189,7 +189,10 @@ class ApplicationController < ActionController::Base
   end
 
   def find_objects_for_index
-    @objects ||= model_class.readable_by(*@read_users, {:include_trash => (params[:include_trash] || 'untrash' == action_name)})
+    @objects ||= model_class.readable_by(*@read_users, {
+      :include_trash => (params[:include_trash] || 'untrash' == action_name),
+      :include_old_versions => params[:include_old_versions]
+    })
     apply_where_limit_order_params
   end
 
diff --git a/services/api/app/controllers/arvados/v1/collections_controller.rb b/services/api/app/controllers/arvados/v1/collections_controller.rb
index 6e77c12a1..cfd1e9dc2 100644
--- a/services/api/app/controllers/arvados/v1/collections_controller.rb
+++ b/services/api/app/controllers/arvados/v1/collections_controller.rb
@@ -27,9 +27,14 @@ class Arvados::V1::CollectionsController < ApplicationController
   end
 
   def find_objects_for_index
+    opts = {}
     if params[:include_trash] || ['destroy', 'trash', 'untrash'].include?(action_name)
-      @objects = Collection.readable_by(*@read_users, {include_trash: true})
+      opts.update({include_trash: true})
     end
+    if params[:include_old_versions]
+      opts.update({include_old_versions: true})
+    end
+    @objects = Collection.readable_by(*@read_users, opts) if !opts.empty?
     super
   end
 
@@ -223,4 +228,17 @@ class Arvados::V1::CollectionsController < ApplicationController
       @select ||= model_class.selectable_attributes - ["manifest_text", "unsigned_manifest_text"]
     end
   end
+
+  def load_filters_param
+    super
+    return if !params[:include_old_versions]
+    @filters = @filters.map do |col, operator, operand|
+      # Replace uuid filters when including past versions
+      if col == 'uuid'
+        ['current_version_uuid', operator, operand]
+      else
+        [col, operator, operand]
+      end
+    end
+  end
 end
diff --git a/services/api/app/models/arvados_model.rb b/services/api/app/models/arvados_model.rb
index c983372ad..801da17db 100644
--- a/services/api/app/models/arvados_model.rb
+++ b/services/api/app/models/arvados_model.rb
@@ -369,7 +369,13 @@ class ArvadosModel < ActiveRecord::Base
         end
 
         self[:name] = new_name
-        self[:uuid] = nil if uuid_was.nil? && !uuid.nil?
+        if uuid_was.nil? && !uuid.nil?
+          self[:uuid] = nil
+          if self.is_a? Collection
+            # Reset so that is assigned to the new UUID
+            self[:current_version_uuid] = nil
+          end
+        end
         conn.exec_query 'SAVEPOINT save_with_unique_name'
         retry
       ensure
diff --git a/services/api/app/models/collection.rb b/services/api/app/models/collection.rb
index 349c002ef..2e79d7460 100644
--- a/services/api/app/models/collection.rb
+++ b/services/api/app/models/collection.rb
@@ -29,7 +29,6 @@ class Collection < ArvadosModel
   validate :ensure_storage_classes_contain_non_empty_strings
   validate :old_versions_cannot_be_updated, on: :update
   before_save :set_file_names
-  before_create :set_current_version_uuid
 
   api_accessible :user, extend: :common do |t|
     t.add :name
@@ -219,10 +218,6 @@ class Collection < ArvadosModel
     ['current_version_uuid']
   end
 
-  def set_current_version_uuid
-    self.current_version_uuid ||= self.uuid
-  end
-
   def save! *args
     # Skip if feature is disabled or saving a new record
     if !Rails.configuration.collection_versioning || new_record?
@@ -547,7 +542,7 @@ class Collection < ArvadosModel
   end
 
   def self.searchable_columns operator
-    super - ["manifest_text", "current_version_uuid"]
+    super - ["manifest_text"]
   end
 
   def self.full_text_searchable_columns
@@ -632,4 +627,10 @@ class Collection < ArvadosModel
       raise ArvadosModel::PermissionDeniedError.new("previous versions cannot be updated")
     end
   end
+
+  def assign_uuid
+    super
+    self.current_version_uuid ||= self.uuid
+    true
+  end
 end
diff --git a/services/api/db/migrate/20181004131141_add_current_version_uuid_to_collection_search_index.rb b/services/api/db/migrate/20181004131141_add_current_version_uuid_to_collection_search_index.rb
new file mode 100644
index 000000000..63e99191a
--- /dev/null
+++ b/services/api/db/migrate/20181004131141_add_current_version_uuid_to_collection_search_index.rb
@@ -0,0 +1,17 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+class AddCurrentVersionUuidToCollectionSearchIndex < ActiveRecord::Migration
+  disable_ddl_transaction!
+
+  def up
+    remove_index :collections, :name => 'collections_search_index'
+    add_index :collections, ["owner_uuid", "modified_by_client_uuid", "modified_by_user_uuid", "portable_data_hash", "uuid", "name", "current_version_uuid"], name: 'collections_search_index', algorithm: :concurrently
+  end
+
+  def down
+    remove_index :collections, :name => 'collections_search_index'
+    add_index :collections, ["owner_uuid", "modified_by_client_uuid", "modified_by_user_uuid", "portable_data_hash", "uuid", "name"], name: 'collections_search_index', algorithm: :concurrently
+  end
+end
diff --git a/services/api/db/structure.sql b/services/api/db/structure.sql
index 67bc6856d..71d630f6e 100644
--- a/services/api/db/structure.sql
+++ b/services/api/db/structure.sql
@@ -1634,7 +1634,7 @@ CREATE INDEX collections_full_text_search_idx ON public.collections USING gin (t
 -- Name: collections_search_index; Type: INDEX; Schema: public; Owner: -
 --
 
-CREATE INDEX collections_search_index ON public.collections USING btree (owner_uuid, modified_by_client_uuid, modified_by_user_uuid, portable_data_hash, uuid, name);
+CREATE INDEX collections_search_index ON public.collections USING btree (owner_uuid, modified_by_client_uuid, modified_by_user_uuid, portable_data_hash, uuid, name, current_version_uuid);
 
 
 --
@@ -3187,3 +3187,5 @@ INSERT INTO schema_migrations (version) VALUES ('20180919001158');
 
 INSERT INTO schema_migrations (version) VALUES ('20181001175023');
 
+INSERT INTO schema_migrations (version) VALUES ('20181004131141');
+
diff --git a/services/api/test/functional/arvados/v1/collections_controller_test.rb b/services/api/test/functional/arvados/v1/collections_controller_test.rb
index 970368479..24a1a09cf 100644
--- a/services/api/test/functional/arvados/v1/collections_controller_test.rb
+++ b/services/api/test/functional/arvados/v1/collections_controller_test.rb
@@ -45,6 +45,17 @@ class Arvados::V1::CollectionsControllerTest < ActionController::TestCase
            "basic Collections index included past version")
   end
 
+  test "get index with include_old_versions" do
+    authorize_with :active
+    get :index, {
+      include_old_versions: true
+    }
+    assert_response :success
+    assert(assigns(:objects).andand.any?, "no Collections returned in index")
+    assert(json_response["items"].any? { |c| c["uuid"] == collections(:collection_owned_by_active_past_version_1).uuid },
+           "past version not included on index")
+  end
+
   test "collections.get returns signed locators, and no unsigned_manifest_text" do
     permit_unsigned_manifests
     authorize_with :active
@@ -1145,4 +1156,21 @@ EOS
     end
     assert_includes(item_uuids, collections(:collection_in_trashed_subproject).uuid)
   end
+
+  test 'can get collection with past versions' do
+    authorize_with :active
+    get :index, {
+      filters: [['uuid','=',collections(:collection_owned_by_active).uuid]],
+      include_old_versions: true
+    }
+    assert_response :success
+    assert_equal 2, assigns(:objects).length
+    assert_equal 2, json_response['items_available']
+    assert_equal 2, json_response['items'].count
+    json_response['items'].each do |c|
+      assert_equal collections(:collection_owned_by_active).uuid,
+                   c['current_version_uuid'],
+                   'response includes a version from a different collection'
+    end
+  end
 end

commit 14cfc3a648e17b4e52fd8cc17c9250291e53afa7
Author: Lucas Di Pentima <ldipentima at veritasgenetics.com>
Date:   Wed Oct 3 17:11:38 2018 -0300

    13561: Avoid collections.index to include old versions
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <ldipentima at veritasgenetics.com>

diff --git a/services/api/app/models/arvados_model.rb b/services/api/app/models/arvados_model.rb
index 4ba4ac771..c983372ad 100644
--- a/services/api/app/models/arvados_model.rb
+++ b/services/api/app/models/arvados_model.rb
@@ -240,7 +240,7 @@ class ArvadosModel < ActiveRecord::Base
     end.compact.uniq
   end
 
-  # Return a query with read permissions restricted to the union of of the
+  # Return a query with read permissions restricted to the union of the
   # permissions of the members of users_list, i.e. if something is readable by
   # any user in users_list, it will be readable in the query returned by this
   # function.
@@ -258,6 +258,7 @@ class ArvadosModel < ActiveRecord::Base
     # Collect the UUIDs of the authorized users.
     sql_table = kwargs.fetch(:table_name, table_name)
     include_trash = kwargs.fetch(:include_trash, false)
+    include_old_versions = kwargs.fetch(:include_old_versions, false)
 
     sql_conds = nil
     user_uuids = users_list.map { |u| u.uuid }
@@ -268,6 +269,11 @@ class ArvadosModel < ActiveRecord::Base
       exclude_trashed_records = "AND #{sql_table}.is_trashed = false"
     end
 
+    exclude_old_versions = ""
+    if !include_old_versions && sql_table == "collections"
+      exclude_old_versions = "AND #{sql_table}.uuid = #{sql_table}.current_version_uuid"
+    end
+
     if users_list.select { |u| u.is_admin }.any?
       # Admin skips most permission checks, but still want to filter on trashed items.
       if !include_trash
@@ -275,7 +281,7 @@ class ArvadosModel < ActiveRecord::Base
           # Only include records where the owner is not trashed
           sql_conds = "NOT EXISTS(SELECT 1 FROM #{PERMISSION_VIEW} "+
                       "WHERE trashed = 1 AND "+
-                      "(#{sql_table}.owner_uuid = target_uuid)) #{exclude_trashed_records}"
+                      "(#{sql_table}.owner_uuid = target_uuid)) #{exclude_trashed_records} #{exclude_old_versions}"
         end
       end
     else
@@ -312,7 +318,7 @@ class ArvadosModel < ActiveRecord::Base
                        "(#{sql_table}.head_uuid IN (:user_uuids) OR #{sql_table}.tail_uuid IN (:user_uuids)))"
       end
 
-      sql_conds = "(#{direct_check} #{owner_check} #{links_cond}) #{exclude_trashed_records}"
+      sql_conds = "(#{direct_check} #{owner_check} #{links_cond}) #{exclude_trashed_records} #{exclude_old_versions}"
 
     end
 
diff --git a/services/api/app/models/collection.rb b/services/api/app/models/collection.rb
index a449b5ef6..349c002ef 100644
--- a/services/api/app/models/collection.rb
+++ b/services/api/app/models/collection.rb
@@ -47,6 +47,9 @@ class Collection < ArvadosModel
     t.add :delete_at
     t.add :trash_at
     t.add :is_trashed
+    t.add :version
+    t.add :current_version_uuid
+    t.add :preserve_version
   end
 
   after_initialize do
diff --git a/services/api/test/fixtures/collections.yml b/services/api/test/fixtures/collections.yml
index 3578651ba..2bc362a4c 100644
--- a/services/api/test/fixtures/collections.yml
+++ b/services/api/test/fixtures/collections.yml
@@ -27,6 +27,21 @@ collection_owned_by_active:
   updated_at: 2014-02-03T17:22:54Z
   manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
   name: owned_by_active
+  version: 2
+
+collection_owned_by_active_past_version_1:
+  uuid: zzzzz-4zz18-znfnqtbbv4spast
+  current_version_uuid: zzzzz-4zz18-bv31uwvy3neko21
+  portable_data_hash: fa7aeb5140e2848d39b416daeef4ffc5+45
+  owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+  created_at: 2014-02-03T17:22:54Z
+  modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+  modified_by_user_uuid: zzzzz-tpzed-d9tiejq69daie8f
+  modified_at: 2014-02-03T15:22:54Z
+  updated_at: 2014-02-03T15:22:54Z
+  manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
+  name: owned_by_active_version_1
+  version: 1
 
 foo_file:
   uuid: zzzzz-4zz18-znfnqtbbv4spc3w
diff --git a/services/api/test/functional/arvados/v1/collections_controller_test.rb b/services/api/test/functional/arvados/v1/collections_controller_test.rb
index e6ecea219..970368479 100644
--- a/services/api/test/functional/arvados/v1/collections_controller_test.rb
+++ b/services/api/test/functional/arvados/v1/collections_controller_test.rb
@@ -41,6 +41,8 @@ class Arvados::V1::CollectionsControllerTest < ActionController::TestCase
     assert(assigns(:objects).andand.any?, "no Collections returned in index")
     refute(json_response["items"].any? { |c| c.has_key?("manifest_text") },
            "basic Collections index included manifest_text")
+    refute(json_response["items"].any? { |c| c["uuid"] == collections(:collection_owned_by_active_past_version_1).uuid },
+           "basic Collections index included past version")
   end
 
   test "collections.get returns signed locators, and no unsigned_manifest_text" do

commit f0a85e273056d0ad440084c11a37c73ce25fb4f6
Author: Lucas Di Pentima <ldipentima at veritasgenetics.com>
Date:   Tue Oct 2 14:51:35 2018 -0300

    13561: Save new version when preserve_version==true or config idle time is up.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <ldipentima at veritasgenetics.com>

diff --git a/services/api/app/models/collection.rb b/services/api/app/models/collection.rb
index 025811cf8..a449b5ef6 100644
--- a/services/api/app/models/collection.rb
+++ b/services/api/app/models/collection.rb
@@ -221,9 +221,19 @@ class Collection < ArvadosModel
   end
 
   def save! *args
-    if !Rails.configuration.collection_versioning || new_record? || (!self.changes.include?('uuid') && current_version_uuid != uuid)
+    # Skip if feature is disabled or saving a new record
+    if !Rails.configuration.collection_versioning || new_record?
       return super
     end
+    # Skip if updating a past version
+    if !self.changes.include?('uuid') && current_version_uuid != uuid
+      return super
+    end
+    # Skip if current version shouldn't (explicitly or implicitly) be preserved
+    if !should_preserve_version?
+      return super
+    end
+
     changes = self.changes
     # Updates that will be synced with older versions
     synced_updates = ['uuid', 'owner_uuid', 'delete_at', 'trash_at', 'is_trashed',
@@ -231,10 +241,18 @@ class Collection < ArvadosModel
     # Updates that will produce a new version
     versionable_updates = ['manifest_text', 'description', 'properties', 'name'] & changes.keys
 
-    if versionable_updates.empty? && synced_updates.empty?
-      # Updates don't include interesting attributes, so don't save a new snapshot nor
-      # sync older versions.
-      return super
+    if versionable_updates.empty?
+      # Keep preserve_version enabled for the next update, if applicable.
+      self.preserve_version ||= self.preserve_version_was
+      if !self.changes.include?('preserve_version')
+        changes.delete('preserve_version')
+      end
+
+      if synced_updates.empty?
+        # Updates don't include interesting attributes, so don't save a new
+        # snapshot nor sync older versions.
+        return super
+      end
     end
 
     # Does row locking (transaction is implicit) because 'version'
@@ -273,6 +291,7 @@ class Collection < ArvadosModel
         snapshot.created_at = created_at
         # Update current version number
         self.version += 1
+        self.preserve_version = false
       end
       # Restore requested changes on the current version
       changes.keys.each do |attr|
@@ -287,6 +306,16 @@ class Collection < ArvadosModel
     end
   end
 
+  def should_preserve_version?
+    idle_threshold = Rails.configuration.preserve_version_if_idle
+    if !self.preserve_version_was &&
+      (idle_threshold < 0 ||
+        (idle_threshold > 0 && self.modified_at_was > db_current_time-idle_threshold.seconds))
+      return false
+    end
+    return true
+  end
+
   def check_encoding
     if manifest_text.encoding.name == 'UTF-8' and manifest_text.valid_encoding?
       true
diff --git a/services/api/config/application.default.yml b/services/api/config/application.default.yml
index f480904c4..c9b7ee988 100644
--- a/services/api/config/application.default.yml
+++ b/services/api/config/application.default.yml
@@ -500,9 +500,16 @@ common:
   # keep_web_service_url: https://download.uuid_prefix.arvadosapi.com/
   keep_web_service_url: false
 
-  # If true, enable collection versioning: With every update, a snapshot of the
-  # collection's previous state is created and linked to the current collection.
+  # If true, enable collection versioning.
+  # When a collection's preserve_version field is true or the current version
+  # is older than the amount of seconds defined on preserve_version_if_idle,
+  # a snapshot of the collection's previous state is created and linked to
+  # the current collection.
   collection_versioning: false
+  #   0 = auto-create a new version on every update.
+  #  -1 = never auto-create new versions.
+  # > 0 = auto-create a new version when older than the specified number of seconds.
+  preserve_version_if_idle: -1
 
 development:
   force_ssl: false
diff --git a/services/api/db/migrate/20181001175023_add_preserve_version_to_collections.rb b/services/api/db/migrate/20181001175023_add_preserve_version_to_collections.rb
new file mode 100644
index 000000000..fbdc397fc
--- /dev/null
+++ b/services/api/db/migrate/20181001175023_add_preserve_version_to_collections.rb
@@ -0,0 +1,9 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+class AddPreserveVersionToCollections < ActiveRecord::Migration
+  def change
+    add_column :collections, :preserve_version, :boolean, default: false
+  end
+end
diff --git a/services/api/db/structure.sql b/services/api/db/structure.sql
index de5e2e127..67bc6856d 100644
--- a/services/api/db/structure.sql
+++ b/services/api/db/structure.sql
@@ -174,7 +174,8 @@ CREATE TABLE public.collections (
     storage_classes_confirmed jsonb DEFAULT '[]'::jsonb,
     storage_classes_confirmed_at timestamp without time zone,
     current_version_uuid character varying,
-    version integer DEFAULT 1 NOT NULL
+    version integer DEFAULT 1 NOT NULL,
+    preserve_version boolean DEFAULT false
 );
 
 
@@ -3184,3 +3185,5 @@ INSERT INTO schema_migrations (version) VALUES ('20180915155335');
 
 INSERT INTO schema_migrations (version) VALUES ('20180919001158');
 
+INSERT INTO schema_migrations (version) VALUES ('20181001175023');
+
diff --git a/services/api/test/unit/collection_test.rb b/services/api/test/unit/collection_test.rb
index 569e589b2..a1008eec4 100644
--- a/services/api/test/unit/collection_test.rb
+++ b/services/api/test/unit/collection_test.rb
@@ -106,8 +106,76 @@ class CollectionTest < ActiveSupport::TestCase
     end
   end
 
+  test "auto-create version after idle setting" do
+    Rails.configuration.collection_versioning = true
+    Rails.configuration.preserve_version_if_idle = 600 # 10 minutes
+    act_as_user users(:active) do
+      # Set up initial collection
+      c = create_collection 'foo', Encoding::US_ASCII
+      assert c.valid?
+      assert_equal 1, c.version
+      assert_equal false, c.preserve_version
+      # Make a versionable update, it shouldn't create a new version yet
+      c.update_attributes!({'name' => 'bar'})
+      c.reload
+      assert_equal 'bar', c.name
+      assert_equal 1, c.version
+      # Update modified_at to trigger a version auto-creation
+      fifteen_min_ago = Time.now - 15.minutes
+      c.update_column('modified_at', fifteen_min_ago) # Update without validations/callbacks
+      c.reload
+      assert_equal fifteen_min_ago.to_i, c.modified_at.to_i
+      c.update_attributes!({'name' => 'baz'})
+      c.reload
+      assert_equal 'baz', c.name
+      assert_equal 2, c.version
+      # Make another update, no new version should be created
+      c.update_attributes!({'name' => 'foobar'})
+      c.reload
+      assert_equal 'foobar', c.name
+      assert_equal 2, c.version
+    end
+  end
+
+  test "preserve_version=false assignment is ignored while being true and not producing a new version" do
+    Rails.configuration.collection_versioning = true
+    Rails.configuration.preserve_version_if_idle = 3600
+    act_as_user users(:active) do
+      # Set up initial collection
+      c = create_collection 'foo', Encoding::US_ASCII
+      assert c.valid?
+      assert_equal 1, c.version
+      assert_equal false, c.preserve_version
+      # This update shouldn't produce a new version, as the idle time is not up
+      c.update_attributes!({
+        'name' => 'bar',
+        'preserve_version' => true
+      })
+      c.reload
+      assert_equal 1, c.version
+      assert_equal 'bar', c.name
+      assert_equal true, c.preserve_version
+      # Make sure preserve_version is not disabled after being enabled, unless
+      # a new version is created.
+      c.update_attributes!({
+        'preserve_version' => false,
+        'replication_desired' => 2
+      })
+      c.reload
+      assert_equal 1, c.version
+      assert_equal 2, c.replication_desired
+      assert_equal true, c.preserve_version
+      c.update_attributes!({'name' => 'foobar'})
+      c.reload
+      assert_equal 2, c.version
+      assert_equal false, c.preserve_version
+      assert_equal 'foobar', c.name
+    end
+  end
+
   test "uuid updates on current version make older versions update their pointers" do
     Rails.configuration.collection_versioning = true
+    Rails.configuration.preserve_version_if_idle = 0
     act_as_system_user do
       # Set up initial collection
       c = create_collection 'foo', Encoding::US_ASCII
@@ -130,6 +198,7 @@ class CollectionTest < ActiveSupport::TestCase
 
   test "older versions' modified_at indicate when they're created" do
     Rails.configuration.collection_versioning = true
+    Rails.configuration.preserve_version_if_idle = 0
     act_as_user users(:active) do
       # Set up initial collection
       c = create_collection 'foo', Encoding::US_ASCII
@@ -163,6 +232,7 @@ class CollectionTest < ActiveSupport::TestCase
 
   test "older versions should no be directly updatable" do
     Rails.configuration.collection_versioning = true
+    Rails.configuration.preserve_version_if_idle = 0
     act_as_user users(:active) do
       # Set up initial collection
       c = create_collection 'foo', Encoding::US_ASCII
@@ -205,6 +275,7 @@ class CollectionTest < ActiveSupport::TestCase
   ].each do |attr, first_val, second_val|
     test "sync #{attr} with older versions" do
       Rails.configuration.collection_versioning = true
+      Rails.configuration.preserve_version_if_idle = 0
       act_as_system_user do
         # Set up initial collection
         c = create_collection 'foo', Encoding::US_ASCII
@@ -246,6 +317,7 @@ class CollectionTest < ActiveSupport::TestCase
   ].each do |versioning, attr, val, new_version_expected|
     test "update #{attr} with versioning #{versioning ? '' : 'not '}enabled should #{new_version_expected ? '' : 'not '}create a new version" do
       Rails.configuration.collection_versioning = versioning
+      Rails.configuration.preserve_version_if_idle = 0
       act_as_user users(:active) do
         # Create initial collection
         c = create_collection 'foo', Encoding::US_ASCII
@@ -280,6 +352,7 @@ class CollectionTest < ActiveSupport::TestCase
 
   test 'with versioning enabled, simultaneous updates increment version correctly' do
     Rails.configuration.collection_versioning = true
+    Rails.configuration.preserve_version_if_idle = 0
     act_as_user users(:active) do
       # Create initial collection
       col = create_collection 'foo', Encoding::US_ASCII

commit 647511030800d228feb6955dfab9cb0a26cbfcfb
Author: Lucas Di Pentima <ldipentima at veritasgenetics.com>
Date:   Mon Oct 1 14:32:12 2018 -0300

    13561: Old collection version's modified_at reflects version's create time.
    
    Don't update this field when syncing past versions' fields with selected
    current updates.
    Also, keep created_at the same as the current version when snapshotting.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <ldipentima at veritasgenetics.com>

diff --git a/services/api/app/models/arvados_model.rb b/services/api/app/models/arvados_model.rb
index c67a3961d..4ba4ac771 100644
--- a/services/api/app/models/arvados_model.rb
+++ b/services/api/app/models/arvados_model.rb
@@ -537,12 +537,12 @@ class ArvadosModel < ActiveRecord::Base
 
   def update_modified_by_fields
     current_time = db_current_time
-    self.created_at = created_at_was || current_time
+    self.created_at ||= created_at_was || current_time
     self.updated_at = current_time
     self.owner_uuid ||= current_default_owner if self.respond_to? :owner_uuid=
-    self.modified_at = current_time
     if !anonymous_updater
       self.modified_by_user_uuid = current_user ? current_user.uuid : nil
+      self.modified_at = current_time
     end
     self.modified_by_client_uuid = current_api_client ? current_api_client.uuid : nil
     true
diff --git a/services/api/app/models/collection.rb b/services/api/app/models/collection.rb
index f051fb344..025811cf8 100644
--- a/services/api/app/models/collection.rb
+++ b/services/api/app/models/collection.rb
@@ -220,7 +220,7 @@ class Collection < ArvadosModel
     self.current_version_uuid ||= self.uuid
   end
 
-  def save!
+  def save! *args
     if !Rails.configuration.collection_versioning || new_record? || (!self.changes.include?('uuid') && current_version_uuid != uuid)
       return super
     end
@@ -256,7 +256,9 @@ class Collection < ArvadosModel
           c.attributes = updates
           # Use a different validation context to skip the 'old_versions_cannot_be_updated'
           # validator, as on this case it is legal to update some fields.
-          c.save(context: :update_old_versions)
+          leave_modified_by_user_alone do
+            c.save(context: :update_old_versions)
+          end
         end
         # Also update current object just in case a new version will be created,
         # as it has to receive the same values for the synced attributes.
@@ -268,6 +270,7 @@ class Collection < ArvadosModel
         # Create a snapshot of the original collection
         snapshot = self.dup
         snapshot.uuid = nil # Reset UUID so it's created as a new record
+        snapshot.created_at = created_at
         # Update current version number
         self.version += 1
       end
diff --git a/services/api/test/unit/collection_test.rb b/services/api/test/unit/collection_test.rb
index 31c76efa3..569e589b2 100644
--- a/services/api/test/unit/collection_test.rb
+++ b/services/api/test/unit/collection_test.rb
@@ -128,9 +128,42 @@ class CollectionTest < ActiveSupport::TestCase
     end
   end
 
+  test "older versions' modified_at indicate when they're created" do
+    Rails.configuration.collection_versioning = true
+    act_as_user users(:active) do
+      # Set up initial collection
+      c = create_collection 'foo', Encoding::US_ASCII
+      assert c.valid?
+      # Make changes so that a new version is created
+      c.update_attributes!({'name' => 'bar'})
+      c.reload
+      assert_equal 2, c.version
+      # Get the old version
+      c_old = Collection.where(current_version_uuid: c.uuid, version: 1).first
+      assert_not_nil c_old
+
+      version_creation_datetime = c_old.modified_at.to_f
+      assert_equal c.created_at.to_f, c_old.created_at.to_f
+      # Current version is updated just a few milliseconds before the version is
+      # saved on the database.
+      assert_operator c.modified_at.to_f, :<, version_creation_datetime
+
+      # Make update on current version so old version get the attribute synced;
+      # its modified_at should not change.
+      new_replication = 3
+      c.update_attributes!({'replication_desired' => new_replication})
+      c.reload
+      assert_equal new_replication, c.replication_desired
+      c_old.reload
+      assert_equal new_replication, c_old.replication_desired
+      assert_equal version_creation_datetime, c_old.modified_at.to_f
+      assert_operator c.modified_at.to_f, :>, c_old.modified_at.to_f
+    end
+  end
+
   test "older versions should no be directly updatable" do
     Rails.configuration.collection_versioning = true
-    act_as_system_user do
+    act_as_user users(:active) do
       # Set up initial collection
       c = create_collection 'foo', Encoding::US_ASCII
       assert c.valid?
@@ -213,7 +246,7 @@ class CollectionTest < ActiveSupport::TestCase
   ].each do |versioning, attr, val, new_version_expected|
     test "update #{attr} with versioning #{versioning ? '' : 'not '}enabled should #{new_version_expected ? '' : 'not '}create a new version" do
       Rails.configuration.collection_versioning = versioning
-      act_as_system_user do
+      act_as_user users(:active) do
         # Create initial collection
         c = create_collection 'foo', Encoding::US_ASCII
         assert c.valid?
@@ -247,7 +280,7 @@ class CollectionTest < ActiveSupport::TestCase
 
   test 'with versioning enabled, simultaneous updates increment version correctly' do
     Rails.configuration.collection_versioning = true
-    act_as_system_user do
+    act_as_user users(:active) do
       # Create initial collection
       col = create_collection 'foo', Encoding::US_ASCII
       assert col.valid?

commit fb61bedcbc49d54708410ed394da133440a9d78f
Author: Lucas Di Pentima <ldipentima at veritasgenetics.com>
Date:   Sat Sep 29 10:20:24 2018 -0300

    13561: Avoid old versions to be updated, with test.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <ldipentima at veritasgenetics.com>

diff --git a/services/api/app/models/collection.rb b/services/api/app/models/collection.rb
index 61946f82b..f051fb344 100644
--- a/services/api/app/models/collection.rb
+++ b/services/api/app/models/collection.rb
@@ -27,6 +27,7 @@ class Collection < ArvadosModel
   validate :ensure_pdh_matches_manifest_text
   validate :ensure_storage_classes_desired_is_not_empty
   validate :ensure_storage_classes_contain_non_empty_strings
+  validate :old_versions_cannot_be_updated, on: :update
   before_save :set_file_names
   before_create :set_current_version_uuid
 
@@ -252,7 +253,10 @@ class Collection < ArvadosModel
           end
         end
         Collection.where('current_version_uuid = ? AND uuid != ?', uuid, uuid).each do |c|
-          c.update_attributes!(updates)
+          c.attributes = updates
+          # Use a different validation context to skip the 'old_versions_cannot_be_updated'
+          # validator, as on this case it is legal to update some fields.
+          c.save(context: :update_old_versions)
         end
         # Also update current object just in case a new version will be created,
         # as it has to receive the same values for the synced attributes.
@@ -585,4 +589,12 @@ class Collection < ArvadosModel
       end
     end
   end
+
+  def old_versions_cannot_be_updated
+    # We check for the '_was' values just in case the update operation
+    # includes a change on current_version_uuid or uuid.
+    if current_version_uuid_was != uuid_was
+      raise ArvadosModel::PermissionDeniedError.new("previous versions cannot be updated")
+    end
+  end
 end
diff --git a/services/api/test/unit/collection_test.rb b/services/api/test/unit/collection_test.rb
index e3c2a999c..31c76efa3 100644
--- a/services/api/test/unit/collection_test.rb
+++ b/services/api/test/unit/collection_test.rb
@@ -128,6 +128,42 @@ class CollectionTest < ActiveSupport::TestCase
     end
   end
 
+  test "older versions should no be directly updatable" do
+    Rails.configuration.collection_versioning = true
+    act_as_system_user do
+      # Set up initial collection
+      c = create_collection 'foo', Encoding::US_ASCII
+      assert c.valid?
+      # Make changes so that a new version is created
+      c.update_attributes!({'name' => 'bar'})
+      c.reload
+      assert_equal 2, c.version
+      # Get the old version
+      c_old = Collection.where(current_version_uuid: c.uuid, version: 1).first
+      assert_not_nil c_old
+      # With collection versioning still being enabled, try to update
+      assert_raises ArvadosModel::PermissionDeniedError do
+        c_old.update_attributes(name: 'this was foo')
+      end
+      c_old.reload
+      assert_equal 'foo', c_old.name
+      # Try to fool the validator attempting to make c_old to look like a
+      # current version, it should also fail.
+      assert_raises ArvadosModel::PermissionDeniedError do
+        c_old.update_attributes(current_version_uuid: c_old.uuid)
+      end
+      c_old.reload
+      assert_equal c.uuid, c_old.current_version_uuid
+      # Now disable collection versioning, it should behave the same way
+      Rails.configuration.collection_versioning = false
+      assert_raises ArvadosModel::PermissionDeniedError do
+        c_old.update_attributes(name: 'this was foo')
+      end
+      c_old.reload
+      assert_equal 'foo', c_old.name
+    end
+  end
+
   [
     ['owner_uuid', 'zzzzz-tpzed-d9tiejq69daie8f', 'zzzzz-tpzed-xurymjxw79nv3jz'],
     ['replication_desired', 2, 3],

commit be8fbfd594717232c4dc7c4d16cdf3bde1137ee0
Author: Lucas Di Pentima <ldipentima at veritasgenetics.com>
Date:   Tue Sep 25 18:44:56 2018 -0300

    13561: Sync selected fields changes with older versions, with tests.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <ldipentima at veritasgenetics.com>

diff --git a/services/api/app/models/collection.rb b/services/api/app/models/collection.rb
index 7e1cd4aff..61946f82b 100644
--- a/services/api/app/models/collection.rb
+++ b/services/api/app/models/collection.rb
@@ -220,32 +220,63 @@ class Collection < ArvadosModel
   end
 
   def save!
-    if !Rails.configuration.collection_versioning || new_record?
+    if !Rails.configuration.collection_versioning || new_record? || (!self.changes.include?('uuid') && current_version_uuid != uuid)
       return super
     end
     changes = self.changes
-    versionable_updates = ['manifest_text', 'description', 'properties', 'name']
-    if (changes.keys & versionable_updates).empty?
-      # Updates don't include interesting attributes, don't save a new snapshot.
+    # Updates that will be synced with older versions
+    synced_updates = ['uuid', 'owner_uuid', 'delete_at', 'trash_at', 'is_trashed',
+                      'replication_desired', 'storage_classes_desired'] & changes.keys
+    # Updates that will produce a new version
+    versionable_updates = ['manifest_text', 'description', 'properties', 'name'] & changes.keys
+
+    if versionable_updates.empty? && synced_updates.empty?
+      # Updates don't include interesting attributes, so don't save a new snapshot nor
+      # sync older versions.
       return super
     end
-    # Create a snapshot of the current collection before saving.
-    # Does row locking because 'version' is incremented.
+
+    # Does row locking (transaction is implicit) because 'version'
+    # may be incremented and the older versions synced.
+    # Note that 'with_lock' reloads the object after locking.
     with_lock do
-      # Note that 'with_lock' reloads the object after locking.
-      # Create a snapshot of the original collection
-      snapshot = self.dup
-      snapshot.uuid = nil # Reset UUID so it's created as a new record
+      # Sync older versions.
+      if !synced_updates.empty?
+        updates = {}
+        synced_updates.each do |attr|
+          if attr == 'uuid'
+            # Point old versions to current version's new UUID
+            updates['current_version_uuid'] = changes[attr].last
+          else
+            updates[attr] = changes[attr].last
+          end
+        end
+        Collection.where('current_version_uuid = ? AND uuid != ?', uuid, uuid).each do |c|
+          c.update_attributes!(updates)
+        end
+        # Also update current object just in case a new version will be created,
+        # as it has to receive the same values for the synced attributes.
+        self.attributes = updates
+      end
+      snapshot = nil
+      # Make a new version if applicable.
+      if !versionable_updates.empty?
+        # Create a snapshot of the original collection
+        snapshot = self.dup
+        snapshot.uuid = nil # Reset UUID so it's created as a new record
+        # Update current version number
+        self.version += 1
+      end
       # Restore requested changes on the current version
       changes.keys.each do |attr|
         next if attr == 'version'
         self.attributes = {attr => changes[attr].last}
       end
-      # Update current version number & save first to avoid index collision
-      self.version += 1
+      # Save current version first to avoid index collision
       super
-      # Save the snapshot with previous state
-      snapshot.save!
+      # Save the snapshot with previous state (if applicable)
+      snapshot.andand.save!
+      return true
     end
   end
 
diff --git a/services/api/test/unit/collection_test.rb b/services/api/test/unit/collection_test.rb
index ee17445a1..e3c2a999c 100644
--- a/services/api/test/unit/collection_test.rb
+++ b/services/api/test/unit/collection_test.rb
@@ -106,17 +106,76 @@ class CollectionTest < ActiveSupport::TestCase
     end
   end
 
+  test "uuid updates on current version make older versions update their pointers" do
+    Rails.configuration.collection_versioning = true
+    act_as_system_user do
+      # Set up initial collection
+      c = create_collection 'foo', Encoding::US_ASCII
+      assert c.valid?
+      assert_equal 1, c.version
+      # Make changes so that a new version is created
+      c.update_attributes!({'name' => 'bar'})
+      c.reload
+      assert_equal 2, c.version
+      assert_equal 2, Collection.where(current_version_uuid: c.uuid).count
+      new_uuid = 'zzzzz-4zz18-somefakeuuidnow'
+      assert_empty Collection.where(uuid: new_uuid)
+      # Update UUID on current version, check that both collections point to it
+      c.update_attributes!({'uuid' => new_uuid})
+      c.reload
+      assert_equal new_uuid, c.uuid
+      assert_equal 2, Collection.where(current_version_uuid: new_uuid).count
+    end
+  end
+
+  [
+    ['owner_uuid', 'zzzzz-tpzed-d9tiejq69daie8f', 'zzzzz-tpzed-xurymjxw79nv3jz'],
+    ['replication_desired', 2, 3],
+    ['storage_classes_desired', ['hot'], ['archive']],
+    ['is_trashed', true, false],
+  ].each do |attr, first_val, second_val|
+    test "sync #{attr} with older versions" do
+      Rails.configuration.collection_versioning = true
+      act_as_system_user do
+        # Set up initial collection
+        c = create_collection 'foo', Encoding::US_ASCII
+        assert c.valid?
+        assert_equal 1, c.version
+        assert_not_equal first_val, c.attributes[attr]
+        # Make changes so that a new version is created and a synced field is
+        # updated on both
+        c.update_attributes!({'name' => 'bar', attr => first_val})
+        c.reload
+        assert_equal 2, c.version
+        assert_equal first_val, c.attributes[attr]
+        assert_equal 2, Collection.where(current_version_uuid: c.uuid).count
+        assert_equal first_val, Collection.where(current_version_uuid: c.uuid, version: 1).first.attributes[attr]
+        # Only make an update on the same synced field & check that the previously
+        # created version also gets it.
+        c.update_attributes!({attr => second_val})
+        c.reload
+        assert_equal 2, c.version
+        assert_equal second_val, c.attributes[attr]
+        assert_equal 2, Collection.where(current_version_uuid: c.uuid).count
+        assert_equal second_val, Collection.where(current_version_uuid: c.uuid, version: 1).first.attributes[attr]
+      end
+    end
+  end
+
   [
-    [false, 'name', 'bar'],
-    [false, 'description', 'The quick brown fox jumps over the lazy dog'],
-    [false, 'properties', {'new_version' => true}],
-    [false, 'manifest_text', ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"],
-    [true, 'name', 'bar'],
-    [true, 'description', 'The quick brown fox jumps over the lazy dog'],
-    [true, 'properties', {'new_version' => true}],
-    [true, 'manifest_text', ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"],
-  ].each do |versioning, attr, val|
-    test "update collection #{attr} with versioning #{versioning ? '' : 'not '}enabled" do
+    [false, 'name', 'bar', false],
+    [false, 'description', 'The quick brown fox jumps over the lazy dog', false],
+    [false, 'properties', {'new_version' => true}, false],
+    [false, 'manifest_text', ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n", false],
+    [true, 'name', 'bar', true],
+    [true, 'description', 'The quick brown fox jumps over the lazy dog', true],
+    [true, 'properties', {'new_version' => true}, true],
+    [true, 'manifest_text', ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n", true],
+    # Non-versionable attribute updates shouldn't create new versions
+    [true, 'replication_desired', 5, false],
+    [false, 'replication_desired', 5, false],
+  ].each do |versioning, attr, val, new_version_expected|
+    test "update #{attr} with versioning #{versioning ? '' : 'not '}enabled should #{new_version_expected ? '' : 'not '}create a new version" do
       Rails.configuration.collection_versioning = versioning
       act_as_system_user do
         # Create initial collection
@@ -131,17 +190,18 @@ class CollectionTest < ActiveSupport::TestCase
         # Update attribute and check if version number should be incremented
         old_value = c.attributes[attr]
         c.update_attributes!({attr => val})
-        assert_equal versioning, c.version == 2
+        assert_equal new_version_expected, c.version == 2
         assert_equal val, c.attributes[attr]
 
-        if versioning
+        if versioning && new_version_expected
           # Search for the snapshot & previous value
           assert_equal 2, Collection.where(current_version_uuid: c.uuid).count
           s = Collection.where(current_version_uuid: c.uuid, version: 1).first
           assert_not_nil s
           assert_equal old_value, s.attributes[attr]
         else
-          # If versioning is disabled, only the current version should exist
+          # If versioning is disabled or no versionable attribute was updated,
+          # only the current version should exist
           assert_equal 1, Collection.where(current_version_uuid: c.uuid).count
           assert_equal c, Collection.where(current_version_uuid: c.uuid).first
         end

commit be338ff6ecdfbc75e094b3e65134c9a841f3209c
Author: Lucas Di Pentima <ldipentima at veritasgenetics.com>
Date:   Mon Sep 24 17:29:19 2018 -0300

    13561: Lock row when saving a new collection version, update tests.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <ldipentima at veritasgenetics.com>

diff --git a/services/api/app/models/collection.rb b/services/api/app/models/collection.rb
index 502586e3e..7e1cd4aff 100644
--- a/services/api/app/models/collection.rb
+++ b/services/api/app/models/collection.rb
@@ -219,30 +219,33 @@ class Collection < ArvadosModel
     self.current_version_uuid ||= self.uuid
   end
 
-  def save *arg
+  def save!
     if !Rails.configuration.collection_versioning || new_record?
       return super
     end
-    versionable_updates = ['manifest_text', 'description', 'properties', 'name'] & self.changed_attributes.keys
-    if versionable_updates.empty?
+    changes = self.changes
+    versionable_updates = ['manifest_text', 'description', 'properties', 'name']
+    if (changes.keys & versionable_updates).empty?
+      # Updates don't include interesting attributes, don't save a new snapshot.
       return super
     end
-    # Create a snapshot of the current collection before saving
-    Collection.transaction do
-      attrs = {}
-      # Collect attributes with pre-update values
-      versionable_updates.each do |attr|
-        attrs[attr] = self.changed_attributes[attr]
-      end
-      # Reset UUID
-      attrs[:uuid] = nil
+    # Create a snapshot of the current collection before saving.
+    # Does row locking because 'version' is incremented.
+    with_lock do
+      # Note that 'with_lock' reloads the object after locking.
+      # Create a snapshot of the original collection
       snapshot = self.dup
-      # Update current version number & save
+      snapshot.uuid = nil # Reset UUID so it's created as a new record
+      # Restore requested changes on the current version
+      changes.keys.each do |attr|
+        next if attr == 'version'
+        self.attributes = {attr => changes[attr].last}
+      end
+      # Update current version number & save first to avoid index collision
       self.version += 1
       super
-      # Save the snapshot with required attributes
-      snapshot.update_attributes!(attrs)
-      return true
+      # Save the snapshot with previous state
+      snapshot.save!
     end
   end
 
diff --git a/services/api/test/unit/collection_test.rb b/services/api/test/unit/collection_test.rb
index f745ca429..ee17445a1 100644
--- a/services/api/test/unit/collection_test.rb
+++ b/services/api/test/unit/collection_test.rb
@@ -116,7 +116,7 @@ class CollectionTest < ActiveSupport::TestCase
     [true, 'properties', {'new_version' => true}],
     [true, 'manifest_text', ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"],
   ].each do |versioning, attr, val|
-    test "collection's #{attr} update with versioning #{versioning ? '' : 'not '}enabled" do
+    test "update collection #{attr} with versioning #{versioning ? '' : 'not '}enabled" do
       Rails.configuration.collection_versioning = versioning
       act_as_system_user do
         # Create initial collection
@@ -130,9 +130,9 @@ class CollectionTest < ActiveSupport::TestCase
 
         # Update attribute and check if version number should be incremented
         old_value = c.attributes[attr]
-        c.update_attribute attr, val
-        assert_equal val, c.attributes[attr]
+        c.update_attributes!({attr => val})
         assert_equal versioning, c.version == 2
+        assert_equal val, c.attributes[attr]
 
         if versioning
           # Search for the snapshot & previous value
@@ -149,6 +149,29 @@ class CollectionTest < ActiveSupport::TestCase
     end
   end
 
+  test 'with versioning enabled, simultaneous updates increment version correctly' do
+    Rails.configuration.collection_versioning = true
+    act_as_system_user do
+      # Create initial collection
+      col = create_collection 'foo', Encoding::US_ASCII
+      assert col.valid?
+      assert_equal 1, col.version
+
+      # Simulate simultaneous updates
+      c1 = Collection.where(uuid: col.uuid).first
+      assert_equal 1, c1.version
+      c1.name = 'bar'
+      c2 = Collection.where(uuid: col.uuid).first
+      c2.description = 'foo collection'
+      c1.save!
+      assert_equal 1, c2.version
+      # with_lock forces a reload, so this shouldn't produce an unique violation error
+      c2.save!
+      assert_equal 3, c2.version
+      assert_equal 'foo collection', c2.description
+    end
+  end
+
   test 'create and update collection and verify file_names' do
     act_as_system_user do
       c = create_collection 'foo', Encoding::US_ASCII

commit 737dcc58241a06e32615624877d3301b90574227
Author: Lucas Di Pentima <ldipentima at veritasgenetics.com>
Date:   Fri Sep 21 13:31:08 2018 -0300

    13561: Add missing copyright notices on migrations
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <ldipentima at veritasgenetics.com>

diff --git a/services/api/db/migrate/20180824152014_add_md5_index_to_containers.rb b/services/api/db/migrate/20180824152014_add_md5_index_to_containers.rb
index a58932e35..82b216351 100644
--- a/services/api/db/migrate/20180824152014_add_md5_index_to_containers.rb
+++ b/services/api/db/migrate/20180824152014_add_md5_index_to_containers.rb
@@ -1,3 +1,7 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
 class AddMd5IndexToContainers < ActiveRecord::Migration
   def up
     ActiveRecord::Base.connection.execute 'CREATE INDEX index_containers_on_reuse_columns on containers (md5(command), cwd, md5(environment), output_path, container_image, md5(mounts), secret_mounts_md5, md5(runtime_constraints))'
diff --git a/services/api/db/migrate/20180904110712_add_runtime_status_to_containers.rb b/services/api/db/migrate/20180904110712_add_runtime_status_to_containers.rb
index 755c7c89e..4c963e6fd 100644
--- a/services/api/db/migrate/20180904110712_add_runtime_status_to_containers.rb
+++ b/services/api/db/migrate/20180904110712_add_runtime_status_to_containers.rb
@@ -1,3 +1,7 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
 class AddRuntimeStatusToContainers < ActiveRecord::Migration
   def change
     add_column :containers, :runtime_status, :jsonb, default: {}
diff --git a/services/api/db/migrate/20180913175443_add_version_info_to_collections.rb b/services/api/db/migrate/20180913175443_add_version_info_to_collections.rb
index b5378d3a1..a624dd9af 100644
--- a/services/api/db/migrate/20180913175443_add_version_info_to_collections.rb
+++ b/services/api/db/migrate/20180913175443_add_version_info_to_collections.rb
@@ -1,3 +1,7 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
 class AddVersionInfoToCollections < ActiveRecord::Migration
   def change
     # Do changes in bulk to save time on huge tables
diff --git a/services/api/db/migrate/20180915155335_set_current_version_uuid_on_collections.rb b/services/api/db/migrate/20180915155335_set_current_version_uuid_on_collections.rb
index b205eae89..12a08e07d 100644
--- a/services/api/db/migrate/20180915155335_set_current_version_uuid_on_collections.rb
+++ b/services/api/db/migrate/20180915155335_set_current_version_uuid_on_collections.rb
@@ -1,3 +1,7 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
 class SetCurrentVersionUuidOnCollections < ActiveRecord::Migration
   def up
     # Set the current version uuid as itself
diff --git a/services/api/db/migrate/20180919001158_recreate_collection_unique_name_index.rb b/services/api/db/migrate/20180919001158_recreate_collection_unique_name_index.rb
index bb921d4e6..640395618 100644
--- a/services/api/db/migrate/20180919001158_recreate_collection_unique_name_index.rb
+++ b/services/api/db/migrate/20180919001158_recreate_collection_unique_name_index.rb
@@ -1,3 +1,7 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
 class RecreateCollectionUniqueNameIndex < ActiveRecord::Migration
   def up
     Collection.transaction do

commit dbfe1d83ff4e77edc77a562022a999e9ba9e2e2b
Author: Lucas Di Pentima <ldipentima at veritasgenetics.com>
Date:   Fri Sep 21 13:18:02 2018 -0300

    13561: Add basic support for collection versioning at the model level.
    
    * Update "unique name" index to ignore collection records that are old versions
    * Add tests
    * Update collection fixtures
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <ldipentima at veritasgenetics.com>

diff --git a/services/api/app/models/collection.rb b/services/api/app/models/collection.rb
index 85b12a377..502586e3e 100644
--- a/services/api/app/models/collection.rb
+++ b/services/api/app/models/collection.rb
@@ -28,6 +28,7 @@ class Collection < ArvadosModel
   validate :ensure_storage_classes_desired_is_not_empty
   validate :ensure_storage_classes_contain_non_empty_strings
   before_save :set_file_names
+  before_create :set_current_version_uuid
 
   api_accessible :user, extend: :common do |t|
     t.add :name
@@ -206,6 +207,45 @@ class Collection < ArvadosModel
     self.manifest_text ||= ''
   end
 
+  def skip_uuid_existence_check
+    # Avoid checking the existence of current_version_uuid, as it's
+    # assigned on creation of a new 'current version' collection, so
+    # the collection's UUID only lives on memory when the validation check
+    # is performed.
+    ['current_version_uuid']
+  end
+
+  def set_current_version_uuid
+    self.current_version_uuid ||= self.uuid
+  end
+
+  def save *arg
+    if !Rails.configuration.collection_versioning || new_record?
+      return super
+    end
+    versionable_updates = ['manifest_text', 'description', 'properties', 'name'] & self.changed_attributes.keys
+    if versionable_updates.empty?
+      return super
+    end
+    # Create a snapshot of the current collection before saving
+    Collection.transaction do
+      attrs = {}
+      # Collect attributes with pre-update values
+      versionable_updates.each do |attr|
+        attrs[attr] = self.changed_attributes[attr]
+      end
+      # Reset UUID
+      attrs[:uuid] = nil
+      snapshot = self.dup
+      # Update current version number & save
+      self.version += 1
+      super
+      # Save the snapshot with required attributes
+      snapshot.update_attributes!(attrs)
+      return true
+    end
+  end
+
   def check_encoding
     if manifest_text.encoding.name == 'UTF-8' and manifest_text.valid_encoding?
       true
@@ -434,11 +474,11 @@ class Collection < ArvadosModel
   end
 
   def self.searchable_columns operator
-    super - ["manifest_text"]
+    super - ["manifest_text", "current_version_uuid"]
   end
 
   def self.full_text_searchable_columns
-    super - ["manifest_text", "storage_classes_desired", "storage_classes_confirmed"]
+    super - ["manifest_text", "storage_classes_desired", "storage_classes_confirmed", "current_version_uuid"]
   end
 
   def self.where *args
diff --git a/services/api/config/application.default.yml b/services/api/config/application.default.yml
index 5a1c22951..f480904c4 100644
--- a/services/api/config/application.default.yml
+++ b/services/api/config/application.default.yml
@@ -500,6 +500,10 @@ common:
   # keep_web_service_url: https://download.uuid_prefix.arvadosapi.com/
   keep_web_service_url: false
 
+  # If true, enable collection versioning: With every update, a snapshot of the
+  # collection's previous state is created and linked to the current collection.
+  collection_versioning: false
+
 development:
   force_ssl: false
   cache_classes: false
diff --git a/services/api/db/migrate/20180919001158_recreate_collection_unique_name_index.rb b/services/api/db/migrate/20180919001158_recreate_collection_unique_name_index.rb
new file mode 100644
index 000000000..bb921d4e6
--- /dev/null
+++ b/services/api/db/migrate/20180919001158_recreate_collection_unique_name_index.rb
@@ -0,0 +1,23 @@
+class RecreateCollectionUniqueNameIndex < ActiveRecord::Migration
+  def up
+    Collection.transaction do
+      remove_index(:collections,
+                   name: 'index_collections_on_owner_uuid_and_name')
+      add_index(:collections, [:owner_uuid, :name],
+                unique: true,
+                where: 'is_trashed = false AND current_version_uuid = uuid',
+                name: 'index_collections_on_owner_uuid_and_name')
+    end
+  end
+
+  def down
+    Collection.transaction do
+      remove_index(:collections,
+                   name: 'index_collections_on_owner_uuid_and_name')
+      add_index(:collections, [:owner_uuid, :name],
+                unique: true,
+                where: 'is_trashed = false',
+                name: 'index_collections_on_owner_uuid_and_name')
+    end
+  end
+end
diff --git a/services/api/db/structure.sql b/services/api/db/structure.sql
index d646d609b..de5e2e127 100644
--- a/services/api/db/structure.sql
+++ b/services/api/db/structure.sql
@@ -1829,7 +1829,7 @@ CREATE INDEX index_collections_on_owner_uuid ON public.collections USING btree (
 -- Name: index_collections_on_owner_uuid_and_name; Type: INDEX; Schema: public; Owner: -
 --
 
-CREATE UNIQUE INDEX index_collections_on_owner_uuid_and_name ON public.collections USING btree (owner_uuid, name) WHERE (is_trashed = false);
+CREATE UNIQUE INDEX index_collections_on_owner_uuid_and_name ON public.collections USING btree (owner_uuid, name) WHERE ((is_trashed = false) AND ((current_version_uuid)::text = (uuid)::text));
 
 
 --
@@ -3182,3 +3182,5 @@ INSERT INTO schema_migrations (version) VALUES ('20180913175443');
 
 INSERT INTO schema_migrations (version) VALUES ('20180915155335');
 
+INSERT INTO schema_migrations (version) VALUES ('20180919001158');
+
diff --git a/services/api/test/fixtures/collections.yml b/services/api/test/fixtures/collections.yml
index 7ff67f82e..3578651ba 100644
--- a/services/api/test/fixtures/collections.yml
+++ b/services/api/test/fixtures/collections.yml
@@ -4,6 +4,7 @@
 
 user_agreement:
   uuid: zzzzz-4zz18-t68oksiu9m80s4y
+  current_version_uuid: zzzzz-4zz18-t68oksiu9m80s4y
   portable_data_hash: b519d9cb706a29fc7ea24dbea2f05851+93
   owner_uuid: zzzzz-tpzed-000000000000000
   created_at: 2013-12-26T19:22:54Z
@@ -16,6 +17,7 @@ user_agreement:
 
 collection_owned_by_active:
   uuid: zzzzz-4zz18-bv31uwvy3neko21
+  current_version_uuid: zzzzz-4zz18-bv31uwvy3neko21
   portable_data_hash: fa7aeb5140e2848d39b416daeef4ffc5+45
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
@@ -28,6 +30,7 @@ collection_owned_by_active:
 
 foo_file:
   uuid: zzzzz-4zz18-znfnqtbbv4spc3w
+  current_version_uuid: zzzzz-4zz18-znfnqtbbv4spc3w
   portable_data_hash: 1f4b0bc7583c2a7f9102c395f4ffc5e3+45
   owner_uuid: zzzzz-tpzed-000000000000000
   created_at: 2015-02-03T17:22:54Z
@@ -40,6 +43,7 @@ foo_file:
 
 bar_file:
   uuid: zzzzz-4zz18-ehbhgtheo8909or
+  current_version_uuid: zzzzz-4zz18-ehbhgtheo8909or
   portable_data_hash: fa7aeb5140e2848d39b416daeef4ffc5+45
   owner_uuid: zzzzz-tpzed-000000000000000
   created_at: 2015-02-03T17:22:54Z
@@ -52,6 +56,7 @@ bar_file:
 
 baz_file:
   uuid: zzzzz-4zz18-y9vne9npefyxh8g
+  current_version_uuid: zzzzz-4zz18-y9vne9npefyxh8g
   portable_data_hash: ea10d51bcf88862dbcc36eb292017dfd+45
   owner_uuid: zzzzz-tpzed-000000000000000
   created_at: 2014-02-03T17:22:54Z
@@ -64,6 +69,7 @@ baz_file:
 
 w_a_z_file:
   uuid: zzzzz-4zz18-25k12570yk134b3
+  current_version_uuid: zzzzz-4zz18-25k12570yk134b3
   portable_data_hash: 8706aadd12a0ebc07d74cae88762ba9e+56
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2015-02-09T10:53:38Z
@@ -76,6 +82,7 @@ w_a_z_file:
 
 multilevel_collection_1:
   uuid: zzzzz-4zz18-pyw8yp9g3pr7irn
+  current_version_uuid: zzzzz-4zz18-pyw8yp9g3pr7irn
   portable_data_hash: 1fd08fc162a5c6413070a8bd0bffc818+150
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
@@ -88,6 +95,7 @@ multilevel_collection_1:
 
 multilevel_collection_2:
   uuid: zzzzz-4zz18-45xf9hw1sxkhl6q
+  current_version_uuid: zzzzz-4zz18-45xf9hw1sxkhl6q
   # All of this collection's files are deep in subdirectories.
   portable_data_hash: 80cf6dd2cf079dd13f272ec4245cb4a8+48
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
@@ -101,6 +109,7 @@ multilevel_collection_2:
 
 docker_image:
   uuid: zzzzz-4zz18-1v45jub259sjjgb
+  current_version_uuid: zzzzz-4zz18-1v45jub259sjjgb
   # This Collection has links with Docker image metadata.
   portable_data_hash: fa3c1a9cb6783f85f2ecda037e07b8c3+167
   owner_uuid: zzzzz-tpzed-000000000000000
@@ -115,6 +124,7 @@ docker_image:
 # tagged docker image with sha256:{hash}.tar filename
 docker_image_1_12:
   uuid: zzzzz-4zz18-1g4g0vhpjn9wq7i
+  current_version_uuid: zzzzz-4zz18-1g4g0vhpjn9wq7i
   portable_data_hash: d740a57097711e08eb9b2a93518f20ab+174
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2016-10-19 08:50:45.653552268 Z
@@ -127,6 +137,7 @@ docker_image_1_12:
 
 unlinked_docker_image:
   uuid: zzzzz-4zz18-d0d8z5wofvfgwad
+  current_version_uuid: zzzzz-4zz18-d0d8z5wofvfgwad
   # This Collection contains a file that looks like a Docker image,
   # but has no Docker metadata links pointing to it.
   portable_data_hash: 9ae44d5792468c58bcf85ce7353c7027+124
@@ -141,6 +152,7 @@ unlinked_docker_image:
 
 empty:
   uuid: zzzzz-4zz18-gs9ooj1h9sd5mde
+  current_version_uuid: zzzzz-4zz18-gs9ooj1h9sd5mde
   # Empty collection owned by anonymous_group is added with rake db:seed.
   portable_data_hash: d41d8cd98f00b204e9800998ecf8427e+0
   owner_uuid: zzzzz-tpzed-000000000000000
@@ -154,6 +166,7 @@ empty:
 
 foo_collection_in_aproject:
   uuid: zzzzz-4zz18-fy296fx3hot09f7
+  current_version_uuid: zzzzz-4zz18-fy296fx3hot09f7
   portable_data_hash: 1f4b0bc7583c2a7f9102c395f4ffc5e3+45
   owner_uuid: zzzzz-j7d0g-v955i6s2oi1cbso
   created_at: 2014-04-21 15:37:48 -0400
@@ -164,6 +177,7 @@ foo_collection_in_aproject:
 
 user_agreement_in_anonymously_accessible_project:
   uuid: zzzzz-4zz18-uukreo9rbgwsujr
+  current_version_uuid: zzzzz-4zz18-uukreo9rbgwsujr
   portable_data_hash: b519d9cb706a29fc7ea24dbea2f05851+93
   owner_uuid: zzzzz-j7d0g-zhxawtyetzwc5f0
   created_at: 2014-06-13 20:42:26 -0800
@@ -174,6 +188,7 @@ user_agreement_in_anonymously_accessible_project:
 
 public_text_file:
   uuid: zzzzz-4zz18-4en62shvi99lxd4
+  current_version_uuid: zzzzz-4zz18-4en62shvi99lxd4
   portable_data_hash: 55713e6a34081eb03609e7ad5fcad129+62
   owner_uuid: zzzzz-j7d0g-zhxawtyetzwc5f0
   created_at: 2015-02-12 16:58:03 -0500
@@ -184,6 +199,7 @@ public_text_file:
 
 baz_collection_name_in_asubproject:
   uuid: zzzzz-4zz18-lsitwcf548ui4oe
+  current_version_uuid: zzzzz-4zz18-lsitwcf548ui4oe
   portable_data_hash: ea10d51bcf88862dbcc36eb292017dfd+45
   owner_uuid: zzzzz-j7d0g-axqo7eu9pwvna1x
   created_at: 2014-04-21 15:37:48 -0400
@@ -194,6 +210,7 @@ baz_collection_name_in_asubproject:
 
 empty_collection_name_in_active_user_home_project:
   uuid: zzzzz-4zz18-5qa38qghh1j3nvv
+  current_version_uuid: zzzzz-4zz18-5qa38qghh1j3nvv
   portable_data_hash: d41d8cd98f00b204e9800998ecf8427e+0
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-08-06 22:11:51.242392533 Z
@@ -203,6 +220,7 @@ empty_collection_name_in_active_user_home_project:
 
 baz_file_in_asubproject:
   uuid: zzzzz-4zz18-0mri2x4u7ftngez
+  current_version_uuid: zzzzz-4zz18-0mri2x4u7ftngez
   portable_data_hash: ea10d51bcf88862dbcc36eb292017dfd+45
   owner_uuid: zzzzz-j7d0g-axqo7eu9pwvna1x
   created_at: 2014-02-03T17:22:54Z
@@ -215,6 +233,7 @@ baz_file_in_asubproject:
 
 collection_to_move_around_in_aproject:
   uuid: zzzzz-4zz18-0mri2x4u7ft1234
+  current_version_uuid: zzzzz-4zz18-0mri2x4u7ft1234
   portable_data_hash: ea10d51bcf88862dbcc36eb292017dfd+45
   owner_uuid: zzzzz-j7d0g-v955i6s2oi1cbso
   created_at: 2014-02-03T17:22:54Z
@@ -229,6 +248,7 @@ collection_to_move_around_in_aproject:
 # because it is not in default scope
 expired_collection:
   uuid: zzzzz-4zz18-mto52zx1s7sn3ih
+  current_version_uuid: zzzzz-4zz18-mto52zx1s7sn3ih
   portable_data_hash: 0b21a217243bfce5617fb9224b95bcb9+49
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
@@ -244,6 +264,7 @@ expired_collection:
 
 trashed_on_next_sweep:
   uuid: zzzzz-4zz18-4guozfh77ewd2f0
+  current_version_uuid: zzzzz-4zz18-4guozfh77ewd2f0
   portable_data_hash: 0b21a217243bfce5617fb9224b95bcb9+49
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2016-12-07T22:01:00.123456Z
@@ -261,6 +282,7 @@ trashed_on_next_sweep:
 # because it is not in default scope
 deleted_on_next_sweep:
   uuid: zzzzz-4zz18-3u1p5umicfpqszp
+  current_version_uuid: zzzzz-4zz18-3u1p5umicfpqszp
   portable_data_hash: 0b21a217243bfce5617fb9224b95bcb9+49
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2016-12-07T22:01:00.234567Z
@@ -276,6 +298,7 @@ deleted_on_next_sweep:
 
 collection_expires_in_future:
   uuid: zzzzz-4zz18-padkqo7yb8d9i3j
+  current_version_uuid: zzzzz-4zz18-padkqo7yb8d9i3j
   portable_data_hash: 0b21a217243bfce5617fb9224b95bcb9+49
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
@@ -290,6 +313,7 @@ collection_expires_in_future:
 
 unique_expired_collection:
   uuid: zzzzz-4zz18-mto52zx1s7sn3jk
+  current_version_uuid: zzzzz-4zz18-mto52zx1s7sn3jk
   portable_data_hash: 4ad199f90029935844dc3f098f4fca2a+49
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
@@ -305,6 +329,7 @@ unique_expired_collection:
 
 unique_expired_collection2:
   uuid: zzzzz-4zz18-mto52zx1s7sn3jr
+  current_version_uuid: zzzzz-4zz18-mto52zx1s7sn3jr
   portable_data_hash: 4ad199f90029935844dc3f098f4fca2b+49
   owner_uuid: zzzzz-tpzed-000000000000000
   created_at: 2014-02-03T17:22:54Z
@@ -326,6 +351,7 @@ unique_expired_collection2:
 #
 real_log_collection:
   uuid: zzzzz-4zz18-op4e2lbej01tcvu
+  current_version_uuid: zzzzz-4zz18-op4e2lbej01tcvu
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-09-01 12:00:00
   modified_at: 2014-09-01 12:00:00
@@ -335,6 +361,7 @@ real_log_collection:
 
 collection_in_home_project_with_same_name_as_in_aproject:
   uuid: zzzzz-4zz18-12342x4u7ftabcd
+  current_version_uuid: zzzzz-4zz18-12342x4u7ftabcd
   portable_data_hash: ea10d51bcf88862dbcc36eb292017dfd+45
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
@@ -347,6 +374,7 @@ collection_in_home_project_with_same_name_as_in_aproject:
 
 collection_in_aproject_with_same_name_as_in_home_project:
   uuid: zzzzz-4zz18-56782x4u7ftefgh
+  current_version_uuid: zzzzz-4zz18-56782x4u7ftefgh
   portable_data_hash: ea10d51bcf88862dbcc36eb292017dfd+45
   owner_uuid: zzzzz-j7d0g-v955i6s2oi1cbso
   created_at: 2014-02-03T17:22:54Z
@@ -359,6 +387,7 @@ collection_in_aproject_with_same_name_as_in_home_project:
 
 collection_owned_by_foo:
   uuid: zzzzz-4zz18-50surkhkbhsp31b
+  current_version_uuid: zzzzz-4zz18-50surkhkbhsp31b
   portable_data_hash: ea10d51bcf88862dbcc36eb292017dfd+45
   manifest_text: ". 73feffa4b7f6bb68e44cf984c85f6e88+3 0:3:baz\n"
   owner_uuid: zzzzz-tpzed-81hsbo6mk8nl05c
@@ -369,6 +398,7 @@ collection_owned_by_foo:
 collection_to_remove_from_subproject:
   # The Workbench tests remove this from subproject.
   uuid: zzzzz-4zz18-subprojgonecoll
+  current_version_uuid: zzzzz-4zz18-subprojgonecoll
   portable_data_hash: 2386ca6e3fffd4be5e197a72c6c80fb2+51
   manifest_text: ". 8258b505536a9ab47baa2f4281cb932a+9 0:9:missingno\n"
   owner_uuid: zzzzz-j7d0g-axqo7eu9pwvna1x
@@ -378,6 +408,7 @@ collection_to_remove_from_subproject:
 
 collection_with_files_in_subdir:
   uuid: zzzzz-4zz18-filesinsubdir00
+  current_version_uuid: zzzzz-4zz18-filesinsubdir00
   name: collection_files_in_subdir
   portable_data_hash: 85877ca2d7e05498dd3d109baf2df106+95
   owner_uuid: zzzzz-tpzed-user1withloadab
@@ -390,6 +421,7 @@ collection_with_files_in_subdir:
 
 graph_test_collection1:
   uuid: zzzzz-4zz18-bv31uwvy3neko22
+  current_version_uuid: zzzzz-4zz18-bv31uwvy3neko22
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   portable_data_hash: fa7aeb5140e2848d39b416daeef4ffc5+45
   manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
@@ -399,6 +431,7 @@ graph_test_collection1:
 
 graph_test_collection2:
   uuid: zzzzz-4zz18-uukreo9rbgwsujx
+  current_version_uuid: zzzzz-4zz18-uukreo9rbgwsujx
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   portable_data_hash: 65b17c95fdbc9800fc48acda4e9dcd0b+93
   manifest_text: ". 6a4ff0499484c6c79c95cd8c566bd25f+249025 0:249025:FOO_General_Public_License,_version_3.pdf\n"
@@ -408,6 +441,7 @@ graph_test_collection2:
 
 graph_test_collection3:
   uuid: zzzzz-4zz18-uukreo9rbgwsujj
+  current_version_uuid: zzzzz-4zz18-uukreo9rbgwsujj
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   portable_data_hash: ea10d51bcf88862dbcc36eb292017dfd+45
   manifest_text: ". 73feffa4b7f6bb68e44cf984c85f6e88+3 0:3:baz\n"
@@ -417,6 +451,7 @@ graph_test_collection3:
 
 collection_1_owned_by_fuse:
   uuid: zzzzz-4zz18-ovx05bfzormx3bg
+  current_version_uuid: zzzzz-4zz18-ovx05bfzormx3bg
   portable_data_hash: fa7aeb5140e2848d39b416daeef4ffc5+45
   owner_uuid: zzzzz-tpzed-0fusedrivertest
   created_at: 2014-02-03T17:22:54Z
@@ -429,6 +464,7 @@ collection_1_owned_by_fuse:
 
 collection_2_owned_by_fuse:
   uuid: zzzzz-4zz18-8ubpy4w74twtwzr
+  current_version_uuid: zzzzz-4zz18-8ubpy4w74twtwzr
   portable_data_hash: 1f4b0bc7583c2a7f9102c395f4ffc5e3+45
   owner_uuid: zzzzz-tpzed-0fusedrivertest
   created_at: 2014-02-03T17:22:54Z
@@ -441,6 +477,7 @@ collection_2_owned_by_fuse:
 
 collection_in_fuse_project:
   uuid: zzzzz-4zz18-vx4mtkjqfrb534f
+  current_version_uuid: zzzzz-4zz18-vx4mtkjqfrb534f
   portable_data_hash: ea10d51bcf88862dbcc36eb292017dfd+45
   owner_uuid: zzzzz-j7d0g-0000ownedbyfuse
   created_at: 2014-02-03T17:22:54Z
@@ -453,6 +490,7 @@ collection_in_fuse_project:
 
 collection_with_no_name_in_aproject:
   uuid: zzzzz-4zz18-00000nonamecoll
+  current_version_uuid: zzzzz-4zz18-00000nonamecoll
   portable_data_hash: 1f4b0bc7583c2a7f9102c395f4ffc5e3+45
   owner_uuid: zzzzz-j7d0g-v955i6s2oi1cbso
   created_at: 2014-04-21 15:37:48 -0400
@@ -462,6 +500,7 @@ collection_with_no_name_in_aproject:
 
 collection_to_search_for_in_aproject:
   uuid: zzzzz-4zz18-abcd6fx123409f7
+  current_version_uuid: zzzzz-4zz18-abcd6fx123409f7
   portable_data_hash: 1f4b0bc7583c2a7f9102c395f4ffc5e3+45
   owner_uuid: zzzzz-j7d0g-v955i6s2oi1cbso
   created_at: 2014-04-21 15:37:48 -0400
@@ -472,6 +511,7 @@ collection_to_search_for_in_aproject:
 
 upload_sandbox:
   uuid: zzzzz-4zz18-js48y3ykkfdfjd3
+  current_version_uuid: zzzzz-4zz18-js48y3ykkfdfjd3
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-12-09 15:03:16
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
@@ -483,6 +523,7 @@ upload_sandbox:
 
 collection_with_unique_words_to_test_full_text_search:
   uuid: zzzzz-4zz18-mnt690klmb51aud
+  current_version_uuid: zzzzz-4zz18-mnt690klmb51aud
   portable_data_hash: fa7aeb5140e2848d39b416daeef4ffc5+45
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
@@ -495,6 +536,8 @@ collection_with_unique_words_to_test_full_text_search:
   description: The quick_brown_fox jumps over the lazy_dog
 
 replication_undesired_unconfirmed:
+  uuid: zzzzz-4zz18-wjxq7uzx2m9jj4a
+  current_version_uuid: zzzzz-4zz18-wjxq7uzx2m9jj4a
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2015-02-07 00:19:28.596506247 Z
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
@@ -504,11 +547,12 @@ replication_undesired_unconfirmed:
   replication_confirmed_at: ~
   replication_confirmed: ~
   updated_at: 2015-02-07 00:19:28.596236608 Z
-  uuid: zzzzz-4zz18-wjxq7uzx2m9jj4a
   manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
   name: replication want=null have=null
 
 replication_desired_2_unconfirmed:
+  uuid: zzzzz-4zz18-3t236wrz4769h7x
+  current_version_uuid: zzzzz-4zz18-3t236wrz4769h7x
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2015-02-07 00:21:35.050333515 Z
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
@@ -518,11 +562,12 @@ replication_desired_2_unconfirmed:
   replication_confirmed_at: ~
   replication_confirmed: ~
   updated_at: 2015-02-07 00:21:35.050126576 Z
-  uuid: zzzzz-4zz18-3t236wrz4769h7x
   manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
   name: replication want=2 have=null
 
 replication_desired_2_confirmed_2:
+  uuid: zzzzz-4zz18-434zv1tnnf2rygp
+  current_version_uuid: zzzzz-4zz18-434zv1tnnf2rygp
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2015-02-07 00:19:28.596506247 Z
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
@@ -532,11 +577,12 @@ replication_desired_2_confirmed_2:
   replication_confirmed_at: 2015-02-07 00:24:52.983381227 Z
   replication_confirmed: 2
   updated_at: 2015-02-07 00:24:52.983381227 Z
-  uuid: zzzzz-4zz18-434zv1tnnf2rygp
   manifest_text: ". acbd18db4cc2f85cedef654fccc4a4d8+3 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo 3:3:bar\n"
   name: replication want=2 have=2
 
 storage_classes_desired_default_unconfirmed:
+  uuid: zzzzz-4zz18-3t236wrz4769tga
+  current_version_uuid: zzzzz-4zz18-3t236wrz4769tga
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2015-02-07 00:21:35.050333515 Z
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
@@ -546,11 +592,12 @@ storage_classes_desired_default_unconfirmed:
   storage_classes_confirmed_at: ~
   storage_classes_confirmed: ~
   updated_at: 2015-02-07 00:21:35.050126576 Z
-  uuid: zzzzz-4zz18-3t236wrz4769tga
   manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
   name: storage classes want=[default] have=[]
 
 storage_classes_desired_default_confirmed_default:
+  uuid: zzzzz-4zz18-3t236wr12769tga
+  current_version_uuid: zzzzz-4zz18-3t236wr12769tga
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2015-02-07 00:21:35.050333515 Z
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
@@ -560,11 +607,12 @@ storage_classes_desired_default_confirmed_default:
   storage_classes_confirmed_at: 2015-02-07 00:21:35.050126576 Z
   storage_classes_confirmed: ["default"]
   updated_at: 2015-02-07 00:21:35.050126576 Z
-  uuid: zzzzz-4zz18-3t236wr12769tga
   manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
   name: storage classes want=[default] have=[default]
 
 storage_classes_desired_archive_confirmed_default:
+  uuid: zzzzz-4zz18-3t236wr12769qqa
+  current_version_uuid: zzzzz-4zz18-3t236wr12769qqa
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2015-02-07 00:21:35.050333515 Z
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
@@ -574,12 +622,12 @@ storage_classes_desired_archive_confirmed_default:
   storage_classes_confirmed_at: ~
   storage_classes_confirmed: ["default"]
   updated_at: 2015-02-07 00:21:35.050126576 Z
-  uuid: zzzzz-4zz18-3t236wr12769qqa
   manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
   name: storage classes want=[archive] have=[default]
 
 collection_with_empty_properties:
   uuid: zzzzz-4zz18-emptyproperties
+  current_version_uuid: zzzzz-4zz18-emptyproperties
   portable_data_hash: fa7aeb5140e2848d39b416daeef4ffc5+45
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2015-02-13T17:22:54Z
@@ -593,6 +641,7 @@ collection_with_empty_properties:
 
 collection_with_one_property:
   uuid: zzzzz-4zz18-withoneproperty
+  current_version_uuid: zzzzz-4zz18-withoneproperty
   portable_data_hash: fa7aeb5140e2848d39b416daeef4ffc5+45
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2015-02-13T17:22:54Z
@@ -608,6 +657,7 @@ collection_with_one_property:
 # The following four collections are used to test combining collections with repeated filenames
 collection_with_repeated_filenames_and_contents_in_two_dirs_1:
   uuid: zzzzz-4zz18-duplicatenames1
+  current_version_uuid: zzzzz-4zz18-duplicatenames1
   portable_data_hash: f3a67fad3a19c31c658982fb8158fa58+144
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
@@ -620,6 +670,7 @@ collection_with_repeated_filenames_and_contents_in_two_dirs_1:
 
 collection_with_repeated_filenames_and_contents_in_two_dirs_2:
   uuid: zzzzz-4zz18-duplicatenames2
+  current_version_uuid: zzzzz-4zz18-duplicatenames2
   portable_data_hash: f3a67fad3a19c31c658982fb8158fa58+144
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
@@ -632,6 +683,7 @@ collection_with_repeated_filenames_and_contents_in_two_dirs_2:
 
 foo_and_bar_files_in_dir:
   uuid: zzzzz-4zz18-foonbarfilesdir
+  current_version_uuid: zzzzz-4zz18-foonbarfilesdir
   portable_data_hash: 6bbac24198d09a93975f60098caf0bdf+62
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
@@ -644,6 +696,7 @@ foo_and_bar_files_in_dir:
 
 multi_level_to_combine:
   uuid: zzzzz-4zz18-pyw8yp9g3ujh45f
+  current_version_uuid: zzzzz-4zz18-pyw8yp9g3ujh45f
   portable_data_hash: 7a6ef4c162a5c6413070a8bd0bffc818+150
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
@@ -657,6 +710,7 @@ multi_level_to_combine:
 # collection with several file types to test view icon enabled state in collection show page
 collection_with_several_supported_file_types:
   uuid: zzzzz-4zz18-supportedtypes1
+  current_version_uuid: zzzzz-4zz18-supportedtypes1
   portable_data_hash: 020d82cf7dedb70fd2b7788b5d0634da+269
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
@@ -669,6 +723,7 @@ collection_with_several_supported_file_types:
 
 collection_with_several_unsupported_file_types:
   uuid: zzzzz-4zz18-supportedtypes2
+  current_version_uuid: zzzzz-4zz18-supportedtypes2
   portable_data_hash: 71ac42f87464ee5f9fd396d560d400c3+59
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
@@ -681,6 +736,7 @@ collection_with_several_unsupported_file_types:
 
 collection_not_readable_by_active:
   uuid: zzzzz-4zz18-cd42uwvy3neko21
+  current_version_uuid: zzzzz-4zz18-cd42uwvy3neko21
   portable_data_hash: bb89eb5140e2848d39b416daeef4ffc5+45
   owner_uuid: zzzzz-tpzed-000000000000000
   created_at: 2014-02-03T17:22:54Z
@@ -693,6 +749,7 @@ collection_not_readable_by_active:
 
 collection_to_remove_and_rename_files:
   uuid: zzzzz-4zz18-a21ux3541sxa8sf
+  current_version_uuid: zzzzz-4zz18-a21ux3541sxa8sf
   portable_data_hash: 80cf6dd2cf079dd13f272ec4245cb4a8+48
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
@@ -705,6 +762,7 @@ collection_to_remove_and_rename_files:
 
 collection_with_tags_owned_by_active:
   uuid: zzzzz-4zz18-taggedcolletion
+  current_version_uuid: zzzzz-4zz18-taggedcolletion
   portable_data_hash: fa7aeb5140e2848d39b416daeef4ffc5+45
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
@@ -720,6 +778,7 @@ collection_with_tags_owned_by_active:
 
 trashed_collection_to_test_name_conflict_on_untrash:
   uuid: zzzzz-4zz18-trashedcolnamec
+  current_version_uuid: zzzzz-4zz18-trashedcolnamec
   portable_data_hash: 80cf6dd2cf079dd13f272ec4245cb4a8+48
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
@@ -735,6 +794,7 @@ trashed_collection_to_test_name_conflict_on_untrash:
 
 same_name_as_trashed_coll_to_test_name_conflict_on_untrash:
   uuid: zzzzz-4zz18-namesameastrash
+  current_version_uuid: zzzzz-4zz18-namesameastrash
   portable_data_hash: 80cf6dd2cf079dd13f272ec4245cb4a8+48
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
@@ -747,6 +807,7 @@ same_name_as_trashed_coll_to_test_name_conflict_on_untrash:
 
 collection_in_trashed_subproject:
   uuid: zzzzz-4zz18-trashedproj2col
+  current_version_uuid: zzzzz-4zz18-trashedproj2col
   portable_data_hash: 80cf6dd2cf079dd13f272ec4245cb4a8+48
   owner_uuid: zzzzz-j7d0g-trashedproject2
   created_at: 2014-02-03T17:22:54Z
@@ -759,6 +820,7 @@ collection_in_trashed_subproject:
 
 collection_with_prop1_value1:
   uuid: zzzzz-4zz18-withprop1value1
+  current_version_uuid: zzzzz-4zz18-withprop1value1
   portable_data_hash: fa7aeb5140e2848d39b416daeef4ffc5+45
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2015-02-13T17:22:54Z
@@ -773,6 +835,7 @@ collection_with_prop1_value1:
 
 collection_with_prop1_value2:
   uuid: zzzzz-4zz18-withprop1value2
+  current_version_uuid: zzzzz-4zz18-withprop1value2
   portable_data_hash: fa7aeb5140e2848d39b416daeef4ffc5+45
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2015-02-13T17:22:54Z
@@ -787,6 +850,7 @@ collection_with_prop1_value2:
 
 collection_with_prop1_value3:
   uuid: zzzzz-4zz18-withprop1value3
+  current_version_uuid: zzzzz-4zz18-withprop1value3
   portable_data_hash: fa7aeb5140e2848d39b416daeef4ffc5+45
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2015-02-13T17:22:54Z
@@ -801,6 +865,7 @@ collection_with_prop1_value3:
 
 collection_with_prop1_other1:
   uuid: zzzzz-4zz18-withprop1other1
+  current_version_uuid: zzzzz-4zz18-withprop1other1
   portable_data_hash: fa7aeb5140e2848d39b416daeef4ffc5+45
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2015-02-13T17:22:54Z
@@ -815,6 +880,7 @@ collection_with_prop1_other1:
 
 collection_with_prop2_1:
   uuid: zzzzz-4zz18-withprop2value1
+  current_version_uuid: zzzzz-4zz18-withprop2value1
   portable_data_hash: fa7aeb5140e2848d39b416daeef4ffc5+45
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2015-02-13T17:22:54Z
@@ -829,6 +895,7 @@ collection_with_prop2_1:
 
 collection_with_prop2_5:
   uuid: zzzzz-4zz18-withprop2value5
+  current_version_uuid: zzzzz-4zz18-withprop2value5
   portable_data_hash: fa7aeb5140e2848d39b416daeef4ffc5+45
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2015-02-13T17:22:54Z
@@ -843,6 +910,7 @@ collection_with_prop2_5:
 
 collection_with_uri_prop:
   uuid: zzzzz-4zz18-withuripropval1
+  current_version_uuid: zzzzz-4zz18-withuripropval1
   portable_data_hash: fa7aeb5140e2848d39b416daeef4ffc5+45
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2015-02-13T17:22:54Z
@@ -866,6 +934,7 @@ collection_<%=i%>_of_10:
   portable_data_hash: ea10d51bcf88862dbcc36eb292017dfd+45
   manifest_text: ". 73feffa4b7f6bb68e44cf984c85f6e88+3 0:3:baz\n"
   uuid: zzzzz-4zz18-10gneyn6brkx<%= i.to_s.rjust(3, '0') %>
+  current_version_uuid: zzzzz-4zz18-10gneyn6brkx<%= i.to_s.rjust(3, '0') %>
   owner_uuid: zzzzz-j7d0g-0010collections
   created_at: <%= i.minute.ago.to_s(:db) %>
   modified_at: <%= i.minute.ago.to_s(:db) %>
@@ -878,6 +947,7 @@ collection_<%=i%>_of_201:
   portable_data_hash: ea10d51bcf88862dbcc36eb292017dfd+45
   manifest_text: ". 73feffa4b7f6bb68e44cf984c85f6e88+3 0:3:baz\n"
   uuid: zzzzz-4zz18-201gneyn6brd<%= i.to_s.rjust(3, '0') %>
+  current_version_uuid: zzzzz-4zz18-201gneyn6brd<%= i.to_s.rjust(3, '0') %>
   owner_uuid: zzzzz-j7d0g-0201collections
   created_at: <%= i.minute.ago.to_s(:db) %>
   modified_at: <%= i.minute.ago.to_s(:db) %>
diff --git a/services/api/test/unit/collection_test.rb b/services/api/test/unit/collection_test.rb
index 8b8c48fe1..f745ca429 100644
--- a/services/api/test/unit/collection_test.rb
+++ b/services/api/test/unit/collection_test.rb
@@ -11,7 +11,7 @@ class CollectionTest < ActiveSupport::TestCase
   def create_collection name, enc=nil
     txt = ". d41d8cd98f00b204e9800998ecf8427e+0 0:0:#{name}.txt\n"
     txt.force_encoding(enc) if enc
-    return Collection.create(manifest_text: txt)
+    return Collection.create(manifest_text: txt, name: name)
   end
 
   test 'accept ASCII manifest_text' do
@@ -106,6 +106,49 @@ class CollectionTest < ActiveSupport::TestCase
     end
   end
 
+  [
+    [false, 'name', 'bar'],
+    [false, 'description', 'The quick brown fox jumps over the lazy dog'],
+    [false, 'properties', {'new_version' => true}],
+    [false, 'manifest_text', ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"],
+    [true, 'name', 'bar'],
+    [true, 'description', 'The quick brown fox jumps over the lazy dog'],
+    [true, 'properties', {'new_version' => true}],
+    [true, 'manifest_text', ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"],
+  ].each do |versioning, attr, val|
+    test "collection's #{attr} update with versioning #{versioning ? '' : 'not '}enabled" do
+      Rails.configuration.collection_versioning = versioning
+      act_as_system_user do
+        # Create initial collection
+        c = create_collection 'foo', Encoding::US_ASCII
+        assert c.valid?
+        assert_equal 'foo', c.name
+
+        # Check current version attributes
+        assert_equal 1, c.version
+        assert_equal c.uuid, c.current_version_uuid
+
+        # Update attribute and check if version number should be incremented
+        old_value = c.attributes[attr]
+        c.update_attribute attr, val
+        assert_equal val, c.attributes[attr]
+        assert_equal versioning, c.version == 2
+
+        if versioning
+          # Search for the snapshot & previous value
+          assert_equal 2, Collection.where(current_version_uuid: c.uuid).count
+          s = Collection.where(current_version_uuid: c.uuid, version: 1).first
+          assert_not_nil s
+          assert_equal old_value, s.attributes[attr]
+        else
+          # If versioning is disabled, only the current version should exist
+          assert_equal 1, Collection.where(current_version_uuid: c.uuid).count
+          assert_equal c, Collection.where(current_version_uuid: c.uuid).first
+        end
+      end
+    end
+  end
+
   test 'create and update collection and verify file_names' do
     act_as_system_user do
       c = create_collection 'foo', Encoding::US_ASCII

commit e37b9b6599f427520100aa262bcd65d6596bdbfd
Author: Lucas Di Pentima <ldipentima at veritasgenetics.com>
Date:   Fri Sep 14 13:11:42 2018 -0300

    13561: Add initial collection version information to the database
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <ldipentima at veritasgenetics.com>

diff --git a/services/api/db/migrate/20180913175443_add_version_info_to_collections.rb b/services/api/db/migrate/20180913175443_add_version_info_to_collections.rb
new file mode 100644
index 000000000..b5378d3a1
--- /dev/null
+++ b/services/api/db/migrate/20180913175443_add_version_info_to_collections.rb
@@ -0,0 +1,10 @@
+class AddVersionInfoToCollections < ActiveRecord::Migration
+  def change
+    # Do changes in bulk to save time on huge tables
+    change_table :collections, :bulk => true do |t|
+      t.string :current_version_uuid
+      t.integer :version, null: false, default: 1
+      t.index [:current_version_uuid, :version], unique: true
+    end
+  end
+end
diff --git a/services/api/db/migrate/20180915155335_set_current_version_uuid_on_collections.rb b/services/api/db/migrate/20180915155335_set_current_version_uuid_on_collections.rb
new file mode 100644
index 000000000..b205eae89
--- /dev/null
+++ b/services/api/db/migrate/20180915155335_set_current_version_uuid_on_collections.rb
@@ -0,0 +1,9 @@
+class SetCurrentVersionUuidOnCollections < ActiveRecord::Migration
+  def up
+    # Set the current version uuid as itself
+    Collection.where(current_version_uuid: nil).update_all("current_version_uuid=uuid")
+  end
+
+  def down
+  end
+end
diff --git a/services/api/db/structure.sql b/services/api/db/structure.sql
index 427c9afb5..d646d609b 100644
--- a/services/api/db/structure.sql
+++ b/services/api/db/structure.sql
@@ -172,7 +172,9 @@ CREATE TABLE public.collections (
     is_trashed boolean DEFAULT false NOT NULL,
     storage_classes_desired jsonb DEFAULT '["default"]'::jsonb,
     storage_classes_confirmed jsonb DEFAULT '[]'::jsonb,
-    storage_classes_confirmed_at timestamp without time zone
+    storage_classes_confirmed_at timestamp without time zone,
+    current_version_uuid character varying,
+    version integer DEFAULT 1 NOT NULL
 );
 
 
@@ -1782,6 +1784,13 @@ CREATE INDEX index_collections_on_created_at ON public.collections USING btree (
 
 
 --
+-- Name: index_collections_on_current_version_uuid_and_version; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE UNIQUE INDEX index_collections_on_current_version_uuid_and_version ON public.collections USING btree (current_version_uuid, version);
+
+
+--
 -- Name: index_collections_on_delete_at; Type: INDEX; Schema: public; Owner: -
 --
 
@@ -3169,3 +3178,7 @@ INSERT INTO schema_migrations (version) VALUES ('20180824155207');
 
 INSERT INTO schema_migrations (version) VALUES ('20180904110712');
 
+INSERT INTO schema_migrations (version) VALUES ('20180913175443');
+
+INSERT INTO schema_migrations (version) VALUES ('20180915155335');
+

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list