diff --git a/kinto/core/storage/postgresql/__init__.py b/kinto/core/storage/postgresql/__init__.py index 283c4c867..275238007 100644 --- a/kinto/core/storage/postgresql/__init__.py +++ b/kinto/core/storage/postgresql/__init__.py @@ -77,7 +77,7 @@ class Storage(StorageBase, MigratorMixin): # MigratorMixin attributes. name = "storage" - schema_version = 21 + schema_version = 22 schema_file = os.path.join(HERE, "schema.sql") migrations_directory = os.path.join(HERE, "migrations") @@ -201,7 +201,7 @@ def resource_timestamp(self, resource_name, parent_id): FROM objects WHERE parent_id = :parent_id AND resource_name = :resource_name - ORDER BY last_modified DESC + ORDER BY as_epoch(last_modified) DESC LIMIT 1 ) -- Timestamp of empty resource. diff --git a/kinto/core/storage/postgresql/migrations/migration_021_022.sql b/kinto/core/storage/postgresql/migrations/migration_021_022.sql new file mode 100644 index 000000000..5eed3f06b --- /dev/null +++ b/kinto/core/storage/postgresql/migrations/migration_021_022.sql @@ -0,0 +1,62 @@ +DROP TRIGGER IF EXISTS tgr_objects_last_modified ON objects; + +CREATE OR REPLACE FUNCTION bump_timestamp() +RETURNS trigger AS $$ +DECLARE + previous BIGINT; + current BIGINT; +BEGIN + previous := NULL; + WITH existing_timestamps AS ( + -- Timestamp of latest record. + ( + SELECT last_modified + FROM objects + WHERE parent_id = NEW.parent_id + AND resource_name = NEW.resource_name + ORDER BY as_epoch(last_modified) DESC + LIMIT 1 + ) + -- Timestamp when resource was empty. + UNION + ( + SELECT last_modified + FROM timestamps + WHERE parent_id = NEW.parent_id + AND resource_name = NEW.resource_name + ) + ) + SELECT as_epoch(MAX(last_modified)) INTO previous + FROM existing_timestamps; + + -- + -- This bumps the current timestamp to 1 msec in the future if the previous + -- timestamp is equal to the current one (or higher if was bumped already). + -- + -- If a bunch of requests from the same user on the same resource + -- arrive in the same millisecond, the unicity constraint can raise + -- an error (operation is cancelled). + -- See https://github.com/mozilla-services/cliquet/issues/25 + -- + current := as_epoch(clock_timestamp()::TIMESTAMP); + IF previous IS NOT NULL AND previous >= current THEN + current := previous + 1; + END IF; + + IF NEW.last_modified IS NULL OR + (previous IS NOT NULL AND as_epoch(NEW.last_modified) = previous) THEN + -- If record does not carry last-modified, or if the one specified + -- is equal to previous, assign it to current (i.e. bump it). + NEW.last_modified := from_epoch(current); + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER tgr_objects_last_modified +BEFORE INSERT OR UPDATE OF data ON objects +FOR EACH ROW EXECUTE PROCEDURE bump_timestamp(); + +-- Bump storage schema version. +INSERT INTO metadata (name, value) VALUES ('storage_schema_version', '22'); diff --git a/kinto/core/storage/postgresql/schema.sql b/kinto/core/storage/postgresql/schema.sql index 47030838d..d0a9a4413 100644 --- a/kinto/core/storage/postgresql/schema.sql +++ b/kinto/core/storage/postgresql/schema.sql @@ -64,8 +64,8 @@ DROP TRIGGER IF EXISTS tgr_objects_last_modified ON objects; CREATE OR REPLACE FUNCTION bump_timestamp() RETURNS trigger AS $$ DECLARE - previous TIMESTAMP; - current TIMESTAMP; + previous BIGINT; + current BIGINT; BEGIN previous := NULL; WITH existing_timestamps AS ( @@ -75,7 +75,7 @@ BEGIN FROM objects WHERE parent_id = NEW.parent_id AND resource_name = NEW.resource_name - ORDER BY last_modified DESC + ORDER BY as_epoch(last_modified) DESC LIMIT 1 ) -- Timestamp when resource was empty. @@ -87,7 +87,7 @@ BEGIN AND resource_name = NEW.resource_name ) ) - SELECT MAX(last_modified) INTO previous + SELECT as_epoch(MAX(last_modified)) INTO previous FROM existing_timestamps; -- @@ -99,16 +99,16 @@ BEGIN -- an error (operation is cancelled). -- See https://github.com/mozilla-services/cliquet/issues/25 -- - current := clock_timestamp(); + current := as_epoch(clock_timestamp()::TIMESTAMP); IF previous IS NOT NULL AND previous >= current THEN - current := previous + INTERVAL '1 milliseconds'; + current := previous + 1; END IF; IF NEW.last_modified IS NULL OR - (previous IS NOT NULL AND as_epoch(NEW.last_modified) = as_epoch(previous)) THEN + (previous IS NOT NULL AND as_epoch(NEW.last_modified) = previous) THEN -- If record does not carry last-modified, or if the one specified -- is equal to previous, assign it to current (i.e. bump it). - NEW.last_modified := current; + NEW.last_modified := from_epoch(current); END IF; RETURN NEW; @@ -131,4 +131,4 @@ INSERT INTO metadata (name, value) VALUES ('created_at', NOW()::TEXT); -- Set storage schema version. -- Should match ``kinto.core.storage.postgresql.PostgreSQL.schema_version`` -INSERT INTO metadata (name, value) VALUES ('storage_schema_version', '21'); +INSERT INTO metadata (name, value) VALUES ('storage_schema_version', '22');