From 5ecce49feb55c6bcd3679ca39a8e6558e2b50796 Mon Sep 17 00:00:00 2001 From: Aliaksandr Kantsevoi Date: Thu, 16 Jan 2025 15:49:02 +0100 Subject: [PATCH 1/5] maroon-storage definition. v0.0.1 An attempt to define function of maroon-storage and raise some questions about internal parts. Also sketched a bit a configuration of storages(how crazy it looks?) --- ...tor. maroon-storage function definition.md | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 migrator. maroon-storage function definition.md diff --git a/migrator. maroon-storage function definition.md b/migrator. maroon-storage function definition.md new file mode 100644 index 0000000..65b0496 --- /dev/null +++ b/migrator. maroon-storage function definition.md @@ -0,0 +1,140 @@ +- [!] Right now I have some problem to clearly separate maroon-engine itself and storage layer. So sometimes I use words migrator-storage/maroon-storage/maroon-engine interchangeably, it is not always clear yet where is the storage function and where is it execution engine function. **Keep that in mind while reading** + +## Problem +- provide consistent(invariants satisfaction) and durable storage built on top of different storage solutions presented in the organization + - we need to solve that problem in order to open a possibility to write simpler/durable code/logic/scenarios/etc (maroon-engine) + +## Function +- maroon-storage works as a black box for (business-logic developer)::role +- can use various repositories + - repository - external DB or any other storage that supports some API and provides some guarantees (some - TBD) +- support ACID transactions + - A + - all updates are done or none + - C + - invariants are satisfied(across different repositories) + - I + - since we'll have sequential read/writes - it's not relevant for migrator + - [!] fine only until we're "single-threaded" + - D + - set of data as durable as repository is durable(if some repository owns some subset of data - we're limited by this repository's durability) +- admin::role can configure maroon-storage to use different repositories + - we need to clearly specify external repositories as our direction right now is to keep the customers data in their DBs + - different or the same types of objects can live in the same or different repositories. Examples: + - all the data in one storage + - part of users live in mongo and part in postgresql and some special users can be created only in that AzureDB in that region due to regulations or whatever + - admin can also add/remove repositories at "any" time + - "any" - of course not any time, but almost. Limitations TBD + - admin can change the parameters of repositories and maroon-engine will migrate the data between repositories accordingly +- takes an exclusive rights on read/write operations in all connected repositories + - all the traffic goes through the maroon-engine +- supported operations for the user + - read object of a type by id + - update(whole object or some fields) object of a type by id + - create object of a type with id + - autogenerated id + - query by some query parameters + +## Implementation thoughts +### Configuration language +- declarative approach +- strong type system + +```python +storage( + types( + User( + v1( + id int + name string + country string + ), + v2( + id int + name string + country string + active bool + ) + ) + ) + repositories ( + mongodb( + id: "eu-users-repository", + connectionParams: {bla bla}, + holdTypes( + User.v1( + id -> users.id, + name -> users.name, + country -> users.country, + ), + User.v2( + id -> users.id, + name -> users.name, + country -> users.country, + active -> users.active + ), + ) + ), + postgresql( + id: "eu-users-repository-new", // imagine that we're migrating users from mongodb to postgres(unified storing approach), but it still should be in some EU-based DC + connectionParams: {...}, + holdTypes( + User.v1( + id -> users.id, + name -> users.name, + country -> users_meta.country (foreing_key: users.id), + ), + User.v2( + id -> users.id, + name -> users.name, + country -> users_meta.country (foreing_key: users.id), + active -> users.active, + ), + ) + ), + postgresql( + id: "us-users-repository", + connectionParams: {...}, + holdTypes( + User.v1( + id -> users.id, + name -> users.name, + country -> users_meta.country (foreing_key: users.id), + ), + User.v2( + id -> users.id, + name -> users.name, + country -> users_meta.country (foreing_key: users.id), + active -> users.active, + ), + ) + ) + ), + location_rules( + priority_migration( + "eu-users-repository" ==> "eu-users-repository-new" + ) + User.v1(country == "USA" => "us-users-repository"), + User.v2(country == "USA" => "us-users-repository"), + User.v1(country == "UK" => ["eu-users-repository", "eu-users-repository-new"]), + User.v2(country == "UK" => ["eu-users-repository", "eu-users-repository-new"]), + ) +) +``` + +### Internal structure + +two parts of migrator's maroon-storage: +- maroon-storage aka source-of-truth on each maroon node + - stores: + - id, type, version + - [?] what is id here? Is it internal maroon-storage's id? What about ids in repositories? Do we treat them as a piece of data or secondary id or what? + - hash(for last n versions and/or last n hours) + - logs of transition between versions: n and n-1 + - functions: + - indexes for querying? +- repositories + - any storage that can support the contract: + - R/W/U/D operations + - strong types (not abstract JSON storage) + - [?] do we really need this guarantee? Isn't enough to keep type information inside of maroon-storage? From 446e7e5790dc4c773f9ad6859026038170941722 Mon Sep 17 00:00:00 2001 From: Aliaksandr Kantsevoi Date: Thu, 16 Jan 2025 17:47:10 +0100 Subject: [PATCH 2/5] few comments --- ...tor. maroon-storage function definition.md | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/migrator. maroon-storage function definition.md b/migrator. maroon-storage function definition.md index 65b0496..b172d6e 100644 --- a/migrator. maroon-storage function definition.md +++ b/migrator. maroon-storage function definition.md @@ -2,7 +2,7 @@ ## Problem - provide consistent(invariants satisfaction) and durable storage built on top of different storage solutions presented in the organization - - we need to solve that problem in order to open a possibility to write simpler/durable code/logic/scenarios/etc (maroon-engine) + - we need to solve that problem in order to open a possibility to write simpler/durable code/logic/scenarios/etc (maroon-engine) ## Function - maroon-storage works as a black box for (business-logic developer)::role @@ -55,6 +55,19 @@ storage( country string active bool ) + migration( + // since new field is non-optional we need to add some code that can perform the transition between v1 and v2 + // underthehood engine will do the heavylifting: + // - introduce a new `active` optional field + // - starts updating value of the field + // - when finishes - it will move the column from optional to non-optional state + to_v2(obj: v1) -> v2 { + is_active := http.call.is_active(obj.id) + return v2{ + active: is_active, + v1...} + } + ) ) ) repositories ( @@ -63,7 +76,7 @@ storage( connectionParams: {bla bla}, holdTypes( User.v1( - id -> users.id, + id -> users.id, // mapping between in-memory object and table/field in a table datastore name -> users.name, country -> users.country, ), @@ -82,7 +95,7 @@ storage( User.v1( id -> users.id, name -> users.name, - country -> users_meta.country (foreing_key: users.id), + country -> users_meta.country (foreing_key: users.id), // compound object that lives in different tables ), User.v2( id -> users.id, @@ -112,6 +125,8 @@ storage( ), location_rules( priority_migration( + // in that case data will be slowly copied from one storage to another + // TODO: we need to have a requirement here that transformation should cover all the fields and it should be checked "eu-users-repository" ==> "eu-users-repository-new" ) User.v1(country == "USA" => "us-users-repository"), @@ -130,9 +145,11 @@ two parts of migrator's maroon-storage: - id, type, version - [?] what is id here? Is it internal maroon-storage's id? What about ids in repositories? Do we treat them as a piece of data or secondary id or what? - hash(for last n versions and/or last n hours) + - in case of - logs of transition between versions: n and n-1 - functions: - - indexes for querying? + - indexes for querying + - [?] how to query effectively the data from different repositories? (do we need to open a conversation of creating indexes in the maroon-storage?) - repositories - any storage that can support the contract: - R/W/U/D operations From f2f8b51b2a1b407001bc2a5c7c83017325aa2079 Mon Sep 17 00:00:00 2001 From: Aliaksandr Kantsevoi Date: Thu, 16 Jan 2025 17:51:58 +0100 Subject: [PATCH 3/5] comments --- ...tor. maroon-storage function definition.md | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/migrator. maroon-storage function definition.md b/migrator. maroon-storage function definition.md index b172d6e..7c02e95 100644 --- a/migrator. maroon-storage function definition.md +++ b/migrator. maroon-storage function definition.md @@ -1,3 +1,5 @@ +#durableExecution + - [!] Right now I have some problem to clearly separate maroon-engine itself and storage layer. So sometimes I use words migrator-storage/maroon-storage/maroon-engine interchangeably, it is not always clear yet where is the storage function and where is it execution engine function. **Keep that in mind while reading** ## Problem @@ -56,12 +58,13 @@ storage( active bool ) migration( - // since new field is non-optional we need to add some code that can perform the transition between v1 and v2 - // underthehood engine will do the heavylifting: - // - introduce a new `active` optional field - // - starts updating value of the field - // - when finishes - it will move the column from optional to non-optional state + # since new field is non-optional we need to add some code that can perform the transition between v1 and v2 + # underthehood engine will do the heavylifting: + # - introduce a new `active` optional field + # - starts updating value of the field + # - when finishes - it will move the column from optional to non-optional state to_v2(obj: v1) -> v2 { + # not very declarative. Other proposals how to solve that situation? is_active := http.call.is_active(obj.id) return v2{ active: is_active, @@ -89,13 +92,13 @@ storage( ) ), postgresql( - id: "eu-users-repository-new", // imagine that we're migrating users from mongodb to postgres(unified storing approach), but it still should be in some EU-based DC + id: "eu-users-repository-new", # imagine that we're migrating users from mongodb to postgres(unified storing approach), but it still should be in some EU-based DC connectionParams: {...}, holdTypes( User.v1( id -> users.id, name -> users.name, - country -> users_meta.country (foreing_key: users.id), // compound object that lives in different tables + country -> users_meta.country (foreing_key: users.id), # compound object that lives in different tables ), User.v2( id -> users.id, @@ -125,8 +128,8 @@ storage( ), location_rules( priority_migration( - // in that case data will be slowly copied from one storage to another - // TODO: we need to have a requirement here that transformation should cover all the fields and it should be checked + # in that case data will be slowly copied from one storage to another + # TODO: we need to have a requirement here that transformation should cover all the fields and it should be checked "eu-users-repository" ==> "eu-users-repository-new" ) User.v1(country == "USA" => "us-users-repository"), From 5b7b7424ab9165cf244c61240eee38e36f443089 Mon Sep 17 00:00:00 2001 From: Aliaksandr Kantsevoi Date: Thu, 16 Jan 2025 20:46:41 +0100 Subject: [PATCH 4/5] add indexes --- ...tor. maroon-storage function definition.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/migrator. maroon-storage function definition.md b/migrator. maroon-storage function definition.md index 7c02e95..c34e3db 100644 --- a/migrator. maroon-storage function definition.md +++ b/migrator. maroon-storage function definition.md @@ -1,5 +1,3 @@ -#durableExecution - - [!] Right now I have some problem to clearly separate maroon-engine itself and storage layer. So sometimes I use words migrator-storage/maroon-storage/maroon-engine interchangeably, it is not always clear yet where is the storage function and where is it execution engine function. **Keep that in mind while reading** ## Problem @@ -50,12 +48,20 @@ storage( id int name string country string + index ( + name: btree + ) ), v2( id int name string country string active bool + age int + index ( + name: btree # btree because we'll need to query ranges + age: hash # hash because we'll need to query exact values + ) ) migration( # since new field is non-optional we need to add some code that can perform the transition between v1 and v2 @@ -66,9 +72,14 @@ storage( to_v2(obj: v1) -> v2 { # not very declarative. Other proposals how to solve that situation? is_active := http.call.is_active(obj.id) - return v2{ + if age := http.call.age_of_user(obj.id); age != nil { + age = default_age + } + return v2{ # choose which fields to setup and which just copy from the old active: is_active, - v1...} + age: age, + v1... + } } ) ) From b50b7ae038eba0d66a5ee2c03e110f57f85b25cc Mon Sep 17 00:00:00 2001 From: Aliaksandr Kantsevoi Date: Fri, 24 Jan 2025 14:32:55 +0100 Subject: [PATCH 5/5] split info between files. Add example of migration --- framework-guarantees-example.md | 198 ++++++++++++++++++ maroon-runner.md | 5 + maroon-source-of-truth.md | 18 ++ maroon-storage.md | 27 +++ ...definition.md => maroong-storage-config.md | 79 ++----- repository.md | 13 ++ 6 files changed, 282 insertions(+), 58 deletions(-) create mode 100644 framework-guarantees-example.md create mode 100644 maroon-runner.md create mode 100644 maroon-source-of-truth.md create mode 100644 maroon-storage.md rename migrator. maroon-storage function definition.md => maroong-storage-config.md (56%) create mode 100644 repository.md diff --git a/framework-guarantees-example.md b/framework-guarantees-example.md new file mode 100644 index 0000000..582cb0e --- /dev/null +++ b/framework-guarantees-example.md @@ -0,0 +1,198 @@ +# guarantees on the framework usage level + +Let's discuss now guarantees that framework will provide for the user. For this I'll be using slightly simplier version of the storage config as in that example my focus would be on the UX for people who will write business-logic. + +[TODO:?]Our goals are: +1. to provide guarantees that running the code is always safe from data definition's perspective. Meaning: + - you'll have all required fields + - they will have the correct type +2. you don't need to perform annoying migrations yourself + +# Transition example + +Let's imagine we have users, they have name and family name. Our code does something and in the end takes name+family-name and sends some confirmation email. And for some reason - we want to get rid of name and family name and want to start using name-and-family-name field. So we need to change the schema, perform the migration, expose the fields, update the code, remove old fields, etc. + +## step 0 +This is what we have for [storage configuration](./maroong-storage-config.md) +```python +storage( + types( + User( + v1[active]( + id int + name str + family_name str + index ( + id: hash, + name: btree + ) + ), + ) + ) + repositories ( + postgresql( + id: "store-for-everything", + connectionParams: {...}, + holdTypes( + User.v1( + id -> users.id, + name -> users.name, + family_name -> users.family_name, + ), + ) + ) + ), + location_rules( + User.v1("store-for-everything"), + ) +) +``` + +this is the code we have on [maroon-runner](./maroon-runner.md) +```python +oltp_maroon { + maroon def (userID: str) { + # do some logic here + # will be executed durably, etc, bla bla + + + # send email to the user + var user = User(storage, index.hash==userID) + + send_email(full_name: user.name + ' ' + family_name) + } +} +``` + +## destination state +we want to add the field that combines name + family-name. And use it in our code + +```python +storage( + types( + User( + v1( + id int + name str + family_name str + index ( + id: hash, + name: btree, + ) + ), + v2[active]( + id int + full_name str # name + family_name + index ( + id: hash, + full_name: btree, + ) + ), + migration( + # what we want here - to leave user a possibility to traverse data back and forth + # in case if we need to roll-back the changes whatever the reason + + # constructor + v1_v2(obj: v1) -> v2 { + # side effects are allowed: idempotent! http/db/etc. calls + return v2{ + full_name: obj.name + ' ' + obj.family_name, + v1... + } + } + # restructor + v2_v1(obj: v2) -> v1 { + # side effects are not allowed + # we need this to populate previous versions + # when we create never versions + return v1{ + name: full_name.split(' ').first, + family_name: full_name.split(' ').second, + v2... + } + } + ) + ) + ) + repositories ( + postgresql( + id: "store-for-everything", + connectionParams: {...}, + holdTypes( + User.v1( + id -> users.id, + name -> users.name, + family_name -> users.family_name, + ), + User.v2( + id -> users.id, + full_name -> users.full_name, + ), + ) + ) + ), + location_rules( + User.v1("store-for-everything"), + User.v2("store-for-everything"), + ) +) +``` + +Keep in mind that at any particular point in time there is only one available type version. In the code you can't combine two versions + +```python +oltp_maroon { + maroon def (userID: str) { + # do some logic here + # will be executed durably, etc, bla bla + + + # send email to the user + var user = User(storage, index.hash==userID) + send_email(full_name: user.full_name) + } +} +``` + +## execution. Step by step(simplified) + +- we have type User_v1 active and used by the code on [maroon-runner](./maroon-runner.md) +- create new type with added field in configurator - [admin::role action] +- send new config to maroon-runner to verify - [admin::role action] +- rejected. Reasons + - no constructor-restructor + - no repositories updates for the new type + [TODO:?] do we want to ask admin/dev to provide that information explicitly? Probably that's ok for the first version. Later - would be nice to make these things in an automatic way + - no location_rules updates for the new type +- admin::role make udpates and sends it for the verification +- maroon-runner accepts the new config +- maroon-runner starts background migration job + - creates necessary indexes/columns/tables in repositories + - creates User_v2 type with the state [constructing] + - starts to populate data for v2 objects + - for each migrated version adds v2 to supported versions in [maroon-source-of-truth](./maroon-source-of-truth.md) + - v2 type is in the [constructing] state and v1 - [active] + - [!] if here dev::role tries to deploy the code that uses v2 - they get an error, because v2 is in [construct] state + - [!] here admin::role can see the migration progress + - exposed through some telemetry channel + - [TODO:?] admin panel is here? + - background migration process finished +- now v2 is in [available] state + - all the indexes created + - all the data is migrated +- now you can change the code and make v2 - active +- dev::role make changes: + - in the config v2 - becomes active, v1 - becomes available + - in the code - now it should use only v2 constructors, fields +- admin::role pushes the changes to maroon-runner +- maroon-runner checks the correctness and accept the changes +- we still have two types: v1 and v2 + - that means we still have info in DB for v1 and v2 and can switch any time we want + - it also means that when we create v2 object - we save enough data to create valid v1 object (by using v2_v1 restructor) + - so we can change v1 and v2 between active/availble state back and forth +- if we sure that we don't need v1 anymore we can perform "compaction" operation. That operation will: + - delete v1 type + - accept absence of constructor/restructor info for v1 + - accept absence of repositories and location_rules info for v1 + - starts background process of deleting not needed columns + - [!] it's a destructive operation. After applying it - we can't go between v1-v2 anymore. The information is lost \ No newline at end of file diff --git a/maroon-runner.md b/maroon-runner.md new file mode 100644 index 0000000..11b0bf7 --- /dev/null +++ b/maroon-runner.md @@ -0,0 +1,5 @@ +Entity that is reponsible for durable code execution: +- runs code +- saves checkpoint's states +- uses [maroon-storage](./maroon-storage.md) +- [TODO:?] replication module is here or it will be higher level abstraction? diff --git a/maroon-source-of-truth.md b/maroon-source-of-truth.md new file mode 100644 index 0000000..70745cf --- /dev/null +++ b/maroon-source-of-truth.md @@ -0,0 +1,18 @@ +- keeps + - k-v objects + - k - key of the object + - v + - hash(last n versions) + - type + - name + - supported type versions + - version + - log of transformations between different object's versions + - indexes (for quicker finding the right objects. Ex: which repository to ask) +- lives on maroon-nodes + - to quicker query + - quicker perform validations: which code we can/can't deploy depends on the types + +- [!] There is difference between type version and object version + - [TODO:?] do we need to keep type version for each object? Or it will be enough to know what's the current active type? + - looks like we need. We can use this information to allow active-type switching diff --git a/maroon-storage.md b/maroon-storage.md new file mode 100644 index 0000000..f87de0f --- /dev/null +++ b/maroon-storage.md @@ -0,0 +1,27 @@ +# Problem to solve +- provide consistent(invariants satisfaction) and durable storage built on top of different storage solutions presented in the organization + - we need to solve that problem in order to open a possibility to write simpler/durable code/logic/scenarios/etc (maroon-engine) + +## Function +- maroon-storage works as a black box for (business-logic developer)::role +- uses one logical instance of [source of truth](./maroon-source-of-truth.md) +- can use various(N > 0) [repositories](./repository.md) +- supports ACID transactions + - A + - all updates are done or none + - C + - invariants are satisfied(across different repositories) + - I + - since we'll have sequential read/writes - it's not relevant for migrator + - [!] fine only until we're "single-threaded" + - D + - set of data as durable as repository is durable(if some repository owns some subset of data - we're limited by this repository's durability) +- admin::role can [configure maroon-storage](./maroong-storage-config.md) to use different repositories +- takes an exclusive rights on read/write operations in all connected repositories + - all the traffic goes through the maroon-engine +- supported operations for the user + - read object of a type by id + - update(whole object or some fields) object of a type by id + - create object of a type with id + - autogenerated id + - query by some query parameters \ No newline at end of file diff --git a/migrator. maroon-storage function definition.md b/maroong-storage-config.md similarity index 56% rename from migrator. maroon-storage function definition.md rename to maroong-storage-config.md index c34e3db..dd09f44 100644 --- a/migrator. maroon-storage function definition.md +++ b/maroong-storage-config.md @@ -1,50 +1,22 @@ -- [!] Right now I have some problem to clearly separate maroon-engine itself and storage layer. So sometimes I use words migrator-storage/maroon-storage/maroon-engine interchangeably, it is not always clear yet where is the storage function and where is it execution engine function. **Keep that in mind while reading** +# Configuration language -## Problem -- provide consistent(invariants satisfaction) and durable storage built on top of different storage solutions presented in the organization - - we need to solve that problem in order to open a possibility to write simpler/durable code/logic/scenarios/etc (maroon-engine) - -## Function -- maroon-storage works as a black box for (business-logic developer)::role -- can use various repositories - - repository - external DB or any other storage that supports some API and provides some guarantees (some - TBD) -- support ACID transactions - - A - - all updates are done or none - - C - - invariants are satisfied(across different repositories) - - I - - since we'll have sequential read/writes - it's not relevant for migrator - - [!] fine only until we're "single-threaded" - - D - - set of data as durable as repository is durable(if some repository owns some subset of data - we're limited by this repository's durability) -- admin::role can configure maroon-storage to use different repositories - - we need to clearly specify external repositories as our direction right now is to keep the customers data in their DBs +- declarative approach +- strong type system +- indexes support + - [TODO:?] do we have indexes on [source of turth](./maroon-source-of-truth.md) level? Or indexes will be spreaded to [repositories](./repository.md) as well? Or these are two different indexes? +- we need to clearly specify external repositories as our direction right now is to keep the customers data in their DBs - different or the same types of objects can live in the same or different repositories. Examples: - all the data in one storage - part of users live in mongo and part in postgresql and some special users can be created only in that AzureDB in that region due to regulations or whatever - admin can also add/remove repositories at "any" time - "any" - of course not any time, but almost. Limitations TBD - admin can change the parameters of repositories and maroon-engine will migrate the data between repositories accordingly -- takes an exclusive rights on read/write operations in all connected repositories - - all the traffic goes through the maroon-engine -- supported operations for the user - - read object of a type by id - - update(whole object or some fields) object of a type by id - - create object of a type with id - - autogenerated id - - query by some query parameters - -## Implementation thoughts -### Configuration language -- declarative approach -- strong type system ```python storage( - types( + types( User( - v1( + v1[active]( id int name string country string @@ -69,7 +41,7 @@ storage( # - introduce a new `active` optional field # - starts updating value of the field # - when finishes - it will move the column from optional to non-optional state - to_v2(obj: v1) -> v2 { + v1_v2(obj: v1) -> v2 { # not very declarative. Other proposals how to solve that situation? is_active := http.call.is_active(obj.id) if age := http.call.age_of_user(obj.id); age != nil { @@ -80,7 +52,17 @@ storage( age: age, v1... } - } + }, + # restructor + # gets the previous version of the object + # doesn't allow side effects + # needed if we want to keep the possibility to roll back on severl versions + v2_v1(obj: v2) -> v1{ + return v1{ + v2... + } + } + ) ) ) @@ -149,23 +131,4 @@ storage( User.v2(country == "UK" => ["eu-users-repository", "eu-users-repository-new"]), ) ) -``` - -### Internal structure - -two parts of migrator's maroon-storage: -- maroon-storage aka source-of-truth on each maroon node - - stores: - - id, type, version - - [?] what is id here? Is it internal maroon-storage's id? What about ids in repositories? Do we treat them as a piece of data or secondary id or what? - - hash(for last n versions and/or last n hours) - - in case of - - logs of transition between versions: n and n-1 - - functions: - - indexes for querying - - [?] how to query effectively the data from different repositories? (do we need to open a conversation of creating indexes in the maroon-storage?) -- repositories - - any storage that can support the contract: - - R/W/U/D operations - - strong types (not abstract JSON storage) - - [?] do we really need this guarantee? Isn't enough to keep type information inside of maroon-storage? +``` \ No newline at end of file diff --git a/repository.md b/repository.md new file mode 100644 index 0000000..74f5db6 --- /dev/null +++ b/repository.md @@ -0,0 +1,13 @@ +Any DB with adapter that conforms a specific [adapter protocol]: +- base K-V operations: R/W/U/D + +The storage will be used mostly as a K-V store. +- [TODO:?] what does it mean for multi-level nested objects? + - do we want some advanced support for multi-level indexing, update, etc in general? + - do we still operate them as one object? + - or we just simply say: we have key, and we have everything else? + +Under the adapter there might be any configuration: +- simple SQLite storage +- complex, distributed, multi-master, bla-bla-bla configuration +- NoSQL/SQL \ No newline at end of file