Repository: mintlayer/mintlayer-core
Pull request #671 (2023-02-03 11:49:05)

Database maps documentation and refactoring

iljakuklic: I pushed the number of changes:

Renamed a bunch of things (improvement suggestions welcome):
* Most types have gotten `Db` prefix to avoid confusion e.g. between `DbMap`s and other kind of maps
* DbIndex -> MapIndex -> DbMapId (to de-emphasise it's a numeric value under the hood)
* `storage_core::info` -> `storage_core::types`

Introduced a new polymorphic wrapper `DbMapsData<T>` that can be used to associate arbitrary data of type `T` with each DB map. The interface is limited to make it hard to use in an inappropriate way. Since it is indexed into with `DbMapId`, there is not much chance of mixup and you no longet neex to extract the numeric index out of it to index into a `Vec`.
* Storage layer uses it to pass DB map info into the backend (names, size hints, etc)  `DbMapsData<DbMapDesc>`
* LMDB uses it to store database handles `DbMapsData<lmdb::Database>`
* SQLite uses it to store pre-calculated queries on per-table basis `DbMapsData<SqlQuery>`
* In-memory backend uses it to store the data
* etc...

Changed the sqlite backend to use string DB map names for table names.

Added a high-level description and backend implementation overview to the top-level `storage_core` crate documentation.

Please let me know what you think. Especially please pay attention if the terminology is still not consistent in places, this stuff is easily missed.
alfiedotwtf: LGTM
TheQuantumPhysicist: Looking good.

Patch:
diff --git a/storage/core/src/adaptor/locking/mod.rs b/storage/core/src/adaptor/locking/mod.rs
index c2709cb4d..b0ac30e88 100644
--- a/storage/core/src/adaptor/locking/mod.rs
+++ b/storage/core/src/adaptor/locking/mod.rs
@@ -20,11 +20,9 @@ mod prefix_iter_rw;
 
 use crate::{
     adaptor::{Construct, CoreOps},
-    backend,
-    info::{DbDesc, DbIndex},
-    Data,
+    backend::{self, PrefixIter, ReadOps, WriteOps},
+    Data, DbDesc, DbMapCount, DbMapId, DbMapsData,
 };
-use backend::{PrefixIter, ReadOps, WriteOps};
 
 use std::{borrow::Cow, collections::BTreeMap};
 use utils::sync;
@@ -33,16 +31,20 @@ use utils::sync;
 pub struct TxRo<'tx, T>(sync::RwLockReadGuard<'tx, T>);
 
 impl<'tx, T: ReadOps> ReadOps for TxRo<'tx, T> {
-    fn get(&self, idx: DbIndex, key: &[u8]) -> crate::Result<Option<Cow<[u8]>>> {
-        self.0.get(idx, key)
+    fn get(&self, map_id: DbMapId, key: &[u8]) -> crate::Result<Option<Cow<[u8]>>> {
+        self.0.get(map_id, key)
     }
 }
 
 impl<'tx, 'i, T: PrefixIter<'i>> PrefixIter<'i> for TxRo<'tx, T> {
     type Iterator = T::Iterator;
 
-    fn prefix_iter<'m: 'i>(&'m self, idx: DbIndex, prefix: Data) -> crate::Result<Self::Iterator> {
-        self.0.prefix_iter(idx, prefix)
+    fn prefix_iter<'m: 'i>(
+        &'m self,
+        map_id: DbMapId,
+        prefix: Data,
+    ) -> crate::Result<Self::Iterator> {
+        self.0.prefix_iter(map_id, prefix)
     }
 }
 
@@ -54,20 +56,20 @@ type DeltaMap = BTreeMap<Data, Option<Data>>;
 // RW transaction holds a write lock to the database and a list of changes performed
 pub struct TxRw<'tx, T> {
     db: sync::RwLockWriteGuard<'tx, T>,
-    deltas: Vec<DeltaMap>,
+    deltas: DbMapsData<DeltaMap>,
 }
 
 impl<'tx, T> TxRw<'tx, T> {
-    fn update(&mut self, idx: DbIndex, key: Data, val: Option<Data>) -> crate::Result<()> {
-        self.deltas[idx.get()].insert(key, val);
+    fn update(&mut self, map_id: DbMapId, key: Data, val: Option<Data>) -> crate::Result<()> {
+        self.deltas[map_id].insert(key, val);
         Ok(())
     }
 }
 
 impl<'tx, T: ReadOps> ReadOps for TxRw<'tx, T> {
-    fn get(&self, idx: DbIndex, key: &[u8]) -> crate::Result<Option<Cow<[u8]>>> {
-        self.deltas[idx.get()].get(key).map_or_else(
-            || self.db.get(idx, key),
+    fn get(&self, map_id: DbMapId, key: &[u8]) -> crate::Result<Option<Cow<[u8]>>> {
+        self.deltas[map_id].get(key).map_or_else(
+            || self.db.get(map_id, key),
             |x| Ok(x.as_deref().map(|p| p.into())),
         )
     }
@@ -76,25 +78,28 @@ impl<'tx, T: ReadOps> ReadOps for TxRw<'tx, T> {
 impl<'tx, 'i, T: PrefixIter<'i>> PrefixIter<'i> for TxRw<'tx, T> {
     type Iterator = prefix_iter_rw::Iter<'i, T>;
 
-    fn prefix_iter<'m: 'i>(&'m self, idx: DbIndex, prefix: Data) -> crate::Result<Self::Iterator> {
-        prefix_iter_rw::iter(self, idx, prefix)
+    fn prefix_iter<'m: 'i>(
+        &'m self,
+        map_id: DbMapId,
+        prefix: Data,
+    ) -> crate::Result<Self::Iterator> {
+        prefix_iter_rw::iter(self, map_id, prefix)
     }
 }
 
 impl<'tx, T> WriteOps for TxRw<'tx, T> {
-    fn put(&mut self, idx: DbIndex, key: Data, val: Data) -> crate::Result<()> {
-        self.update(idx, key, Some(val))
+    fn put(&mut self, map_id: DbMapId, key: Data, val: Data) -> crate::Result<()> {
+        self.update(map_id, key, Some(val))
     }
 
-    fn del(&mut self, idx: DbIndex, key: &[u8]) -> crate::Result<()> {
-        self.update(idx, key.to_vec(), None)
+    fn del(&mut self, map_id: DbMapId, key: &[u8]) -> crate::Result<()> {
+        self.update(map_id, key.to_vec(), None)
     }
 }
 
 impl<'tx, T: ReadOps + WriteOps> backend::TxRw for TxRw<'tx, T> {
     fn commit(mut self) -> crate::Result<()> {
-        let entries = self.deltas.into_iter().enumerate().map(|(i, m)| (DbIndex::new(i), m));
-        for (idx, kvmap) in entries {
+        for (idx, kvmap) in self.deltas.into_iter_with_id() {
             for (key, val) in kvmap {
                 match val {
                     None => self.db.del(idx, &key)?,
@@ -108,7 +113,7 @@ impl<'tx, T: ReadOps + WriteOps> backend::TxRw for TxRw<'tx, T> {
 
 pub struct TransactionLockImpl<T> {
     db: sync::Arc<sync::RwLock<T>>,
-    num_maps: usize,
+    num_maps: DbMapCount,
 }
 
 impl<T> Clone for TransactionLockImpl<T> {
@@ -136,7 +141,7 @@ impl<'tx, T: 'tx + ReadOps + WriteOps> backend::TransactionalRw<'tx> for Transac
     fn transaction_rw<'st: 'tx>(&'st self, _: Option<usize>) -> crate::Result<Self::TxRw> {
         Ok(TxRw {
             db: self.db.write().expect("lock to be alive"),
-            deltas: vec![BTreeMap::new(); self.num_maps],
+            deltas: DbMapsData::new(self.num_maps, |_| BTreeMap::new()),
         })
     }
 }
@@ -166,7 +171,7 @@ where
     type Impl = TransactionLockImpl<T>;
 
     fn open(self, desc: DbDesc) -> crate::Result<Self::Impl> {
-        let num_maps = desc.len();
+        let num_maps = desc.db_map_count();
         let db = sync::Arc::new(sync::RwLock::new(T::construct(self.0, desc)?));
         Ok(TransactionLockImpl { db, num_maps })
     }
diff --git a/storage/core/src/adaptor/locking/prefix_iter_rw.rs b/storage/core/src/adaptor/locking/prefix_iter_rw.rs
index 34acd1b14..04f0a5d41 100644
--- a/storage/core/src/adaptor/locking/prefix_iter_rw.rs
+++ b/storage/core/src/adaptor/locking/prefix_iter_rw.rs
@@ -15,7 +15,7 @@
 
 //! Internal functions and types used in the implementation of prefix iterator for RW transactions
 
-use super::{Data, DbIndex, PrefixIter, TxRw};
+use super::{Data, DbMapId, PrefixIter, TxRw};
 use itertools::EitherOrBoth;
 
 // The prefix iterator type for mutable transaction is a fairly complicated type. Here, we
@@ -50,12 +50,12 @@ fn merger(item: EitherOrBoth<DataPair, DataPairRef<'_>>) -> Option<(Data, Data)>
 /// Create the iterator
 pub fn iter<'tx, 'i, 'm: 'i, T: PrefixIter<'i>>(
     tx: &'m TxRw<'tx, T>,
-    idx: DbIndex,
+    map_id: DbMapId,
     prefix: Data,
 ) -> crate::Result<Iter<'i, T>> {
     // Initialize the iterator over the underlying db and the deltas
-    let db_iter = tx.db.prefix_iter(idx, prefix.clone())?;
-    let delta_iter = crate::util::PrefixIter::new(&tx.deltas[idx.get()], prefix);
+    let db_iter = tx.db.prefix_iter(map_id, prefix.clone())?;
+    let delta_iter = crate::util::PrefixIter::new(&tx.deltas[map_id], prefix);
 
     // Merge the entries from the two iterators
     let iter = itertools::merge_join_by(db_iter, delta_iter, comparator as KeyCompareFn)
diff --git a/storage/core/src/adaptor/mod.rs b/storage/core/src/adaptor/mod.rs
index 126dff07d..e4c20eadd 100644
--- a/storage/core/src/adaptor/mod.rs
+++ b/storage/core/src/adaptor/mod.rs
@@ -27,7 +27,7 @@ pub trait Construct: Sized {
     type From;
 
     /// Construct storage from the initializer and database description
-    fn construct(init: Self::From, desc: crate::info::DbDesc) -> crate::Result<Self>;
+    fn construct(init: Self::From, desc: crate::DbDesc) -> crate::Result<Self>;
 }
 
 /// Core operations on storage without transaction support
diff --git a/storage/core/src/backend.rs b/storage/core/src/backend.rs
index 91187ae6d..4cefe4531 100644
--- a/storage/core/src/backend.rs
+++ b/storage/core/src/backend.rs
@@ -19,10 +19,7 @@ use std::borrow::Cow;
 
 use utils::shallow_clone::ShallowClone;
 
-pub use crate::{
-    info::{DbDesc, DbIndex},
-    Data,
-};
+pub use crate::{Data, DbDesc, DbMapId};
 
 /// Types providing capability of iterating over keys with given prefix
 pub trait PrefixIter<'i> {
@@ -30,22 +27,26 @@ pub trait PrefixIter<'i> {
     type Iterator: 'i + Iterator<Item = (Data, Data)>;
 
     /// Get iterator over key-value pairs where the key has given prefix
-    fn prefix_iter<'m: 'i>(&'m self, idx: DbIndex, prefix: Data) -> crate::Result<Self::Iterator>;
+    fn prefix_iter<'m: 'i>(
+        &'m self,
+        map_id: DbMapId,
+        prefix: Data,
+    ) -> crate::Result<Self::Iterator>;
 }
 
 /// Read-only database operations
 pub trait ReadOps: for<'i> PrefixIter<'i> {
     /// Get value associated with given key.
-    fn get(&self, idx: DbIndex, key: &[u8]) -> crate::Result<Option<Cow<[u8]>>>;
+    fn get(&self, map_id: DbMapId, key: &[u8]) -> crate::Result<Option<Cow<[u8]>>>;
 }
 
 /// Write database operation
 pub trait WriteOps {
     /// Set value associated with given key.
-    fn put(&mut self, idx: DbIndex, key: Data, val: Data) -> crate::Result<()>;
+    fn put(&mut self, map_id: DbMapId, key: Data, val: Data) -> crate::Result<()>;
 
     /// Delete the value associated with given key.
-    fn del(&mut self, idx: DbIndex, key: &[u8]) -> crate::Result<()>;
+    fn del(&mut self, map_id: DbMapId, key: &[u8]) -> crate::Result<()>;
 }
 
 /// Read-only transaction
diff --git a/storage/core/src/info.rs b/storage/core/src/info.rs
deleted file mode 100644
index 8e80fb60f..000000000
--- a/storage/core/src/info.rs
+++ /dev/null
@@ -1,79 +0,0 @@
-// Copyright (c) 2022 RBB S.r.l
-// opensource@mintlayer.org
-// SPDX-License-Identifier: MIT
-// Licensed under the MIT License;
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-//! Low-level runtime database layout description
-
-/// Database index type, just a thin wrapper over usize
-#[derive(Eq, PartialEq, PartialOrd, Ord, Clone, Copy, Debug)]
-pub struct DbIndex(usize);
-
-impl DbIndex {
-    /// New index
-    pub const fn new(idx: usize) -> Self {
-        DbIndex(idx)
-    }
-
-    /// Get the index as usize
-    pub const fn get(&self) -> usize {
-        self.0
-    }
-}
-
-/// Description of one key-value store in a database
-#[derive(Eq, PartialEq, Clone, Debug)]
-pub struct MapDesc {
-    /// Key-value map name
-    pub name: String,
-    /// Value size hint
-    pub size_hint: core::ops::Range<usize>,
-}
-
-impl MapDesc {
-    pub fn new(name: impl Into<String>) -> Self {
-        let size_hint = 0..usize::MAX;
-        let name = name.into();
-        Self { name, size_hint }
-    }
-}
-
-/// Description of a database, i.e. a collection of key-value maps
-#[derive(Eq, PartialEq, Clone, Debug)]
-pub struct DbDesc(Vec<MapDesc>);
-
-#[allow(clippy::len_without_is_empty)]
-impl DbDesc {
-    /// Number of databases
-    pub fn len(&self) -> usize {
-        self.0.len()
-    }
-
-    /// Iterate over map descriptions of this database
-    pub fn iter(&self) -> impl '_ + Iterator<Item = &MapDesc> + ExactSizeIterator {
-        self.0.iter()
-    }
-}
-
-impl std::ops::Index<DbIndex> for DbDesc {
-    type Output = MapDesc;
-    fn index(&self, idx: DbIndex) -> &MapDesc {
-        &self.0[idx.0]
-    }
-}
-
-impl FromIterator<MapDesc> for DbDesc {
-    fn from_iter<T: IntoIterator<Item = MapDesc>>(iter: T) -> Self {
-        Self(iter.into_iter().collect())
-    }
-}
diff --git a/storage/core/src/lib.rs b/storage/core/src/lib.rs
index bd3c73571..c5b035fa5 100644
--- a/storage/core/src/lib.rs
+++ b/storage/core/src/lib.rs
@@ -14,17 +14,91 @@
 // limitations under the License.
 
 //! Definitions used to implement storage backends
+//!
+//! # High-level overview
+//!
+//! A database can be thought of as a collection of key-value maps.
+//!
+//! ```notest
+//! Map<DbMapId, Map<Key, Value>>
+//! ```
+//!
+//! [DbMapId] is used to identify a particular key-value map. `Key` and `Value` are raw byte
+//! sequences ([Data]). To access a particular value, the database needs to be indexed first by a
+//! [DbMapId] (to get the key-value map) and then by a key.
+//!
+//! The inner key-value map is often referred to as DB map or even just map. The set of DB maps is
+//! fixed for the duration of backend lifetime but their contents may change.
+//!
+//! ## Database description
+//!
+//! The backend is given access to a collection of metadata describing the database structure in
+//! an instance of [DbDesc]. Most importantly, it communicates the number of DB maps and contains
+//! information about the individual maps in [DbMapDesc], most notably the name.
+//!
+//! # Backend implementation guidelines
+//!
+//! Implementing a storage backend involves defining a couple of traits satisfying a number of
+//! traits defined in [crate::backend].
+//!
+//! ## Initialization
+//!
+//! The main entry point is a type implementing the [Backend] trait. This type is used to identify
+//! the backend. The object of this type contains information used to initialize the backend such
+//! as database path and various settings.
+//!
+//! Database is started by calling [Backend::open] which uses the backend-specific information
+//! together with database description to initialize the database. That involves opening the
+//! database and, if necessary, setting up backend-specific representation of DB maps. The
+//! [types::DbMapsData::transform] method can be used to transform backend-agnostic information
+//! about DB maps into backend-specific handle to the DB maps.
+//!
+//! The result of opening a backend is something of type [Backend::Impl] that implements the
+//! [backend::BackendImpl] trait. It represents an initialized and running database.
+//!
+//! ## Transactions
+//!
+//! Currently, all data accesses in backend happen through transactions. This restriction may be
+//! lifted in the future, allowing primitive operations to be performed without a transaction for
+//! performance reasons.
+//!
+//! Starting read-only and read-write transactions is defined by implementing the
+//! [backend::TransactionalRo] and [backend::TransactionalRw] traits on [Backend::Impl].
+//!
+//! Committing a read-write transaction is defined by implementing [backend::TxRw]. A read-only
+//! transaction type has to be marked with the [backend::TxRo] trait.
+//!
+//! Rolling back read-write transaction and closing read-only transaction is defined by dropping
+//! the corresponding transaction object. If any special processing is required, [Drop] has to be
+//! implemented.
+//!
+//! ## Implementing operations
+//!
+//! Individual database operations are defined by implementing the following traits:
+//!
+//! * [backend::ReadOps]
+//! * [backend::WriteOps]
+//! * [backend::PrefixIter]
+//! * Implementing [Iterator] for the type returned by [backend::PrefixIter::prefix_iter]
+//!
+//! ## Adaptor pattern
+//!
+//! Sometimes, it is useful to create a database backend that wraps another backend and adds
+//! functionality. Use cases include but are not limited to logging and caching.
 
 pub mod adaptor;
 pub mod backend;
 pub mod error;
-pub mod info;
+pub mod types;
 pub mod util;
 
 // Re-export some commonly used items
 pub use backend::Backend;
 pub use error::Error;
-pub use info::{DbDesc, DbIndex};
+pub use types::{DbDesc, DbMapCount, DbMapDesc, DbMapId, DbMapsData};
 
+/// Raw byte sequences, used to represent store keys and values
 pub type Data = Vec<u8>;
+
+/// A `Result` type specialized for storage
 pub type Result<T> = std::result::Result<T, Error>;
diff --git a/storage/core/src/types.rs b/storage/core/src/types.rs
new file mode 100644
index 000000000..3a66fbb03
--- /dev/null
+++ b/storage/core/src/types.rs
@@ -0,0 +1,185 @@
+// Copyright (c) 2022-2023 RBB S.r.l
+// opensource@mintlayer.org
+// SPDX-License-Identifier: MIT
+// Licensed under the MIT License;
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Database backend support building blocks
+
+use core::ops::Range;
+
+/// Used to identify particular key-value map in the database
+#[derive(Eq, PartialEq, PartialOrd, Ord, Clone, Copy, Debug)]
+pub struct DbMapId(usize);
+
+impl DbMapId {
+    /// New index
+    pub const fn new(id: usize) -> Self {
+        Self(id)
+    }
+
+    /// Get the index as usize
+    pub const fn as_usize(&self) -> usize {
+        self.0
+    }
+}
+
+/// Number of key-value maps in the database
+#[derive(Eq, PartialEq, PartialOrd, Ord, Clone, Copy, Debug)]
+pub struct DbMapCount(usize);
+
+impl DbMapCount {
+    /// Convert to `usize`
+    pub const fn as_usize(self) -> usize {
+        self.0
+    }
+
+    /// Iterator over all database indices
+    pub fn indices(self) -> impl Iterator<Item = DbMapId> + ExactSizeIterator {
+        (0..self.0).map(DbMapId::new)
+    }
+}
+
+/// Associate data of type `T` with each DB map
+///
+/// This is used to associate certain data with each DB map. The data can be metadata containing
+/// names and other information about the maps, like [DbMapDesc]. It can also be used by the
+/// backend to store for example handles to open key-value maps.
+///
+/// The interface of this type is deliberately constrained to make it hard to use inappropriately.
+/// It can be constructed in two ways:
+/// 1. The [Self::new] method takes in the number of DB maps. The map count is in a type wrapper to
+///    make it harder to use arbitrary number there.
+/// 2. The [Self::transform] method is used to derive one piece of data from another. It preserves
+///    the data count and ensures the DB map IDs remain consistent between the original and the
+///    derived [DbMapsData] instance.
+/// 3. The data can only be accessed by a [DbMapId] so it's only indexed into using a valid DB
+///    map identifier.
+#[derive(Eq, PartialEq, Clone, Debug)]
+pub struct DbMapsData<T>(Vec<T>);
+
+impl<T> DbMapsData<T> {
+    /// New maps data for given number of key-value maps and initialization function
+    pub fn new(map_count: DbMapCount, constructor: impl FnMut(DbMapId) -> T) -> Self {
+        Self(map_count.indices().map(constructor).collect())
+    }
+
+    /// Get the number of maps
+    pub fn db_map_count(&self) -> DbMapCount {
+        DbMapCount(self.0.len())
+    }
+
+    /// Apply given function to each map and collect the results
+    pub fn transform<U>(&self, func: impl FnMut(&T) -> U) -> DbMapsData<U> {
+        DbMapsData(self.0.iter().map(func).collect::<Vec<_>>())
+    }
+
+    /// Like [Self::transform] but fallible
+    pub fn try_transform<U, E>(
+        &self,
+        func: impl FnMut(&T) -> Result<U, E>,
+    ) -> Result<DbMapsData<U>, E> {
+        self.0.iter().map(func).collect::<Result<Vec<_>, _>>().map(DbMapsData)
+    }
+
+    /// Convert into iterator over map indices together with associated data
+    pub fn into_iter_with_id(self) -> impl Iterator<Item = (DbMapId, T)> + ExactSizeIterator {
+        self.0.into_iter().enumerate().map(|(i, m)| (DbMapId::new(i), m))
+    }
+}
+
+impl<T> std::ops::Index<DbMapId> for DbMapsData<T> {
+    type Output = T;
+    fn index(&self, map_id: DbMapId) -> &Self::Output {
+        &self.0[map_id.0]
+    }
+}
+
+impl<T> std::ops::IndexMut<DbMapId> for DbMapsData<T> {
+    fn index_mut(&mut self, map_id: DbMapId) -> &mut Self::Output {
+        &mut self.0[map_id.0]
+    }
+}
+
+/// Description of one key-value store in a database
+#[derive(Eq, PartialEq, Clone, Debug)]
+pub struct DbMapDesc {
+    /// Key-value map name
+    name: String,
+    /// Value size hint
+    value_size_hint: Range<usize>,
+}
+
+impl DbMapDesc {
+    /// New DB map description
+    pub fn new(name: impl Into<String>) -> Self {
+        Self::new_with_details(name.into(), 0..usize::MAX)
+    }
+
+    /// New DB map description with all details
+    pub fn new_with_details(name: String, value_size_hint: Range<usize>) -> Self {
+        Self {
+            name,
+            value_size_hint,
+        }
+    }
+
+    /// Get DB map name
+    pub fn name(&self) -> &str {
+        &self.name
+    }
+
+    /// Get value size hint
+    pub fn value_size_hint(&self) -> &Range<usize> {
+        &self.value_size_hint
+    }
+}
+
+/// Metadata about the whole database
+pub struct DbDesc {
+    map_descs: DbMapsData<DbMapDesc>,
+}
+
+impl DbDesc {
+    /// Get descriptions of individual key-value maps
+    pub fn db_maps(&self) -> &DbMapsData<DbMapDesc> {
+        &self.map_descs
+    }
+
+    /// Number of maps in the database
+    pub fn db_map_count(&self) -> DbMapCount {
+        self.map_descs.db_map_count()
+    }
+}
+
+/// Internal constructors.
+///
+/// Only to be used by storage framework and testing. Not for use by backend implementations.
+pub mod construct {
+    use super::{DbDesc, DbMapDesc, DbMapsData};
+
+    /// Construct database description.
+    pub fn db_desc(iter: impl Iterator<Item = DbMapDesc>) -> DbDesc {
+        let map_descs = DbMapsData(iter.collect());
+        assert_names_unique(&map_descs);
+        DbDesc { map_descs }
+    }
+
+    fn assert_names_unique(maps: &DbMapsData<DbMapDesc>) {
+        let set: std::collections::BTreeSet<_> = maps.0.iter().map(|desc| &desc.name).collect();
+        assert_eq!(
+            set.len(),
+            maps.db_map_count().as_usize(),
+            "Duplicate map names found"
+        );
+    }
+}
diff --git a/storage/inmemory/src/lib.rs b/storage/inmemory/src/lib.rs
index fcc2686c8..547f6b5c0 100644
--- a/storage/inmemory/src/lib.rs
+++ b/storage/inmemory/src/lib.rs
@@ -13,7 +13,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-use storage_core::{adaptor, backend, util, Data, DbDesc, DbIndex};
+use storage_core::{adaptor, backend, util, Data, DbDesc, DbMapId, DbMapsData};
 
 use std::{borrow::Cow, collections::BTreeMap};
 
@@ -29,11 +29,11 @@ impl<'i> Iterator for PrefixIter<'i> {
     }
 }
 
-pub struct StorageMaps(Vec<Map>);
+pub struct StorageMaps(DbMapsData<Map>);
 
 impl backend::ReadOps for StorageMaps {
-    fn get(&self, idx: DbIndex, key: &[u8]) -> storage_core::Result<Option<Cow<[u8]>>> {
-        Ok(self.0[idx.get()].get(key).map(|p| p.into()))
+    fn get(&self, map_id: DbMapId, key: &[u8]) -> storage_core::Result<Option<Cow<[u8]>>> {
+        Ok(self.0[map_id].get(key).map(|p| p.into()))
     }
 }
 
@@ -42,24 +42,21 @@ impl<'i> backend::PrefixIter<'i> for StorageMaps {
 
     fn prefix_iter<'m: 'i>(
         &'m self,
-        idx: DbIndex,
+        map_id: DbMapId,
         prefix: Data,
     ) -> storage_core::Result<Self::Iterator> {
-        Ok(PrefixIter(util::PrefixIter::new(
-            &self.0[idx.get()],
-            prefix,
-        )))
+        Ok(PrefixIter(util::PrefixIter::new(&self.0[map_id], prefix)))
     }
 }
 
 impl backend::WriteOps for StorageMaps {
-    fn put(&mut self, idx: DbIndex, key: Data, val: Data) -> storage_core::Result<()> {
-        let _ = self.0[idx.get()].insert(key, val);
+    fn put(&mut self, map_id: DbMapId, key: Data, val: Data) -> storage_core::Result<()> {
+        let _ = self.0[map_id].insert(key, val);
         Ok(())
     }
 
-    fn del(&mut self, idx: DbIndex, key: &[u8]) -> storage_core::Result<()> {
-        let _ = self.0[idx.get()].remove(key);
+    fn del(&mut self, map_id: DbMapId, key: &[u8]) -> storage_core::Result<()> {
+        let _ = self.0[map_id].remove(key);
         Ok(())
     }
 }
@@ -68,7 +65,7 @@ impl adaptor::Construct for StorageMaps {
     type From = ();
 
     fn construct(_: (), desc: DbDesc) -> storage_core::Result<Self> {
-        Ok(Self(vec![Map::new(); desc.len()]))
+        Ok(Self(desc.db_maps().transform(|_| Map::new())))
     }
 }
 
diff --git a/storage/lmdb/src/lib.rs b/storage/lmdb/src/lib.rs
index d607ef699..0b55eec1e 100644
--- a/storage/lmdb/src/lib.rs
+++ b/storage/lmdb/src/lib.rs
@@ -26,25 +26,12 @@ use lmdb::Cursor;
 use resize_callback::MapResizeCallback;
 use storage_core::{
     backend::{self, TransactionalRo, TransactionalRw},
-    info::{DbDesc, MapDesc},
-    Data, DbIndex,
+    Data, DbDesc, DbMapDesc, DbMapId, DbMapsData,
 };
 use utils::sync::Arc;
 
 pub use lmdb::{DatabaseResizeInfo, DatabaseResizeSettings};
 
-/// Identifiers of the list of databases (key-value maps)
-#[derive(Eq, PartialEq, Debug, Clone)]
-struct DbList(Vec<lmdb::Database>);
-
-impl std::ops::Index<DbIndex> for DbList {
-    type Output = lmdb::Database;
-
-    fn index(&self, index: DbIndex) -> &Self::Output {
-        &self.0[index.get()]
-    }
-}
-
 /// LMDB iterator over entries with given key prefix
 pub struct PrefixIter<'tx, C> {
     /// Underlying iterator
@@ -83,11 +70,13 @@ impl<'s, 'i, Tx: lmdb::Transaction> backend::PrefixIter<'i> for DbTx<'s, Tx> {
 
     fn prefix_iter<'t: 'i>(
         &'t self,
-        idx: DbIndex,
+        map_id: DbMapId,
         prefix: Data,
     ) -> storage_core::Result<Self::Iterator> {
-        let cursor =
-            self.tx.open_ro_cursor(self.backend.dbs[idx]).or_else(error::process_with_err)?;
+        let cursor = self
+            .tx
+            .open_ro_cursor(self.backend.dbs[map_id])
+            .or_else(error::process_with_err)?;
         let iter = if prefix.is_empty() {
             cursor.into_iter_start()
         } else {
@@ -98,24 +87,25 @@ impl<'s, 'i, Tx: lmdb::Transaction> backend::PrefixIter<'i> for DbTx<'s, Tx> {
 }
 
 impl<Tx: lmdb::Transaction> backend::ReadOps for DbTx<'_, Tx> {
-    fn get(&self, idx: DbIndex, key: &[u8]) -> storage_core::Result<Option<Cow<[u8]>>> {
+    fn get(&self, map_id: DbMapId, key: &[u8]) -> storage_core::Result<Option<Cow<[u8]>>> {
         self.tx
-            .get(self.backend.dbs[idx], &key)
+            .get(self.backend.dbs[map_id], &key)
             .map_or_else(error::process_with_none, |x| Ok(Some(x.into())))
     }
 }
 
 impl backend::WriteOps for DbTx<'_, lmdb::RwTransaction<'_>> {
-    fn put(&mut self, idx: DbIndex, key: Data, val: Data) -> storage_core::Result<()> {
+    fn put(&mut self, map_id: DbMapId, key: Data, val: Data) -> storage_core::Result<()> {
+        let db = self.backend.dbs[map_id];
         self.tx
-            .put(self.backend.dbs[idx], &key, &val, lmdb::WriteFlags::empty())
+            .put(db, &key, &val, lmdb::WriteFlags::empty())
             .map_err(|err| self.backend.schedule_map_resize_if_map_full(err))
             .or_else(error::process_with_unit)
     }
 
-    fn del(&mut self, idx: DbIndex, key: &[u8]) -> storage_core::Result<()> {
+    fn del(&mut self, map_id: DbMapId, key: &[u8]) -> storage_core::Result<()> {
         self.tx
-            .del(self.backend.dbs[idx], &key, None)
+            .del(self.backend.dbs[map_id], &key, None)
             .map_err(|err| self.backend.schedule_map_resize_if_map_full(err))
             .or_else(error::process_with_unit)
     }
@@ -137,7 +127,7 @@ pub struct LmdbImpl {
     env: Arc<lmdb::Environment>,
 
     /// List of open databases
-    dbs: DbList,
+    dbs: DbMapsData<lmdb::Database>,
 
     /// Schedule a database resize of the database map
     map_resize_scheduled: Arc<AtomicBool>,
@@ -253,8 +243,8 @@ impl Lmdb {
         self
     }
 
-    fn open_db(env: &lmdb::Environment, desc: &MapDesc) -> storage_core::Result<lmdb::Database> {
-        let name = Some(desc.name.as_ref());
+    fn open_db(env: &lmdb::Environment, desc: &DbMapDesc) -> storage_core::Result<lmdb::Database> {
+        let name = Some(desc.name());
         let flags = lmdb::DatabaseFlags::default();
         env.create_db(name, flags).or_else(error::process_with_err)
     }
@@ -282,17 +272,13 @@ impl backend::Backend for Lmdb {
         }
         .set_resize_settings(self.resize_settings)
         .set_resize_callback(self.resize_callback.take())
-        .set_max_dbs(desc.len() as u32)
+        .set_max_dbs(desc.db_map_count().as_usize() as u32)
         .set_flags(self.flags)
         .open(&self.path)
         .or_else(error::process_with_err)?;
 
         // Set up all the databases
-        let dbs = desc
-            .iter()
-            .map(|desc| Self::open_db(&environment, desc))
-            .collect::<storage_core::Result<Vec<_>>>()
-            .map(DbList)?;
+        let dbs = desc.db_maps().try_transform(|desc| Self::open_db(&environment, desc))?;
 
         Ok(LmdbImpl {
             env: Arc::new(environment),
diff --git a/storage/sqlite/src/lib.rs b/storage/sqlite/src/lib.rs
index cfdc6e9ac..abc7648a3 100644
--- a/storage/sqlite/src/lib.rs
+++ b/storage/sqlite/src/lib.rs
@@ -28,8 +28,7 @@ use crate::queries::SqliteQueries;
 use error::process_sqlite_error;
 use storage_core::{
     backend::{self, TransactionalRo, TransactionalRw},
-    info::DbDesc,
-    Data, DbIndex,
+    Data, DbDesc, DbMapId,
 };
 use utils::shallow_clone::ShallowClone;
 use utils::sync::Arc;
@@ -106,14 +105,14 @@ impl<'s, 'i> backend::PrefixIter<'i> for DbTx<'s> {
 
     fn prefix_iter<'t: 'i>(
         &'t self,
-        idx: DbIndex,
+        map_id: DbMapId,
         prefix: Data,
     ) -> storage_core::Result<Self::Iterator> {
         // TODO check if prefix.is_empty()
         // TODO Perform the filtering in the SQL query itself
         let mut stmt = self
             .connection
-            .prepare_cached(self.queries[idx].prefix_iter_query.as_str())
+            .prepare_cached(self.queries[map_id].prefix_iter_query.as_str())
             .map_err(process_sqlite_error)?;
 
         let mut rows = stmt.query(()).map_err(process_sqlite_error)?;
@@ -134,10 +133,10 @@ impl<'s, 'i> backend::PrefixIter<'i> for DbTx<'s> {
 }
 
 impl backend::ReadOps for DbTx<'_> {
-    fn get(&self, idx: DbIndex, key: &[u8]) -> storage_core::Result<Option<Cow<[u8]>>> {
+    fn get(&self, map_id: DbMapId, key: &[u8]) -> storage_core::Result<Option<Cow<[u8]>>> {
         let mut stmt = self
             .connection
-            .prepare_cached(self.queries[idx].get_query.as_str())
+            .prepare_cached(self.queries[map_id].get_query.as_str())
             .map_err(process_sqlite_error)?;
 
         let params = (key,);
@@ -151,10 +150,10 @@ impl backend::ReadOps for DbTx<'_> {
 }
 
 impl backend::WriteOps for DbTx<'_> {
-    fn put(&mut self, idx: DbIndex, key: Data, val: Data) -> storage_core::Result<()> {
+    fn put(&mut self, map_id: DbMapId, key: Data, val: Data) -> storage_core::Result<()> {
         let mut stmt = self
             .connection
-            .prepare_cached(self.queries[idx].put_query.as_str())
+            .prepare_cached(self.queries[map_id].put_query.as_str())
             .map_err(process_sqlite_error)?;
 
         let params = (key, val);
@@ -163,10 +162,10 @@ impl backend::WriteOps for DbTx<'_> {
         Ok(())
     }
 
-    fn del(&mut self, idx: DbIndex, key: &[u8]) -> storage_core::Result<()> {
+    fn del(&mut self, map_id: DbMapId, key: &[u8]) -> storage_core::Result<()> {
         let mut stmt = self
             .connection
-            .prepare_cached(self.queries[idx].delete_query.as_str())
+            .prepare_cached(self.queries[map_id].delete_query.as_str())
             .map_err(process_sqlite_error)?;
 
         let params = (key,);
@@ -262,8 +261,8 @@ impl Sqlite {
             .prepare_cached("SELECT name FROM sqlite_master WHERE type='table' AND name=?")?;
 
         // Check if the required tables exist and if needed create them
-        for idx in 0..desc.len() {
-            let table_name = queries::db_table_name(idx);
+        for idx in desc.db_map_count().indices() {
+            let table_name = &desc.db_maps()[idx].name();
             // Check if table is missing
             let is_missing = exists_stmt
                 .query_row([&table_name], |row| row.get::<usize, String>(0))
@@ -271,13 +270,14 @@ impl Sqlite {
                 .is_none();
             // Create the table if needed
             if is_missing {
-                connection.execute(queries::create_table_query(&table_name).as_str(), ())?;
+                connection.execute(queries::create_table_query(table_name).as_str(), ())?;
             }
         }
         drop(exists_stmt);
 
         // Set statement cache to fit all the prepared statements we use
-        connection.set_prepared_statement_cache_capacity(max(desc.len() * 4, 16));
+        let statement_cap = max(desc.db_map_count().as_usize() * 4, 16);
+        connection.set_prepared_statement_cache_capacity(statement_cap);
 
         Ok(connection)
     }
@@ -298,7 +298,7 @@ impl backend::Backend for Sqlite {
             .into());
         }
 
-        let queries = SqliteQueries::new(&desc);
+        let queries = desc.db_maps().transform(queries::SqliteQuery::from_desc);
 
         let connection = self.open_db(desc).map_err(process_sqlite_error)?;
 
diff --git a/storage/sqlite/src/queries.rs b/storage/sqlite/src/queries.rs
index 74be58b65..2a9f75954 100644
--- a/storage/sqlite/src/queries.rs
+++ b/storage/sqlite/src/queries.rs
@@ -13,17 +13,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-use storage_core::{DbDesc, DbIndex};
-
-/// Return the prefix of the database key/value tables
-#[inline]
-pub fn db_table_name(idx: usize) -> String {
-    format!("db_{idx}")
-}
+use storage_core::{DbMapDesc, DbMapsData};
 
 /// Returns an SQL query to create a table
 #[inline]
-pub fn create_table_query(table_name: &String) -> String {
+pub fn create_table_query(table_name: &str) -> String {
     format!("CREATE TABLE {table_name}(key BLOB PRIMARY KEY NOT NULL, value BLOB NOT NULL)")
 }
 
@@ -40,30 +34,17 @@ pub struct SqliteQuery {
     pub delete_query: String,
 }
 
-/// Holds typical SQL queries like for retrieving, inserting, deleting key/values
-#[derive(Eq, PartialEq, Debug, Clone)]
-pub struct SqliteQueries(Vec<SqliteQuery>);
-
-impl SqliteQueries {
-    pub fn new(desc: &DbDesc) -> Self {
-        // Set up all the databases
-        let queries = (0..desc.len())
-            .map(|i| SqliteQuery {
-                get_query: format!("SELECT value FROM db_{i} WHERE key = ?"),
-                prefix_iter_query: format!("SELECT key, value FROM db_{i} ORDER BY key"),
-                put_query: format!("INSERT or REPLACE into db_{i} values(?, ?)"),
-                delete_query: format!("DELETE FROM db_{i} WHERE key = ?"),
-            })
-            .collect();
-
-        Self(queries)
+impl SqliteQuery {
+    pub fn from_desc(desc: &DbMapDesc) -> Self {
+        let name = desc.name();
+        Self {
+            get_query: format!("SELECT value FROM {name} WHERE key = ?"),
+            prefix_iter_query: format!("SELECT key, value FROM {name} ORDER BY key"),
+            put_query: format!("INSERT or REPLACE into {name} values(?, ?)"),
+            delete_query: format!("DELETE FROM {name} WHERE key = ?"),
+        }
     }
 }
 
-impl std::ops::Index<DbIndex> for SqliteQueries {
-    type Output = SqliteQuery;
-
-    fn index(&self, index: DbIndex) -> &Self::Output {
-        &self.0[index.get()]
-    }
-}
+/// Holds typical SQL queries like for retrieving, inserting, deleting key/values
+pub type SqliteQueries = DbMapsData<SqliteQuery>;
diff --git a/storage/src/database/internal.rs b/storage/src/database/internal.rs
index a788e3af5..84282b117 100644
--- a/storage/src/database/internal.rs
+++ b/storage/src/database/internal.rs
@@ -21,7 +21,7 @@ use crate::schema;
 use serialization::{encoded::Encoded, EncodeLike};
 use storage_core::{
     backend::{self, PrefixIter, ReadOps},
-    Backend, DbIndex,
+    Backend, DbMapId,
 };
 
 /// Map high-level transaction type to the backend-specific implementation type
@@ -42,10 +42,10 @@ impl<'tx, B: Backend, Sch> TxImpl for super::TransactionRw<'tx, B, Sch> {
 #[allow(clippy::type_complexity)]
 pub fn get<DbMap: schema::DbMap, Tx: ReadOps, K: EncodeLike<DbMap::Key>>(
     dbtx: &Tx,
-    idx: DbIndex,
+    map_id: DbMapId,
     key: K,
 ) -> crate::Result<Option<Encoded<Cow<[u8]>, DbMap::Value>>> {
-    key.using_encoded(|key| dbtx.get(idx, key).map(|x| x.map(Encoded::from_bytes_unchecked)))
+    key.using_encoded(|key| dbtx.get(map_id, key).map(|x| x.map(Encoded::from_bytes_unchecked)))
 }
 
 /// Iterator over DB map entries
@@ -61,10 +61,10 @@ impl<DbMap: schema::DbMap, I: Iterator<Item = (DbMap::Key, Encoded<Vec<u8>, DbMa
 
 pub fn prefix_iter<'tx, DbMap: schema::DbMap, Tx: PrefixIter<'tx>>(
     dbtx: &'tx Tx,
-    idx: DbIndex,
+    map_id: DbMapId,
     prefix: Vec<u8>,
 ) -> crate::Result<impl 'tx + EntryIterator<DbMap>> {
-    dbtx.prefix_iter(idx, prefix).map(|iter| {
+    dbtx.prefix_iter(map_id, prefix).map(|iter| {
         iter.map(|(k, v)| {
             (
                 Encoded::from_bytes_unchecked(k).decode(),
diff --git a/storage/src/database/mod.rs b/storage/src/database/mod.rs
index 3990f008d..32d82252a 100644
--- a/storage/src/database/mod.rs
+++ b/storage/src/database/mod.rs
@@ -24,7 +24,7 @@ use internal::{EntryIterator, TxImpl};
 
 use crate::schema::{self, Schema};
 use serialization::{encoded::Encoded, Encode, EncodeLike};
-use storage_core::{backend, Backend, DbIndex};
+use storage_core::{backend, Backend, DbMapId};
 
 /// The main storage type
 pub struct Storage<B: Backend, Sch> {
@@ -52,10 +52,9 @@ impl<B: Backend, Sch> utils::shallow_clone::ShallowClone for Storage<B, Sch> whe
 impl<B: Backend, Sch: Schema> Storage<B, Sch> {
     /// Create new storage with given backend
     pub fn new(backend: B) -> crate::Result<Self> {
-        Ok(Self {
-            backend: backend.open(Sch::desc_iter().collect())?,
-            _schema: Default::default(),
-        })
+        let backend = backend.open(storage_core::types::construct::db_desc(Sch::desc_iter()))?;
+        let _schema = std::marker::PhantomData;
+        Ok(Self { backend, _schema })
     }
 
     /// Dump raw database contents into a data structure
@@ -65,10 +64,9 @@ impl<B: Backend, Sch: Schema> Storage<B, Sch> {
 
     /// Start a read-only transaction
     pub fn transaction_ro<'tx, 'st: 'tx>(&'st self) -> crate::Result<TransactionRo<'tx, B, Sch>> {
-        Ok(TransactionRo {
-            dbtx: backend::TransactionalRo::transaction_ro(&self.backend)?,
-            _schema: Default::default(),
-        })
+        let dbtx = backend::TransactionalRo::transaction_ro(&self.backend)?;
+        let _schema = std::marker::PhantomData;
+        Ok(TransactionRo { dbtx, _schema })
     }
 
     /// Start a read-write transaction
@@ -76,10 +74,9 @@ impl<B: Backend, Sch: Schema> Storage<B, Sch> {
         &'st self,
         size: Option<usize>,
     ) -> crate::Result<TransactionRw<'tx, B, Sch>> {
-        Ok(TransactionRw {
-            dbtx: backend::TransactionalRw::transaction_rw(&self.backend, size)?,
-            _schema: Default::default(),
-        })
+        let dbtx = backend::TransactionalRw::transaction_rw(&self.backend, size)?;
+        let _schema = std::marker::PhantomData;
+        Ok(TransactionRw { dbtx, _schema })
     }
 }
 
@@ -141,16 +138,16 @@ impl<'tx, B: Backend, Sch: Schema> TransactionRw<'tx, B, Sch> {
 /// Represents an immutable view of a key-value map
 pub struct MapRef<'tx, Tx: internal::TxImpl, DbMap: schema::DbMap> {
     dbtx: &'tx Tx::Impl,
-    idx: DbIndex,
+    map_id: DbMapId,
     _phantom: std::marker::PhantomData<fn() -> DbMap>,
 }
 
 impl<'tx, Tx: TxImpl, DbMap: schema::DbMap> MapRef<'tx, Tx, DbMap> {
-    fn new(dbtx: &'tx Tx::Impl, idx: DbIndex) -> Self {
+    fn new(dbtx: &'tx Tx::Impl, map_id: DbMapId) -> Self {
         let _phantom = Default::default();
         Self {
             dbtx,
-            idx,
+            map_id,
             _phantom,
         }
     }
@@ -166,7 +163,7 @@ where
         &self,
         key: K,
     ) -> crate::Result<Option<Encoded<Cow<[u8]>, DbMap::Value>>> {
-        internal::get::<DbMap, _, _>(self.dbtx, self.idx, key)
+        internal::get::<DbMap, _, _>(self.dbtx, self.map_id, key)
     }
 
     /// Iterator over entries with key starting with given prefix
@@ -175,7 +172,7 @@ where
         Pfx: Encode,
         DbMap::Key: HasPrefix<Pfx>,
     {
-        internal::prefix_iter(self.dbtx, self.idx, prefix.encode())
+        internal::prefix_iter(self.dbtx, self.map_id, prefix.encode())
     }
 
     /// Iterator over decoded entries with key starting with given prefix
@@ -187,24 +184,23 @@ where
         Pfx: Encode,
         DbMap::Key: HasPrefix<Pfx>,
     {
-        internal::prefix_iter::<DbMap, <Tx as TxImpl>::Impl>(self.dbtx, self.idx, prefix.encode())
-            .map(|item| item.map(|(k, v)| (k, v.decode())))
+        self.prefix_iter(prefix).map(|item| item.map(|(k, v)| (k, v.decode())))
     }
 }
 
 /// Represents a mutable view of a key-value map
 pub struct MapMut<'tx, Tx: TxImpl, DbMap: schema::DbMap> {
     dbtx: &'tx mut Tx::Impl,
-    idx: DbIndex,
+    map_id: DbMapId,
     _phantom: std::marker::PhantomData<fn() -> DbMap>,
 }
 
 impl<'tx, Tx: TxImpl, DbMap: schema::DbMap> MapMut<'tx, Tx, DbMap> {
-    fn new(dbtx: &'tx mut Tx::Impl, idx: DbIndex) -> Self {
+    fn new(dbtx: &'tx mut Tx::Impl, map_id: DbMapId) -> Self {
         let _phantom = Default::default();
         Self {
             dbtx,
-            idx,
+            map_id,
             _phantom,
         }
     }
@@ -220,7 +216,7 @@ where
         &self,
         key: K,
     ) -> crate::Result<Option<Encoded<Cow<[u8]>, DbMap::Value>>> {
-        internal::get::<DbMap, _, _>(self.dbtx, self.idx, key)
+        internal::get::<DbMap, _, _>(self.dbtx, self.map_id, key)
     }
 
     /// Iterator over entries with key starting with given prefix
@@ -229,7 +225,7 @@ where
         Pfx: Encode,
         DbMap::Key: HasPrefix<Pfx>,
     {
-        internal::prefix_iter(self.dbtx, self.idx, prefix.encode())
+        internal::prefix_iter(self.dbtx, self.map_id, prefix.encode())
     }
 }
 
@@ -243,12 +239,12 @@ where
         key: K,
         value: V,
     ) -> crate::Result<()> {
-        backend::WriteOps::put(self.dbtx, self.idx, key.encode(), value.encode())
+        backend::WriteOps::put(self.dbtx, self.map_id, key.encode(), value.encode())
     }
 
     /// Remove value associated with given key.
     pub fn del<K: EncodeLike<DbMap::Key>>(&mut self, key: K) -> crate::Result<()> {
-        key.using_encoded(|key| backend::WriteOps::del(self.dbtx, self.idx, key))
+        key.using_encoded(|key| backend::WriteOps::del(self.dbtx, self.map_id, key))
     }
 }
 
diff --git a/storage/src/database/raw.rs b/storage/src/database/raw.rs
index d5d9d36ed..babe6b01b 100644
--- a/storage/src/database/raw.rs
+++ b/storage/src/database/raw.rs
@@ -26,29 +26,29 @@ pub use storage_core::Data;
 
 /// Database index, parametrized by schema
 ///
-/// This is basically a type-safe version of [storage_core::DbIndex].
-pub struct DbIndex<Sch> {
-    idx: storage_core::DbIndex,
+/// This is basically a type-safe version of [storage_core::DbMapId].
+pub struct DbMapId<Sch> {
+    idx: storage_core::DbMapId,
     _phantom: std::marker::PhantomData<fn() -> Sch>,
 }
 
-impl<Sch> DbIndex<Sch> {
-    fn from_idx_unchecked(idx: storage_core::DbIndex) -> Self {
+impl<Sch> DbMapId<Sch> {
+    fn from_idx_unchecked(idx: storage_core::DbMapId) -> Self {
         let _phantom = Default::default();
         Self { idx, _phantom }
     }
 
     fn from_usize_unchecked(idx: usize) -> Self {
-        Self::from_idx_unchecked(storage_core::DbIndex::new(idx))
+        Self::from_idx_unchecked(storage_core::DbMapId::new(idx))
     }
 
     /// Get index as usize
     pub fn as_usize(&self) -> usize {
-        self.idx.get()
+        self.idx.as_usize()
     }
 }
 
-impl<Sch: Schema> DbIndex<Sch> {
+impl<Sch: Schema> DbMapId<Sch> {
     /// New database index from the database identifier
     pub fn new<M: schema::DbMap, I>() -> Self
     where
@@ -66,52 +66,52 @@ impl<Sch: Schema> DbIndex<Sch> {
     /// Database index from key-value map name
     pub fn from_name<S: AsRef<str>>(name: S) -> Option<Self> {
         Sch::desc_iter()
-            .position(|desc| desc.name == name.as_ref())
+            .position(|desc| desc.name() == name.as_ref())
             .map(Self::from_usize_unchecked)
     }
 
     /// Get index info
-    pub fn info(&self) -> storage_core::info::MapDesc {
+    pub fn info(&self) -> storage_core::DbMapDesc {
         Sch::desc_iter()
-            .nth(self.idx.get())
+            .nth(self.idx.as_usize())
             .expect("index to be in range due to schema")
     }
 
     /// Get map name at this index
     pub fn name(&self) -> String {
-        self.info().name
+        self.info().name().into()
     }
 }
 
-impl<Sch> Ord for DbIndex<Sch> {
+impl<Sch> Ord for DbMapId<Sch> {
     fn cmp(&self, other: &Self) -> std::cmp::Ordering {
         self.idx.cmp(&other.idx)
     }
 }
 
-impl<Sch> PartialOrd for DbIndex<Sch> {
+impl<Sch> PartialOrd for DbMapId<Sch> {
     fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
         self.idx.partial_cmp(&other.idx)
     }
 }
 
-impl<Sch> PartialEq for DbIndex<Sch> {
+impl<Sch> PartialEq for DbMapId<Sch> {
     fn eq(&self, other: &Self) -> bool {
         self.idx.eq(&other.idx)
     }
 }
 
-impl<Sch> Eq for DbIndex<Sch> {}
+impl<Sch> Eq for DbMapId<Sch> {}
 
-impl<Sch> Clone for DbIndex<Sch> {
+impl<Sch> Clone for DbMapId<Sch> {
     fn clone(&self) -> Self {
         Self::from_idx_unchecked(self.idx)
     }
 }
 
-impl<Sch> Copy for DbIndex<Sch> {}
+impl<Sch> Copy for DbMapId<Sch> {}
 
-impl<Sch> std::fmt::Debug for DbIndex<Sch> {
+impl<Sch> std::fmt::Debug for DbMapId<Sch> {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         self.idx.fmt(f)
     }
@@ -121,7 +121,7 @@ impl<Sch> std::fmt::Debug for DbIndex<Sch> {
 pub type MapContents = BTreeMap<Data, Data>;
 
 /// Low-level representation of the whole storage
-pub type StorageContents<Sch> = BTreeMap<DbIndex<Sch>, MapContents>;
+pub type StorageContents<Sch> = BTreeMap<DbMapId<Sch>, MapContents>;
 
 /// Get raw database by dumping database data
 pub fn dump_storage<B: Backend, Sch: Schema>(
@@ -131,9 +131,9 @@ pub fn dump_storage<B: Backend, Sch: Schema>(
     Sch::desc_iter()
         .enumerate()
         .map(|(idx, _dbinfo)| {
-            let idx = storage_core::DbIndex::new(idx);
+            let idx = storage_core::DbMapId::new(idx);
             let items = dbtx.dbtx.prefix_iter(idx, Vec::new())?;
-            Ok((DbIndex::from_idx_unchecked(idx), items.collect()))
+            Ok((DbMapId::from_idx_unchecked(idx), items.collect()))
         })
         .collect::<crate::Result<StorageContents<Sch>>>()
 }
@@ -152,28 +152,28 @@ mod test {
 
     #[test]
     fn indices() {
-        type TestDbIndex = DbIndex<TestSchema>;
-        let idx0: TestDbIndex = DbIndex::from_usize_unchecked(0);
-        let idx1: TestDbIndex = DbIndex::from_usize_unchecked(1);
+        type TestDbMapId = DbMapId<TestSchema>;
+        let idx0: TestDbMapId = DbMapId::from_usize_unchecked(0);
+        let idx1: TestDbMapId = DbMapId::from_usize_unchecked(1);
 
-        assert_eq!(TestDbIndex::new::<Db0, _>(), idx0);
-        assert_eq!(TestDbIndex::new::<Db1, _>(), idx1);
+        assert_eq!(TestDbMapId::new::<Db0, _>(), idx0);
+        assert_eq!(TestDbMapId::new::<Db1, _>(), idx1);
 
-        assert_eq!(TestDbIndex::from_usize(0), Some(idx0));
-        assert_eq!(TestDbIndex::from_usize(1), Some(idx1));
-        assert_eq!(TestDbIndex::from_usize(2), None);
+        assert_eq!(TestDbMapId::from_usize(0), Some(idx0));
+        assert_eq!(TestDbMapId::from_usize(1), Some(idx1));
+        assert_eq!(TestDbMapId::from_usize(2), None);
 
-        assert_eq!(TestDbIndex::from_name("Db0"), Some(idx0));
-        assert_eq!(TestDbIndex::from_name("Db1"), Some(idx1));
-        assert_eq!(TestDbIndex::from_name("DbX"), None);
+        assert_eq!(TestDbMapId::from_name("Db0"), Some(idx0));
+        assert_eq!(TestDbMapId::from_name("Db1"), Some(idx1));
+        assert_eq!(TestDbMapId::from_name("DbX"), None);
     }
 
     #[test]
     fn basic_dump() {
         utils::concurrency::model(|| {
             let storage = Storage::<_, TestSchema>::new(InMemory::new()).unwrap();
-            let db1 = DbIndex::new::<Db0, _>();
-            let db2 = DbIndex::new::<Db1, _>();
+            let db1 = DbMapId::new::<Db0, _>();
+            let db2 = DbMapId::new::<Db1, _>();
 
             {
                 // Check the DB dump is empty initially
diff --git a/storage/src/schema.rs b/storage/src/schema.rs
index e0f9357ae..24ff31681 100644
--- a/storage/src/schema.rs
+++ b/storage/src/schema.rs
@@ -15,7 +15,7 @@
 
 //! Describe the database schema at type level
 
-pub use storage_core::{info::MapDesc, DbIndex};
+pub use storage_core::{DbMapDesc, DbMapId};
 
 /// Describes single key-value map
 pub trait DbMap: 'static {
@@ -34,24 +34,21 @@ pub trait DbMap: 'static {
 
 /// What constitutes a valid database schema
 pub trait Schema: internal::Sealed + 'static {
-    type DescIter: Iterator<Item = MapDesc>;
+    type DescIter: Iterator<Item = DbMapDesc>;
     fn desc_iter() -> Self::DescIter;
 }
 
 impl Schema for () {
-    type DescIter = std::iter::Empty<MapDesc>;
+    type DescIter = std::iter::Empty<DbMapDesc>;
     fn desc_iter() -> Self::DescIter {
         std::iter::empty()
     }
 }
 
 impl<M: DbMap, Rest: Schema> Schema for (M, Rest) {
-    type DescIter = std::iter::Chain<std::iter::Once<MapDesc>, Rest::DescIter>;
+    type DescIter = std::iter::Chain<std::iter::Once<DbMapDesc>, Rest::DescIter>;
     fn desc_iter() -> Self::DescIter {
-        let map_desc = MapDesc {
-            name: M::NAME.to_string(),
-            size_hint: M::SIZE_HINT,
-        };
+        let map_desc = DbMapDesc::new_with_details(M::NAME.to_string(), M::SIZE_HINT);
         std::iter::once(map_desc).chain(Rest::desc_iter())
     }
 }
@@ -59,13 +56,13 @@ impl<M: DbMap, Rest: Schema> Schema for (M, Rest) {
 /// Require a schema to contain given map (identified by a type tag)
 pub trait HasDbMap<M: DbMap, I>: Schema {
     /// Index of the map in the schema
-    const INDEX: DbIndex;
+    const INDEX: DbMapId;
 }
 impl<M: DbMap, Rest: Schema> HasDbMap<M, ()> for (M, Rest) {
-    const INDEX: DbIndex = DbIndex::new(0);
+    const INDEX: DbMapId = DbMapId::new(0);
 }
 impl<M: DbMap, Head: DbMap, Rest: HasDbMap<M, I>, I> HasDbMap<M, (I,)> for (Head, Rest) {
-    const INDEX: DbIndex = DbIndex::new(Rest::INDEX.get() + 1);
+    const INDEX: DbMapId = DbMapId::new(Rest::INDEX.as_usize() + 1);
 }
 
 mod internal {
@@ -123,8 +120,8 @@ mod test {
     #[test]
     fn schema() {
         // Check calculated column indices
-        assert_eq!(<MySchema as HasDbMap<DBIdx0, _>>::INDEX, DbIndex::new(0));
-        assert_eq!(<MySchema as HasDbMap<DBIdx1, _>>::INDEX, DbIndex::new(1));
-        assert_eq!(<MySchema as HasDbMap<DBIdx2, _>>::INDEX, DbIndex::new(2));
+        assert_eq!(<MySchema as HasDbMap<DBIdx0, _>>::INDEX, DbMapId::new(0));
+        assert_eq!(<MySchema as HasDbMap<DBIdx1, _>>::INDEX, DbMapId::new(1));
+        assert_eq!(<MySchema as HasDbMap<DBIdx2, _>>::INDEX, DbMapId::new(2));
     }
 }


Test patch:
diff --git a/storage/backend-test-suite/src/basic.rs b/storage/backend-test-suite/src/basic.rs
index 469493b94..7ef4fc7ae 100644
--- a/storage/backend-test-suite/src/basic.rs
+++ b/storage/backend-test-suite/src/basic.rs
@@ -22,13 +22,13 @@ fn put_and_commit<B: Backend, F: BackendFn<B>>(backend_fn: Arc<F>) {
 
     // Create a transaction, modify storage and abort transaction
     let mut dbtx = store.transaction_rw(None).unwrap();
-    dbtx.put(IDX.0, b"hello".to_vec(), b"world".to_vec()).unwrap();
+    dbtx.put(MAPID.0, b"hello".to_vec(), b"world".to_vec()).unwrap();
     dbtx.commit().expect("commit to succeed");
 
     // Check the modification did not happen
     let dbtx = store.transaction_ro().unwrap();
     assert_eq!(
-        dbtx.get(IDX.0, b"hello").unwrap().as_ref().map(|v| v.as_ref()).unwrap(),
+        dbtx.get(MAPID.0, b"hello").unwrap().as_ref().map(|v| v.as_ref()).unwrap(),
         b"world".as_ref()
     );
     drop(dbtx);
@@ -39,12 +39,12 @@ fn put_and_abort<B: Backend, F: BackendFn<B>>(backend_fn: Arc<F>) {
 
     // Create a transaction, modify storage and abort transaction
     let mut dbtx = store.transaction_rw(None).unwrap();
-    dbtx.put(IDX.0, b"hello".to_vec(), b"world".to_vec()).unwrap();
+    dbtx.put(MAPID.0, b"hello".to_vec(), b"world".to_vec()).unwrap();
     drop(dbtx);
 
     // Check the modification did not happen
     let dbtx = store.transaction_ro().unwrap();
-    assert_eq!(dbtx.get(IDX.0, b"hello"), Ok(None));
+    assert_eq!(dbtx.get(MAPID.0, b"hello"), Ok(None));
     drop(dbtx);
 }
 
@@ -53,36 +53,36 @@ fn put_two_under_different_keys<B: Backend, F: BackendFn<B>>(backend_fn: Arc<F>)
 
     // Create a transaction, modify storage and commit
     let mut dbtx = store.transaction_rw(None).unwrap();
-    dbtx.put(IDX.0, b"a".to_vec(), b"0".to_vec()).unwrap();
-    dbtx.put(IDX.0, b"b".to_vec(), b"1".to_vec()).unwrap();
+    dbtx.put(MAPID.0, b"a".to_vec(), b"0".to_vec()).unwrap();
+    dbtx.put(MAPID.0, b"b".to_vec(), b"1".to_vec()).unwrap();
     dbtx.commit().expect("commit to succeed");
 
     // Check the values are in place
     let dbtx = store.transaction_ro().unwrap();
     assert_eq!(
-        dbtx.get(IDX.0, b"a").unwrap().as_ref().map(|v| v.as_ref()).unwrap(),
+        dbtx.get(MAPID.0, b"a").unwrap().as_ref().map(|v| v.as_ref()).unwrap(),
         b"0".as_ref()
     );
     assert_eq!(
-        dbtx.get(IDX.0, b"b").unwrap().as_ref().map(|v| v.as_ref()).unwrap(),
+        dbtx.get(MAPID.0, b"b").unwrap().as_ref().map(|v| v.as_ref()).unwrap(),
         b"1".as_ref()
     );
     drop(dbtx);
 
     // Create a transaction, modify storage and abort
     let mut dbtx = store.transaction_rw(None).unwrap();
-    dbtx.put(IDX.0, b"a".to_vec(), b"00".to_vec()).unwrap();
-    dbtx.put(IDX.0, b"b".to_vec(), b"11".to_vec()).unwrap();
+    dbtx.put(MAPID.0, b"a".to_vec(), b"00".to_vec()).unwrap();
+    dbtx.put(MAPID.0, b"b".to_vec(), b"11".to_vec()).unwrap();
     drop(dbtx);
 
     // Check the modification did not happen
     let dbtx = store.transaction_ro().unwrap();
     assert_eq!(
-        dbtx.get(IDX.0, b"a").unwrap().as_ref().map(|v| v.as_ref()).unwrap(),
+        dbtx.get(MAPID.0, b"a").unwrap().as_ref().map(|v| v.as_ref()).unwrap(),
         b"0".as_ref()
     );
     assert_eq!(
-        dbtx.get(IDX.0, b"b").unwrap().as_ref().map(|v| v.as_ref()).unwrap(),
+        dbtx.get(MAPID.0, b"b").unwrap().as_ref().map(|v| v.as_ref()).unwrap(),
         b"1".as_ref()
     );
     drop(dbtx);
@@ -92,21 +92,21 @@ fn put_twice_then_commit_read_last<B: Backend, F: BackendFn<B>>(backend_fn: Arc<
     let store = backend_fn().open(desc(1)).expect("db open to succeed");
 
     let mut dbtx = store.transaction_rw(None).unwrap();
-    dbtx.put(IDX.0, b"hello".to_vec(), b"a".to_vec()).unwrap();
+    dbtx.put(MAPID.0, b"hello".to_vec(), b"a".to_vec()).unwrap();
     assert_eq!(
-        dbtx.get(IDX.0, b"hello").unwrap().as_ref().map(|v| v.as_ref()).unwrap(),
+        dbtx.get(MAPID.0, b"hello").unwrap().as_ref().map(|v| v.as_ref()).unwrap(),
         b"a".as_ref(),
     );
-    dbtx.put(IDX.0, b"hello".to_vec(), b"b".to_vec()).unwrap();
+    dbtx.put(MAPID.0, b"hello".to_vec(), b"b".to_vec()).unwrap();
     assert_eq!(
-        dbtx.get(IDX.0, b"hello").unwrap().as_ref().map(|v| v.as_ref()).unwrap(),
+        dbtx.get(MAPID.0, b"hello").unwrap().as_ref().map(|v| v.as_ref()).unwrap(),
         b"b".as_ref(),
     );
     dbtx.commit().expect("commit to succeed");
 
     let dbtx = store.transaction_ro().unwrap();
     assert_eq!(
-        dbtx.get(IDX.0, b"hello").unwrap().as_ref().map(|v| v.as_ref()).unwrap(),
+        dbtx.get(MAPID.0, b"hello").unwrap().as_ref().map(|v| v.as_ref()).unwrap(),
         b"b".as_ref(),
     );
 }
@@ -115,14 +115,14 @@ fn put_iterator_count_matches<B: Backend, F: BackendFn<B>>(backend_fn: Arc<F>) {
     let store = backend_fn().open(desc(1)).expect("db open to succeed");
 
     let mut dbtx = store.transaction_rw(None).unwrap();
-    dbtx.put(IDX.0, vec![0x00], vec![]).unwrap();
-    dbtx.put(IDX.0, vec![0x01], vec![]).unwrap();
-    dbtx.put(IDX.0, vec![0x02], vec![]).unwrap();
-    dbtx.put(IDX.0, vec![0x03], vec![]).unwrap();
+    dbtx.put(MAPID.0, vec![0x00], vec![]).unwrap();
+    dbtx.put(MAPID.0, vec![0x01], vec![]).unwrap();
+    dbtx.put(MAPID.0, vec![0x02], vec![]).unwrap();
+    dbtx.put(MAPID.0, vec![0x03], vec![]).unwrap();
     dbtx.commit().expect("commit to succeed");
 
     let dbtx = store.transaction_ro().unwrap();
-    assert_eq!(dbtx.prefix_iter(IDX.0, vec![]).unwrap().count(), 4);
+    assert_eq!(dbtx.prefix_iter(MAPID.0, vec![]).unwrap().count(), 4);
 }
 
 fn put_and_iterate_over_prefixes<B: Backend, F: BackendFn<B>>(backend_fn: Arc<F>) {
@@ -130,16 +130,16 @@ fn put_and_iterate_over_prefixes<B: Backend, F: BackendFn<B>>(backend_fn: Arc<F>
 
     // Populate the database with some values
     let mut dbtx = store.transaction_rw(None).unwrap();
-    dbtx.put(IDX.0, b"ac".to_vec(), b"2".to_vec()).unwrap();
-    dbtx.put(IDX.0, b"bf".to_vec(), b"7".to_vec()).unwrap();
-    dbtx.put(IDX.0, b"ab".to_vec(), b"1".to_vec()).unwrap();
-    dbtx.put(IDX.0, b"aca".to_vec(), b"3".to_vec()).unwrap();
-    dbtx.put(IDX.0, b"bz".to_vec(), b"8".to_vec()).unwrap();
-    dbtx.put(IDX.0, b"x".to_vec(), b"9".to_vec()).unwrap();
-    dbtx.put(IDX.0, b"bb".to_vec(), b"6".to_vec()).unwrap();
-    dbtx.put(IDX.0, b"b".to_vec(), b"5".to_vec()).unwrap();
-    dbtx.put(IDX.0, b"acb".to_vec(), b"4".to_vec()).unwrap();
-    dbtx.put(IDX.0, b"aa".to_vec(), b"0".to_vec()).unwrap();
+    dbtx.put(MAPID.0, b"ac".to_vec(), b"2".to_vec()).unwrap();
+    dbtx.put(MAPID.0, b"bf".to_vec(), b"7".to_vec()).unwrap();
+    dbtx.put(MAPID.0, b"ab".to_vec(), b"1".to_vec()).unwrap();
+    dbtx.put(MAPID.0, b"aca".to_vec(), b"3".to_vec()).unwrap();
+    dbtx.put(MAPID.0, b"bz".to_vec(), b"8".to_vec()).unwrap();
+    dbtx.put(MAPID.0, b"x".to_vec(), b"9".to_vec()).unwrap();
+    dbtx.put(MAPID.0, b"bb".to_vec(), b"6".to_vec()).unwrap();
+    dbtx.put(MAPID.0, b"b".to_vec(), b"5".to_vec()).unwrap();
+    dbtx.put(MAPID.0, b"acb".to_vec(), b"4".to_vec()).unwrap();
+    dbtx.put(MAPID.0, b"aa".to_vec(), b"0".to_vec()).unwrap();
     dbtx.commit().expect("commit to succeed");
 
     // Check for a non-existent prefix
@@ -147,7 +147,7 @@ fn put_and_iterate_over_prefixes<B: Backend, F: BackendFn<B>>(backend_fn: Arc<F>
         store
             .transaction_ro()
             .unwrap()
-            .prefix_iter(IDX.0, b"foo".to_vec())
+            .prefix_iter(MAPID.0, b"foo".to_vec())
             .unwrap()
             .next(),
         None,
@@ -156,7 +156,8 @@ fn put_and_iterate_over_prefixes<B: Backend, F: BackendFn<B>>(backend_fn: Arc<F>
     // Check for items that are supposed to be present
     let check = |range: std::ops::RangeInclusive<usize>, prefix: Data| {
         let dbtx = store.transaction_ro().unwrap();
-        let vals: Vec<_> = dbtx.prefix_iter(IDX.0, prefix.clone()).unwrap().map(|x| x.1).collect();
+        let vals: Vec<_> =
+            dbtx.prefix_iter(MAPID.0, prefix.clone()).unwrap().map(|x| x.1).collect();
         let expected: Vec<_> = range.map(|x| Data::from(x.to_string())).collect();
         assert_eq!(vals, expected, "prefix={prefix:?}");
         drop(dbtx);
@@ -172,7 +173,7 @@ fn put_and_iterate_over_prefixes<B: Backend, F: BackendFn<B>>(backend_fn: Arc<F>
 
 // Check for items that are supposed to be present
 fn check_prefix<Tx: ReadOps>(dbtx: &Tx, prefix: Data, expected: &[(&str, &str)]) {
-    let entries = dbtx.prefix_iter(IDX.0, prefix).unwrap();
+    let entries = dbtx.prefix_iter(MAPID.0, prefix).unwrap();
     let expected = expected
         .iter()
         .map(|(x, y)| (Data::from(x.to_string()), Data::from(y.to_string())));
@@ -189,12 +190,12 @@ fn put_and_iterate_delete_some<B: Backend, F: BackendFn<B>>(backend_fn: Arc<F>)
 
     // Populate the database with some
     let mut dbtx = store.transaction_rw(None).unwrap();
-    dbtx.put(IDX.0, b"aa".to_vec(), b"0".to_vec()).unwrap();
-    dbtx.put(IDX.0, b"ab".to_vec(), b"1".to_vec()).unwrap();
-    dbtx.put(IDX.0, b"ac".to_vec(), b"2".to_vec()).unwrap();
-    dbtx.put(IDX.0, b"aca".to_vec(), b"3".to_vec()).unwrap();
-    dbtx.put(IDX.0, b"acb".to_vec(), b"4".to_vec()).unwrap();
-    dbtx.put(IDX.0, b"b".to_vec(), b"5".to_vec()).unwrap();
+    dbtx.put(MAPID.0, b"aa".to_vec(), b"0".to_vec()).unwrap();
+    dbtx.put(MAPID.0, b"ab".to_vec(), b"1".to_vec()).unwrap();
+    dbtx.put(MAPID.0, b"ac".to_vec(), b"2".to_vec()).unwrap();
+    dbtx.put(MAPID.0, b"aca".to_vec(), b"3".to_vec()).unwrap();
+    dbtx.put(MAPID.0, b"acb".to_vec(), b"4".to_vec()).unwrap();
+    dbtx.put(MAPID.0, b"b".to_vec(), b"5".to_vec()).unwrap();
     // Check db contents
     check_prefix(&dbtx, b"".to_vec(), &expected_full_0);
     check_prefix(&dbtx, b"aa".to_vec(), &expected_aa_0);
@@ -214,8 +215,8 @@ fn put_and_iterate_delete_some<B: Backend, F: BackendFn<B>>(backend_fn: Arc<F>)
 
     // Delete some entries
     let mut dbtx = store.transaction_rw(None).unwrap();
-    dbtx.del(IDX.0, b"aca").unwrap();
-    dbtx.del(IDX.0, b"ab").unwrap();
+    dbtx.del(MAPID.0, b"aca").unwrap();
+    dbtx.del(MAPID.0, b"ab").unwrap();
     // Check updated contents
     check_prefix(&dbtx, b"".to_vec(), &expected_full_1);
     check_prefix(&dbtx, b"aa".to_vec(), &expected_aa_1);
@@ -232,8 +233,8 @@ fn put_and_iterate_delete_some<B: Backend, F: BackendFn<B>>(backend_fn: Arc<F>)
 
     // Delete the items, this time for real
     let mut dbtx = store.transaction_rw(None).unwrap();
-    dbtx.del(IDX.0, b"aca").unwrap();
-    dbtx.del(IDX.0, b"ab").unwrap();
+    dbtx.del(MAPID.0, b"aca").unwrap();
+    dbtx.del(MAPID.0, b"ab").unwrap();
     // Check updated contents
     check_prefix(&dbtx, b"".to_vec(), &expected_full_1);
     check_prefix(&dbtx, b"aa".to_vec(), &expected_aa_1);
diff --git a/storage/backend-test-suite/src/concurrent.rs b/storage/backend-test-suite/src/concurrent.rs
index 81115084d..efc8196c9 100644
--- a/storage/backend-test-suite/src/concurrent.rs
+++ b/storage/backend-test-suite/src/concurrent.rs
@@ -21,7 +21,7 @@ fn setup<B: Backend>(backend: B, init: Vec<u8>) -> B::Impl {
     let store = backend.open(desc(1)).expect("db open to succeed");
 
     let mut dbtx = store.transaction_rw(None).unwrap();
-    dbtx.put(IDX.0, TEST_KEY.to_vec(), init).unwrap();
+    dbtx.put(MAPID.0, TEST_KEY.to_vec(), init).unwrap();
     dbtx.commit().unwrap();
 
     store
@@ -34,14 +34,14 @@ fn read_initialize_race<B: Backend, F: BackendFn<B>>(backend_fn: Arc<F>) {
         let store = store.clone();
         move || {
             let mut dbtx = store.transaction_rw(None).unwrap();
-            dbtx.put(IDX.0, TEST_KEY.to_vec(), vec![2]).unwrap();
+            dbtx.put(MAPID.0, TEST_KEY.to_vec(), vec![2]).unwrap();
             dbtx.commit().unwrap();
         }
     });
 
     let dbtx = store.transaction_ro().unwrap();
     let expected = [None, Some([2].as_ref())];
-    assert!(expected.contains(&dbtx.get(IDX.0, TEST_KEY).unwrap().as_ref().map(|v| v.as_ref())));
+    assert!(expected.contains(&dbtx.get(MAPID.0, TEST_KEY).unwrap().as_ref().map(|v| v.as_ref())));
     drop(dbtx);
 
     thr0.join().unwrap();
@@ -54,7 +54,7 @@ fn read_write_race<B: Backend, F: BackendFn<B>>(backend_fn: Arc<F>) {
         let store = store.clone();
         move || {
             let mut dbtx = store.transaction_rw(None).unwrap();
-            dbtx.put(IDX.0, TEST_KEY.to_vec(), vec![2]).unwrap();
+            dbtx.put(MAPID.0, TEST_KEY.to_vec(), vec![2]).unwrap();
             dbtx.commit().unwrap();
         }
     });
@@ -62,7 +62,7 @@ fn read_write_race<B: Backend, F: BackendFn<B>>(backend_fn: Arc<F>) {
     let dbtx = store.transaction_ro().unwrap();
     let expected = [[0u8].as_ref(), [2].as_ref()];
     assert!(expected
-        .contains(&dbtx.get(IDX.0, TEST_KEY).unwrap().as_ref().map(|v| v.as_ref()).unwrap()));
+        .contains(&dbtx.get(MAPID.0, TEST_KEY).unwrap().as_ref().map(|v| v.as_ref()).unwrap()));
     drop(dbtx);
 
     thr0.join().unwrap();
@@ -75,9 +75,9 @@ fn commutative_read_modify_write<B: Backend, F: BackendFn<B>>(backend_fn: Arc<F>
         let store = store.clone();
         move || {
             let mut dbtx = store.transaction_rw(None).unwrap();
-            let v = dbtx.get(IDX.0, TEST_KEY).unwrap().unwrap();
+            let v = dbtx.get(MAPID.0, TEST_KEY).unwrap().unwrap();
             let b = v.first().unwrap();
-            dbtx.put(IDX.0, TEST_KEY.to_vec(), vec![b + 5]).unwrap();
+            dbtx.put(MAPID.0, TEST_KEY.to_vec(), vec![b + 5]).unwrap();
             dbtx.commit().unwrap();
         }
     });
@@ -85,9 +85,9 @@ fn commutative_read_modify_write<B: Backend, F: BackendFn<B>>(backend_fn: Arc<F>
         let store = store.clone();
         move || {
             let mut dbtx = store.transaction_rw(None).unwrap();
-            let v = dbtx.get(IDX.0, TEST_KEY).unwrap().unwrap();
+            let v = dbtx.get(MAPID.0, TEST_KEY).unwrap().unwrap();
             let b = v.first().unwrap();
-            dbtx.put(IDX.0, TEST_KEY.to_vec(), vec![b + 3]).unwrap();
+            dbtx.put(MAPID.0, TEST_KEY.to_vec(), vec![b + 3]).unwrap();
             dbtx.commit().unwrap();
         }
     });
@@ -97,7 +97,7 @@ fn commutative_read_modify_write<B: Backend, F: BackendFn<B>>(backend_fn: Arc<F>
 
     let dbtx = store.transaction_ro().unwrap();
     assert_eq!(
-        dbtx.get(IDX.0, TEST_KEY).unwrap().as_ref().map(|v| v.as_ref()).unwrap(),
+        dbtx.get(MAPID.0, TEST_KEY).unwrap().as_ref().map(|v| v.as_ref()).unwrap(),
         [8].as_ref()
     );
 }
@@ -112,7 +112,7 @@ fn threaded_reads_consistent<B: Backend, F: BackendFn<B>>(backend_fn: Arc<F>) {
             store
                 .transaction_ro()
                 .unwrap()
-                .get(IDX.0, TEST_KEY)
+                .get(MAPID.0, TEST_KEY)
                 .unwrap()
                 .unwrap()
                 .as_ref()
@@ -124,7 +124,7 @@ fn threaded_reads_consistent<B: Backend, F: BackendFn<B>>(backend_fn: Arc<F>) {
             store
                 .transaction_ro()
                 .unwrap()
-                .get(IDX.0, TEST_KEY)
+                .get(MAPID.0, TEST_KEY)
                 .unwrap()
                 .unwrap()
                 .as_ref()
@@ -143,7 +143,7 @@ fn write_different_keys_and_iterate<B: Backend, F: BackendFn<B>>(backend_fn: Arc
         let store = store.clone();
         move || {
             let mut dbtx = store.transaction_rw(None).unwrap();
-            dbtx.put(IDX.0, vec![0x01], vec![0xf1]).unwrap();
+            dbtx.put(MAPID.0, vec![0x01], vec![0xf1]).unwrap();
             dbtx.commit().unwrap();
         }
     });
@@ -151,7 +151,7 @@ fn write_different_keys_and_iterate<B: Backend, F: BackendFn<B>>(backend_fn: Arc
         let store = store.clone();
         move || {
             let mut dbtx = store.transaction_rw(None).unwrap();
-            dbtx.put(IDX.0, vec![0x02], vec![0xf2]).unwrap();
+            dbtx.put(MAPID.0, vec![0x02], vec![0xf2]).unwrap();
             dbtx.commit().unwrap();
         }
     });
@@ -160,7 +160,7 @@ fn write_different_keys_and_iterate<B: Backend, F: BackendFn<B>>(backend_fn: Arc
     thr1.join().unwrap();
 
     let dbtx = store.transaction_ro().unwrap();
-    let contents = dbtx.prefix_iter(IDX.0, vec![]).unwrap();
+    let contents = dbtx.prefix_iter(MAPID.0, vec![]).unwrap();
     let expected = [(vec![0x01], vec![0xf1]), (vec![0x02], vec![0xf2])];
     assert!(contents.eq(expected));
 }
diff --git a/storage/backend-test-suite/src/model.rs b/storage/backend-test-suite/src/model.rs
index 8a0118bbe..c234935ce 100644
--- a/storage/backend-test-suite/src/model.rs
+++ b/storage/backend-test-suite/src/model.rs
@@ -15,7 +15,7 @@
 
 //! A dead-simple reference implementation of some aspects of a storage backend
 
-use storage_core::{backend, Data, DbIndex};
+use storage_core::{backend, Data, DbMapId};
 
 /// A modifying action to apply to a backend or a model
 #[derive(Debug, PartialEq, Eq, Clone)]
@@ -38,19 +38,19 @@ impl WriteAction {
 
 pub trait ApplyActions {
     /// Apply a sequence of actions to a transaction
-    fn apply_actions(&mut self, idx: DbIndex, iter: impl Iterator<Item = WriteAction>) {
-        iter.for_each(|act| self.apply_action(idx, act));
+    fn apply_actions(&mut self, map_id: DbMapId, iter: impl Iterator<Item = WriteAction>) {
+        iter.for_each(|act| self.apply_action(map_id, act));
     }
 
     /// Apply one action to a transaction
-    fn apply_action(&mut self, idx: DbIndex, action: WriteAction);
+    fn apply_action(&mut self, map_id: DbMapId, action: WriteAction);
 }
 
 impl<T: backend::WriteOps> ApplyActions for T {
-    fn apply_action(&mut self, idx: DbIndex, action: WriteAction) {
+    fn apply_action(&mut self, map_id: DbMapId, action: WriteAction) {
         match action {
-            WriteAction::Put(key, val) => self.put(idx, key, val).expect("put to succeed"),
-            WriteAction::Del(key) => self.del(idx, key.as_ref()).expect("del to succeed"),
+            WriteAction::Put(key, val) => self.put(map_id, key, val).expect("put to succeed"),
+            WriteAction::Del(key) => self.del(map_id, key.as_ref()).expect("del to succeed"),
         }
     }
 }
@@ -73,14 +73,14 @@ impl Model {
     }
 
     /// New model obtained by dumping a database
-    pub fn from_db<B: backend::BackendImpl>(storage: &B, idx: DbIndex) -> Self {
+    pub fn from_db<B: backend::BackendImpl>(storage: &B, map_id: DbMapId) -> Self {
         let dbtx = storage.transaction_ro().unwrap();
-        Self::from_tx(&dbtx, idx)
+        Self::from_tx(&dbtx, map_id)
     }
 
     /// New model obtained by dumping a database in a transaction. May contain uncommitted changes.
-    pub fn from_tx<Tx: for<'tx> backend::PrefixIter<'tx>>(tx: &Tx, idx: DbIndex) -> Self {
-        Model(backend::PrefixIter::prefix_iter(tx, idx, Data::new()).unwrap().collect())
+    pub fn from_tx<Tx: for<'tx> backend::PrefixIter<'tx>>(tx: &Tx, map_id: DbMapId) -> Self {
+        Model(backend::PrefixIter::prefix_iter(tx, map_id, Data::new()).unwrap().collect())
     }
 
     /// Get the inner map
diff --git a/storage/backend-test-suite/src/prelude.rs b/storage/backend-test-suite/src/prelude.rs
index d7cdbaf82..8fbd06346 100644
--- a/storage/backend-test-suite/src/prelude.rs
+++ b/storage/backend-test-suite/src/prelude.rs
@@ -19,7 +19,7 @@ pub use storage_core::{
     backend::{
         Backend, Data, PrefixIter, ReadOps, TransactionalRo, TransactionalRw, TxRo, TxRw, WriteOps,
     },
-    info::{self, DbDesc, DbIndex, MapDesc},
+    DbDesc, DbMapCount, DbMapDesc, DbMapId, DbMapsData,
 };
 pub use utils::{sync, thread};
 
@@ -29,12 +29,12 @@ pub use std::{mem::drop, sync::Arc};
 pub trait BackendFn<B: Backend>: 'static + Fn() -> B + Send + Sync {}
 impl<B: Backend, F: 'static + Fn() -> B + Send + Sync> BackendFn<B> for F {}
 
-/// A couple of DB index constants
-pub const IDX: (DbIndex, DbIndex) = (DbIndex::new(0), DbIndex::new(1));
+/// A couple of DB map ID constants
+pub const MAPID: (DbMapId, DbMapId) = (DbMapId::new(0), DbMapId::new(1));
 
 /// Sample database description with `n` maps
 pub fn desc(n: usize) -> DbDesc {
-    (0..n).map(|x| MapDesc::new(format!("map_{x:02}"))).collect()
+    storage_core::types::construct::db_desc((0..n).map(|i| DbMapDesc::new(format!("map_{i:02}"))))
 }
 
 /// Run tests with backend using proptest
diff --git a/storage/backend-test-suite/src/property.rs b/storage/backend-test-suite/src/property.rs
index 5a49df4e1..4f6a97c77 100644
--- a/storage/backend-test-suite/src/property.rs
+++ b/storage/backend-test-suite/src/property.rs
@@ -22,17 +22,17 @@ use proptest::prelude::Strategy;
 mod gen {
     use super::WriteAction;
     pub use proptest::prelude::*;
-    use storage_core::{Data, DbIndex};
+    use storage_core::{Data, DbMapId};
 
-    pub fn idx(num_dbs: usize) -> impl Strategy<Value = DbIndex> {
-        (0..num_dbs).prop_map(DbIndex::new)
+    pub fn map_id(num_dbs: usize) -> impl Strategy<Value = DbMapId> {
+        (0..num_dbs).prop_map(DbMapId::new)
     }
 
     pub fn entries(
         num_dbs: usize,
         num_entries: impl Into<proptest::collection::SizeRange>,
-    ) -> impl Strategy<Value = std::collections::BTreeMap<(DbIndex, Data), Data>> {
-        proptest::collection::btree_map((idx(num_dbs), big_key()), any::<Data>(), num_entries)
+    ) -> impl Strategy<Value = std::collections::BTreeMap<(DbMapId, Data), Data>> {
+        proptest::collection::btree_map((map_id(num_dbs), big_key()), any::<Data>(), num_entries)
     }
 
     // Generate key from a set of keys with given cardinality. Lower cardinality encourages
@@ -72,44 +72,44 @@ fn overwrite_and_abort<B: Backend, F: BackendFn<B>>(backend_fn: Arc<F>) {
 
             // Check the store returns None for given key initially
             let dbtx = store.transaction_ro().unwrap();
-            assert_eq!(dbtx.get(IDX.0, key.as_ref()), Ok(None));
+            assert_eq!(dbtx.get(MAPID.0, key.as_ref()), Ok(None));
             drop(dbtx);
 
             // Create a transaction, put the value in the storage and commit
             let mut dbtx = store.transaction_rw(None).unwrap();
-            dbtx.put(IDX.0, key.clone(), val0.clone()).unwrap();
+            dbtx.put(MAPID.0, key.clone(), val0.clone()).unwrap();
             dbtx.commit().expect("commit to succeed");
 
             // Check the values are in place
             let dbtx = store.transaction_ro().unwrap();
             assert_eq!(
-                dbtx.get(IDX.0, key.as_ref()).unwrap().as_ref().map(|v| v.as_ref()).unwrap(),
+                dbtx.get(MAPID.0, key.as_ref()).unwrap().as_ref().map(|v| v.as_ref()).unwrap(),
                 val0.as_ref() as &[u8]
             );
             drop(dbtx);
 
             // Create a transaction, modify storage and abort
             let mut dbtx = store.transaction_rw(None).unwrap();
-            dbtx.put(IDX.0, key.clone(), val1.clone()).unwrap();
+            dbtx.put(MAPID.0, key.clone(), val1.clone()).unwrap();
             drop(dbtx);
 
             // Check the store still contains the original value
             let dbtx = store.transaction_ro().unwrap();
             assert_eq!(
-                dbtx.get(IDX.0, key.as_ref()).unwrap().as_ref().map(|v| v.as_ref()).unwrap(),
+                dbtx.get(MAPID.0, key.as_ref()).unwrap().as_ref().map(|v| v.as_ref()).unwrap(),
                 val0.as_ref() as &[u8]
             );
             drop(dbtx);
 
             // Create a transaction, overwrite the value and commit
             let mut dbtx = store.transaction_rw(None).unwrap();
-            dbtx.put(IDX.0, key.clone(), val1.clone()).unwrap();
+            dbtx.put(MAPID.0, key.clone(), val1.clone()).unwrap();
             dbtx.commit().expect("commit to succeed");
 
             // Check the key now stores the new value
             let dbtx = store.transaction_ro().unwrap();
             assert_eq!(
-                dbtx.get(IDX.0, key.as_ref()).unwrap().as_ref().map(|v| v.as_ref()).unwrap(),
+                dbtx.get(MAPID.0, key.as_ref()).unwrap().as_ref().map(|v| v.as_ref()).unwrap(),
                 val1.as_ref() as &[u8]
             );
             drop(dbtx);
@@ -175,13 +175,13 @@ fn last_write_wins<B: Backend, F: BackendFn<B>>(backend_fn: Arc<F>) {
             // Add all entries to the database
             let mut dbtx = store.transaction_rw(None).unwrap();
             for val in vals.into_iter() {
-                dbtx.put(IDX.0, key.clone(), val).unwrap();
+                dbtx.put(MAPID.0, key.clone(), val).unwrap();
             }
             dbtx.commit().unwrap();
 
             let dbtx = store.transaction_ro().unwrap();
             assert_eq!(
-                dbtx.get(IDX.0, key.as_ref()).unwrap().as_ref().map(|v| v.as_ref()),
+                dbtx.get(MAPID.0, key.as_ref()).unwrap().as_ref().map(|v| v.as_ref()),
                 last.as_deref()
             );
         },
@@ -196,7 +196,7 @@ fn add_and_delete_some<B: Backend, F: BackendFn<B>>(backend_fn: Arc<F>) {
         (
             gen::entries(NUM_DBS, 0usize..20),
             gen::entries(NUM_DBS, 0usize..20),
-            proptest::collection::vec((gen::idx(NUM_DBS), gen::big_key()), 0usize..10),
+            proptest::collection::vec((gen::map_id(NUM_DBS), gen::big_key()), 0usize..10),
         ),
         |backend, (entries1, entries2, extra_keys)| {
             let store = backend.open(desc(NUM_DBS)).expect("db open to succeed");
@@ -259,9 +259,9 @@ fn add_modify_abort_modify_commit<B: Backend, F: BackendFn<B>>(backend_fn: Arc<F
 
             // Pre-populate the db with initial data, check the contents against the model
             let mut dbtx = store.transaction_rw(None).unwrap();
-            dbtx.apply_actions(IDX.0, to_prepopulate.into_iter());
+            dbtx.apply_actions(MAPID.0, to_prepopulate.into_iter());
             dbtx.commit().unwrap();
-            assert_eq!(model, Model::from_db(&store, IDX.0));
+            assert_eq!(model, Model::from_db(&store, MAPID.0));
 
             // Apply another set of changes but abort the transaction
             let tx_model = {
@@ -270,10 +270,10 @@ fn add_modify_abort_modify_commit<B: Backend, F: BackendFn<B>>(backend_fn: Arc<F
                 tx_model
             };
             let mut dbtx = store.transaction_rw(None).unwrap();
-            dbtx.apply_actions(IDX.0, to_abort.into_iter());
-            assert_eq!(tx_model, Model::from_tx(&dbtx, IDX.0));
+            dbtx.apply_actions(MAPID.0, to_abort.into_iter());
+            assert_eq!(tx_model, Model::from_tx(&dbtx, MAPID.0));
             drop(dbtx);
-            assert_eq!(model, Model::from_db(&store, IDX.0));
+            assert_eq!(model, Model::from_db(&store, MAPID.0));
 
             // Apply a different set of operations, commit, check they have been performed
             let model = {
@@ -282,9 +282,9 @@ fn add_modify_abort_modify_commit<B: Backend, F: BackendFn<B>>(backend_fn: Arc<F
                 model
             };
             let mut dbtx = store.transaction_rw(None).unwrap();
-            dbtx.apply_actions(IDX.0, to_commit.into_iter());
+            dbtx.apply_actions(MAPID.0, to_commit.into_iter());
             dbtx.commit().unwrap();
-            assert_eq!(model, Model::from_db(&store, IDX.0));
+            assert_eq!(model, Model::from_db(&store, MAPID.0));
         },
     )
 }
@@ -299,23 +299,23 @@ fn add_modify_abort_replay_commit<B: Backend, F: BackendFn<B>>(backend_fn: Arc<F
 
             // Pre-populate the db with initial data, check the contents against the model
             let mut dbtx = store.transaction_rw(None).unwrap();
-            dbtx.apply_actions(IDX.0, initial.into_iter());
+            dbtx.apply_actions(MAPID.0, initial.into_iter());
             dbtx.commit().unwrap();
 
-            let initial_model = Model::from_db(&store, IDX.0);
+            let initial_model = Model::from_db(&store, MAPID.0);
 
             // Apply another set of changes but abort the transaction, check nothing changed
             let mut dbtx = store.transaction_rw(None).unwrap();
-            dbtx.apply_actions(IDX.0, actions.clone().into_iter());
-            let modified_model = Model::from_tx(&dbtx, IDX.0);
+            dbtx.apply_actions(MAPID.0, actions.clone().into_iter());
+            let modified_model = Model::from_tx(&dbtx, MAPID.0);
             drop(dbtx);
-            assert_eq!(Model::from_db(&store, IDX.0), initial_model);
+            assert_eq!(Model::from_db(&store, MAPID.0), initial_model);
 
             // Apply the same changes again, and check that we get to the same state after commit
             let mut dbtx = store.transaction_rw(None).unwrap();
-            dbtx.apply_actions(IDX.0, actions.into_iter());
+            dbtx.apply_actions(MAPID.0, actions.into_iter());
             dbtx.commit().unwrap();
-            assert_eq!(modified_model, Model::from_db(&store, IDX.0));
+            assert_eq!(modified_model, Model::from_db(&store, MAPID.0));
         },
     )
 }
@@ -330,17 +330,17 @@ fn db_writes_do_not_interfere<B: Backend, F: BackendFn<B>>(backend_fn: Arc<F>) {
 
             // Apply one set of operations to key-value map 0
             let mut dbtx = store.transaction_rw(None).unwrap();
-            dbtx.apply_actions(IDX.0, actions0.into_iter());
+            dbtx.apply_actions(MAPID.0, actions0.into_iter());
             dbtx.commit().unwrap();
-            let model = Model::from_db(&store, IDX.0);
+            let model = Model::from_db(&store, MAPID.0);
 
             // Apply another set of operations to key-value map 1
             let mut dbtx = store.transaction_rw(None).unwrap();
-            dbtx.apply_actions(IDX.1, actions1.into_iter());
+            dbtx.apply_actions(MAPID.1, actions1.into_iter());
             dbtx.commit().unwrap();
 
             // The values in key-value map 0 should remain untouched by the second set of changes
-            assert_eq!(model, Model::from_db(&store, IDX.0));
+            assert_eq!(model, Model::from_db(&store, MAPID.0));
         },
     )
 }
@@ -359,10 +359,10 @@ fn empty_after_abort<B: Backend, F: BackendFn<B>>(backend_fn: Arc<F>) {
             // Apply one set of operations to key-value map 0
             let model = Model::from_actions(actions.clone());
             let mut dbtx = store.transaction_rw(None).unwrap();
-            dbtx.apply_actions(IDX.0, actions.into_iter());
+            dbtx.apply_actions(MAPID.0, actions.into_iter());
             for key in &keys {
                 assert_eq!(
-                    dbtx.get(IDX.0, key).unwrap().as_ref().map(|v| v.as_ref()),
+                    dbtx.get(MAPID.0, key).unwrap().as_ref().map(|v| v.as_ref()),
                     model.get(key)
                 );
             }
@@ -370,7 +370,7 @@ fn empty_after_abort<B: Backend, F: BackendFn<B>>(backend_fn: Arc<F>) {
 
             let dbtx = store.transaction_ro().unwrap();
             for key in &keys {
-                assert_eq!(dbtx.get(IDX.0, key), Ok(None));
+                assert_eq!(dbtx.get(MAPID.0, key), Ok(None));
             }
         },
     )
@@ -397,40 +397,40 @@ fn prefix_iteration<B: Backend, F: BackendFn<B>>(backend_fn: Arc<F>) {
 
             // Populate the database
             let mut dbtx = store.transaction_rw(None).unwrap();
-            dbtx.apply_actions(IDX.0, actions_a.iter().chain(actions_b.iter()).cloned());
+            dbtx.apply_actions(MAPID.0, actions_a.iter().chain(actions_b.iter()).cloned());
             dbtx.commit().unwrap();
 
             // Check iteration over keys prefixed "a"
             let model_a = Model::from_actions(actions_a);
             let dbtx = store.transaction_ro().unwrap();
-            let iter_a = dbtx.prefix_iter(IDX.0, vec![b'a']).unwrap();
+            let iter_a = dbtx.prefix_iter(MAPID.0, vec![b'a']).unwrap();
             assert!(model_a.into_iter().eq(iter_a));
             drop(dbtx);
 
             // Check iteration over keys prefixed "b"
             let model_b = Model::from_actions(actions_b);
             let dbtx = store.transaction_ro().unwrap();
-            let iter_b = dbtx.prefix_iter(IDX.0, vec![b'b']).unwrap();
+            let iter_b = dbtx.prefix_iter(MAPID.0, vec![b'b']).unwrap();
             assert!(model_b.into_iter().eq(iter_b));
             drop(dbtx);
 
             // Check there are no entries prefixed "c"
             let dbtx = store.transaction_ro().unwrap();
-            assert_eq!(dbtx.prefix_iter(IDX.0, vec![b'c']).unwrap().next(), None);
+            assert_eq!(dbtx.prefix_iter(MAPID.0, vec![b'c']).unwrap().next(), None);
             drop(dbtx);
 
             // Take all entries prefixed "a" and remove them
             let mut dbtx = store.transaction_rw(None).unwrap();
             let keys_a: Vec<_> =
-                dbtx.prefix_iter(IDX.0, vec![b'a']).unwrap().map(|(k, _)| k).collect();
+                dbtx.prefix_iter(MAPID.0, vec![b'a']).unwrap().map(|(k, _)| k).collect();
             for key in keys_a {
-                dbtx.del(IDX.0, &key).unwrap();
+                dbtx.del(MAPID.0, &key).unwrap();
             }
             dbtx.commit().unwrap();
 
             // Check there are no entries prefixed "a"
             let dbtx = store.transaction_ro().unwrap();
-            assert_eq!(dbtx.prefix_iter(IDX.0, vec![b'a']).unwrap().next(), None);
+            assert_eq!(dbtx.prefix_iter(MAPID.0, vec![b'a']).unwrap().next(), None);
             drop(dbtx);
         },
     )
@@ -446,13 +446,13 @@ fn post_commit_consistency<B: Backend, F: BackendFn<B>>(backend_fn: Arc<F>) {
             let store = backend.open(desc(1)).expect("db open to succeed");
 
             let mut dbtx = store.transaction_rw(None).unwrap();
-            dbtx.apply_actions(IDX.0, actions.into_iter());
-            let model = Model::from_tx(&dbtx, IDX.0);
+            dbtx.apply_actions(MAPID.0, actions.into_iter());
+            let model = Model::from_tx(&dbtx, MAPID.0);
             dbtx.commit().unwrap();
 
             // The state from the transaction just before committing should de the same as the
             // state of the database after the commit.
-            assert_eq!(Model::from_db(&store, IDX.0), model);
+            assert_eq!(Model::from_db(&store, MAPID.0), model);
         },
     )
 }
diff --git a/storage/lmdb/src/resize_tests.rs b/storage/lmdb/src/resize_tests.rs
index ea42a45ee..7c2b267b1 100644
--- a/storage/lmdb/src/resize_tests.rs
+++ b/storage/lmdb/src/resize_tests.rs
@@ -82,7 +82,8 @@ fn auto_map_resize_between_txs(#[case] seed: Seed) {
             MapResizeCallback::new(resize_callback),
         );
 
-        let lmdb_impl = lmdb.open(DbDesc::from_iter(vec![MapDesc::new("SomeDb")])).unwrap();
+        let desc = storage_core::types::construct::db_desc([DbMapDesc::new("SomeDb")].into_iter());
+        let lmdb_impl = lmdb.open(desc).unwrap();
 
         // generate random values with a predefined target size that surpasses the current map size
         let data = create_random_data_map_with_target_byte_size(
@@ -97,7 +98,7 @@ fn auto_map_resize_between_txs(#[case] seed: Seed) {
         // write many key/val values, and while they're being written, expect that database map will grow
         for (key, val) in &data {
             let mut rw_tx = lmdb_impl.transaction_rw(None).unwrap();
-            rw_tx.put(DbIndex::new(0), key.clone(), val.clone()).unwrap();
+            rw_tx.put(DbMapId::new(0), key.clone(), val.clone()).unwrap();
             match rw_tx.commit() {
                 Ok(_) => resizes_via_commit_count += 1,
                 Err(e) => panic!("Failed to commit: {e:?}"),
@@ -115,7 +116,7 @@ fn auto_map_resize_between_txs(#[case] seed: Seed) {
         // ensure data is successfully written
         let ro_tx = lmdb_impl.transaction_ro().unwrap();
         for (key, val) in data {
-            assert_eq!(ro_tx.get(DbIndex::new(0), &key).unwrap().unwrap(), val);
+            assert_eq!(ro_tx.get(DbMapId::new(0), &key).unwrap().unwrap(), val);
         }
 
         assert!(resizes_via_commit_count > 0, "Not a single resize was scheduled after a transaction... this is very unlikely based on the test structure");
@@ -152,7 +153,8 @@ fn auto_map_resize_between_puts(#[case] seed: Seed) {
             MapResizeCallback::new(resize_callback),
         );
 
-        let lmdb_impl = lmdb.open(DbDesc::from_iter(vec![MapDesc::new("SomeDb")])).unwrap();
+        let desc = storage_core::types::construct::db_desc([DbMapDesc::new("SomeDb")].into_iter());
+        let lmdb_impl = lmdb.open(desc).unwrap();
 
         // generate random values with a predefined target size that surpasses the current map size
         let data = create_random_data_map_with_target_byte_size(
@@ -176,7 +178,7 @@ fn auto_map_resize_between_puts(#[case] seed: Seed) {
             let mut data_written_in_this_cycle = Vec::new();
             while !data_stack.is_empty() {
                 let (key, val) = data_stack.last().unwrap();
-                match rw_tx.put(DbIndex::new(0), key.clone(), val.clone()) {
+                match rw_tx.put(DbMapId::new(0), key.clone(), val.clone()) {
                     Ok(_) => {
                         // on success, we continue writing and take record of that
                         data_written_in_this_cycle.push(data_stack.pop().unwrap());
@@ -209,7 +211,7 @@ fn auto_map_resize_between_puts(#[case] seed: Seed) {
         // ensure data is successfully written
         let ro_tx = lmdb_impl.transaction_ro().unwrap();
         for (key, val) in data {
-            assert_eq!(ro_tx.get(DbIndex::new(0), &key).unwrap().unwrap(), val);
+            assert_eq!(ro_tx.get(DbMapId::new(0), &key).unwrap().unwrap(), val);
         }
 
         assert!(