diff --git a/doc/adr/2025-01-21-sql-access-to-pico-api.md b/doc/adr/2025-01-21-sql-access-to-pico-api.md
new file mode 100644
index 0000000000000000000000000000000000000000..76481c62e7e59409f6b1b7cbbea97142040e60e6
--- /dev/null
+++ b/doc/adr/2025-01-21-sql-access-to-pico-api.md
@@ -0,0 +1,473 @@
+<!--
+The template is not strict. You can use any ADR structure that feels best for a particular case.
+See these ADR examples for inspiration:
+- [Cassandra SEP - Ganeral Purpose Transactions](https://cwiki.apache.org/confluence/display/CASSANDRA/CEP-15%3A+General+Purpose+Transactions)
+- [Rust RFC - Lifetime Ellision](https://github.com/rust-lang/rfcs/blob/master/text/0141-lifetime-elision.md)
+- [TiKV - Use Joint Consensus](https://github.com/tikv/rfcs/blob/master/text/0054-joint-consensus.md)
+-->
+- status: "accepted"
+- decision-makers: Egor Ivkov, Konstantin Osipov
+- issue: https://git.picodata.io/core/picodata/-/issues/696
+
+<!--
+consulted: list everyone whose opinions are sought (typically subject-matter experts); and with whom there is a two-way communication
+informed: list everyone who is kept up-to-date on progress; and with whom there is a one-way communication
+-->
+
+--------------------------------
+
+# SQL access to pico. API
+
+## Context and Problem Statement
+
+### Background
+The Picodata API exposes administrative and operational functions (e.g., Raft state management, configuration, plugin operations) that currently require direct access to the picodata admin tool. To reduce reliance on this tool and empower users to perform administrative tasks directly via SQL, we aim to expose these functions through SQL interfaces.
+
+### Key Requirements
+#### Function Categories:
+
+Scalar Functions: Retrieve system state (e.g., raft_term(), local_config(), JSON utilities).
+
+Side-Effecting Procedures: Modify state (e.g., raft_compact_log(), expel(instance), plugin operations).
+
+Table-Valued Functions (TVFs): Return tabular data (e.g., raft_log(), generate_series()).
+
+#### Goals:
+
+Enable SQL-based access to administrative tasks (e.g., Raft log compaction, plugin management).
+
+Minimize dependencies on the picodata admin tool (except for disaster recovery).
+
+Ensure compatibility with existing systems (Tarantool, SQLite VDBE).
+
+#### Challenges
+Integration Complexity:
+
+TVFs (e.g., raft_log()) need virtual table implementation from SQLite.
+
+Security: Ensure administrative functions are accessible only to authorized users.
+
+Decision Drivers
+Minimal Code Changes: Prefer solutions that leverage existing Tarantool/SQLite infrastructure.
+
+Usability: Ensure SQL syntax aligns with user expectations (e.g., CALL for procedures).
+
+## API
+
+API that we want to access through SQL.
+
+### Picodate API
+
+#### Funtions describing state without arguments (scalar functions)
+Return single value
+Some return a map
+
+1. Raft
+  - raft_status()
+  OR
+  - raft_term()
+  - raft_commit()
+  - raft_leader()
+  - raft_state() (Follower | Leader | ...)
+  - raft_id()
+  - raft_applied()
+2. Other
+  - picodata_version()
+  - local_config()
+  - vshard_config()
+
+  - whoami()
+
+  - local_vclock()
+
+
+#### Side-effecting functions modifying state and stored data (procedures)
+With arguments
+Return single value
+
+1. Raft
+  - raft_compact_log(10)
+  - raft_read_index()
+  - raft_wait_index(index)
+<!-- How to specify and parse op, as map? -->
+  - raft_propose(op)
+
+2. General
+  - exit(code)
+  - expel(instance)
+  - cas(op), batch_cas(ops)
+  - abort_ddl(index)
+  - wait_ddl_finalize(index)
+  - wait_vclock(vclock)
+
+3. Plugin (probably modify data)
+  - install_plugin(name, version, opts)
+  - enable_plugin(name, version, opts)
+  - disable_plugin(name, version, opts)
+  - remove_plugin(name, version, opts)
+  - servie_append_tier(plugin_name, version, service_name, tier, opts)
+  - servie_remove_tier(plugin_name, version, service_name, tier, opts)
+  - migration_up(name, version, opts)
+  - migration_down(name, version, opts)
+
+4. Testing
+  - raft_propose_nop()
+  - inject_error()
+
+#### Return virtual table
+Take arguments
+
+1. generate_series(1, 10)
+2. raft_log() can be a sysview _vraft_log
+
+### JSON
+
+There are twenty-six scalar functions and operators:
+
+- json(json)
+- jsonb(json)
+- json_array(value1,value2,...)
+- jsonb_array(value1,value2,...)
+- json_array_length(json)
+- json_array_length(json,path)
+- json_error_position(json)
+- json_extract(json,path,...)
+- jsonb_extract(json,path,...)
+- json -> path
+- json ->> path
+- json_insert(json,path,value,...)
+- jsonb_insert(json,path,value,...)
+- json_object(label1,value1,...)
+- jsonb_object(label1,value1,...)
+- json_patch(json1,json2)
+- jsonb_patch(json1,json2)
+- json_pretty(json)
+- json_remove(json,path,...)
+- jsonb_remove(json,path,...)
+- json_replace(json,path,value,...)
+- jsonb_replace(json,path,value,...)
+- json_set(json,path,value,...)
+- jsonb_set(json,path,value,...)
+- json_type(json)
+- json_type(json,path)
+- json_valid(json)
+- json_valid(json,flags)
+- json_quote(value)
+
+There are four aggregate SQL functions:
+
+- json_group_array(value)
+- jsonb_group_array(value)
+- json_group_object(label,value)
+- jsonb_group_object(name,value)
+
+The two table-valued functions are:
+
+- json_each(json)
+- json_each(json,path)
+- json_tree(json)
+- json_tree(json,path)
+
+## Solutions
+
+As we have seen from previous sections there are 3 types of functions that we need to consider. Here
+are the proposed solutions for these function types.
+
+1. Scalar functions with and without arguments - tarantool func.create
+2. Side effecting functions with arguments -  tarantool func.create | CALL syntax in our parser, call Rust builtin fn
+3. Functions that return virtual table - VDBE virtual table + TVF
+
+There are also two cases where sysviews can be used: _vraft_log, _vraft_state.
+
+Aggregate functions are skipped as they are not the point of this ADR.
+
+In later sections these and other possible solutions are discussed in detail.
+
+## Considered Options
+
+### CALL syntax - builtin procedures
+
+<!-- Chosen option: "title of option 1", because justification. e.g., only option, which meets k.o. criterion decision driver
+| which resolves … | … | comes out best (see below). -->
+
+Built-in procedures can be defined as Rust functions at compile time and called from SQL on our side - not getting into VDBE.
+From SQL syntax and semantics standpoint they are similar to stored procedures: `CALL name ( [ argument ] [, ...] )`
+
+In terms of Rust a module with built in procedures will look like this:
+
+```rust
+/// Here we define a new proc, let it be "add" for the sake of example
+fn proc_add(args: &[Value]) -> Result<Value> {
+    if let [Value::Int(a), Value::Int(b)] = args {
+        Ok(Value::Int(a+b))
+    } else {
+        Err(Error::InvalidArgs)
+    }
+}
+
+// ...
+
+/// Match function name and execute it
+fn execute_proc(proc_name: &str, args: &[Value]) -> _ {
+    match proc_name {
+        "add" => proc_add(args),
+        // ...
+    }
+}
+
+// in sql.rs
+pub fn dispatch(mut query: Query<RouterRuntime>) -> traft::Result<Tuple> {
+    // ...
+    if query.is_call() && is_builtin(proc_name) {
+        execute_proc(..)
+    } else if query.is_ddl()? || query.is_acl()? {
+        // ...
+    } else {
+        // ...
+    }
+}
+```
+
+Example query:
+```sql
+call add(1, 2);
+```
+
+### VDBE Virtual Tables and TVF
+
+For this we need virtual table mechanism from SQLite - https://sqlite.org/vtab.html#epovtab
+The support for virtual tables (beta) was added in 2006-08-12 (3.3.7).
+I assume the commit [Basic parsing of CREATE VIRTUAL TABLE statements. (CVS 3210)](https://www.sqlite.org/src/info/66370cb99bd93abb) is the start.
+This was a long time ago so it is logical to assume that cherrypick process will be hard.
+
+When we have this mechanism we'll need to implement the SQLite virtual table module for each of the TVFs and make
+each an eponymous virtual table:
+
+> Some virtual tables exist automatically in the "main" schema of every database connection in which their module is registered, even without a CREATE VIRTUAL TABLE statement.
+> Such virtual tables are called "eponymous virtual tables". To use an eponymous virtual table, simply use the module name as if it were a table.
+> Eponymous virtual tables exist in the "main" schema only, so they will not work if prefixed with a different schema name.
+
+SQLite module that should be provided for each TVF.
+```c
+struct sqlite3_module {
+  int iVersion;
+  int (*xCreate)(sqlite3*, void *pAux,
+               int argc, char *const*argv,
+               sqlite3_vtab **ppVTab,
+               char **pzErr);
+  int (*xConnect)(sqlite3*, void *pAux,
+               int argc, char *const*argv,
+               sqlite3_vtab **ppVTab,
+               char **pzErr);
+  int (*xBestIndex)(sqlite3_vtab *pVTab, sqlite3_index_info*);
+  int (*xDisconnect)(sqlite3_vtab *pVTab);
+  int (*xDestroy)(sqlite3_vtab *pVTab);
+  int (*xOpen)(sqlite3_vtab *pVTab, sqlite3_vtab_cursor **ppCursor);
+  int (*xClose)(sqlite3_vtab_cursor*);
+  int (*xFilter)(sqlite3_vtab_cursor*, int idxNum, const char *idxStr,
+                int argc, sqlite3_value **argv);
+  int (*xNext)(sqlite3_vtab_cursor*);
+  int (*xEof)(sqlite3_vtab_cursor*);
+  int (*xColumn)(sqlite3_vtab_cursor*, sqlite3_context*, int);
+  int (*xRowid)(sqlite3_vtab_cursor*, sqlite_int64 *pRowid);
+  int (*xUpdate)(sqlite3_vtab *, int, sqlite3_value **, sqlite_int64 *);
+  int (*xBegin)(sqlite3_vtab *pVTab);
+  int (*xSync)(sqlite3_vtab *pVTab);
+  int (*xCommit)(sqlite3_vtab *pVTab);
+  int (*xRollback)(sqlite3_vtab *pVTab);
+  int (*xFindFunction)(sqlite3_vtab *pVtab, int nArg, const char *zName,
+                     void (**pxFunc)(sqlite3_context*,int,sqlite3_value**),
+                     void **ppArg);
+  int (*xRename)(sqlite3_vtab *pVtab, const char *zNew);
+  /* The methods above are in version 1 of the sqlite_module object. Those
+  ** below are for version 2 and greater. */
+  int (*xSavepoint)(sqlite3_vtab *pVTab, int);
+  int (*xRelease)(sqlite3_vtab *pVTab, int);
+  int (*xRollbackTo)(sqlite3_vtab *pVTab, int);
+  /* The methods above are in versions 1 and 2 of the sqlite_module object.
+  ** Those below are for version 3 and greater. */
+  int (*xShadowName)(const char*);
+  /* The methods above are in versions 1 through 3 of the sqlite_module object.
+  ** Those below are for version 4 and greater. */
+  int (*xIntegrity)(sqlite3_vtab *pVTab, const char *zSchema,
+                    const char *zTabName, int mFlags, char **pzErr);
+};
+```
+
+An example implementation of `generate_series` table valued function can be found [here](https://www.sqlite.org/src/artifact?ci=trunk&filename=ext/misc/series.c)
+
+A virtual table needs to contain hidden columns to be used like a table-valued function in the FROM clause of a SELECT statement. The arguments to the table-valued function become constraints on the HIDDEN columns of the virtual table.
+
+> For example, the "generate_series" extension (located in the ext/misc/series.c file in the source tree) implements an eponymous virtual table with the following schema:
+>
+> CREATE TABLE generate_series(
+>   value,
+>   start HIDDEN,
+>   stop HIDDEN,
+>   step HIDDEN
+> );
+>
+> The sqlite3_module.xBestIndex method in the implementation of this table checks for equality constraints against the HIDDEN columns, and uses those as input parameters to determine the range of integer "value" outputs to generate. Reasonable defaults are used for any unconstrained columns. For example, to list all integers between 5 and 50:
+> SELECT value FROM generate_series(5,50);
+>
+> The previous query is equivalent to the following:
+> SELECT value FROM generate_series WHERE start=5 AND stop=50;
+>
+> Arguments on the virtual table name are matched to hidden columns in order. The number of arguments can be less than the number of hidden columns, in which case the latter hidden columns are unconstrained. However, an error results if there are more arguments than there are hidden columns in the virtual table.
+
+### Tarantool box.schema.func.create() / VDBE application defined functions
+
+There are also application defined SQL functions - https://www.sqlite.org/appfunc.html
+And it seems they were incorporated to Tarantool as stored Lua procedures with `box.schema.func.create()`,
+one just has to add the `exports = {'SQL'}` section:
+
+```lua
+box.schema.func.create('_DECODE',
+   {language = 'LUA',
+    returns = 'string',
+    body = [[function (field, key)
+             -- If Tarantool version < 2.10.1, replace next line with
+             -- return require('msgpack').decode(field)[key]
+             return field[key]
+             end]],
+    is_sandboxed = false,
+    -- If Tarantool version < 2.10.1, replace next line with
+    -- param_list = {'string', 'string'},
+    param_list = {'map', 'string'},
+    exports = {'LUA', 'SQL'},
+    is_deterministic = true})
+```
+Source: https://www.tarantool.io/en/doc/2.11/reference/reference_sql/sql_plus_lua/
+
+This means both scalar functions and side-effecting procedures can be easily implemented through tarantool's Lua functions.
+
+As a sidenote in older revisions of tarantool the implementation was closer to application defined functions in SQLite. So if it is needed
+we can try to ressurect the C API from SQLite to define application defined functions:
+
+Excerpt from [tarantool RFC](../../tarantool-sys/doc/rfc/4182-persistent-lua-functions.md):
+Currently Tarantool has a ``box.internal.sql_create_function`` mechanism  to
+make Lua functions callable from SQL statements.
+```
+sql_create_function("func_name", "type", func_lua_object,
+                    func_arg_num, is_deterministic);
+```
+That internally calls
+```
+int
+sql_create_function_v2(sql * db, const char *zFunc,
+			enum field_type returns, int nArg,
+			int flags,
+			void *p,
+			void (*xSFunc) (sql_context *, int,
+					sql_value **),
+			void (*xStep) (sql_context *, int,
+					  sql_value **),
+			void (*xFinal) (sql_context *),
+			void (*xDestroy) (void *));
+```
+With prepared context
+```
+struct lua_sql_func_info {
+	int func_ref;
+} func_info = {.func_ref = luaL_ref(L, LUA_REGISTRYINDEX);};
+
+sql_create_function_v2(db, normalized_name, type,
+                       func_arg_num,
+                       is_deterministic ? SQL_DETERMINISTIC : 0,
+                       func_info, lua_sql_call,
+                       NULL, NULL, lua_sql_destroy);
+```
+
+### Tarantool sys view
+
+In Tarantool, a [sysview](https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_space/system_views/) (system view)
+is a virtual read-only table that provides access to metadata or system information about the database
+The sysview interface is defined in [sysview.c](https://git.picodata.io/core/tarantool/-/blob/2.11.5-picodata/src/box/sysview.c?ref_type=heads)
+
+VDBE has access to sysviews as illustrated by the exmaple below
+where a user with id = 0 is selected from a view _vuser.
+```
+tarantool> box.execute([[SELECT * FROM "_vuser" WHERE id=0;]])
+---
+- metadata:
+  - name: id
+    type: unsigned
+  - name: owner
+    type: unsigned
+  - name: name
+    type: string
+  - name: type
+    type: string
+  - name: auth
+    type: map
+  - name: auth_history
+    type: array
+  - name: last_modified
+    type: unsigned
+  rows:
+  - [0, 1, 'guest', 'user', {'md5': 'md5084e0343a0486ff05530df6c705c8bb4'}, [], 1740497477869135863]
+...
+```
+
+Sysview were thoought to be good fit for functions that don't take value and describe the state of the database. It might seems we could
+define them in C providing a corresponding vtab from `sysview.c` and call Rust from C to get the actual value for example of local `config`.
+
+But there might be some heavy refactoring involved as sysviews in tarantool are created only for select existing spaces and the space ids are hardcoded - they are light wrappers around system tables.
+
+The easiest way to make a sysview is to create a system space, insert values there and make a sysview on top of it. While it might work for raft_state and raft_log as it is already a space, it is not ideal for
+config.
+
+For example here they use exact ids and take indexes of original spaces.
+
+```c
+static struct index *
+sysview_space_create_index(struct space *space, struct index_def *def)
+{
+	assert(def->type == TREE);
+
+	struct sysview_engine *sysview = (struct sysview_engine *)space->engine;
+	if (!mempool_is_initialized(&sysview->iterator_pool)) {
+		mempool_create(&sysview->iterator_pool, cord_slab_cache(),
+			       sizeof(struct sysview_iterator));
+	}
+
+	uint32_t source_space_id;
+	uint32_t source_index_id;
+	sysview_filter_f filter;
+
+	switch (def->space_id) {
+	case BOX_VSPACE_ID:
+		source_space_id = BOX_SPACE_ID;
+		source_index_id = def->iid;
+		filter = vspace_filter;
+		break;
+	case BOX_VINDEX_ID:
+		source_space_id = BOX_INDEX_ID;
+		source_index_id = def->iid;
+		filter = vspace_filter;
+		break;
+	case BOX_VUSER_ID:
+		source_space_id = BOX_USER_ID;
+		source_index_id = def->iid;
+		filter = vuser_filter;
+		break;
+
+	// ...
+
+  default:
+		diag_set(ClientError, ER_MODIFY_INDEX,
+			 def->name, space_name(space),
+			 "unknown space for system view");
+		return NULL;
+	}
+
+	struct sysview_index *index = xcalloc(1, sizeof(*index));
+	index_create(&index->base, (struct engine *)sysview,
+		     &sysview_index_vtab, def);
+
+	index->source_space_id = source_space_id;
+	index->source_index_id = source_index_id;
+	index->filter = filter;
+	return &index->base;
+}
+```
diff --git a/doc/adr/adr-template.md b/doc/adr/adr-template.md
index d65225e1172128692278885084bb414f14ecc76c..c3404ea3241901df047c932dfba7f2ce82e0cc97 100644
--- a/doc/adr/adr-template.md
+++ b/doc/adr/adr-template.md
@@ -5,8 +5,8 @@ See these ADR examples for inspiration:
 - [Rust RFC - Lifetime Ellision](https://github.com/rust-lang/rfcs/blob/master/text/0141-lifetime-elision.md)
 - [TiKV - Use Joint Consensus](https://github.com/tikv/rfcs/blob/master/text/0054-joint-consensus.md)
 -->
-status: "rejected | accepted | deprecated | … | superseded by ADR-0123 (add link)" <!-- Proposed status left out as we consider an MR as proposition -->
-decision-makers: list everyone involved in the decision
+- status: "rejected | accepted | deprecated | … | superseded by ADR-0123 (add link)" <!-- Proposed status left out as we consider an MR as proposition -->
+- decision-makers: list everyone involved in the decision
 
 <!--
 consulted: list everyone whose opinions are sought (typically subject-matter experts); and with whom there is a two-way communication
@@ -33,7 +33,7 @@ You may want to articulate the problem in form of a question and add links to gi
 
 ## Decision Outcome
 
-<!-- Chosen option: "title of option 1", because justification. e.g., only option, which meets k.o. criterion decision driver 
+<!-- Chosen option: "title of option 1", because justification. e.g., only option, which meets k.o. criterion decision driver
 | which resolves … | … | comes out best (see below). -->
 
 ### Consequences