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.
- kinto/__init__.py +92 -0
- kinto/__main__.py +249 -0
- kinto/authorization.py +134 -0
- kinto/config/__init__.py +94 -0
- kinto/config/kinto.tpl +270 -0
- kinto/contribute.json +27 -0
- kinto/core/__init__.py +246 -0
- kinto/core/authentication.py +48 -0
- kinto/core/authorization.py +311 -0
- kinto/core/cache/__init__.py +131 -0
- kinto/core/cache/memcached.py +112 -0
- kinto/core/cache/memory.py +104 -0
- kinto/core/cache/postgresql/__init__.py +178 -0
- kinto/core/cache/postgresql/schema.sql +23 -0
- kinto/core/cache/testing.py +208 -0
- kinto/core/cornice/__init__.py +93 -0
- kinto/core/cornice/cors.py +144 -0
- kinto/core/cornice/errors.py +40 -0
- kinto/core/cornice/pyramidhook.py +373 -0
- kinto/core/cornice/renderer.py +89 -0
- kinto/core/cornice/resource.py +205 -0
- kinto/core/cornice/service.py +641 -0
- kinto/core/cornice/util.py +138 -0
- kinto/core/cornice/validators/__init__.py +94 -0
- kinto/core/cornice/validators/_colander.py +142 -0
- kinto/core/cornice/validators/_marshmallow.py +182 -0
- kinto/core/cornice_swagger/__init__.py +92 -0
- kinto/core/cornice_swagger/converters/__init__.py +21 -0
- kinto/core/cornice_swagger/converters/exceptions.py +6 -0
- kinto/core/cornice_swagger/converters/parameters.py +90 -0
- kinto/core/cornice_swagger/converters/schema.py +249 -0
- kinto/core/cornice_swagger/swagger.py +725 -0
- kinto/core/cornice_swagger/templates/index.html +73 -0
- kinto/core/cornice_swagger/templates/index_script_template.html +21 -0
- kinto/core/cornice_swagger/util.py +42 -0
- kinto/core/cornice_swagger/views.py +78 -0
- kinto/core/decorators.py +74 -0
- kinto/core/errors.py +216 -0
- kinto/core/events.py +301 -0
- kinto/core/initialization.py +738 -0
- kinto/core/listeners/__init__.py +9 -0
- kinto/core/metrics.py +94 -0
- kinto/core/openapi.py +115 -0
- kinto/core/permission/__init__.py +202 -0
- kinto/core/permission/memory.py +167 -0
- kinto/core/permission/postgresql/__init__.py +489 -0
- kinto/core/permission/postgresql/migrations/migration_001_002.sql +18 -0
- kinto/core/permission/postgresql/schema.sql +41 -0
- kinto/core/permission/testing.py +487 -0
- kinto/core/resource/__init__.py +1311 -0
- kinto/core/resource/model.py +412 -0
- kinto/core/resource/schema.py +502 -0
- kinto/core/resource/viewset.py +230 -0
- kinto/core/schema.py +119 -0
- kinto/core/scripts.py +50 -0
- kinto/core/statsd.py +1 -0
- kinto/core/storage/__init__.py +436 -0
- kinto/core/storage/exceptions.py +53 -0
- kinto/core/storage/generators.py +58 -0
- kinto/core/storage/memory.py +651 -0
- kinto/core/storage/postgresql/__init__.py +1131 -0
- kinto/core/storage/postgresql/client.py +120 -0
- kinto/core/storage/postgresql/migrations/migration_001_002.sql +10 -0
- kinto/core/storage/postgresql/migrations/migration_002_003.sql +33 -0
- kinto/core/storage/postgresql/migrations/migration_003_004.sql +18 -0
- kinto/core/storage/postgresql/migrations/migration_004_005.sql +20 -0
- kinto/core/storage/postgresql/migrations/migration_005_006.sql +11 -0
- kinto/core/storage/postgresql/migrations/migration_006_007.sql +74 -0
- kinto/core/storage/postgresql/migrations/migration_007_008.sql +66 -0
- kinto/core/storage/postgresql/migrations/migration_008_009.sql +41 -0
- kinto/core/storage/postgresql/migrations/migration_009_010.sql +98 -0
- kinto/core/storage/postgresql/migrations/migration_010_011.sql +14 -0
- kinto/core/storage/postgresql/migrations/migration_011_012.sql +9 -0
- kinto/core/storage/postgresql/migrations/migration_012_013.sql +71 -0
- kinto/core/storage/postgresql/migrations/migration_013_014.sql +14 -0
- kinto/core/storage/postgresql/migrations/migration_014_015.sql +95 -0
- kinto/core/storage/postgresql/migrations/migration_015_016.sql +4 -0
- kinto/core/storage/postgresql/migrations/migration_016_017.sql +81 -0
- kinto/core/storage/postgresql/migrations/migration_017_018.sql +25 -0
- kinto/core/storage/postgresql/migrations/migration_018_019.sql +8 -0
- kinto/core/storage/postgresql/migrations/migration_019_020.sql +7 -0
- kinto/core/storage/postgresql/migrations/migration_020_021.sql +68 -0
- kinto/core/storage/postgresql/migrations/migration_021_022.sql +62 -0
- kinto/core/storage/postgresql/migrations/migration_022_023.sql +5 -0
- kinto/core/storage/postgresql/migrations/migration_023_024.sql +6 -0
- kinto/core/storage/postgresql/migrations/migration_024_025.sql +6 -0
- kinto/core/storage/postgresql/migrator.py +98 -0
- kinto/core/storage/postgresql/pool.py +55 -0
- kinto/core/storage/postgresql/schema.sql +143 -0
- kinto/core/storage/testing.py +1857 -0
- kinto/core/storage/utils.py +37 -0
- kinto/core/testing.py +182 -0
- kinto/core/utils.py +553 -0
- kinto/core/views/__init__.py +0 -0
- kinto/core/views/batch.py +163 -0
- kinto/core/views/errors.py +145 -0
- kinto/core/views/heartbeat.py +106 -0
- kinto/core/views/hello.py +69 -0
- kinto/core/views/openapi.py +35 -0
- kinto/core/views/version.py +50 -0
- kinto/events.py +3 -0
- kinto/plugins/__init__.py +0 -0
- kinto/plugins/accounts/__init__.py +94 -0
- kinto/plugins/accounts/authentication.py +63 -0
- kinto/plugins/accounts/scripts.py +61 -0
- kinto/plugins/accounts/utils.py +13 -0
- kinto/plugins/accounts/views.py +136 -0
- kinto/plugins/admin/README.md +3 -0
- kinto/plugins/admin/VERSION +1 -0
- kinto/plugins/admin/__init__.py +40 -0
- kinto/plugins/admin/build/VERSION +1 -0
- kinto/plugins/admin/build/assets/index-CYFwtKtL.css +6 -0
- kinto/plugins/admin/build/assets/index-DJ0m93zA.js +149 -0
- kinto/plugins/admin/build/assets/logo-VBRiKSPX.png +0 -0
- kinto/plugins/admin/build/index.html +18 -0
- kinto/plugins/admin/public/help.html +25 -0
- kinto/plugins/admin/views.py +42 -0
- kinto/plugins/default_bucket/__init__.py +191 -0
- kinto/plugins/flush.py +28 -0
- kinto/plugins/history/__init__.py +65 -0
- kinto/plugins/history/listener.py +181 -0
- kinto/plugins/history/views.py +66 -0
- kinto/plugins/openid/__init__.py +131 -0
- kinto/plugins/openid/utils.py +14 -0
- kinto/plugins/openid/views.py +193 -0
- kinto/plugins/prometheus.py +300 -0
- kinto/plugins/statsd.py +85 -0
- kinto/schema_validation.py +135 -0
- kinto/views/__init__.py +34 -0
- kinto/views/admin.py +195 -0
- kinto/views/buckets.py +45 -0
- kinto/views/collections.py +58 -0
- kinto/views/contribute.py +39 -0
- kinto/views/groups.py +90 -0
- kinto/views/permissions.py +235 -0
- kinto/views/records.py +133 -0
- kinto-23.2.1.dist-info/METADATA +232 -0
- kinto-23.2.1.dist-info/RECORD +142 -0
- kinto-23.2.1.dist-info/WHEEL +5 -0
- kinto-23.2.1.dist-info/entry_points.txt +5 -0
- kinto-23.2.1.dist-info/licenses/LICENSE +13 -0
- 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');
|