diff --git a/bigchaindb/backend/localmongodb/query.py b/bigchaindb/backend/localmongodb/query.py index dbd072d5..557dfa1c 100644 --- a/bigchaindb/backend/localmongodb/query.py +++ b/bigchaindb/backend/localmongodb/query.py @@ -32,6 +32,24 @@ def get_transaction(conn, transaction_id): pass +@register_query(LocalMongoDBConnection) +def store_metadata(conn, metadata): + try: + return conn.run( + conn.collection('metadata') + .insert_many(metadata, ordered=False)) + except DuplicateKeyError: + pass + + +@register_query(LocalMongoDBConnection) +def get_metadata(conn, txn_ids): + return conn.run( + conn.collection('metadata') + .find({'id': {'$in': txn_ids}}, + projection={'_id': False})) + + @register_query(LocalMongoDBConnection) def store_asset(conn, asset): try: diff --git a/bigchaindb/backend/localmongodb/schema.py b/bigchaindb/backend/localmongodb/schema.py index e6c8e40e..b42b1bb2 100644 --- a/bigchaindb/backend/localmongodb/schema.py +++ b/bigchaindb/backend/localmongodb/schema.py @@ -27,7 +27,7 @@ def create_database(conn, dbname): @register_schema(LocalMongoDBConnection) def create_tables(conn, dbname): - for table_name in ['transactions', 'assets', 'blocks']: + for table_name in ['transactions', 'assets', 'blocks', 'metadata']: logger.info('Create `%s` table.', table_name) # create the table # TODO: read and write concerns can be declared here @@ -39,6 +39,7 @@ def create_indexes(conn, dbname): create_transactions_secondary_index(conn, dbname) create_assets_secondary_index(conn, dbname) create_blocks_secondary_index(conn, dbname) + create_metadata_secondary_index(conn, dbname) @register_schema(LocalMongoDBConnection) @@ -86,3 +87,15 @@ def create_assets_secondary_index(conn, dbname): def create_blocks_secondary_index(conn, dbname): conn.conn[dbname]['blocks']\ .create_index([('height', DESCENDING)], name='height') + + +def create_metadata_secondary_index(conn, dbname): + logger.info('Create `assets` secondary index.') + + # the id is the txid of the transaction where metadata was defined + conn.conn[dbname]['metadata'].create_index('id', + name='txn_id', + unique=True) + + # full text search index + conn.conn[dbname]['metadata'].create_index([('$**', TEXT)], name='text') diff --git a/bigchaindb/backend/query.py b/bigchaindb/backend/query.py index efbcfcd7..f8ea879f 100644 --- a/bigchaindb/backend/query.py +++ b/bigchaindb/backend/query.py @@ -33,6 +33,20 @@ def store_asset(connection, asset): raise NotImplementedError +@singledispatch +def store_metadata(connection, metadata): + """Write an asset to the asset table. + + Args: + asset (dict): the asset. + + Returns: + The result of the operation. + """ + + raise NotImplementedError + + @singledispatch def store_transaction(connection, signed_transaction): """Same as write_transaction.""" diff --git a/bigchaindb/tendermint/lib.py b/bigchaindb/tendermint/lib.py index 67f899e4..6247f96e 100644 --- a/bigchaindb/tendermint/lib.py +++ b/bigchaindb/tendermint/lib.py @@ -51,11 +51,18 @@ class BigchainDB(Bigchain): if asset['data']: backend.query.store_asset(self.connection, asset) + metadata = transaction.pop('metadata') + txn_metadata = {'id': transaction['id'], + 'metadata': metadata} + + backend.query.store_metadata(self.connection, [txn_metadata]) + return backend.query.store_transaction(self.connection, transaction) def get_transaction(self, transaction_id, include_status=False): transaction = backend.query.get_transaction(self.connection, transaction_id) asset = backend.query.get_asset(self.connection, transaction_id) + metadata = backend.query.get_metadata(self.connection, [transaction_id]) if transaction: if asset: @@ -63,6 +70,13 @@ class BigchainDB(Bigchain): else: transaction['asset'] = {'data': None} + if 'metadata' not in transaction: + metadata = metadata[0] if metadata else None + if metadata: + metadata = metadata.get('metadata') + + transaction.update({'metadata': metadata}) + transaction = Transaction.from_dict(transaction) if include_status: diff --git a/tests/backend/localmongodb/test_queries.py b/tests/backend/localmongodb/test_queries.py index 5456a68f..b8b08995 100644 --- a/tests/backend/localmongodb/test_queries.py +++ b/tests/backend/localmongodb/test_queries.py @@ -77,6 +77,49 @@ def test_text_search(): test_text_search('assets') +def test_write_metadata(): + from bigchaindb.backend import connect, query + conn = connect() + + metadata = [ + {'id': 1, 'data': '1'}, + {'id': 2, 'data': '2'}, + {'id': 3, 'data': '3'} + ] + + # write the assets + query.store_metadata(conn, deepcopy(metadata)) + + # check that 3 assets were written to the database + cursor = conn.db.metadata.find({}, projection={'_id': False})\ + .sort('id', pymongo.ASCENDING) + + assert cursor.count() == 3 + assert list(cursor) == metadata + + +def test_get_metadata(): + from bigchaindb.backend import connect, query + conn = connect() + + metadata = [ + {'id': 1, 'metadata': None}, + {'id': 2, 'metadata': {'key': 'value'}}, + {'id': 3, 'metadata': '3'}, + ] + + conn.db.metadata.insert_many(deepcopy(metadata), ordered=False) + + for meta in metadata: + assert query.get_metadata(conn, [meta['id']]) + + +def test_text_metadata(): + from ..mongodb.test_queries import test_text_search + + test_text_search('metadata') + + def test_get_owned_ids(signed_create_tx, user_pk): from bigchaindb.backend import connect, query conn = connect() diff --git a/tests/web/test_metadata.py b/tests/web/test_metadata.py index 22c025ae..68efacc7 100644 --- a/tests/web/test_metadata.py +++ b/tests/web/test_metadata.py @@ -3,6 +3,7 @@ import pytest METADATA_ENDPOINT = '/api/v1/metadata/' +@pytest.mark.tendermint def test_get_metadata_with_empty_text_search(client): res = client.get(METADATA_ENDPOINT + '?search=') assert res.json == {'status': 400, @@ -10,6 +11,7 @@ def test_get_metadata_with_empty_text_search(client): assert res.status_code == 400 +@pytest.mark.tendermint def test_get_metadata_with_missing_text_search(client): res = client.get(METADATA_ENDPOINT) assert res.status_code == 400 @@ -85,3 +87,64 @@ def test_get_metadata_limit(client, b): res = client.get(METADATA_ENDPOINT + '?search=meta&limit=1') assert res.status_code == 200 assert len(res.json) == 1 + + +@pytest.mark.bdb +@pytest.mark.tendermint +def test_get_metadata_tendermint(client, tb): + from bigchaindb.models import Transaction + + b = tb + + # test returns empty list when no assets are found + res = client.get(METADATA_ENDPOINT + '?search=abc') + assert res.json == [] + assert res.status_code == 200 + + # create asset + asset = {'msg': 'abc'} + metadata = {'key': 'my_meta'} + tx = Transaction.create([b.me], [([b.me], 1)], metadata=metadata, + asset=asset).sign([b.me_private]) + + b.store_transaction(tx) + + # test that metadata is returned + res = client.get(METADATA_ENDPOINT + '?search=my_meta') + assert res.status_code == 200 + assert len(res.json) == 1 + assert res.json[0] == { + 'metadata': {'key': 'my_meta'}, + 'id': tx.id + } + + +@pytest.mark.bdb +@pytest.mark.tendermint +def test_get_metadata_limit_tendermint(client, tb): + from bigchaindb.models import Transaction + + b = tb + + # create two assets + asset1 = {'msg': 'abc 1'} + meta1 = {'key': 'meta 1'} + tx1 = Transaction.create([b.me], [([b.me], 1)], metadata=meta1, + asset=asset1).sign([b.me_private]) + b.store_transaction(tx1) + + asset2 = {'msg': 'abc 2'} + meta2 = {'key': 'meta 2'} + tx2 = Transaction.create([b.me], [([b.me], 1)], metadata=meta2, + asset=asset2).sign([b.me_private]) + b.store_transaction(tx2) + + # test that both assets are returned without limit + res = client.get(METADATA_ENDPOINT + '?search=meta') + assert res.status_code == 200 + assert len(res.json) == 2 + + # test that only one asset is returned when using limit=1 + res = client.get(METADATA_ENDPOINT + '?search=meta&limit=1') + assert res.status_code == 200 + assert len(res.json) == 1