From 2859cbdb94a101bf577f9241aad7bd99856fff65 Mon Sep 17 00:00:00 2001
From: godzie44 <godzie@yandex.ru>
Date: Fri, 19 Jul 2024 10:30:05 +0300
Subject: [PATCH] feature(plugin): add plugin SQL

---
 doc/sql/query.ebnf                      |  38 +-
 sbroad-cartridge/src/api/exec_query.rs  |   3 +
 sbroad-core/src/backend/sql/ir.rs       |   6 +
 sbroad-core/src/backend/sql/tree.rs     |   1 +
 sbroad-core/src/executor.rs             |  12 +-
 sbroad-core/src/executor/ir.rs          |   1 +
 sbroad-core/src/frontend/sql.rs         | 288 ++++++++++++-
 sbroad-core/src/frontend/sql/ast.rs     |  24 ++
 sbroad-core/src/frontend/sql/query.pest |  22 +-
 sbroad-core/src/ir.rs                   |  52 ++-
 sbroad-core/src/ir/api/parameter.rs     |   7 +-
 sbroad-core/src/ir/block.rs             |   2 +
 sbroad-core/src/ir/distribution.rs      |   4 +
 sbroad-core/src/ir/expression/types.rs  |   4 +
 sbroad-core/src/ir/node.rs              |  41 +-
 sbroad-core/src/ir/node/plugin.rs       | 523 ++++++++++++++++++++++++
 sbroad-core/src/ir/tree/expression.rs   |   1 +
 sbroad-core/src/ir/tree/relation.rs     |   3 +-
 sbroad-core/src/ir/tree/subtree.rs      |   3 +-
 19 files changed, 1009 insertions(+), 26 deletions(-)
 create mode 100644 sbroad-core/src/ir/node/plugin.rs

diff --git a/doc/sql/query.ebnf b/doc/sql/query.ebnf
index f8b14009c..5c884df3d 100644
--- a/doc/sql/query.ebnf
+++ b/doc/sql/query.ebnf
@@ -106,17 +106,18 @@ grant       ::= 'GRANT' (
                     | role
                 )
                 'TO' (role | user)
-ddl         ::= (alter_procedure | create_index | create_procedure | create_table
-                | drop_index | drop_procedure | drop_table | alter_system)
-                ('OPTION' '(' ('TIMEOUT' '=' double)')')?
+ddl         ::= (alter_plugin | alter_procedure | alter_system
+                | create_index | create_plugin | create_procedure | create_table
+                | drop_index | drop_plugin | drop_procedure | drop_table)
 alter_system ::= 'ALTER' 'SYSTEM'
                     (
                         'RESET' ('ALL' | param_name)
                         | 'SET' param_name ('=' | 'TO') ('DEFAULT' | param_value)
                     )
                     ('FOR' ('ALL' 'TIERS' | 'TIER' tier))?
+                  ('OPTION' '(' ('TIMEOUT' '=' double)')')?
 alter_procedure ::= 'ALTER' 'PROCEDURE' procedure ('(' type (',' type)* ')')?
-                     'RENAME' 'TO' procedure
+                     'RENAME' 'TO' procedure ('OPTION' '(' ('TIMEOUT' '=' double)')')?
 create_index ::= 'CREATE' 'UNIQUE'? 'INDEX' index 'ON' table
                  ('USING' ('TREE' | 'HASH' | 'RTREE' | 'BITSET'))?
                  '(' column (',' column)* ')' ('WITH' '('
@@ -143,12 +144,12 @@ create_index ::= 'CREATE' 'UNIQUE'? 'INDEX' index 'ON' table
                              | ('HINT' '=' ('TRUE' | 'FALSE'))
                          )
                      )*
-                 ')')?
+                 ')')? ('OPTION' '(' ('TIMEOUT' '=' double)')')?
 create_procedure ::= 'CREATE' 'PROCEDURE' procedure '(' type (',' type)* ')'
                      ('LANGUAGE' 'SQL')? (
                          ('AS' '$$' (insert | update | delete) '$$')
                          | ('BEGIN' 'ATOMIC' (insert | update | delete) 'END')
-                     )
+                     ) ('OPTION' '(' ('TIMEOUT' '=' double)')')?
 create_role    ::= 'CREATE' 'ROLE' role
 create_table   ::= 'CREATE' 'TABLE' table
                    '('
@@ -157,6 +158,7 @@ create_table   ::= 'CREATE' 'TABLE' table
                    ')'
                    ('USING' ('MEMTX' | 'VINYL'))?
                    (('DISTRIBUTED' (('BY' '(' column (',' column)* ')' ('IN' 'TIER' tier)?) | 'GLOBALLY'))?)?
+                   ('OPTION' '(' ('TIMEOUT' '=' double)')')?
 create_user    ::= 'CREATE' 'USER' user 'WITH'? 'PASSWORD' "'" password "'"
                    ('USING' ('CHAP-SHA1' | 'LDAP' | 'MD5'))?
 alter_user     ::= 'ALTER' 'USER' user
@@ -166,11 +168,29 @@ alter_user     ::= 'ALTER' 'USER' user
                        | 'PASSWORD' "'" password "'" ('USING' ('CHAP-SHA1' | 'LDAP' | 'MD5'))?
                        | 'RENAME' 'TO' user
                    )
-drop_index     ::= 'DROP' 'INDEX' index
-drop_procedure ::= 'DROP' 'PROCEDURE' procedure ('(' type (',' type)* ')')?
-drop_table     ::= 'DROP' 'TABLE' table
+drop_index     ::= 'DROP' 'INDEX' index ('OPTION' '(' ('TIMEOUT' '=' double)')')?
+drop_procedure ::= 'DROP' 'PROCEDURE' procedure ('(' type (',' type)* ')')? ('OPTION' '(' ('TIMEOUT' '=' double)')')?
+drop_table     ::= 'DROP' 'TABLE' table ('OPTION' '(' ('TIMEOUT' '=' double)')')?
 drop_role      ::= 'DROP' 'ROLE' role
 drop_user      ::= 'DROP' 'USER' user
+create_plugin  ::= 'CREATE' 'PLUGIN' ('IF' 'NOT' 'EXISTS')? plugin version
+                    ('OPTION' '(' ('TIMEOUT' '=' double)')')?
+drop_plugin    ::= 'DROP' 'PLUGIN' ('IF' 'EXISTS')? plugin version ('WITH' 'DATA')?
+                    ('OPTION' '(' ('TIMEOUT' '=' double)')')?
+alter_plugin  ::= 'ALTER' 'PLUGIN' plugin (
+                        (version (
+                            'ENABLE' | 'DISABLE'
+                            | ('ADD' 'SERVICE' service 'TO' 'TIER' tier)
+                            | ('REMOVE' 'SERVICE' service 'FROM' 'TIER' tier)
+                            | ('SET' ((service'.'key '=' text) (',' service'.'key '=' text)*))
+                        ))
+                        ('OPTION' '(' ('TIMEOUT' '=' double)')')?
+                        | ('MIGRATE' 'TO' version)
+                          ( 'OPTION' '('
+                            (('TIMEOUT' | 'ROLLBACK_TIMEOUT') '=' double)
+                            (',' (('TIMEOUT' | 'ROLLBACK_TIMEOUT') '=' double))*')'
+                          )?
+                   )
 type        ::= 'BOOL'
                 | 'BOOLEAN'
                 | 'DATETIME'
diff --git a/sbroad-cartridge/src/api/exec_query.rs b/sbroad-cartridge/src/api/exec_query.rs
index 1ea8b976b..2020ece1a 100644
--- a/sbroad-cartridge/src/api/exec_query.rs
+++ b/sbroad-cartridge/src/api/exec_query.rs
@@ -44,6 +44,9 @@ fn dispatch_query_inner(args: &RawBytes) -> anyhow::Result<RawProcResult> {
         if let Ok(true) = query.is_block() {
             bail!("blocks of commands are not supported");
         }
+        if let Ok(true) = query.is_plugin() {
+            bail!("plugin of commands are not supported");
+        }
 
         let metadata = try_get_metadata_from_plan(query.get_exec_plan())?;
         let dispatch_result = query.dispatch()?;
diff --git a/sbroad-core/src/backend/sql/ir.rs b/sbroad-core/src/backend/sql/ir.rs
index a9a360295..a7a73324f 100644
--- a/sbroad-core/src/backend/sql/ir.rs
+++ b/sbroad-core/src/backend/sql/ir.rs
@@ -389,6 +389,12 @@ impl ExecutionPlan {
                                     ),
                                 ));
                             }
