kinto 23.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. kinto/__init__.py +92 -0
  2. kinto/__main__.py +249 -0
  3. kinto/authorization.py +134 -0
  4. kinto/config/__init__.py +94 -0
  5. kinto/config/kinto.tpl +270 -0
  6. kinto/contribute.json +27 -0
  7. kinto/core/__init__.py +246 -0
  8. kinto/core/authentication.py +48 -0
  9. kinto/core/authorization.py +311 -0
  10. kinto/core/cache/__init__.py +131 -0
  11. kinto/core/cache/memcached.py +112 -0
  12. kinto/core/cache/memory.py +104 -0
  13. kinto/core/cache/postgresql/__init__.py +178 -0
  14. kinto/core/cache/postgresql/schema.sql +23 -0
  15. kinto/core/cache/testing.py +208 -0
  16. kinto/core/cornice/__init__.py +93 -0
  17. kinto/core/cornice/cors.py +144 -0
  18. kinto/core/cornice/errors.py +40 -0
  19. kinto/core/cornice/pyramidhook.py +373 -0
  20. kinto/core/cornice/renderer.py +89 -0
  21. kinto/core/cornice/resource.py +205 -0
  22. kinto/core/cornice/service.py +641 -0
  23. kinto/core/cornice/util.py +138 -0
  24. kinto/core/cornice/validators/__init__.py +94 -0
  25. kinto/core/cornice/validators/_colander.py +142 -0
  26. kinto/core/cornice/validators/_marshmallow.py +182 -0
  27. kinto/core/cornice_swagger/__init__.py +92 -0
  28. kinto/core/cornice_swagger/converters/__init__.py +21 -0
  29. kinto/core/cornice_swagger/converters/exceptions.py +6 -0
  30. kinto/core/cornice_swagger/converters/parameters.py +90 -0
  31. kinto/core/cornice_swagger/converters/schema.py +249 -0
  32. kinto/core/cornice_swagger/swagger.py +725 -0
  33. kinto/core/cornice_swagger/templates/index.html +73 -0
  34. kinto/core/cornice_swagger/templates/index_script_template.html +21 -0
  35. kinto/core/cornice_swagger/util.py +42 -0
  36. kinto/core/cornice_swagger/views.py +78 -0
  37. kinto/core/decorators.py +74 -0
  38. kinto/core/errors.py +216 -0
  39. kinto/core/events.py +301 -0
  40. kinto/core/initialization.py +738 -0
  41. kinto/core/listeners/__init__.py +9 -0
  42. kinto/core/metrics.py +94 -0
  43. kinto/core/openapi.py +115 -0
  44. kinto/core/permission/__init__.py +202 -0
  45. kinto/core/permission/memory.py +167 -0
  46. kinto/core/permission/postgresql/__init__.py +489 -0
  47. kinto/core/permission/postgresql/migrations/migration_001_002.sql +18 -0
  48. kinto/core/permission/postgresql/schema.sql +41 -0
  49. kinto/core/permission/testing.py +487 -0
  50. kinto/core/resource/__init__.py +1311 -0
  51. kinto/core/resource/model.py +412 -0
  52. kinto/core/resource/schema.py +502 -0
  53. kinto/core/resource/viewset.py +230 -0
  54. kinto/core/schema.py +119 -0
  55. kinto/core/scripts.py +50 -0
  56. kinto/core/statsd.py +1 -0
  57. kinto/core/storage/__init__.py +436 -0
  58. kinto/core/storage/exceptions.py +53 -0
  59. kinto/core/storage/generators.py +58 -0
  60. kinto/core/storage/memory.py +651 -0
  61. kinto/core/storage/postgresql/__init__.py +1131 -0
  62. kinto/core/storage/postgresql/client.py +120 -0
  63. kinto/core/storage/postgresql/migrations/migration_001_002.sql +10 -0
  64. kinto/core/storage/postgresql/migrations/migration_002_003.sql +33 -0
  65. kinto/core/storage/postgresql/migrations/migration_003_004.sql +18 -0
  66. kinto/core/storage/postgresql/migrations/migration_004_005.sql +20 -0
  67. kinto/core/storage/postgresql/migrations/migration_005_006.sql +11 -0
  68. kinto/core/storage/postgresql/migrations/migration_006_007.sql +74 -0
  69. kinto/core/storage/postgresql/migrations/migration_007_008.sql +66 -0
  70. kinto/core/storage/postgresql/migrations/migration_008_009.sql +41 -0
  71. kinto/core/storage/postgresql/migrations/migration_009_010.sql +98 -0
  72. kinto/core/storage/postgresql/migrations/migration_010_011.sql +14 -0
  73. kinto/core/storage/postgresql/migrations/migration_011_012.sql +9 -0
  74. kinto/core/storage/postgresql/migrations/migration_012_013.sql +71 -0
  75. kinto/core/storage/postgresql/migrations/migration_013_014.sql +14 -0
  76. kinto/core/storage/postgresql/migrations/migration_014_015.sql +95 -0
  77. kinto/core/storage/postgresql/migrations/migration_015_016.sql +4 -0
  78. kinto/core/storage/postgresql/migrations/migration_016_017.sql +81 -0
  79. kinto/core/storage/postgresql/migrations/migration_017_018.sql +25 -0
  80. kinto/core/storage/postgresql/migrations/migration_018_019.sql +8 -0
  81. kinto/core/storage/postgresql/migrations/migration_019_020.sql +7 -0
  82. kinto/core/storage/postgresql/migrations/migration_020_021.sql +68 -0
  83. kinto/core/storage/postgresql/migrations/migration_021_022.sql +62 -0
  84. kinto/core/storage/postgresql/migrations/migration_022_023.sql +5 -0
  85. kinto/core/storage/postgresql/migrations/migration_023_024.sql +6 -0
  86. kinto/core/storage/postgresql/migrations/migration_024_025.sql +6 -0
  87. kinto/core/storage/postgresql/migrator.py +98 -0
  88. kinto/core/storage/postgresql/pool.py +55 -0
  89. kinto/core/storage/postgresql/schema.sql +143 -0
  90. kinto/core/storage/testing.py +1857 -0
  91. kinto/core/storage/utils.py +37 -0
  92. kinto/core/testing.py +182 -0
  93. kinto/core/utils.py +553 -0
  94. kinto/core/views/__init__.py +0 -0
  95. kinto/core/views/batch.py +163 -0
  96. kinto/core/views/errors.py +145 -0
  97. kinto/core/views/heartbeat.py +106 -0
  98. kinto/core/views/hello.py +69 -0
  99. kinto/core/views/openapi.py +35 -0
  100. kinto/core/views/version.py +50 -0
  101. kinto/events.py +3 -0
  102. kinto/plugins/__init__.py +0 -0
  103. kinto/plugins/accounts/__init__.py +94 -0
  104. kinto/plugins/accounts/authentication.py +63 -0
  105. kinto/plugins/accounts/scripts.py +61 -0
  106. kinto/plugins/accounts/utils.py +13 -0
  107. kinto/plugins/accounts/views.py +136 -0
  108. kinto/plugins/admin/README.md +3 -0
  109. kinto/plugins/admin/VERSION +1 -0
  110. kinto/plugins/admin/__init__.py +40 -0
  111. kinto/plugins/admin/build/VERSION +1 -0
  112. kinto/plugins/admin/build/assets/index-CYFwtKtL.css +6 -0
  113. kinto/plugins/admin/build/assets/index-DJ0m93zA.js +149 -0
  114. kinto/plugins/admin/build/assets/logo-VBRiKSPX.png +0 -0
  115. kinto/plugins/admin/build/index.html +18 -0
  116. kinto/plugins/admin/public/help.html +25 -0
  117. kinto/plugins/admin/views.py +42 -0
  118. kinto/plugins/default_bucket/__init__.py +191 -0
  119. kinto/plugins/flush.py +28 -0
  120. kinto/plugins/history/__init__.py +65 -0
  121. kinto/plugins/history/listener.py +181 -0
  122. kinto/plugins/history/views.py +66 -0
  123. kinto/plugins/openid/__init__.py +131 -0
  124. kinto/plugins/openid/utils.py +14 -0
  125. kinto/plugins/openid/views.py +193 -0
  126. kinto/plugins/prometheus.py +300 -0
  127. kinto/plugins/statsd.py +85 -0
  128. kinto/schema_validation.py +135 -0
  129. kinto/views/__init__.py +34 -0
  130. kinto/views/admin.py +195 -0
  131. kinto/views/buckets.py +45 -0
  132. kinto/views/collections.py +58 -0
  133. kinto/views/contribute.py +39 -0
  134. kinto/views/groups.py +90 -0
  135. kinto/views/permissions.py +235 -0
  136. kinto/views/records.py +133 -0
  137. kinto-23.2.1.dist-info/METADATA +232 -0
  138. kinto-23.2.1.dist-info/RECORD +142 -0
  139. kinto-23.2.1.dist-info/WHEEL +5 -0
  140. kinto-23.2.1.dist-info/entry_points.txt +5 -0
  141. kinto-23.2.1.dist-info/licenses/LICENSE +13 -0
  142. kinto-23.2.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,120 @@
