diff --git a/mypy.ini b/mypy.ini --- a/mypy.ini +++ b/mypy.ini @@ -24,6 +24,9 @@ [mypy-django.*] ignore_missing_imports = True +[mypy-msgpack.*] +ignore_missing_imports = True + [mypy-multiprocessing.util] ignore_missing_imports = True diff --git a/requirements-swh-journal.txt b/requirements-swh-journal.txt --- a/requirements-swh-journal.txt +++ b/requirements-swh-journal.txt @@ -1 +1 @@ -swh.journal >= 0.3.2 +swh.journal >= 0.4 diff --git a/requirements-swh.txt b/requirements-swh.txt --- a/requirements-swh.txt +++ b/requirements-swh.txt @@ -1,3 +1,3 @@ -swh.core[db,http] >= 0.0.94 -swh.model >= 0.3.4 +swh.core[db,http] >= 0.1.0 +swh.model >= 0.4.0 swh.objstorage >= 0.0.40 diff --git a/sql/upgrades/158.sql b/sql/upgrades/158.sql new file mode 100644 --- /dev/null +++ b/sql/upgrades/158.sql @@ -0,0 +1,76 @@ +-- SWH DB schema upgrade +-- from_version: 157 +-- to_version: 158 +-- description: Add the extra_headers column in the revision table + +-- latest schema version +insert into dbversion(version, release, description) + values(158, now(), 'Work Still In Progress'); + +-- Adapt the revision table for the new extra_headers column +alter table revision add column (extra_headers bytea[][]); + +-- Adapt the revision_entry type for the new extra_headers attribute +alter type revision_entry add attribute (extra_headers bytea[][]); + +-- Create entries in revision from tmp_revision +create or replace function swh_revision_add() + returns void + language plpgsql +as $$ +begin + perform swh_person_add_from_revision(); + + insert into revision (id, date, date_offset, date_neg_utc_offset, committer_date, committer_date_offset, committer_date_neg_utc_offset, type, directory, message, author, committer, metadata, synthetic, extra_headers) + select t.id, t.date, t.date_offset, t.date_neg_utc_offset, t.committer_date, t.committer_date_offset, t.committer_date_neg_utc_offset, t.type, t.directory, t.message, a.id, c.id, t.metadata, t.synthetic, t.extra_headers + from tmp_revision t + left join person a on a.fullname = t.author_fullname + left join person c on c.fullname = t.committer_fullname; + return; +end +$$; + +-- "git style" revision log. Similar to swh_revision_list(), but returning all +-- information associated to each revision, and expanding authors/committers +create or replace function swh_revision_log(root_revisions bytea[], num_revs bigint default NULL) + returns setof revision_entry + language sql + stable +as $$ + select t.id, r.date, r.date_offset, r.date_neg_utc_offset, + r.committer_date, r.committer_date_offset, r.committer_date_neg_utc_offset, + r.type, r.directory, r.message, + a.id, a.fullname, a.name, a.email, + c.id, c.fullname, c.name, c.email, + r.metadata, r.synthetic, r.extra_headers, t.parents, r.object_id + from swh_revision_list(root_revisions, num_revs) as t + left join revision r on t.id = r.id + left join person a on a.id = r.author + left join person c on c.id = r.committer; +$$; + +create or replace function swh_revision_list_by_object_id( + min_excl bigint, + max_incl bigint +) + returns setof revision_entry + language sql + stable +as $$ + with revs as ( + select * from revision + where object_id > min_excl and object_id <= max_incl + ) + select r.id, r.date, r.date_offset, r.date_neg_utc_offset, + r.committer_date, r.committer_date_offset, r.committer_date_neg_utc_offset, + r.type, r.directory, r.message, + a.id, a.fullname, a.name, a.email, c.id, c.fullname, c.name, c.email, r.metadata, r.synthetic, r.extra_headers, + array(select rh.parent_id::bytea from revision_history rh where rh.id = r.id order by rh.parent_rank) + as parents, r.object_id + from revs r + left join person a on a.id = r.author + left join person c on c.id = r.committer + order by r.object_id; +$$; + +-- TODO: add the migration magic query... diff --git a/swh/storage/cassandra/converters.py b/swh/storage/cassandra/converters.py --- a/swh/storage/cassandra/converters.py +++ b/swh/storage/cassandra/converters.py @@ -22,7 +22,6 @@ ) from swh.model.hashutil import DEFAULT_ALGORITHMS -from ..converters import git_headers_to_db, db_to_git_headers from .common import Row @@ -33,11 +32,11 @@ # non-recursively convert it as a dict but make a deep copy. db_revision = deepcopy(attr.asdict(revision, recurse=False)) metadata = revision.metadata - if metadata and "extra_headers" in metadata: - db_revision["metadata"]["extra_headers"] = git_headers_to_db( - metadata["extra_headers"] - ) + extra_headers = revision.extra_headers + if not extra_headers and metadata and "extra_headers" in metadata: + extra_headers = db_revision["metadata"].pop("extra_headers") db_revision["metadata"] = json.dumps(db_revision["metadata"]) + db_revision["extra_headers"] = extra_headers db_revision["type"] = db_revision["type"].value return db_revision @@ -45,13 +44,16 @@ def revision_from_db(db_revision: Row, parents: Tuple[Sha1Git]) -> Revision: revision = db_revision._asdict() # type: ignore metadata = json.loads(revision.pop("metadata", None)) - if metadata and "extra_headers" in metadata: - extra_headers = db_to_git_headers(metadata["extra_headers"]) - metadata["extra_headers"] = extra_headers + extra_headers = revision.pop("extra_headers", ()) + if not extra_headers and metadata and "extra_headers" in metadata: + extra_headers = metadata.pop("extra_headers") + if extra_headers is None: + extra_headers = () return Revision( parents=parents, type=RevisionType(revision.pop("type")), metadata=metadata, + extra_headers=extra_headers, **revision, ) diff --git a/swh/storage/cassandra/cql.py b/swh/storage/cassandra/cql.py --- a/swh/storage/cassandra/cql.py +++ b/swh/storage/cassandra/cql.py @@ -425,6 +425,7 @@ "committer", "synthetic", "metadata", + "extra_headers", ] @_prepared_exists_statement("revision") diff --git a/swh/storage/cassandra/schema.py b/swh/storage/cassandra/schema.py --- a/swh/storage/cassandra/schema.py +++ b/swh/storage/cassandra/schema.py @@ -97,9 +97,10 @@ committer person, synthetic boolean, -- true iff revision has been created by Software Heritage - metadata text - -- extra metadata as JSON(tarball checksums, - -- extra commit information, etc...) + metadata text, + -- extra metadata as JSON(tarball checksums, etc...) + extra_headers frozen> > + -- extra commit information as (tuple(key, value), ...) ); diff --git a/swh/storage/converters.py b/swh/storage/converters.py --- a/swh/storage/converters.py +++ b/swh/storage/converters.py @@ -7,7 +7,7 @@ from typing import Optional, Dict -from swh.core.utils import decode_with_escape, encode_with_unescape +from swh.core.utils import encode_with_unescape from swh.model import identifiers from swh.model.hashutil import MultiHash @@ -64,31 +64,10 @@ } -def git_headers_to_db(git_headers): - """Convert git headers to their database representation. - - We convert the bytes to unicode by decoding them into utf-8 and replacing - invalid utf-8 sequences with backslash escapes. - - """ - ret = [] - for key, values in git_headers: - if isinstance(values, list): - ret.append([key, [decode_with_escape(value) for value in values]]) - else: - ret.append([key, decode_with_escape(values)]) - - return ret - - def db_to_git_headers(db_git_headers): ret = [] - for key, values in db_git_headers: - if isinstance(values, list): - ret.append([key, [encode_with_unescape(value) for value in values]]) - else: - ret.append([key, encode_with_unescape(values)]) - + for key, value in db_git_headers: + ret.append([key.encode("utf-8"), encode_with_unescape(value)]) return ret @@ -168,13 +147,6 @@ committer = author_to_db(revision["committer"]) committer_date = date_to_db(revision["committer_date"]) - metadata = revision["metadata"] - - if metadata and "extra_headers" in metadata: - metadata = metadata.copy() - extra_headers = git_headers_to_db(metadata["extra_headers"]) - metadata["extra_headers"] = extra_headers - return { "id": revision["id"], "author_fullname": author["fullname"], @@ -192,8 +164,9 @@ "type": revision["type"], "directory": revision["directory"], "message": revision["message"], - "metadata": metadata, + "metadata": revision["metadata"], "synthetic": revision["synthetic"], + "extra_headers": revision["extra_headers"], "parents": [ {"id": revision["id"], "parent_id": parent, "parent_rank": i,} for i, parent in enumerate(revision["parents"]) @@ -227,18 +200,17 @@ db_revision["committer_date_neg_utc_offset"], ) - metadata = db_revision["metadata"] - - if metadata and "extra_headers" in metadata: - extra_headers = db_to_git_headers(metadata["extra_headers"]) - metadata["extra_headers"] = extra_headers - parents = [] if "parents" in db_revision: for parent in db_revision["parents"]: if parent: parents.append(parent) + metadata = db_revision["metadata"] + extra_headers = db_revision.get("extra_headers", ()) + if not extra_headers and metadata and "extra_headers" in metadata: + extra_headers = db_to_git_headers(metadata.pop("extra_headers")) + ret = { "id": db_revision["id"], "author": author, @@ -250,6 +222,7 @@ "message": db_revision["message"], "metadata": metadata, "synthetic": db_revision["synthetic"], + "extra_headers": extra_headers, "parents": parents, } diff --git a/swh/storage/db.py b/swh/storage/db.py --- a/swh/storage/db.py +++ b/swh/storage/db.py @@ -423,6 +423,7 @@ "committer_email", "metadata", "synthetic", + "extra_headers", ] revision_get_cols = revision_add_cols + ["parents"] diff --git a/swh/storage/sql/30-swh-schema.sql b/swh/storage/sql/30-swh-schema.sql --- a/swh/storage/sql/30-swh-schema.sql +++ b/swh/storage/sql/30-swh-schema.sql @@ -17,7 +17,7 @@ -- latest schema version insert into dbversion(version, release, description) - values(157, now(), 'Work In Progress'); + values(158, now(), 'Work In Progress'); -- a SHA1 checksum create domain sha1 as bytea check (length(value) = 20); @@ -239,7 +239,8 @@ metadata jsonb, -- extra metadata (tarball checksums, extra commit information, etc...) object_id bigserial, date_neg_utc_offset boolean, - committer_date_neg_utc_offset boolean + committer_date_neg_utc_offset boolean, + extra_headers bytea[][] -- extra headers (used in hash computation) ); comment on table revision is 'A revision represents the state of a source code tree at a specific point in time'; @@ -258,6 +259,7 @@ comment on column revision.synthetic is 'True iff revision has been synthesized by Software Heritage'; comment on column revision.metadata is 'Extra revision metadata'; comment on column revision.object_id is 'Non-intrinsic, sequential object identifier'; +comment on column revision.extra_headers is 'Extra revision headers; used in revision hash computation'; -- either this table or the sha1_git[] column on the revision table diff --git a/swh/storage/sql/40-swh-func.sql b/swh/storage/sql/40-swh-func.sql --- a/swh/storage/sql/40-swh-func.sql +++ b/swh/storage/sql/40-swh-func.sql @@ -454,6 +454,7 @@ committer_email bytea, metadata jsonb, synthetic boolean, + extra_headers bytea[][], parents bytea[], object_id bigint ); @@ -471,7 +472,7 @@ r.type, r.directory, r.message, a.id, a.fullname, a.name, a.email, c.id, c.fullname, c.name, c.email, - r.metadata, r.synthetic, t.parents, r.object_id + r.metadata, r.synthetic, r.extra_headers, t.parents, r.object_id from swh_revision_list(root_revisions, num_revs) as t left join revision r on t.id = r.id left join person a on a.id = r.author @@ -528,8 +529,8 @@ begin perform swh_person_add_from_revision(); - insert into revision (id, date, date_offset, date_neg_utc_offset, committer_date, committer_date_offset, committer_date_neg_utc_offset, type, directory, message, author, committer, metadata, synthetic) - select t.id, t.date, t.date_offset, t.date_neg_utc_offset, t.committer_date, t.committer_date_offset, t.committer_date_neg_utc_offset, t.type, t.directory, t.message, a.id, c.id, t.metadata, t.synthetic + insert into revision (id, date, date_offset, date_neg_utc_offset, committer_date, committer_date_offset, committer_date_neg_utc_offset, type, directory, message, author, committer, metadata, synthetic, extra_headers) + select t.id, t.date, t.date_offset, t.date_neg_utc_offset, t.committer_date, t.committer_date_offset, t.committer_date_neg_utc_offset, t.type, t.directory, t.message, a.id, c.id, t.metadata, t.synthetic, t.extra_headers from tmp_revision t left join person a on a.fullname = t.author_fullname left join person c on c.fullname = t.committer_fullname; @@ -791,7 +792,7 @@ select r.id, r.date, r.date_offset, r.date_neg_utc_offset, r.committer_date, r.committer_date_offset, r.committer_date_neg_utc_offset, r.type, r.directory, r.message, - a.id, a.fullname, a.name, a.email, c.id, c.fullname, c.name, c.email, r.metadata, r.synthetic, + a.id, a.fullname, a.name, a.email, c.id, c.fullname, c.name, c.email, r.metadata, r.synthetic, r.extra_headers, array(select rh.parent_id::bytea from revision_history rh where rh.id = r.id order by rh.parent_rank) as parents, r.object_id from revs r diff --git a/swh/storage/tests/storage_data.py b/swh/storage/tests/storage_data.py --- a/swh/storage/tests/storage_data.py +++ b/swh/storage/tests/storage_data.py @@ -217,12 +217,12 @@ "metadata": { "checksums": {"sha1": "tarball-sha1", "sha256": "tarball-sha256",}, "signed-off-by": "some-dude", - "extra_headers": [ - ["gpgsig", b"test123"], - ["mergetag", b"foo\\bar"], - ["mergetag", b"\x22\xaf\x89\x80\x01\x00"], - ], }, + "extra_headers": ( + (b"gpgsig", b"test123"), + (b"mergetag", b"foo\\bar"), + (b"mergetag", b"\x22\xaf\x89\x80\x01\x00"), + ), "synthetic": True, } @@ -253,6 +253,7 @@ "type": "git", "directory": hash_to_bytes("8505808532953da7d2581741f01b29c04b1cb9ab"), # dir2 "metadata": None, + "extra_headers": (), "synthetic": False, } @@ -283,6 +284,7 @@ "type": "git", "directory": hash_to_bytes("8505808532953da7d2581741f01b29c04b1cb9ab"), # dir2 "metadata": None, + "extra_headers": (), "synthetic": True, } @@ -315,6 +317,7 @@ "type": "git", "directory": hash_to_bytes("34f335a750111ca0a8b64d8034faec9eedc396be"), # dir "metadata": None, + "extra_headers": (), "synthetic": False, } diff --git a/swh/storage/tests/test_converters.py b/swh/storage/tests/test_converters.py --- a/swh/storage/tests/test_converters.py +++ b/swh/storage/tests/test_converters.py @@ -85,6 +85,7 @@ "committer_email": b"comm-email", "metadata": {}, "synthetic": False, + "extra_headers": (), "parents": [123, 456], } ) @@ -109,6 +110,7 @@ "message": b"commit message", "metadata": {}, "synthetic": False, + "extra_headers": (), "parents": [123, 456], } @@ -147,14 +149,3 @@ "target": b"revision-id", "target_type": "revision", } - - -def test_db_to_git_headers(): - raw_data = [ - ["gpgsig", b"garbage\x89a\x43b\x14"], - ["extra", [b"foo\\\\\\o", b"bar\\", b"inval\\\\\x99id"]], - ] - - db_data = converters.git_headers_to_db(raw_data) - loop = converters.db_to_git_headers(db_data) - assert raw_data == loop diff --git a/swh/storage/tests/test_revision_bw_compat.py b/swh/storage/tests/test_revision_bw_compat.py new file mode 100644 --- /dev/null +++ b/swh/storage/tests/test_revision_bw_compat.py @@ -0,0 +1,48 @@ +# Copyright (C) 2020 The Software Heritage developers +# See the AUTHORS file at the top-level directory of this distribution +# License: GNU General Public License version 3, or any later version +# See top-level LICENSE file for more information + +import attr + +from swh.core.utils import decode_with_escape +from swh.model.model import Revision +from swh.storage import get_storage +from swh.storage.tests.storage_data import data +from swh.storage.tests.test_storage import db_transaction + + +def headers_to_db(git_headers): + return [[key, decode_with_escape(value)] for key, value in git_headers] + + +def test_revision_extra_header_in_metadata(swh_storage_backend_config): + storage = get_storage(**swh_storage_backend_config) + rev = Revision.from_dict(data.revision) + + md_w_extra = dict( + rev.metadata.items(), + extra_headers=headers_to_db( + [ + ["gpgsig", b"test123"], + ["mergetag", b"foo\\bar"], + ["mergetag", b"\x22\xaf\x89\x80\x01\x00"], + ] + ), + ) + + bw_rev = attr.evolve(rev, extra_headers=()) + object.__setattr__(bw_rev, "metadata", md_w_extra) + assert bw_rev.extra_headers == () + + assert storage.revision_add([bw_rev]) == {"revision:add": 1} + + # check data in the db are old format + with db_transaction(storage) as (_, cur): + cur.execute("SELECT metadata, extra_headers FROM revision") + metadata, extra_headers = cur.fetchone() + assert extra_headers == [] + assert metadata == bw_rev.metadata + + # check the Revision build from revision_get is the original, "new style", Revision + assert [Revision.from_dict(x) for x in storage.revision_get([rev.id])] == [rev]