+                            Node::Plugin(_) => {
+                                return Err(SbroadError::Unsupported(
+                                    Entity::Node,
+                                    Some("Plugin are not supported in the generated SQL".into()),
+                                ));
+                            }
                             Node::Relational(rel) => match rel {
                                 Relational::Except { .. } => sql.push_str("EXCEPT"),
                                 Relational::GroupBy { .. } => sql.push_str("GROUP BY"),
diff --git a/sbroad-core/src/backend/sql/tree.rs b/sbroad-core/src/backend/sql/tree.rs
index e29b0a6c2..4dca82acc 100644
--- a/sbroad-core/src/backend/sql/tree.rs
+++ b/sbroad-core/src/backend/sql/tree.rs
@@ -752,6 +752,7 @@ impl<'p> SyntaxPlan<'p> {
             Node::Ddl(..) => panic!("DDL node {node:?} is not supported in the syntax plan"),
             Node::Acl(..) => panic!("ACL node {node:?} is not supported in the syntax plan"),
             Node::Block(..) => panic!("Block node {node:?} is not supported in the syntax plan"),
+            Node::Plugin(..) => panic!("Plugin node {node:?} is not supported in the syntax plan"),
             Node::Invalid(..) | Node::Parameter(..) => {
                 let sn = SyntaxNode::new_parameter(id);
                 self.nodes.push_sn_plan(sn);
diff --git a/sbroad-core/src/executor.rs b/sbroad-core/src/executor.rs
index 40554f155..da2c96d3b 100644
--- a/sbroad-core/src/executor.rs
+++ b/sbroad-core/src/executor.rs
@@ -144,13 +144,13 @@ where
                 }
                 plan.version_map = table_version_map;
             }
-            if !plan.is_ddl()? && !plan.is_acl()? {
+            if !plan.is_ddl()? && !plan.is_acl()? && !plan.is_plugin()? {
                 cache.put(key, plan.clone())?;
             }
         }
         if plan.is_block()? {
             plan.bind_params(params)?;
-        } else if !plan.is_ddl()? && !plan.is_acl()? {
+        } else if !plan.is_ddl()? && !plan.is_acl()? && !plan.is_plugin()? {
             plan.bind_params(params)?;
             plan.apply_options()?;
             plan.optimize()?;
@@ -342,6 +342,14 @@ where
         self.exec_plan.get_ir_plan().is_acl()
     }
 
+    /// Checks that query is for plugin.
+    ///
+    /// # Errors
+    /// - Plan is invalid
+    pub fn is_plugin(&self) -> Result<bool, SbroadError> {
+        self.exec_plan.get_ir_plan().is_plugin()
+    }
+
     /// Checks that query is an empty query.
     pub fn is_empty(&self) -> bool {
         self.exec_plan.get_ir_plan().is_empty()
diff --git a/sbroad-core/src/executor/ir.rs b/sbroad-core/src/executor/ir.rs
index f68740ef3..c5ca37882 100644
--- a/sbroad-core/src/executor/ir.rs
+++ b/sbroad-core/src/executor/ir.rs
@@ -805,6 +805,7 @@ impl ExecutionPlan {
                 NodeOwned::Invalid { .. }
                 | NodeOwned::Ddl { .. }
                 | NodeOwned::Acl { .. }
+                | NodeOwned::Plugin { .. }
                 | NodeOwned::Block { .. } => {
                     panic!("Unexpected node in `take_subtree`: {node:?}")
                 }
diff --git a/sbroad-core/src/frontend/sql.rs b/sbroad-core/src/frontend/sql.rs
index 092c7dcca..2831d62b7 100644
--- a/sbroad-core/src/frontend/sql.rs
+++ b/sbroad-core/src/frontend/sql.rs
@@ -48,7 +48,7 @@ use crate::ir::operator::{
 use crate::ir::relation::{Column, ColumnRole, TableKind, Type as RelationType};
 use crate::ir::tree::traversal::{LevelNode, PostOrder, EXPR_CAPACITY};
 use crate::ir::value::Value;
-use crate::ir::{OptionKind, OptionParamValue, OptionSpec, Plan};
+use crate::ir::{node::plugin, OptionKind, OptionParamValue, OptionSpec, Plan};
 use crate::otm::child_span;
 
 use crate::errors::Entity::AST;
@@ -58,6 +58,10 @@ use crate::ir::acl::{GrantRevokeType, Privilege};
 use crate::ir::aggregates::AggregateKind;
 use crate::ir::expression::NewColumnsSource;
 use crate::ir::helpers::RepeatableState;
+use crate::ir::node::plugin::{
+    AppendServiceToTier, ChangeConfig, CreatePlugin, DisablePlugin, DropPlugin, EnablePlugin,
+    MigrateTo, MigrateToOpts, RemoveServiceFromTier, ServiceSettings, SettingsPair,
+};
 use crate::ir::transformation::redistribution::ColumnPosition;
 use crate::warn;
 use sbroad_proc::otm_child_span;
@@ -2596,6 +2600,257 @@ fn parse_select(
     select_expr.populate_plan(plan)
 }
 
+fn parse_create_plugin(
+    ast: &AbstractSyntaxTree,
+    node: &ParseNode,
+) -> Result<CreatePlugin, SbroadError> {
+    let mut if_not_exists = false;
+    let first_node_idx = node.first_child();
+    let plugin_name_child_idx = if let Rule::IfNotExists = ast.nodes.get_node(first_node_idx)?.rule
+    {
+        if_not_exists = true;
+        1
+    } else {
+        0
+    };
+
+    let plugin_name_idx = node.child_n(plugin_name_child_idx);
+    let name = parse_identifier(ast, plugin_name_idx)?;
+
+    let version_idx = node.child_n(plugin_name_child_idx + 1);
+    let version = parse_identifier(ast, version_idx)?;
+
+    let mut timeout = plugin::get_default_timeout();
+    if let Some(timeout_child_id) = node.children.get(plugin_name_child_idx + 2) {
+        timeout = get_timeout(ast, *timeout_child_id)?;
+    }
+
+    Ok(CreatePlugin {
+        name,
+        version,
+        if_not_exists,
+        timeout,
+    })
+}
+
+fn parse_drop_plugin(
+    ast: &AbstractSyntaxTree,
+    node: &ParseNode,
+) -> Result<DropPlugin, SbroadError> {
+    let mut if_exists = false;
+    let first_node_idx = node.first_child();
+    let plugin_name_child_idx = if let Rule::IfExists = ast.nodes.get_node(first_node_idx)?.rule {
+        if_exists = true;
+        1
+    } else {
+        0
+    };
+
+    let plugin_name_idx = node.child_n(plugin_name_child_idx);
+    let name = parse_identifier(ast, plugin_name_idx)?;
+    let version_idx = node.child_n(plugin_name_child_idx + 1);
+    let version = parse_identifier(ast, version_idx)?;
+
+    let mut with_data = false;
+    let timeout_idx = if let Rule::WithData = ast.nodes.get_node(plugin_name_child_idx + 2)?.rule {
+        with_data = true;
+        plugin_name_child_idx + 3
+    } else {
+        plugin_name_child_idx + 2
+    };
+
+    let mut timeout = plugin::get_default_timeout();
+    if let Some(timeout_child_id) = node.children.get(timeout_idx) {
+        timeout = get_timeout(ast, *timeout_child_id)?;
+    }
+
+    Ok(DropPlugin {
+        name,
+        version,
+        if_exists,
+        with_data,
+        timeout,
+    })
+}
+
+fn parse_alter_plugin(
+    ast: &AbstractSyntaxTree,
+    plan: &mut Plan,
+    node: &ParseNode,
+) -> Result<Option<NodeId>, SbroadError> {
+    let plugin_name_idx = node.first_child();
+    let plugin_name = parse_identifier(ast, plugin_name_idx)?;
+
+    let idx = node.child_n(1);
+    let node = ast.nodes.get_node(idx)?;
+    let plugin_expr = match node.rule {
+        Rule::EnablePlugin => {
+            let version_idx = node.first_child();
+            let version = parse_identifier(ast, version_idx)?;
+
+            let mut timeout = plugin::get_default_timeout();
+            if let Some(timeout_child_id) = node.children.get(1) {
+                timeout = get_timeout(ast, *timeout_child_id)?;
+            }
+
+            Some(
+                plan.nodes.push(
+                    EnablePlugin {
+                        name: plugin_name,
+                        version,
+                        timeout,
+                    }
+                    .into(),
+                ),
+            )
+        }
+        Rule::DisablePlugin => {
+            let version_idx = node.first_child();
+            let version = parse_identifier(ast, version_idx)?;
+
+            let mut timeout = plugin::get_default_timeout();
+            if let Some(timeout_child_id) = node.children.get(1) {
+                timeout = get_timeout(ast, *timeout_child_id)?;
+            }
+
+            Some(
+                plan.nodes.push(
+                    DisablePlugin {
+                        name: plugin_name,
+                        version,
+                        timeout,
+                    }
+                    .into(),
+                ),
+            )
+        }
+        Rule::MigrateTo => {
+            let version_idx = node.first_child();
+            let version = parse_identifier(ast, version_idx)?;
+            let opts = parse_plugin_opts::<MigrateToOpts>(ast, node, 1, |opts, node, idx| {
+                match node.rule {
+                    Rule::Timeout => {
+                        opts.timeout = get_timeout(ast, idx)?;
+                    }
+                    Rule::RollbackTimeout => {
+                        opts.rollback_timeout = get_timeout(ast, idx)?;
+                    }
+                    _ => {}
+                };
+                Ok(())
+            })?;
+
+            Some(
+                plan.nodes.push(
+                    MigrateTo {
+                        name: plugin_name,
+                        version,
+                        opts,
+                    }
+                    .into(),
+                ),
+            )
+        }
+        Rule::AddServiceToTier => {
+            let version_idx = node.first_child();
+            let version = parse_identifier(ast, version_idx)?;
+            let service_idx = node.child_n(1);
+            let service_name = parse_identifier(ast, service_idx)?;
+            let tier_idx = node.child_n(2);
+            let tier = parse_identifier(ast, tier_idx)?;
+
+            let mut timeout = plugin::get_default_timeout();
+            if let Some(timeout_child_id) = node.children.get(3) {
+                timeout = get_timeout(ast, *timeout_child_id)?;
+            }
+
+            Some(
+                plan.nodes.push(
+                    AppendServiceToTier {
+                        plugin_name,
+                        version,
+                        service_name,
+                        tier,
+                        timeout,
+                    }
+                    .into(),
+                ),
+            )
+        }
+        Rule::RemoveServiceFromTier => {
+            let version_idx = node.first_child();
+            let version = parse_identifier(ast, version_idx)?;
+            let service_idx = node.child_n(1);
+            let service_name = parse_identifier(ast, service_idx)?;
+            let tier_idx = node.child_n(2);
+            let tier = parse_identifier(ast, tier_idx)?;
+
+            let mut timeout = plugin::get_default_timeout();
+            if let Some(timeout_child_id) = node.children.get(3) {
+                timeout = get_timeout(ast, *timeout_child_id)?;
+            }
+
+            Some(
+                plan.nodes.push(
+                    RemoveServiceFromTier {
+                        plugin_name,
+                        version,
+                        service_name,
+                        tier,
+                        timeout,
+                    }
+                    .into(),
+                ),
+            )
+        }
+        Rule::ChangeConfig => {
+            let version_idx = node.first_child();
+            let version = parse_identifier(ast, version_idx)?;
+
+            let mut key_value_grouped: HashMap<SmolStr, Vec<SettingsPair>> = HashMap::new();
+            let mut timeout = plugin::get_default_timeout();
+
+            for &i in &node.children[1..] {
+                let next_node = ast.nodes.get_node(i)?;
+                if next_node.rule == Rule::ConfigKV {
+                    let svc_idx = next_node.child_n(0);
+                    let svc = parse_identifier(ast, svc_idx)?;
+                    let key_idx = next_node.child_n(1);
+                    let key = parse_identifier(ast, key_idx)?;
+                    let value_idx = next_node.child_n(2);
+                    let value = parse_string_literal(ast, value_idx)?;
+                    let entry = key_value_grouped.entry(svc).or_default();
+                    entry.push(SettingsPair { key, value });
+                } else {
+                    timeout = get_timeout(ast, i)?;
+                    break;
+                }
+            }
+
+            Some(
+                plan.nodes.push(
+                    ChangeConfig {
+                        plugin_name,
+                        version,
+                        key_value_grouped: key_value_grouped
+                            .into_iter()
+                            .map(|(service, settings)| ServiceSettings {
+                                name: service,
+                                pairs: settings,
+                            })
+                            .collect::<Vec<_>>(),
+                        timeout,
+                    }
+                    .into(),
+                ),
+            )
+        }
+        _ => None,
+    };
+
+    Ok(plugin_expr)
+}
+
 /// Generate an alias for the unnamed projection expressions.
 #[must_use]
 pub fn get_unnamed_column_alias(pos: usize) -> SmolStr {
@@ -3811,6 +4066,21 @@ impl AbstractSyntaxTree {
                     let plan_id = plan.nodes.push(create_role.into());
                     map.add(id, plan_id);
                 }
+                Rule::CreatePlugin => {
+                    let create_plugin = parse_create_plugin(self, node)?;
+                    let plan_id = plan.nodes.push(create_plugin.into());
+                    map.add(id, plan_id);
+                }
+                Rule::DropPlugin => {
+                    let drop_plugin = parse_drop_plugin(self, node)?;
+                    let plan_id = plan.nodes.push(drop_plugin.into());
+                    map.add(id, plan_id);
+                }
+                Rule::AlterPlugin => {
+                    if let Some(node_id) = parse_alter_plugin(self, &mut plan, node)? {
+                        map.add(id, node_id);
+                    }
+                }
                 Rule::SetParam => {
                     let set_param_node = parse_set_param(self, node)?;
                     let plan_id = plan.nodes.push(set_param_node.into());
@@ -3907,3 +4177,19 @@ impl Plan {
 pub mod ast;
 pub mod ir;
 pub mod tree;
+
+fn parse_plugin_opts<T: Default>(
+    ast: &AbstractSyntaxTree,
+    node: &ParseNode,
+    start_from: usize,
+    mut with_opt_node: impl FnMut(&mut T, &ParseNode, usize) -> Result<(), SbroadError>,
+) -> Result<T, SbroadError> {
+    let mut opts = T::default();
+
+    for &param_idx in &node.children[start_from..] {
+        let param_node = ast.nodes.get_node(param_idx)?;
+        with_opt_node(&mut opts, param_node, param_idx)?;
+    }
+
+    Ok(opts)
+}
diff --git a/sbroad-core/src/frontend/sql/ast.rs b/sbroad-core/src/frontend/sql/ast.rs
index 64be86560..0b61f1791 100644
--- a/sbroad-core/src/frontend/sql/ast.rs
+++ b/sbroad-core/src/frontend/sql/ast.rs
@@ -35,6 +35,30 @@ impl ParseNode {
             value,
         }
     }
+
+    /// Return first child from node children.
+    ///
+    /// # Panics
+    ///
+    /// Panics a children array is empty.
+    pub(super) fn first_child(&self) -> usize {
+        *self
+            .children
+            .first()
+            .expect("could not find first child in node")
+    }
+
+    /// Return a nth child from node children.
+    ///
+    /// # Panics
+    ///
+    /// Panics if there is no n-child in a children array.
+    pub(super) fn child_n(&self, n: usize) -> usize {
+        *self
+            .children
+            .get(n)
+            .unwrap_or_else(|| panic!("could find {n} child in node"))
+    }
 }
 
 /// A storage arena of the parse nodes
diff --git a/sbroad-core/src/frontend/sql/query.pest b/sbroad-core/src/frontend/sql/query.pest
index 2c397badd..ab49f8d03 100644
--- a/sbroad-core/src/frontend/sql/query.pest
+++ b/sbroad-core/src/frontend/sql/query.pest
@@ -1,4 +1,4 @@
-Command = _{ SOI ~ (Query | ExplainQuery | Block | DDL | ACL | EmptyQuery) ~ EOF }
+Command = _{ SOI ~ (Query | ExplainQuery | Block | DDL | ACL | Plugin | EmptyQuery) ~ EOF }
 
 // Helper rule to denote we have to update plan relations from metadata
 // (with Table which name corresponds to current node).
@@ -8,6 +8,26 @@ Table     = @{ Identifier }
 ScanTable = { Table }
 ScanCteOrTable = @{ Table }
 
+Plugin = _{ CreatePlugin | DropPlugin | AlterPlugin }
+    CreatePlugin = { ^"create" ~ ^"plugin" ~ IfNotExists? ~ Identifier ~ PluginVersion ~ TimeoutOption? }
+        PluginVersion = @{ Unsigned ~ "." ~ Unsigned ~ "." ~ Unsigned }
+        IfNotExists = { ^"if" ~ ^"not" ~ ^"exists" }
+    DropPlugin = { ^"drop" ~ ^"plugin" ~ IfExists? ~ Identifier ~ PluginVersion ~ WithData? ~ TimeoutOption? }
+        IfExists = { ^"if" ~ ^"exists" }
+        WithData = { ^"with" ~ ^"data" }
+    AlterPlugin = { ^"alter" ~ ^"plugin" ~ Identifier ~ AlterVariant }
+        AlterVariant = _{ EnablePlugin | DisablePlugin | MigrateTo | AddServiceToTier | RemoveServiceFromTier | ChangeConfig }
+        EnablePlugin = { PluginVersion ~  ^"enable" ~ TimeoutOption? }
+        DisablePlugin = { PluginVersion ~ ^"disable" ~ TimeoutOption? }
+        MigrateTo = { ^"migrate" ~ ^"to" ~ PluginVersion ~ MigrateUpOption? }
+            RollbackTimeout = { ^"rollback_timeout" ~ "=" ~ Duration }
+            MigrateUpOptionParam = _{ Timeout | RollbackTimeout }
+            MigrateUpOption = _{ ^"option" ~ "(" ~ MigrateUpOptionParam ~ ("," ~ MigrateUpOptionParam)* ~ ")" }
+        AddServiceToTier = { PluginVersion ~ ^"add" ~ ^"service" ~ Identifier ~ ^"to" ~ ^"tier" ~ Identifier ~ TimeoutOption? }
+        RemoveServiceFromTier = { PluginVersion ~ ^"remove" ~ ^"service" ~ Identifier ~ ^"from" ~ ^"tier" ~ Identifier ~ TimeoutOption? }
+        ChangeConfig = { PluginVersion ~ ^"set" ~ ConfigKV ~ ("," ~ ConfigKV)* ~ TimeoutOption? }
+            ConfigKV = { Identifier ~ ^"." ~ Identifier ~ "=" ~ SingleQuotedString }
+
 ACL = _{ DropRole | DropUser | CreateRole | CreateUser | AlterUser | GrantPrivilege | RevokePrivilege }
     CreateUser = {
         ^"create" ~ ^"user" ~ Identifier ~ (^"with")? ~ ^"password" ~ SingleQuotedString ~
diff --git a/sbroad-core/src/ir.rs b/sbroad-core/src/ir.rs
index e9094fd3e..52473088f 100644
--- a/sbroad-core/src/ir.rs
+++ b/sbroad-core/src/ir.rs
@@ -22,11 +22,15 @@ use tarantool::tlua;
 use operator::Arithmetic;
 use relation::{Table, Type};
 
+use self::parameters::Parameters;
+use self::relation::Relations;
+use self::transformation::redistribution::MotionPolicy;
 use crate::errors::Entity::Query;
 use crate::errors::{Action, Entity, SbroadError, TypeError};
 use crate::executor::engine::helpers::to_user;
 use crate::executor::engine::TableVersionMap;
 use crate::ir::helpers::RepeatableState;
+use crate::ir::node::plugin::{MutPlugin, Plugin};
 use crate::ir::node::{
     Alias, ArenaType, ArithmeticExpr, BoolExpr, Case, Cast, Concat, Constant, ExprInParentheses,
     GroupBy, Having, Insert, Limit, Motion, MutNode, Node, Node136, Node224, Node32, Node64,
@@ -42,10 +46,6 @@ use crate::ir::undo::TransformationLog;
 use crate::ir::value::Value;
 use crate::{collection, error, warn};
 
-use self::parameters::Parameters;
-use self::relation::Relations;
-use self::transformation::redistribution::MotionPolicy;
-
 // TODO: remove when rust version in bumped in module
 #[allow(elided_lifetimes_in_associated_constant)]
 pub mod acl;
@@ -142,6 +142,10 @@ impl Nodes {
                 Node96::StableFunction(stable_func) => {
                     Node::Expression(Expression::StableFunction(stable_func))
                 }
+                Node96::CreatePlugin(create) => Node::Plugin(Plugin::Create(create)),
+                Node96::EnablePlugin(enable) => Node::Plugin(Plugin::Enable(enable)),
+                Node96::DisablePlugin(disable) => Node::Plugin(Plugin::Disable(disable)),
+                Node96::DropPlugin(drop) => Node::Plugin(Plugin::Drop(drop)),
             }),
             ArenaType::Arena136 => self
                 .arena136
@@ -163,6 +167,10 @@ impl Nodes {
                     Node136::RenameRoutine(rename_routine) => {
                         Node::Ddl(Ddl::RenameRoutine(rename_routine))
                     }
+                    Node136::MigrateTo(migrate) => Node::Plugin(Plugin::MigrateTo(migrate)),
+                    Node136::ChangeConfig(change_config) => {
+                        Node::Plugin(Plugin::ChangeConfig(change_config))
+                    }
                 }),
             ArenaType::Arena224 => self
                 .arena224
@@ -171,6 +179,12 @@ impl Nodes {
                     Node224::CreateIndex(create_index) => Node::Ddl(Ddl::CreateIndex(create_index)),
                     Node224::CreateTable(create_table) => Node::Ddl(Ddl::CreateTable(create_table)),
                     Node224::Invalid(inv) => Node::Invalid(inv),
+                    Node224::AppendServiceToTier(append) => {
+                        Node::Plugin(Plugin::AppendServiceToTier(append))
+                    }
+                    Node224::RemoveServiceFromTier(remove) => {
+                        Node::Plugin(Plugin::RemoveServiceFromTier(remove))
+                    }
                 }),
         }
     }
@@ -270,6 +284,10 @@ impl Nodes {
                     Node96::StableFunction(stable_func) => {
                         MutNode::Expression(MutExpression::StableFunction(stable_func))
                     }
+                    Node96::CreatePlugin(create) => MutNode::Plugin(MutPlugin::Create(create)),
+                    Node96::EnablePlugin(enable) => MutNode::Plugin(MutPlugin::Enable(enable)),
+                    Node96::DisablePlugin(disable) => MutNode::Plugin(MutPlugin::Disable(disable)),
+                    Node96::DropPlugin(drop) => MutNode::Plugin(MutPlugin::Drop(drop)),
                 }),
             ArenaType::Arena136 => {
                 self.arena136
@@ -303,6 +321,12 @@ impl Nodes {
                         Node136::RenameRoutine(rename_routine) => {
                             MutNode::Ddl(MutDdl::RenameRoutine(rename_routine))
                         }
+                        Node136::MigrateTo(migrate) => {
+                            MutNode::Plugin(MutPlugin::MigrateTo(migrate))
+                        }
+                        Node136::ChangeConfig(change_config) => {
+                            MutNode::Plugin(MutPlugin::ChangeConfig(change_config))
+                        }
                     })
             }
             ArenaType::Arena224 => {
@@ -316,6 +340,12 @@ impl Nodes {
                         Node224::CreateTable(create_table) => {
                             MutNode::Ddl(MutDdl::CreateTable(create_table))
                         }
+                        Node224::AppendServiceToTier(append) => {
+                            MutNode::Plugin(MutPlugin::AppendServiceToTier(append))
+                        }
+                        Node224::RemoveServiceFromTier(remove) => {
+                            MutNode::Plugin(MutPlugin::RemoveServiceFromTier(remove))
+                        }
                     })
             }
         }
