From 529568fa656eb13aa2a8702787c1ab95ae57ba0e Mon Sep 17 00:00:00 2001 From: tmichaeldb Date: Thu, 26 Oct 2023 17:11:43 -0700 Subject: [PATCH 1/2] Add support for CREATE, UPDATE, DROP skill --- .../parser/dialects/mindsdb/__init__.py | 1 + mindsdb_sql/parser/dialects/mindsdb/lexer.py | 2 + mindsdb_sql/parser/dialects/mindsdb/parser.py | 23 +++++ mindsdb_sql/parser/dialects/mindsdb/skills.py | 93 +++++++++++++++++++ tests/test_parser/test_mindsdb/test_skills.py | 49 ++++++++++ 5 files changed, 168 insertions(+) create mode 100644 mindsdb_sql/parser/dialects/mindsdb/skills.py create mode 100644 tests/test_parser/test_mindsdb/test_skills.py diff --git a/mindsdb_sql/parser/dialects/mindsdb/__init__.py b/mindsdb_sql/parser/dialects/mindsdb/__init__.py index 4ab0a9dd..d7e5d4ba 100644 --- a/mindsdb_sql/parser/dialects/mindsdb/__init__.py +++ b/mindsdb_sql/parser/dialects/mindsdb/__init__.py @@ -16,6 +16,7 @@ from .chatbot import CreateChatBot, UpdateChatBot, DropChatBot from .trigger import CreateTrigger, DropTrigger from .knowledge_base import CreateKnowledgeBase, DropKnowledgeBase +from .skills import CreateSkill, DropSkill, UpdateSkill # remove it in next release CreateDatasource = CreateDatabase diff --git a/mindsdb_sql/parser/dialects/mindsdb/lexer.py b/mindsdb_sql/parser/dialects/mindsdb/lexer.py index 19c200ee..74780f3b 100644 --- a/mindsdb_sql/parser/dialects/mindsdb/lexer.py +++ b/mindsdb_sql/parser/dialects/mindsdb/lexer.py @@ -31,6 +31,7 @@ class MindsDBLexer(Lexer): ENGINE, TRAIN, PREDICT, PARAMETERS, JOB, CHATBOT, EVERY,PROJECT, ANOMALY, DETECTION, KNOWLEDGE_BASE, KNOWLEDGE_BASES, + SKILL, # SHOW/DDL Keywords @@ -119,6 +120,7 @@ class MindsDBLexer(Lexer): KNOWLEDGE_BASE = r'\bKNOWLEDGE[_|\s]BASE\b' KNOWLEDGE_BASES = r'\bKNOWLEDGE[_|\s]BASES\b' + SKILL = r'\bSKILL\b' # Misc SET = r'\bSET\b' diff --git a/mindsdb_sql/parser/dialects/mindsdb/parser.py b/mindsdb_sql/parser/dialects/mindsdb/parser.py index bd9799dd..2537dd84 100644 --- a/mindsdb_sql/parser/dialects/mindsdb/parser.py +++ b/mindsdb_sql/parser/dialects/mindsdb/parser.py @@ -17,6 +17,7 @@ from mindsdb_sql.parser.dialects.mindsdb.evaluate import Evaluate from mindsdb_sql.parser.dialects.mindsdb.create_file import CreateFile from mindsdb_sql.parser.dialects.mindsdb.knowledge_base import CreateKnowledgeBase, DropKnowledgeBase +from mindsdb_sql.parser.dialects.mindsdb.skills import CreateSkill, DropSkill, UpdateSkill from mindsdb_sql.exceptions import ParsingException from mindsdb_sql.parser.dialects.mindsdb.lexer import MindsDBLexer from mindsdb_sql.parser.dialects.mindsdb.retrain_predictor import RetrainPredictor @@ -85,6 +86,9 @@ class MindsDBParser(Parser): 'drop_trigger', 'create_kb', 'drop_kb', + 'create_skill', + 'drop_skill', + 'update_skill' ) def query(self, p): return p[0] @@ -130,6 +134,25 @@ def create_kb(self, p): def drop_kb(self, p): return DropKnowledgeBase(name=p.identifier, if_exists=p.if_exists_or_empty) + # -- Skills -- + @_('CREATE SKILL if_not_exists_or_empty identifier USING kw_parameter_list') + def create_skill(self, p): + params = p.kw_parameter_list + + return CreateSkill( + name=p.identifier, + type=params.pop('type'), + params=params + ) + + @_('DROP SKILL if_exists_or_empty identifier') + def drop_skill(self, p): + return DropSkill(name=p.identifier, if_exists=p.if_exists_or_empty) + + @_('UPDATE SKILL identifier SET kw_parameter_list') + def update_skill(self, p): + return UpdateSkill(name=p.identifier, updated_params=p.kw_parameter_list) + # -- ChatBot -- @_('CREATE CHATBOT identifier USING kw_parameter_list') def create_chat_bot(self, p): diff --git a/mindsdb_sql/parser/dialects/mindsdb/skills.py b/mindsdb_sql/parser/dialects/mindsdb/skills.py new file mode 100644 index 00000000..91bb3bff --- /dev/null +++ b/mindsdb_sql/parser/dialects/mindsdb/skills.py @@ -0,0 +1,93 @@ +from mindsdb_sql.parser.ast.base import ASTNode +from mindsdb_sql.parser.utils import indent + + +class CreateSkill(ASTNode): + """ + Node for creating a new skill + """ + + def __init__(self, name, type, params, if_not_exists=False, *args, **kwargs): + """ + Parameters: + name (Identifier): name of the skill to create + type (str): type of the skill to create + params (dict): USING parameters to create the skill with + if_not_exists (bool): if True, do not raise an error if the skill exists + """ + super().__init__(*args, **kwargs) + self.name = name + self.type = type + self.params = params + self.if_not_exists = if_not_exists + + def to_tree(self, level=0, *args, **kwargs): + ind = indent(level) + out_str = f'{ind}CreateSkill(' \ + f'if_not_exists={self.if_not_exists}' \ + f'name={self.name.to_string()}, ' \ + f'type={self.type}, ' \ + f'params={self.params})' + return out_str + + def get_string(self, *args, **kwargs): + using_ar = [f'{k}={repr(v)}' for k, v in self.params.items()] + using_str = ', '.join(using_ar) + + out_str = f'CREATE SKILL {"IF NOT EXISTS" if self.if_not_exists else ""}{self.name.to_string()} USING {using_str}' + return out_str + + +class UpdateSkill(ASTNode): + """ + Node for updating a skill + """ + + def __init__(self, name, updated_params, *args, **kwargs): + """ + Parameters: + name (Identifier): name of the skill to update + updated_params (dict): new SET parameters of the skill to update + """ + super().__init__(*args, **kwargs) + self.name = name + self.params = updated_params + + def to_tree(self, level=0, *args, **kwargs): + ind = indent(level) + out_str = f'{ind}UpdateSkill(' \ + f'name={self.name.to_string()}, ' \ + f'updated_params={self.params})' + return out_str + + def get_string(self, *args, **kwargs): + set_ar = [f'{k}={repr(v)}' for k, v in self.params.items()] + set_str = ', '.join(set_ar) + + out_str = f'UPDATE SKILL {self.name.to_string()} SET {set_str}' + return out_str + + +class DropSkill(ASTNode): + """ + Node for dropping a skill + """ + + def __init__(self, name, if_exists=False, *args, **kwargs): + """ + Parameters: + name (Identifier): name of the skill to drop + if_exists (bool): if True, do not raise an error if the skill does not exist + """ + super().__init__(*args, **kwargs) + self.name = name + self.if_exists = if_exists + + def to_tree(self, level=0, *args, **kwargs): + ind = indent(level) + out_str = f'{ind}DropSkill(if_exists={self.if_exists}, name={self.name.to_string()})' + return out_str + + def get_string(self, *args, **kwargs): + out_str = f'DROP SKILL {"IF EXISTS" if self.if_exists else ""}{str(self.name.to_string())}' + return out_str diff --git a/tests/test_parser/test_mindsdb/test_skills.py b/tests/test_parser/test_mindsdb/test_skills.py new file mode 100644 index 00000000..1e73a73d --- /dev/null +++ b/tests/test_parser/test_mindsdb/test_skills.py @@ -0,0 +1,49 @@ +from mindsdb_sql import parse_sql +from mindsdb_sql.parser.dialects.mindsdb import * +from mindsdb_sql.parser.ast import * + + +class TestSkills: + def test_create_skill(self): + sql = ''' + create skill my_skill + using + type = 'knowledge_base', + source ='my_knowledge_base' + ''' + ast = parse_sql(sql, dialect='mindsdb') + expected_ast = CreateSkill( + name=Identifier('my_skill'), + type='knowledge_base', + params={'source': 'my_knowledge_base'} + ) + assert str(ast) == str(expected_ast) + assert ast.to_tree() == expected_ast.to_tree() + + def test_update_skill(self): + sql = ''' + update skill my_skill + set + source = 'new_source' + ''' + ast = parse_sql(sql, dialect='mindsdb') + expected_params = { + 'source': 'new_source' + } + expected_ast = UpdateSkill( + name=Identifier('my_skill'), + updated_params=expected_params + ) + assert str(ast) == str(expected_ast) + assert ast.to_tree() == expected_ast.to_tree() + + def test_drop_skill(self): + sql = ''' + drop skill my_skill + ''' + ast = parse_sql(sql, dialect='mindsdb') + expected_ast = DropSkill( + name=Identifier('my_skill'), + ) + assert str(ast) == str(expected_ast) + assert ast.to_tree() == expected_ast.to_tree() From 66837ca7e7664edbd1ed2639281d318d27929f79 Mon Sep 17 00:00:00 2001 From: tmichaeldb Date: Tue, 31 Oct 2023 15:16:28 -0700 Subject: [PATCH 2/2] Fixed "if not exists" for create skill --- mindsdb_sql/parser/dialects/mindsdb/parser.py | 3 ++- mindsdb_sql/parser/dialects/mindsdb/skills.py | 7 ++++--- tests/test_parser/test_mindsdb/test_skills.py | 21 +++++++++++++++---- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/mindsdb_sql/parser/dialects/mindsdb/parser.py b/mindsdb_sql/parser/dialects/mindsdb/parser.py index 2537dd84..99530df6 100644 --- a/mindsdb_sql/parser/dialects/mindsdb/parser.py +++ b/mindsdb_sql/parser/dialects/mindsdb/parser.py @@ -142,7 +142,8 @@ def create_skill(self, p): return CreateSkill( name=p.identifier, type=params.pop('type'), - params=params + params=params, + if_not_exists=p.if_not_exists_or_empty ) @_('DROP SKILL if_exists_or_empty identifier') diff --git a/mindsdb_sql/parser/dialects/mindsdb/skills.py b/mindsdb_sql/parser/dialects/mindsdb/skills.py index 91bb3bff..2a03d2bd 100644 --- a/mindsdb_sql/parser/dialects/mindsdb/skills.py +++ b/mindsdb_sql/parser/dialects/mindsdb/skills.py @@ -31,10 +31,11 @@ def to_tree(self, level=0, *args, **kwargs): return out_str def get_string(self, *args, **kwargs): - using_ar = [f'{k}={repr(v)}' for k, v in self.params.items()] + using_ar = [f'type={repr(self.type)}'] + using_ar += [f'{k}={repr(v)}' for k, v in self.params.items()] using_str = ', '.join(using_ar) - out_str = f'CREATE SKILL {"IF NOT EXISTS" if self.if_not_exists else ""}{self.name.to_string()} USING {using_str}' + out_str = f'CREATE SKILL {"IF NOT EXISTS " if self.if_not_exists else ""}{self.name.to_string()} USING {using_str}' return out_str @@ -89,5 +90,5 @@ def to_tree(self, level=0, *args, **kwargs): return out_str def get_string(self, *args, **kwargs): - out_str = f'DROP SKILL {"IF EXISTS" if self.if_exists else ""}{str(self.name.to_string())}' + out_str = f'DROP SKILL {"IF EXISTS " if self.if_exists else ""}{str(self.name.to_string())}' return out_str diff --git a/tests/test_parser/test_mindsdb/test_skills.py b/tests/test_parser/test_mindsdb/test_skills.py index 1e73a73d..b79a4faf 100644 --- a/tests/test_parser/test_mindsdb/test_skills.py +++ b/tests/test_parser/test_mindsdb/test_skills.py @@ -6,7 +6,7 @@ class TestSkills: def test_create_skill(self): sql = ''' - create skill my_skill + create skill if not exists my_skill using type = 'knowledge_base', source ='my_knowledge_base' @@ -15,10 +15,15 @@ def test_create_skill(self): expected_ast = CreateSkill( name=Identifier('my_skill'), type='knowledge_base', - params={'source': 'my_knowledge_base'} + params={'source': 'my_knowledge_base'}, + if_not_exists=True ) assert str(ast) == str(expected_ast) assert ast.to_tree() == expected_ast.to_tree() + + # Parse again after rendering to catch problems with rendering. + ast = parse_sql(str(ast), dialect='mindsdb') + assert str(ast) == str(expected_ast) def test_update_skill(self): sql = ''' @@ -37,13 +42,21 @@ def test_update_skill(self): assert str(ast) == str(expected_ast) assert ast.to_tree() == expected_ast.to_tree() + # Parse again after rendering to catch problems with rendering. + ast = parse_sql(str(ast), dialect='mindsdb') + assert str(ast) == str(expected_ast) + def test_drop_skill(self): sql = ''' - drop skill my_skill + drop skill if exists my_skill ''' ast = parse_sql(sql, dialect='mindsdb') expected_ast = DropSkill( - name=Identifier('my_skill'), + name=Identifier('my_skill'), if_exists=True ) assert str(ast) == str(expected_ast) assert ast.to_tree() == expected_ast.to_tree() + + # Parse again after rendering to catch problems with rendering. + ast = parse_sql(str(ast), dialect='mindsdb') + assert str(ast) == str(expected_ast)