1
+ import contextlib
2
+ import logging
3
+ import warnings
4
+ from collections import defaultdict
5
+
6
+ import transaction as zope_transaction
7
+
8
+ from kinto.core.storage import exceptions
9
+ from kinto.core.utils import sqlalchemy
10
+
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ BLACKLISTED_SETTINGS = [
15
+ "backend",
16
+ "max_fetch_size",
17
+ "max_size_bytes",
18
+ "prefix",
19
+ "hosts",
20
+ ]
21
+
22
+
23
+ class PostgreSQLClient:
24
+ def __init__(self, session_factory, commit_manually, invalidate):
25
+ self.session_factory = session_factory
26
+ self.commit_manually = commit_manually
27
+ self.invalidate = invalidate
28
+
29
+ @contextlib.contextmanager
30
+ def connect(self, readonly=False, force_commit=False):
31
+ """
32
+ Pulls a connection from the pool when context is entered and
33
+ returns it when context is exited.
34
+
35
+ A COMMIT is performed on the current transaction if everything went
36
+ well. Otherwise transaction is ROLLBACK, and everything cleaned up.
37
+ """
38
+ commit_manually = self.commit_manually and not readonly
39
+ session = None
40
+ try:
41
+ # Pull connection from pool.
42
+ session = self.session_factory()
43
+ # Start context
44
+ yield session
45
+ if not readonly and not self.commit_manually:
46
+ # Mark session as dirty.
47
+ self.invalidate(session)
48
+ # Success
49
+ if commit_manually:
50
+ session.commit()
51
+ elif force_commit:
52
+ # Commit like would do a succesful request.
53
+ zope_transaction.commit()
54
+
55
+ except sqlalchemy.exc.IntegrityError as e:
56
+ logger.error(e, exc_info=True)
57
+ if commit_manually: # pragma: no branch
58
+ session.rollback()
59
+ raise exceptions.IntegrityError(original=e) from e
60
+ except sqlalchemy.exc.SQLAlchemyError as e:
61
+ logger.error(e, exc_info=True)
62
+ if session and commit_manually:
63
+ session.rollback()
64
+ raise exceptions.BackendError(original=e) from e
65
+ finally:
66
+ if session and self.commit_manually:
67
+ # Give back to pool if commit done manually.
68
+ session.close()
69
+
70
+
71
+ # Reuse existing client if same URL.
72
+ _CLIENTS = defaultdict(dict)
73
+
74
+
75
+ def create_from_config(config, prefix="", with_transaction=True):
76
+ """Create a PostgreSQLClient client using settings in the provided config."""
77
+ if sqlalchemy is None:
78
+ message = (
79
+ "PostgreSQL SQLAlchemy dependency missing. "
80
+ "Refer to installation section in documentation."
81
+ )
82
+ raise ImportWarning(message)
83
+
84
+ from sqlalchemy.orm import scoped_session, sessionmaker
85
+ from zope.sqlalchemy import invalidate, register
86
+
87
+ settings = {**config.get_settings()}
88
+
89
+ # Custom Kinto settings, unsupported by SQLAlchemy.
90
+ blacklist = [prefix + setting for setting in BLACKLISTED_SETTINGS]
91
+ filtered_settings = {k: v for k, v in settings.items() if k not in blacklist}
92
+ transaction_per_request = with_transaction and filtered_settings.pop(
93
+ "transaction_per_request", False
94
+ )
95
+ url = filtered_settings[prefix + "url"]
96
+ existing_client = _CLIENTS[transaction_per_request].get(url)
97
+ if existing_client:
98
+ msg = f"Reuse existing PostgreSQL connection. Parameters {prefix}* will be ignored."
99
+ warnings.warn(msg)
100
+ return existing_client
101
+
102
+ # Initialize SQLAlchemy engine from filtered_settings.
103
+ poolclass_key = prefix + "poolclass"
104
+ filtered_settings.setdefault(
105
+ poolclass_key, ("kinto.core.storage.postgresql.pool.QueuePoolWithMaxBacklog")
106
+ )
107
+ filtered_settings[poolclass_key] = config.maybe_dotted(filtered_settings[poolclass_key])
108
+ engine = sqlalchemy.engine_from_config(filtered_settings, prefix=prefix, url=url)
109
+
110
+ # Initialize thread-safe session factory.
111
+ session_factory = scoped_session(sessionmaker(bind=engine))
112
+ if transaction_per_request:
113
+ # Plug with Pyramid transaction manager
114
+ register(session_factory)
115
+
116
+ # Store one client per URI.
117
+ commit_manually = not transaction_per_request
118
+ client = PostgreSQLClient(session_factory, commit_manually, invalidate)
119
+ _CLIENTS[transaction_per_request][url] = client
120
+ return client
@@ -0,0 +1,10 @@
1
+ ALTER FUNCTION as_epoch(TIMESTAMP) IMMUTABLE;
2
+
3
+ DROP INDEX IF EXISTS idx_records_last_modified_epoch;
4
+ CREATE INDEX idx_records_last_modified_epoch ON records(as_epoch(last_modified));
5
+
6
+ DROP INDEX IF EXISTS idx_deleted_last_modified_epoch;
7
+ CREATE INDEX idx_deleted_last_modified_epoch ON deleted(as_epoch(last_modified));
8
+
9
+ -- Bump storage schema version.
10
+ INSERT INTO metadata (name, value) VALUES ('storage_schema_version', '2');
@@ -0,0 +1,33 @@
1
+ ALTER TABLE records DROP CONSTRAINT records_id_user_id_resource_name_last_modified_key CASCADE;
2
+ CREATE UNIQUE INDEX idx_records_user_id_resource_name_last_modified
3
+ ON records(user_id, resource_name, last_modified DESC);
4
+
5
+ ALTER TABLE deleted DROP CONSTRAINT deleted_id_user_id_resource_name_last_modified_key CASCADE;
6
+ CREATE UNIQUE INDEX idx_deleted_user_id_resource_name_last_modified
7
+ ON deleted(user_id, resource_name, last_modified DESC);
8
+
9
+ CREATE OR REPLACE FUNCTION resource_timestamp(uid VARCHAR, resource VARCHAR)
10
+ RETURNS TIMESTAMP AS $$
11
+ DECLARE
12
+ ts_records TIMESTAMP;
13
+ ts_deleted TIMESTAMP;
14
+ BEGIN
15
+ SELECT last_modified INTO ts_records
16
+ FROM records
17
+ WHERE user_id = uid
18
+ AND resource_name = resource
19
+ ORDER BY last_modified DESC LIMIT 1;
20
+
21
+ SELECT last_modified INTO ts_deleted
22
+ FROM deleted
23
+ WHERE user_id = uid
24
+ AND resource_name = resource
25
+ ORDER BY last_modified DESC LIMIT 1;
26
+
27
+ -- Latest of records/deleted or current if empty
28
+ RETURN coalesce(greatest(ts_deleted, ts_records), localtimestamp);
29
+ END;
30
+ $$ LANGUAGE plpgsql;
31
+
32
+ -- Bump storage schema version.
33
+ INSERT INTO metadata (name, value) VALUES ('storage_schema_version', '3');
@@ -0,0 +1,18 @@
1
+ ALTER TABLE records
2
+ ALTER COLUMN id DROP DEFAULT,
3
+ ALTER COLUMN id SET DATA TYPE TEXT,
4
+ ALTER COLUMN user_id SET DATA TYPE TEXT,
5
+ ALTER COLUMN resource_name SET DATA TYPE TEXT;
6
+
7
+ ALTER TABLE deleted
8
+ ALTER COLUMN id DROP DEFAULT,
9
+ ALTER COLUMN id SET DATA TYPE TEXT,
10
+ ALTER COLUMN user_id SET DATA TYPE TEXT,
11
+ ALTER COLUMN resource_name SET DATA TYPE TEXT;
12
+
13
+
14
+ DROP EXTENSION IF EXISTS "uuid-ossp";
15
+
16
+
17
+ -- Bump storage schema version.
18
+ INSERT INTO metadata (name, value) VALUES ('storage_schema_version', '4');
@@ -0,0 +1,20 @@
1
+ DROP INDEX IF EXISTS idx_records_user_id;
2
+ DROP INDEX IF EXISTS idx_records_resource_name;
3
+ DROP INDEX IF EXISTS idx_records_last_modified;
4
+ DROP INDEX IF EXISTS idx_records_id;
5
+
6
+ ALTER TABLE records
7
+ ADD PRIMARY KEY (id, user_id, resource_name);
8
+
9
+
10
+ DROP INDEX IF EXISTS idx_deleted_id;
11
+ DROP INDEX IF EXISTS idx_deleted_user_id;
12
+ DROP INDEX IF EXISTS idx_deleted_resource_name;
13
+ DROP INDEX IF EXISTS idx_deleted_last_modified;
14
+
15
+ ALTER TABLE deleted
16
+ ADD PRIMARY KEY (id, user_id, resource_name);
17
+
18
+
19
+ -- Bump storage schema version.
20
+ INSERT INTO metadata (name, value) VALUES ('storage_schema_version', '5');
@@ -0,0 +1,11 @@
1
+ --
2
+ -- Switch records data column to JSONB.
3
+ -- (requires PostgreSQL 9.4+)
4
+ --
5
+ ALTER TABLE records
6
+ ALTER COLUMN data DROP DEFAULT,
7
+ ALTER COLUMN data SET DATA TYPE JSONB USING data::TEXT::JSONB,
8
+ ALTER COLUMN data SET DEFAULT '{}'::JSONB;
9
+
10
+ -- Bump storage schema version.
11
+ INSERT INTO metadata (name, value) VALUES ('storage_schema_version', '6');
@@ -0,0 +1,74 @@
1
+ ALTER TABLE records RENAME COLUMN resource_name TO collection_id;
2
+ ALTER TABLE records RENAME COLUMN user_id TO parent_id;
3
+
4
+ ALTER INDEX idx_records_user_id_resource_name_last_modified
5
+ RENAME TO idx_records_parent_id_collection_id_last_modified;
6
+
7
+ ALTER TABLE deleted RENAME COLUMN resource_name TO collection_id;
8
+ ALTER TABLE deleted RENAME COLUMN user_id TO parent_id;
9
+
10
+ ALTER INDEX idx_deleted_user_id_resource_name_last_modified
11
+ RENAME TO idx_deleted_parent_id_collection_id_last_modified;
12
+
13
+ ALTER FUNCTION resource_timestamp(VARCHAR, VARCHAR)
14
+ RENAME TO collection_timestamp;
15
+
16
+
17
+ CREATE OR REPLACE FUNCTION collection_timestamp(uid VARCHAR, resource VARCHAR)
18
+ RETURNS TIMESTAMP AS $$
19
+ DECLARE
20
+ ts_records TIMESTAMP;
21
+ ts_deleted TIMESTAMP;
22
+ BEGIN
23
+ --
24
+ -- This is fast because an index was created for ``parent_id``,
25
+ -- ``collection_id``, and ``last_modified`` with descending sorting order.
26
+ --
27
+ SELECT last_modified INTO ts_records
28
+ FROM records
29
+ WHERE parent_id = uid
30
+ AND collection_id = resource
31
+ ORDER BY last_modified DESC LIMIT 1;
32
+
33
+ SELECT last_modified INTO ts_deleted
34
+ FROM deleted
35
+ WHERE parent_id = uid
36
+ AND collection_id = resource
37
+ ORDER BY last_modified DESC LIMIT 1;
38
+
39
+ -- Latest of records/deleted or current if empty
40
+ RETURN coalesce(greatest(ts_deleted, ts_records), localtimestamp);
41
+ END;
42
+ $$ LANGUAGE plpgsql;
43
+
44
+
45
+ CREATE OR REPLACE FUNCTION bump_timestamp()
46
+ RETURNS trigger AS $$
47
+ DECLARE
48
+ previous TIMESTAMP;
49
+ current TIMESTAMP;
50
+ BEGIN
51
+ --
52
+ -- This bumps the current timestamp to 1 msec in the future if the previous
53
+ -- timestamp is equal to the current one (or higher if was bumped already).
54
+ --
55
+ -- If a bunch of requests from the same user on the same collection
56
+ -- arrive in the same millisecond, the unicity constraint can raise
57
+ -- an error (operation is cancelled).
58
+ -- See https://github.com/mozilla-services/cliquet/issues/25
59
+ --
60
+ previous := collection_timestamp(NEW.parent_id, NEW.collection_id);
61
+ current := localtimestamp;
62
+
63
+ IF previous >= current THEN
64
+ current := previous + INTERVAL '1 milliseconds';
65
+ END IF;
66
+
67
+ NEW.last_modified := current;
68
+
69
+ RETURN NEW;
70
+ END;
71
+ $$ LANGUAGE plpgsql;
72
+
73
+ -- Bump storage schema version.
74
+ INSERT INTO metadata (name, value) VALUES ('storage_schema_version', '7');
@@ -0,0 +1,66 @@
1
+ --
2
+ -- Helper that returns the current collection timestamp.
3
+ --
4
+ CREATE OR REPLACE FUNCTION collection_timestamp(uid VARCHAR, resource VARCHAR)
5
+ RETURNS TIMESTAMP AS $$
6
+ DECLARE
7
+ ts_records TIMESTAMP;
8
+ ts_deleted TIMESTAMP;
9
+ BEGIN
10
+ --
11
+ -- This is fast because an index was created for ``parent_id``,
12
+ -- ``collection_id``, and ``last_modified`` with descending sorting order.
13
+ --
14
+ SELECT last_modified INTO ts_records
15
+ FROM records
16
+ WHERE parent_id = uid
17
+ AND collection_id = resource
18
+ ORDER BY last_modified DESC LIMIT 1;
19
+
20
+ SELECT last_modified INTO ts_deleted
21
+ FROM deleted
22
+ WHERE parent_id = uid
23
+ AND collection_id = resource
24
+ ORDER BY last_modified DESC LIMIT 1;
25
+
26
+ -- Latest of records/deleted or current if empty
27
+ RETURN coalesce(greatest(ts_deleted, ts_records), clock_timestamp());
28
+ END;
29
+ $$ LANGUAGE plpgsql;
30
+
31
+ --
32
+ -- Triggers to set last_modified on INSERT/UPDATE
33
+ --
34
+ DROP TRIGGER IF EXISTS tgr_records_last_modified ON records;
35
+ DROP TRIGGER IF EXISTS tgr_deleted_last_modified ON deleted;
36
+
37
+ CREATE OR REPLACE FUNCTION bump_timestamp()
38
+ RETURNS trigger AS $$
39
+ DECLARE
40
+ previous TIMESTAMP;
41
+ current TIMESTAMP;
42
+ BEGIN
43
+ --
44
+ -- This bumps the current timestamp to 1 msec in the future if the previous
45
+ -- timestamp is equal to the current one (or higher if was bumped already).
46
+ --
47
+ -- If a bunch of requests from the same user on the same collection
48
+ -- arrive in the same millisecond, the unicity constraint can raise
49
+ -- an error (operation is cancelled).
50
+ -- See https://github.com/mozilla-services/cliquet/issues/25
51
+ --
52
+ previous := collection_timestamp(NEW.parent_id, NEW.collection_id);
53
+ current := clock_timestamp();
54
+
55
+ IF previous >= current THEN
56
+ current := previous + INTERVAL '1 milliseconds';
57
+ END IF;
58
+
59
+ NEW.last_modified := current;
60
+
61
+ RETURN NEW;
62
+ END;
63
+ $$ LANGUAGE plpgsql;
64
+
65
+ -- Bump storage schema version.
66
+ INSERT INTO metadata (name, value) VALUES ('storage_schema_version', '8');
@@ -0,0 +1,41 @@
1
+ CREATE OR REPLACE FUNCTION from_epoch(epoch BIGINT) RETURNS TIMESTAMP AS $$
2
+ BEGIN
3
+ RETURN TIMESTAMP WITH TIME ZONE 'epoch' + epoch * INTERVAL '1 millisecond';
4
+ END;
5
+ $$ LANGUAGE plpgsql
6
+ IMMUTABLE;
7
+
8
+
9
+ CREATE OR REPLACE FUNCTION bump_timestamp()
10
+ RETURNS trigger AS $$
11
+ DECLARE
12
+ previous TIMESTAMP;
13
+ current TIMESTAMP;
14
+
15
+ BEGIN
16
+ --
17
+ -- This bumps the current timestamp to 1 msec in the future if the previous
18
+ -- timestamp is equal to the current one (or higher if was bumped already).
19
+ --
20
+ -- If a bunch of requests from the same user on the same collection
21
+ -- arrive in the same millisecond, the unicity constraint can raise
22
+ -- an error (operation is cancelled).
23
+ -- See https://github.com/mozilla-services/cliquet/issues/25
24
+ --
25
+ previous := collection_timestamp(NEW.parent_id, NEW.collection_id);
26
+
27
+ IF NEW.last_modified IS NULL THEN
28
+ current := clock_timestamp();
29
+ IF previous >= current THEN
30
+ current := previous + INTERVAL '1 milliseconds';
31
+ END IF;
32
+ NEW.last_modified := current;
33
+ END IF;
34
+
35
+ RETURN NEW;
36
+ END;
37
+ $$ LANGUAGE plpgsql;
38
+
39
+
40
+ -- Bump storage schema version.
41
+ INSERT INTO metadata (name, value) VALUES ('storage_schema_version', '9');
@@ -0,0 +1,98 @@
1
+ CREATE TABLE IF NOT EXISTS timestamps (
2
+ parent_id TEXT NOT NULL,
3
+ collection_id TEXT NOT NULL,
4
+ last_modified TIMESTAMP NOT NULL,
5
+ PRIMARY KEY (parent_id, collection_id)
6
+ );
7
+
8
+
9
+ CREATE OR REPLACE FUNCTION collection_timestamp(uid VARCHAR, resource VARCHAR)
10
+ RETURNS TIMESTAMP AS $$
11
+ DECLARE
12
+ ts TIMESTAMP;
13
+ BEGIN
14
+ ts := NULL;
15
+
16
+ SELECT last_modified INTO ts
17
+ FROM timestamps
18
+ WHERE parent_id = uid
19
+ AND collection_id = resource;
20
+
21
+ IF ts IS NULL THEN
22
+ ts := clock_timestamp();
23
+ INSERT INTO timestamps (parent_id, collection_id, last_modified)
24
+ VALUES (uid, resource, ts);
25
+ END IF;
26
+
27
+ RETURN ts;
28
+ END;
29
+ $$ LANGUAGE plpgsql;
30
+
31
+ DROP TRIGGER IF EXISTS tgr_records_last_modified ON records;
32
+ DROP TRIGGER IF EXISTS tgr_deleted_last_modified ON deleted;
33
+
34
+ CREATE OR REPLACE FUNCTION bump_timestamp()
35
+ RETURNS trigger AS $$
36
+ DECLARE
37
+ previous TIMESTAMP;
38
+ current TIMESTAMP;
39
+
40
+ BEGIN
41
+ previous := NULL;
42
+ SELECT last_modified INTO previous
43
+ FROM timestamps
44
+ WHERE parent_id = NEW.parent_id
45
+ AND collection_id = NEW.collection_id;
46
+
47
+ --
48
+ -- This bumps the current timestamp to 1 msec in the future if the previous
49
+ -- timestamp is equal to the current one (or higher if was bumped already).
50
+ --
51
+ -- If a bunch of requests from the same user on the same collection
52
+ -- arrive in the same millisecond, the unicity constraint can raise
53
+ -- an error (operation is cancelled).
54
+ -- See https://github.com/mozilla-services/cliquet/issues/25
55
+ --
56
+ current := clock_timestamp();
57
+ IF previous IS NOT NULL AND previous >= current THEN
58
+ current := previous + INTERVAL '1 milliseconds';
59
+ END IF;
60
+
61
+
62
+ IF NEW.last_modified IS NULL THEN
63
+ -- If record does not carry last-modified, assign it to current.
64
+ NEW.last_modified := current;
65
+ ELSE
66
+ -- Use record last-modified as collection timestamp.
67
+ IF previous IS NULL OR NEW.last_modified > previous THEN
68
+ current := NEW.last_modified;
69
+ END IF;
70
+ END IF;
71
+
72
+ --
73
+ -- Upsert current collection timestamp.
74
+ --
75
+ WITH upsert AS (
76
+ UPDATE timestamps SET last_modified = current
77
+ WHERE parent_id = NEW.parent_id AND collection_id = NEW.collection_id
78
+ RETURNING *
79
+ )
80
+ INSERT INTO timestamps (parent_id, collection_id, last_modified)
81
+ SELECT NEW.parent_id, NEW.collection_id, current
82
+ WHERE NOT EXISTS (SELECT * FROM upsert);
83
+
84
+ RETURN NEW;
85
+ END;
86
+ $$ LANGUAGE plpgsql;
87
+
88
+ CREATE TRIGGER tgr_records_last_modified
89
+ BEFORE INSERT OR UPDATE ON records
90
+ FOR EACH ROW EXECUTE PROCEDURE bump_timestamp();
91
+
92
+ CREATE TRIGGER tgr_deleted_last_modified
93
+ BEFORE INSERT OR UPDATE ON deleted
94
+ FOR EACH ROW EXECUTE PROCEDURE bump_timestamp();
95
+
96
+
97
+ -- Bump storage schema version.
98
+ INSERT INTO metadata (name, value) VALUES ('storage_schema_version', '10');
@@ -0,0 +1,14 @@
1
+ DROP TRIGGER IF EXISTS tgr_records_last_modified ON records;
2
+ DROP TRIGGER IF EXISTS tgr_deleted_last_modified ON deleted;
3
+
4
+ CREATE TRIGGER tgr_records_last_modified
5
+ BEFORE INSERT OR UPDATE ON records
6
+ FOR EACH ROW EXECUTE PROCEDURE bump_timestamp();
7
+
8
+ CREATE TRIGGER tgr_deleted_last_modified
9
+ BEFORE INSERT OR UPDATE ON deleted
10
+ FOR EACH ROW EXECUTE PROCEDURE bump_timestamp();
11
+
12
+
13
+ -- Bump storage schema version.
14
+ INSERT INTO metadata (name, value) VALUES ('storage_schema_version', '11');
@@ -0,0 +1,9 @@
1
+ -- Select all existing records and delete their tombstone if any.
2
+ DELETE FROM deleted d
3
+ USING records r
4
+ WHERE d.id = r.id
5
+ AND d.parent_id = r.parent_id
6
+ AND d.collection_id = r.collection_id;
7
+
8
+ -- Bump storage schema version.
9
+ INSERT INTO metadata (name, value) VALUES ('storage_schema_version', '12');
@@ -0,0 +1,71 @@
1
+ --
2
+ -- Triggers to set last_modified on INSERT/UPDATE
3
+ --
4
+ DROP TRIGGER IF EXISTS tgr_records_last_modified ON records;
5
+ DROP TRIGGER IF EXISTS tgr_deleted_last_modified ON deleted;
6
+
7
+ CREATE OR REPLACE FUNCTION bump_timestamp()
8
+ RETURNS trigger AS $$
9
+ DECLARE
10
+ previous TIMESTAMP;
11
+ current TIMESTAMP;
12
+
13
+ BEGIN
14
+ previous := NULL;
15
+ SELECT last_modified INTO previous
16
+ FROM timestamps
17
+ WHERE parent_id = NEW.parent_id
18
+ AND collection_id = NEW.collection_id;
19
+
20
+ --
21
+ -- This bumps the current timestamp to 1 msec in the future if the previous
22
+ -- timestamp is equal to the current one (or higher if was bumped already).
23
+ --
24
+ -- If a bunch of requests from the same user on the same collection
25
+ -- arrive in the same millisecond, the unicity constraint can raise
26
+ -- an error (operation is cancelled).
27
+ -- See https://github.com/mozilla-services/cliquet/issues/25
28
+ --
29
+ current := clock_timestamp();
30
+ IF previous IS NOT NULL AND previous >= current THEN
31
+ current := previous + INTERVAL '1 milliseconds';
32
+ END IF;
33
+
34
+ IF NEW.last_modified IS NULL OR
35
+ (previous IS NOT NULL AND as_epoch(NEW.last_modified) = as_epoch(previous)) THEN
36
+ -- If record does not carry last-modified, or if the one specified
37
+ -- is equal to previous, assign it to current (i.e. bump it).
38
+ NEW.last_modified := current;
39
+ ELSE
40
+ -- Use record last-modified as collection timestamp.
41
+ IF previous IS NULL OR NEW.last_modified > previous THEN
42
+ current := NEW.last_modified;
43
+ END IF;
44
+ END IF;
45
+
46
+ --
47
+ -- Upsert current collection timestamp.
48
+ --
49
+ WITH upsert AS (
50
+ UPDATE timestamps SET last_modified = current
51
+ WHERE parent_id = NEW.parent_id AND collection_id = NEW.collection_id
52
+ RETURNING *
53
+ )
54
+ INSERT INTO timestamps (parent_id, collection_id, last_modified)
55
+ SELECT NEW.parent_id, NEW.collection_id, current
56
+ WHERE NOT EXISTS (SELECT * FROM upsert);
57
+
58
+ RETURN NEW;
59
+ END;
60
+ $$ LANGUAGE plpgsql;
61
+
62
+ CREATE TRIGGER tgr_records_last_modified
63
+ BEFORE INSERT OR UPDATE ON records
64
+ FOR EACH ROW EXECUTE PROCEDURE bump_timestamp();
65
+
66
+ CREATE TRIGGER tgr_deleted_last_modified
67
+ BEFORE INSERT OR UPDATE ON deleted
68
+ FOR EACH ROW EXECUTE PROCEDURE bump_timestamp();
69
+
70
+ -- Bump storage schema version.
71
+ INSERT INTO metadata (name, value) VALUES ('storage_schema_version', '13');
@@ -0,0 +1,14 @@
1
+ DROP TRIGGER IF EXISTS tgr_records_last_modified ON records;
2
+ DROP TRIGGER IF EXISTS tgr_deleted_last_modified ON deleted;
3
+
4
+ CREATE TRIGGER tgr_records_last_modified
5
+ BEFORE INSERT OR UPDATE OF data ON records
6
+ FOR EACH ROW EXECUTE PROCEDURE bump_timestamp();
7
+
8
+ CREATE TRIGGER tgr_deleted_last_modified
9
+ BEFORE INSERT ON deleted
10
+ FOR EACH ROW EXECUTE PROCEDURE bump_timestamp();
11
+
12
+
13
+ -- Bump storage schema version.
14
+ INSERT INTO metadata (name, value) VALUES ('storage_schema_version', '14');