@@ -1337,6 +1367,15 @@ impl Plan {
         Ok(matches!(top, Node::Acl(_)))
     }
 
+    /// Checks that plan is a plugin query.
+    ///
+    /// # Errors
+    /// - top node doesn't exist in the plan or is invalid.
+    pub fn is_plugin(&self) -> Result<bool, SbroadError> {
+        let top_id = self.get_top()?;
+        Ok(matches!(self.get_node(top_id)?, Node::Plugin(_)))
+    }
+
     /// Set top node of plan
     /// # Errors
     /// - top node doesn't exist in the plan.
@@ -1360,7 +1399,8 @@ impl Plan {
             | Node::Ddl(..)
             | Node::Invalid(..)
             | Node::Acl(..)
-            | Node::Block(..) => Err(SbroadError::Invalid(
+            | Node::Block(..)
+            | Node::Plugin(..) => Err(SbroadError::Invalid(
                 Entity::Node,
                 Some(format_smolstr!("node is not Relational type: {node:?}")),
             )),
@@ -1380,6 +1420,7 @@ impl Plan {
             | MutNode::Ddl(..)
             | MutNode::Invalid(..)
             | MutNode::Acl(..)
+            | MutNode::Plugin(..)
             | MutNode::Block(..) => Err(SbroadError::Invalid(
                 Entity::Node,
                 Some("Node is not relational".into()),
@@ -1429,6 +1470,7 @@ impl Plan {
             | MutNode::Ddl(..)
             | MutNode::Invalid(..)
             | MutNode::Acl(..)
+            | MutNode::Plugin(..)
             | MutNode::Block(..) => Err(SbroadError::Invalid(
                 Entity::Node,
                 Some(format_smolstr!(
diff --git a/sbroad-core/src/ir/api/parameter.rs b/sbroad-core/src/ir/api/parameter.rs
index 2a16c6b89..91b9902fd 100644
--- a/sbroad-core/src/ir/api/parameter.rs
+++ b/sbroad-core/src/ir/api/parameter.rs
@@ -354,7 +354,11 @@ impl<'binder> ParamsBinder<'binder> {
                         }
                     }
                 },
-                Node::Invalid(..) | Node::Parameter(..) | Node::Ddl(..) | Node::Acl(..) => {}
+                Node::Invalid(..)
+                | Node::Parameter(..)
+                | Node::Ddl(..)
+                | Node::Acl(..)
+                | Node::Plugin(_) => {}
             }
         }
 
@@ -529,6 +533,7 @@ impl<'binder> ParamsBinder<'binder> {
                 MutNode::Invalid(..)
                 | MutNode::Parameter(..)
                 | MutNode::Ddl(..)
+                | MutNode::Plugin(_)
                 | MutNode::Acl(..) => {}
             }
         }
diff --git a/sbroad-core/src/ir/block.rs b/sbroad-core/src/ir/block.rs
index e1d0c3d11..1891ac175 100644
--- a/sbroad-core/src/ir/block.rs
+++ b/sbroad-core/src/ir/block.rs
@@ -21,6 +21,7 @@ impl Plan {
             | Node::Ddl(..)
             | Node::Acl(..)
             | Node::Invalid(..)
+            | Node::Plugin(_)
             | Node::Parameter(..) => Err(SbroadError::Invalid(
                 Entity::Node,
                 Some(format_smolstr!(
@@ -43,6 +44,7 @@ impl Plan {
             | MutNode::Ddl(..)
             | MutNode::Acl(..)
             | MutNode::Invalid(..)
+            | MutNode::Plugin(_)
             | MutNode::Parameter(..) => Err(SbroadError::Invalid(
                 Entity::Node,
                 Some(format_smolstr!(
diff --git a/sbroad-core/src/ir/distribution.rs b/sbroad-core/src/ir/distribution.rs
index ac2168c8b..1e8451f59 100644
--- a/sbroad-core/src/ir/distribution.rs
+++ b/sbroad-core/src/ir/distribution.rs
@@ -833,6 +833,10 @@ impl Plan {
                 Entity::Distribution,
                 Some("Failed to get distribution for an invalid node.".to_smolstr()),
             )),
+            Node::Plugin(_) => Err(SbroadError::Invalid(
+                Entity::Distribution,
+                Some("Failed to get distribution for a PLUGIN block node.".to_smolstr()),
+            )),
         }
     }
 
diff --git a/sbroad-core/src/ir/expression/types.rs b/sbroad-core/src/ir/expression/types.rs
index cd0c8e71a..ae6614412 100644
--- a/sbroad-core/src/ir/expression/types.rs
+++ b/sbroad-core/src/ir/expression/types.rs
@@ -39,6 +39,10 @@ impl Plan {
                 Entity::Node,
                 Some("code block node has no type".to_smolstr()),
             )),
+            Node::Plugin(_) => Err(SbroadError::Invalid(
+                Entity::Node,
+                Some("Plugin node has no type".to_smolstr()),
+            )),
         }
     }
 }
diff --git a/sbroad-core/src/ir/node.rs b/sbroad-core/src/ir/node.rs
index bcd47df43..cd3ddf822 100644
--- a/sbroad-core/src/ir/node.rs
+++ b/sbroad-core/src/ir/node.rs
@@ -13,6 +13,11 @@ use tarantool::{
     space::SpaceEngineType,
 };
 
+use super::{
+    ddl::AlterSystemType,
+    expression::{cast, FunctionFeature, TrimKind},
+    operator::{self, ConflictStrategy, JoinKind, OrderByElement, UpdateStrategy},
+};
 use crate::ir::{
     acl::{AlterOption, GrantRevokeType},
     ddl::{ColumnDef, Language, ParamDef, SetParamScopeType, SetParamValue},
@@ -22,17 +27,16 @@ use crate::ir::{
     transformation::redistribution::{ColumnPosition, MotionPolicy, Program},
     value::Value,
 };
-
-use super::{
-    ddl::AlterSystemType,
-    expression::{cast, FunctionFeature, TrimKind},
-    operator::{self, ConflictStrategy, JoinKind, OrderByElement, UpdateStrategy},
+use plugin::{
+    AppendServiceToTier, ChangeConfig, CreatePlugin, DisablePlugin, DropPlugin, EnablePlugin,
+    MigrateTo, MutPlugin, Plugin, PluginOwned, RemoveServiceFromTier,
 };
 
 pub mod acl;
 pub mod block;
 pub mod ddl;
 pub mod expression;
+pub mod plugin;
 pub mod relational;
 
 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Hash, Copy)]
@@ -1106,6 +1110,10 @@ pub enum Node96 {
     StableFunction(StableFunction),
     DropProc(DropProc),
     Insert(Insert),
+    CreatePlugin(CreatePlugin),
+    EnablePlugin(EnablePlugin),
+    DisablePlugin(DisablePlugin),
+    DropPlugin(DropPlugin),
 }
 
 impl Node96 {
@@ -1119,6 +1127,10 @@ impl Node96 {
             Node96::StableFunction(stable_func) => {
                 NodeOwned::Expression(ExprOwned::StableFunction(stable_func))
             }
+            Node96::DropPlugin(drop_plugin) => NodeOwned::Plugin(PluginOwned::Drop(drop_plugin)),
+            Node96::CreatePlugin(create) => NodeOwned::Plugin(PluginOwned::Create(create)),
+            Node96::EnablePlugin(enable) => NodeOwned::Plugin(PluginOwned::Enable(enable)),
+            Node96::DisablePlugin(disable) => NodeOwned::Plugin(PluginOwned::Disable(disable)),
         }
     }
 }
@@ -1136,6 +1148,8 @@ pub enum Node136 {
     GrantPrivilege(GrantPrivilege),
     RevokePrivilege(RevokePrivilege),
     Update(Update),
+    MigrateTo(MigrateTo),
+    ChangeConfig(ChangeConfig),
 }
 
 impl Node136 {
@@ -1160,6 +1174,10 @@ impl Node136 {
             Node136::RenameRoutine(rename_routine) => {
                 NodeOwned::Ddl(DdlOwned::RenameRoutine(rename_routine))
             }
+            Node136::MigrateTo(migrate) => NodeOwned::Plugin(PluginOwned::MigrateTo(migrate)),
+            Node136::ChangeConfig(change_config) => {
+                NodeOwned::Plugin(PluginOwned::ChangeConfig(change_config))
+            }
         }
     }
 }
@@ -1170,6 +1188,8 @@ pub enum Node224 {
     Invalid(Invalid),
     CreateTable(CreateTable),
     CreateIndex(CreateIndex),
+    AppendServiceToTier(AppendServiceToTier),
+    RemoveServiceFromTier(RemoveServiceFromTier),
 }
 
 impl Node224 {
@@ -1183,6 +1203,12 @@ impl Node224 {
                 NodeOwned::Ddl(DdlOwned::CreateIndex(create_index))
             }
             Node224::Invalid(inv) => NodeOwned::Invalid(inv),
+            Node224::AppendServiceToTier(append) => {
+                NodeOwned::Plugin(PluginOwned::AppendServiceToTier(append))
+            }
+            Node224::RemoveServiceFromTier(remove) => {
+                NodeOwned::Plugin(PluginOwned::RemoveServiceFromTier(remove))
+            }
         }
     }
 }
@@ -1235,6 +1261,7 @@ pub enum Node<'nodes> {
     Block(Block<'nodes>),
     Parameter(&'nodes Parameter),
     Invalid(&'nodes Invalid),
+    Plugin(Plugin<'nodes>),
 }
 
 #[allow(clippy::module_name_repetitions)]
@@ -1247,6 +1274,7 @@ pub enum MutNode<'nodes> {
     Block(MutBlock<'nodes>),
     Parameter(&'nodes mut Parameter),
     Invalid(&'nodes mut Invalid),
+    Plugin(MutPlugin<'nodes>),
 }
 
 impl Node<'_> {
@@ -1260,6 +1288,7 @@ impl Node<'_> {
             Node::Block(block) => NodeOwned::Block(block.get_block_owned()),
             Node::Parameter(param) => NodeOwned::Parameter((*param).clone()),
             Node::Invalid(inv) => NodeOwned::Invalid((*inv).clone()),
+            Node::Plugin(plugin) => NodeOwned::Plugin(plugin.get_plugin_owned()),
         }
     }
 }
@@ -1275,6 +1304,7 @@ pub enum NodeOwned {
     Block(BlockOwned),
     Parameter(Parameter),
     Invalid(Invalid),
+    Plugin(PluginOwned),
 }
 
 impl From<NodeOwned> for NodeAligned {
@@ -1287,6 +1317,7 @@ impl From<NodeOwned> for NodeAligned {
             NodeOwned::Invalid(inv) => inv.into(),
             NodeOwned::Parameter(param) => param.into(),
             NodeOwned::Relational(rel) => rel.into(),
+            NodeOwned::Plugin(p) => p.into(),
         }
     }
 }
diff --git a/sbroad-core/src/ir/node/plugin.rs b/sbroad-core/src/ir/node/plugin.rs
new file mode 100644
index 000000000..1717fe562
--- /dev/null
+++ b/sbroad-core/src/ir/node/plugin.rs
@@ -0,0 +1,523 @@
+use crate::errors::{Entity, SbroadError};
+use crate::ir::node::{Node136, Node224, Node96, NodeAligned};
+use crate::ir::{Node, NodeId, Plan};
+use serde::{Deserialize, Serialize};
+use smol_str::{format_smolstr, SmolStr};
+use tarantool::decimal::Decimal;
+
+#[must_use]
+pub fn get_default_timeout() -> Decimal {
+    Decimal::from(10)
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+pub struct MigrateToOpts {
+    pub timeout: Decimal,
+    pub rollback_timeout: Decimal,
+}
+
+impl Default for MigrateToOpts {
+    fn default() -> Self {
+        MigrateToOpts {
+            timeout: get_default_timeout(),
+            rollback_timeout: get_default_timeout(),
+        }
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, PartialOrd, Ord)]
+pub struct SettingsPair {
+    pub key: SmolStr,
+    pub value: SmolStr,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, PartialOrd, Ord)]
+pub struct ServiceSettings {
+    pub name: SmolStr,
+    pub pairs: Vec<SettingsPair>,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+pub struct CreatePlugin {
+    pub name: SmolStr,
+    pub version: SmolStr,
+    pub if_not_exists: bool,
+    pub timeout: Decimal,
+}
+
+impl From<CreatePlugin> for NodeAligned {
+    fn from(value: CreatePlugin) -> Self {
+        Self::Node96(Node96::CreatePlugin(value))
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+pub struct EnablePlugin {
+    pub name: SmolStr,
+    pub version: SmolStr,
+    pub timeout: Decimal,
+}
+
+impl From<EnablePlugin> for NodeAligned {
+    fn from(value: EnablePlugin) -> Self {
+        Self::Node96(Node96::EnablePlugin(value))
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+pub struct DisablePlugin {
+    pub name: SmolStr,
+    pub version: SmolStr,
+    pub timeout: Decimal,
+}
+
+impl From<DisablePlugin> for NodeAligned {
+    fn from(value: DisablePlugin) -> Self {
+        Self::Node96(Node96::DisablePlugin(value))
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+pub struct DropPlugin {
+    pub name: SmolStr,
+    pub version: SmolStr,
+    pub if_exists: bool,
+    pub with_data: bool,
+    pub timeout: Decimal,
+}
+
+impl From<DropPlugin> for NodeAligned {
+    fn from(value: DropPlugin) -> Self {
+        Self::Node96(Node96::DropPlugin(value))
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+pub struct MigrateTo {
+    pub name: SmolStr,
+    pub version: SmolStr,
+    pub opts: MigrateToOpts,
+}
+
+impl From<MigrateTo> for NodeAligned {
+    fn from(value: MigrateTo) -> Self {
+        Self::Node136(Node136::MigrateTo(value))
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+pub struct AppendServiceToTier {
+    pub plugin_name: SmolStr,
+    pub version: SmolStr,
+    pub service_name: SmolStr,
+    pub tier: SmolStr,
+    pub timeout: Decimal,
+}
+
+impl From<AppendServiceToTier> for NodeAligned {
+    fn from(value: AppendServiceToTier) -> Self {
+        Self::Node224(Node224::AppendServiceToTier(value))
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+pub struct RemoveServiceFromTier {
+    pub plugin_name: SmolStr,
+    pub version: SmolStr,
+    pub service_name: SmolStr,
+    pub tier: SmolStr,
+    pub timeout: Decimal,
+}
+
+impl From<RemoveServiceFromTier> for NodeAligned {
+    fn from(value: RemoveServiceFromTier) -> Self {
+        Self::Node224(Node224::RemoveServiceFromTier(value))
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+pub struct ChangeConfig {
+    pub plugin_name: SmolStr,
+    pub version: SmolStr,
+    pub key_value_grouped: Vec<ServiceSettings>,
+    pub timeout: Decimal,
+}
+
+impl From<ChangeConfig> for NodeAligned {
+    fn from(value: ChangeConfig) -> Self {
+        Self::Node136(Node136::ChangeConfig(value))
+    }
+}
+
+#[derive(Debug, Eq, PartialEq, Serialize)]
+pub enum MutPlugin<'a> {
+    Create(&'a mut CreatePlugin),
+    Enable(&'a mut EnablePlugin),
+    Disable(&'a mut DisablePlugin),
+    Drop(&'a mut DropPlugin),
+    MigrateTo(&'a mut MigrateTo),
+    AppendServiceToTier(&'a mut AppendServiceToTier),
+    RemoveServiceFromTier(&'a mut RemoveServiceFromTier),
+    ChangeConfig(&'a mut ChangeConfig),
+}
+
+/// Represent a plugin query.
+#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
+pub enum Plugin<'a> {
+    /// Create a new plugin.
+    Create(&'a CreatePlugin),
+    /// Enable plugin.
+    Enable(&'a EnablePlugin),
+    /// Disable plugin.
+    Disable(&'a DisablePlugin),
+    /// Remove plugin from system.
+    Drop(&'a DropPlugin),
+    /// Run installed plugin migrations.
+    MigrateTo(&'a MigrateTo),
+    /// Append plugin service to a tier.
+    AppendServiceToTier(&'a AppendServiceToTier),
+    /// Remove plugin service from tier.
+    RemoveServiceFromTier(&'a RemoveServiceFromTier),
+    /// Change plugin service configuration.
+    ChangeConfig(&'a ChangeConfig),
+}
+
+impl<'a> Plugin<'a> {
+    #[must_use]
+    pub fn get_plugin_owned(&self) -> PluginOwned {
+        match self {
+            Plugin::Create(create) => PluginOwned::Create((*create).clone()),
+            Plugin::Enable(enable) => PluginOwned::Enable((*enable).clone()),
+            Plugin::Disable(disable) => PluginOwned::Disable((*disable).clone()),
+            Plugin::Drop(drop) => PluginOwned::Drop((*drop).clone()),
+            Plugin::MigrateTo(migrate_to) => PluginOwned::MigrateTo((*migrate_to).clone()),
+            Plugin::AppendServiceToTier(add_to_tier) => {
+                PluginOwned::AppendServiceToTier((*add_to_tier).clone())
+            }
+            Plugin::RemoveServiceFromTier(rm_from_tier) => {
+                PluginOwned::RemoveServiceFromTier((*rm_from_tier).clone())
+            }
+            Plugin::ChangeConfig(change_config) => {
+                PluginOwned::ChangeConfig((*change_config).clone())
+            }
+        }
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+pub enum PluginOwned {
+    /// Create a new plugin.
+    Create(CreatePlugin),
+    /// Enable plugin.
+    Enable(EnablePlugin),
+    /// Disable plugin.
+    Disable(DisablePlugin),
+    /// Remove plugin from system.
+    Drop(DropPlugin),
+    /// Run installed plugin migrations.
+    MigrateTo(MigrateTo),
+    /// Append plugin service to a tier.
+    AppendServiceToTier(AppendServiceToTier),
+    /// Remove plugin service from tier.
+    RemoveServiceFromTier(RemoveServiceFromTier),
+    /// Change plugin service configuration.
+    ChangeConfig(ChangeConfig),
+}
+
+impl From<PluginOwned> for NodeAligned {
+    fn from(value: PluginOwned) -> Self {
+        match value {
+            PluginOwned::Create(create) => create.into(),
+            PluginOwned::Enable(enable) => enable.into(),
+            PluginOwned::Disable(disable) => disable.into(),
+            PluginOwned::Drop(drop) => drop.into(),
+            PluginOwned::MigrateTo(migrate) => migrate.into(),
+            PluginOwned::AppendServiceToTier(add) => add.into(),
+            PluginOwned::RemoveServiceFromTier(rm) => rm.into(),
+            PluginOwned::ChangeConfig(change_config) => change_config.into(),
+        }
+    }
+}
+
+impl Plan {
+    /// Get a reference to a plugin node.
+    ///
+    /// # Errors
+    /// - the node is not a block node.
+    pub fn get_plugin_node(&self, node_id: NodeId) -> Result<Plugin, SbroadError> {
+        let node = self.get_node(node_id)?;
+        match node {
+            Node::Plugin(plugin) => Ok(plugin),
+            _ => Err(SbroadError::Invalid(
+                Entity::Node,
+                Some(format_smolstr!(
+                    "node {node:?} (id {node_id}) is not Block type"
+                )),
+            )),
+        }
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::{
+        get_default_timeout, AppendServiceToTier, ChangeConfig, CreatePlugin, DisablePlugin,
+        DropPlugin, EnablePlugin, MigrateTo, MigrateToOpts, PluginOwned, RemoveServiceFromTier,
+        ServiceSettings, SettingsPair,
+    };
+    use crate::executor::engine::mock::RouterConfigurationMock;
+    use crate::frontend::sql::ast::AbstractSyntaxTree;
+    use crate::frontend::Ast;
+    use crate::ir::node::{ArenaType, NodeId};
+    use crate::ir::Plan;
+    use smol_str::SmolStr;
+    use tarantool::decimal::Decimal;
+
+    #[test]
+    fn test_plugin_parsing() {
+        struct TestCase {
+            sql: &'static str,
+            arena_type: ArenaType,
+            expected: PluginOwned,
+        }
+
+        let test_cases = &[
+            TestCase {
+                sql: r#"CREATE PLUGIN "abc" 0.1.1"#,
+                arena_type: ArenaType::Arena96,
+                expected: PluginOwned::Create(CreatePlugin {
+                    name: SmolStr::from("abc"),
+                    version: SmolStr::from("0.1.1"),
+                    if_not_exists: false,
+                    timeout: get_default_timeout(),
+                }),
+            },
+            TestCase {
+                sql: r#"CREATE PLUGIN "test_plugin" 0.0.1"#,
+                arena_type: ArenaType::Arena96,
+                expected: PluginOwned::Create(CreatePlugin {
+                    name: SmolStr::from("test_plugin"),
+                    version: SmolStr::from("0.0.1"),
+                    if_not_exists: false,
+                    timeout: get_default_timeout(),
+                }),
+            },
+            TestCase {
+                sql: r#"CREATE PLUGIN IF NOT EXISTS "abc" 0.1.1"#,
+                arena_type: ArenaType::Arena96,
+                expected: PluginOwned::Create(CreatePlugin {
+                    name: SmolStr::from("abc"),
+                    version: SmolStr::from("0.1.1"),
+                    if_not_exists: true,
+                    timeout: get_default_timeout(),
+                }),
+            },
+            TestCase {
+                sql: r#"CREATE PLUGIN IF NOT EXISTS "abcde" 0.1.2 option(timeout=1)"#,
+                arena_type: ArenaType::Arena96,
+                expected: PluginOwned::Create(CreatePlugin {
+                    name: SmolStr::from("abcde"),
+                    version: SmolStr::from("0.1.2"),
+                    if_not_exists: true,
+                    timeout: Decimal::from(1),
+                }),
+            },
+            TestCase {
+                sql: r#"ALTER PLUGIN "abc" 1.1.1 ENABLE"#,
+                arena_type: ArenaType::Arena96,
+                expected: PluginOwned::Enable(EnablePlugin {
+                    name: SmolStr::from("abc"),
+                    version: SmolStr::from("1.1.1"),
+                    timeout: Decimal::from(10),
+                }),
+            },
+            TestCase {
+                sql: r#"ALTER PLUGIN "abc" 1.1.1 ENABLE option(timeout=1)"#,
+                arena_type: ArenaType::Arena96,
+                expected: PluginOwned::Enable(EnablePlugin {
+                    name: SmolStr::from("abc"),
+                    version: SmolStr::from("1.1.1"),
+                    timeout: Decimal::from(1),
+                }),
+            },
+            TestCase {
+                sql: r#"ALTER PLUGIN "abc" 1.1.1 DISABLE option(timeout=1)"#,
+                arena_type: ArenaType::Arena96,
+                expected: PluginOwned::Disable(DisablePlugin {
+                    name: SmolStr::from("abc"),
+                    version: SmolStr::from("1.1.1"),
+                    timeout: Decimal::from(1),
+                }),
+            },
+            TestCase {
+                sql: r#"DROP PLUGIN "abc" 1.1.1 option(timeout=1)"#,
+                arena_type: ArenaType::Arena96,
+                expected: PluginOwned::Drop(DropPlugin {
+                    name: SmolStr::from("abc"),
+                    version: SmolStr::from("1.1.1"),
+                    if_exists: false,
+                    with_data: false,
+                    timeout: Decimal::from(1),
+                }),
+            },
+            TestCase {
+                sql: r#"DROP PLUGIN IF EXISTS "abcde" 1.1.1 WITH DATA option(timeout=10)"#,
+                arena_type: ArenaType::Arena96,
+                expected: PluginOwned::Drop(DropPlugin {
+                    name: SmolStr::from("abcde"),
+                    version: SmolStr::from("1.1.1"),
+                    if_exists: true,
+                    with_data: true,
+                    timeout: Decimal::from(10),
+                }),
+            },
+            TestCase {
+                sql: r#"ALTER PLUGIN "abc" MIGRATE TO 0.1.0"#,
+                arena_type: ArenaType::Arena136,
+                expected: PluginOwned::MigrateTo(MigrateTo {
+                    name: SmolStr::from("abc"),
+                    version: SmolStr::from("0.1.0"),
+                    opts: MigrateToOpts {
+                        timeout: get_default_timeout(),
+                        rollback_timeout: get_default_timeout(),
+                    },
+                }),
+            },
+            TestCase {
+                sql: r#"ALTER PLUGIN "abc" MIGRATE TO 0.1.0 option(timeout=11, rollback_timeout=12)"#,
+                arena_type: ArenaType::Arena136,
+                expected: PluginOwned::MigrateTo(MigrateTo {
+                    name: SmolStr::from("abc"),
+                    version: SmolStr::from("0.1.0"),
+                    opts: MigrateToOpts {
+                        timeout: Decimal::from(11),
+                        rollback_timeout: Decimal::from(12),
+                    },
+                }),
+            },
+            TestCase {
+                sql: r#"ALTER PLUGIN "abc" 0.1.0 ADD SERVICE "svc1" TO TIER "tier1" option(timeout=1)"#,
+                arena_type: ArenaType::Arena224,
+                expected: PluginOwned::AppendServiceToTier(AppendServiceToTier {
+                    service_name: SmolStr::from("svc1"),
+                    plugin_name: SmolStr::from("abc"),
+                    version: SmolStr::from("0.1.0"),
+                    tier: SmolStr::from("tier1"),
+                    timeout: Decimal::from(1),
+                }),
+            },
+            TestCase {
+                sql: r#"ALTER PLUGIN "abc" 0.1.0 REMOVE SERVICE "svc1" FROM TIER "tier1" option(timeout=11)"#,
+                arena_type: ArenaType::Arena224,
+                expected: PluginOwned::RemoveServiceFromTier(RemoveServiceFromTier {
+                    service_name: SmolStr::from("svc1"),
+                    plugin_name: SmolStr::from("abc"),
+                    version: SmolStr::from("0.1.0"),
+                    tier: SmolStr::from("tier1"),
+                    timeout: Decimal::from(11),
+                }),
+            },
+            TestCase {
+                sql: r#"ALTER PLUGIN "abc" 0.1.0 SET "svc1"."key1" = '{"a": 1, "b": 2}' option(timeout=12)"#,
+                arena_type: ArenaType::Arena136,
+                expected: PluginOwned::ChangeConfig(ChangeConfig {
+                    plugin_name: SmolStr::from("abc"),
+                    version: SmolStr::from("0.1.0"),
+                    key_value_grouped: vec![ServiceSettings {
+                        name: SmolStr::from("svc1"),
+                        pairs: vec![SettingsPair {
+                            key: SmolStr::from("key1"),
+                            value: SmolStr::from("{\"a\": 1, \"b\": 2}"),
+                        }],
+                    }],
+                    timeout: Decimal::from(12),
+                }),
+            },
+            TestCase {
+                sql: r#"ALTER PLUGIN "abc" 0.1.0 SET "svc1"."key1" = 'a', "svc2"."key2" = 'b', "svc3"."key3" = 'c' option(timeout=11)"#,
+                arena_type: ArenaType::Arena136,
+                expected: PluginOwned::ChangeConfig(ChangeConfig {
+                    plugin_name: SmolStr::from("abc"),
+                    version: SmolStr::from("0.1.0"),
+                    key_value_grouped: vec![
+                        ServiceSettings {
+                            name: SmolStr::from("svc1"),
+                            pairs: vec![SettingsPair {
+                                key: SmolStr::from("key1"),
+                                value: SmolStr::from("a"),
+                            }],
+                        },
+                        ServiceSettings {
+                            name: SmolStr::from("svc2"),
+                            pairs: vec![SettingsPair {
+                                key: SmolStr::from("key2"),
+                                value: SmolStr::from("b"),
+                            }],
+                        },
+                        ServiceSettings {
+                            name: SmolStr::from("svc3"),
+                            pairs: vec![SettingsPair {
+                                key: SmolStr::from("key3"),
+                                value: SmolStr::from("c"),
+                            }],
+                        },
+                    ],
+                    timeout: Decimal::from(11),
+                }),
+            },
+            TestCase {
+                sql: r#"ALTER PLUGIN "abc" 0.1.0 SET "svc1"."key1" = 'a', "svc1"."key2" = 'b'"#,
+                arena_type: ArenaType::Arena136,
+                expected: PluginOwned::ChangeConfig(ChangeConfig {
+                    plugin_name: SmolStr::from("abc"),
+                    version: SmolStr::from("0.1.0"),
+                    key_value_grouped: vec![ServiceSettings {
+                        name: SmolStr::from("svc1"),
+                        pairs: vec![
+                            SettingsPair {
+                                key: SmolStr::from("key1"),
+                                value: SmolStr::from("a"),
+                            },
+                            SettingsPair {
+                                key: SmolStr::from("key2"),
+                                value: SmolStr::from("b"),
+                            },
+                        ],
+                    }],
+                    timeout: Decimal::from(10),
+                }),
+            },
+        ];
+
+        for tc in test_cases {
+            let metadata = &RouterConfigurationMock::new();
+            let plan: Plan = AbstractSyntaxTree::transform_into_plan(tc.sql, metadata).unwrap();
+            let node = plan
+                .get_plugin_node(NodeId {
+                    offset: 0,
+                    arena_type: tc.arena_type,
+                })
+                .unwrap()
+                .get_plugin_owned();
+            let node = if let PluginOwned::ChangeConfig(ChangeConfig {
+                plugin_name,
+                version,
+                mut key_value_grouped,
+                timeout,
+            }) = node
+            {
+                key_value_grouped.sort();
+                PluginOwned::ChangeConfig(ChangeConfig {
+                    plugin_name,
+                    version,
+                    key_value_grouped,
+                    timeout,
+                })
+            } else {
+                node
+            };
+
+            assert_eq!(node, tc.expected, "from sql: `{}`", tc.sql);
+        }
+    }
+}
diff --git a/sbroad-core/src/ir/tree/expression.rs b/sbroad-core/src/ir/tree/expression.rs
index 5d708111b..467efe2fa 100644
--- a/sbroad-core/src/ir/tree/expression.rs
+++ b/sbroad-core/src/ir/tree/expression.rs
@@ -172,6 +172,7 @@ fn expression_next<'nodes>(iter: &mut impl ExpressionTreeIterator<'nodes>) -> Op
                 | Node::Ddl(_)
                 | Node::Relational(_)
                 | Node::Invalid(_)
+                | Node::Plugin(_)
                 | Node::Parameter(_) => None,
             }
         }
diff --git a/sbroad-core/src/ir/tree/relation.rs b/sbroad-core/src/ir/tree/relation.rs
index 6399b3097..fe25f5824 100644
--- a/sbroad-core/src/ir/tree/relation.rs
+++ b/sbroad-core/src/ir/tree/relation.rs
@@ -115,7 +115,8 @@ fn relational_next<'nodes>(iter: &mut impl RelationalTreeIterator<'nodes>) -> Op
             | Node::Invalid(_)
             | Node::Ddl(_)
             | Node::Acl(_)
-            | Node::Block(_),
+            | Node::Block(_)
+            | Node::Plugin(_),
         )
         | None => None,
     }
diff --git a/sbroad-core/src/ir/tree/subtree.rs b/sbroad-core/src/ir/tree/subtree.rs
index 0db2a6332..2a505a2c7 100644
--- a/sbroad-core/src/ir/tree/subtree.rs
+++ b/sbroad-core/src/ir/tree/subtree.rs
@@ -205,7 +205,8 @@ fn subtree_next<'plan>(
             | Node::Parameter(..)
             | Node::Ddl(..)
             | Node::Acl(..)
-            | Node::Block(..) => None,
+            | Node::Block(..)
+            | Node::Plugin(..) => None,
             Node::Expression(expr) => match expr {
                 Expression::Alias { .. }
                 | Expression::ExprInParentheses { .. }
-- 
GitLab