MindsDB 25.7.3.0__py3-none-any.whl → 25.8.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of MindsDB might be problematic. Click here for more details.

Files changed (102) hide show
  1. mindsdb/__about__.py +1 -1
  2. mindsdb/__main__.py +11 -1
  3. mindsdb/api/a2a/common/server/server.py +16 -6
  4. mindsdb/api/executor/command_executor.py +215 -150
  5. mindsdb/api/executor/datahub/datanodes/project_datanode.py +14 -3
  6. mindsdb/api/executor/planner/plan_join.py +3 -0
  7. mindsdb/api/executor/planner/plan_join_ts.py +117 -100
  8. mindsdb/api/executor/planner/query_planner.py +1 -0
  9. mindsdb/api/executor/sql_query/steps/apply_predictor_step.py +54 -85
  10. mindsdb/api/executor/sql_query/steps/fetch_dataframe.py +21 -24
  11. mindsdb/api/executor/sql_query/steps/fetch_dataframe_partition.py +9 -3
  12. mindsdb/api/executor/sql_query/steps/subselect_step.py +11 -8
  13. mindsdb/api/executor/utilities/mysql_to_duckdb_functions.py +264 -0
  14. mindsdb/api/executor/utilities/sql.py +30 -0
  15. mindsdb/api/http/initialize.py +18 -44
  16. mindsdb/api/http/namespaces/agents.py +23 -20
  17. mindsdb/api/http/namespaces/chatbots.py +83 -120
  18. mindsdb/api/http/namespaces/file.py +1 -1
  19. mindsdb/api/http/namespaces/jobs.py +38 -60
  20. mindsdb/api/http/namespaces/tree.py +69 -61
  21. mindsdb/api/http/namespaces/views.py +56 -72
  22. mindsdb/api/mcp/start.py +2 -0
  23. mindsdb/api/mysql/mysql_proxy/utilities/dump.py +3 -2
  24. mindsdb/integrations/handlers/autogluon_handler/requirements.txt +1 -1
  25. mindsdb/integrations/handlers/autosklearn_handler/requirements.txt +1 -1
  26. mindsdb/integrations/handlers/bigquery_handler/bigquery_handler.py +25 -5
  27. mindsdb/integrations/handlers/chromadb_handler/chromadb_handler.py +3 -3
  28. mindsdb/integrations/handlers/db2_handler/db2_handler.py +19 -23
  29. mindsdb/integrations/handlers/flaml_handler/requirements.txt +1 -1
  30. mindsdb/integrations/handlers/gong_handler/__about__.py +2 -0
  31. mindsdb/integrations/handlers/gong_handler/__init__.py +30 -0
  32. mindsdb/integrations/handlers/gong_handler/connection_args.py +37 -0
  33. mindsdb/integrations/handlers/gong_handler/gong_handler.py +164 -0
  34. mindsdb/integrations/handlers/gong_handler/gong_tables.py +508 -0
  35. mindsdb/integrations/handlers/gong_handler/icon.svg +25 -0
  36. mindsdb/integrations/handlers/gong_handler/test_gong_handler.py +125 -0
  37. mindsdb/integrations/handlers/google_calendar_handler/google_calendar_tables.py +82 -73
  38. mindsdb/integrations/handlers/hubspot_handler/requirements.txt +1 -1
  39. mindsdb/integrations/handlers/huggingface_handler/__init__.py +8 -12
  40. mindsdb/integrations/handlers/huggingface_handler/finetune.py +203 -223
  41. mindsdb/integrations/handlers/huggingface_handler/huggingface_handler.py +360 -383
  42. mindsdb/integrations/handlers/huggingface_handler/requirements.txt +7 -7
  43. mindsdb/integrations/handlers/huggingface_handler/requirements_cpu.txt +7 -7
  44. mindsdb/integrations/handlers/huggingface_handler/settings.py +25 -25
  45. mindsdb/integrations/handlers/langchain_handler/langchain_handler.py +83 -77
  46. mindsdb/integrations/handlers/lightwood_handler/requirements.txt +4 -4
  47. mindsdb/integrations/handlers/litellm_handler/litellm_handler.py +5 -2
  48. mindsdb/integrations/handlers/litellm_handler/settings.py +2 -1
  49. mindsdb/integrations/handlers/openai_handler/constants.py +11 -30
  50. mindsdb/integrations/handlers/openai_handler/helpers.py +27 -34
  51. mindsdb/integrations/handlers/openai_handler/openai_handler.py +14 -12
  52. mindsdb/integrations/handlers/pgvector_handler/pgvector_handler.py +106 -90
  53. mindsdb/integrations/handlers/postgres_handler/postgres_handler.py +41 -39
  54. mindsdb/integrations/handlers/salesforce_handler/constants.py +215 -0
  55. mindsdb/integrations/handlers/salesforce_handler/salesforce_handler.py +141 -80
  56. mindsdb/integrations/handlers/salesforce_handler/salesforce_tables.py +0 -1
  57. mindsdb/integrations/handlers/tpot_handler/requirements.txt +1 -1
  58. mindsdb/integrations/handlers/web_handler/urlcrawl_helpers.py +32 -17
  59. mindsdb/integrations/handlers/web_handler/web_handler.py +19 -22
  60. mindsdb/integrations/libs/llm/config.py +0 -14
  61. mindsdb/integrations/libs/llm/utils.py +0 -15
  62. mindsdb/integrations/libs/vectordatabase_handler.py +10 -1
  63. mindsdb/integrations/utilities/files/file_reader.py +5 -19
  64. mindsdb/integrations/utilities/handler_utils.py +32 -12
  65. mindsdb/integrations/utilities/rag/rerankers/base_reranker.py +1 -1
  66. mindsdb/interfaces/agents/agents_controller.py +246 -149
  67. mindsdb/interfaces/agents/constants.py +0 -1
  68. mindsdb/interfaces/agents/langchain_agent.py +11 -6
  69. mindsdb/interfaces/data_catalog/data_catalog_loader.py +4 -4
  70. mindsdb/interfaces/database/database.py +38 -13
  71. mindsdb/interfaces/database/integrations.py +20 -5
  72. mindsdb/interfaces/database/projects.py +174 -23
  73. mindsdb/interfaces/database/views.py +86 -60
  74. mindsdb/interfaces/jobs/jobs_controller.py +103 -110
  75. mindsdb/interfaces/knowledge_base/controller.py +33 -6
  76. mindsdb/interfaces/knowledge_base/evaluate.py +2 -1
  77. mindsdb/interfaces/knowledge_base/executor.py +24 -0
  78. mindsdb/interfaces/knowledge_base/preprocessing/document_preprocessor.py +6 -10
  79. mindsdb/interfaces/knowledge_base/preprocessing/text_splitter.py +73 -0
  80. mindsdb/interfaces/query_context/context_controller.py +111 -145
  81. mindsdb/interfaces/skills/skills_controller.py +18 -6
  82. mindsdb/interfaces/storage/db.py +40 -6
  83. mindsdb/interfaces/variables/variables_controller.py +8 -15
  84. mindsdb/utilities/config.py +5 -3
  85. mindsdb/utilities/fs.py +54 -17
  86. mindsdb/utilities/functions.py +72 -60
  87. mindsdb/utilities/log.py +38 -6
  88. mindsdb/utilities/ps.py +7 -7
  89. {mindsdb-25.7.3.0.dist-info → mindsdb-25.8.2.0.dist-info}/METADATA +282 -268
  90. {mindsdb-25.7.3.0.dist-info → mindsdb-25.8.2.0.dist-info}/RECORD +94 -92
  91. mindsdb/integrations/handlers/anyscale_endpoints_handler/__about__.py +0 -9
  92. mindsdb/integrations/handlers/anyscale_endpoints_handler/__init__.py +0 -20
  93. mindsdb/integrations/handlers/anyscale_endpoints_handler/anyscale_endpoints_handler.py +0 -290
  94. mindsdb/integrations/handlers/anyscale_endpoints_handler/creation_args.py +0 -14
  95. mindsdb/integrations/handlers/anyscale_endpoints_handler/icon.svg +0 -4
  96. mindsdb/integrations/handlers/anyscale_endpoints_handler/requirements.txt +0 -2
  97. mindsdb/integrations/handlers/anyscale_endpoints_handler/settings.py +0 -51
  98. mindsdb/integrations/handlers/anyscale_endpoints_handler/tests/test_anyscale_endpoints_handler.py +0 -212
  99. /mindsdb/integrations/handlers/{anyscale_endpoints_handler/tests/__init__.py → gong_handler/requirements.txt} +0 -0
  100. {mindsdb-25.7.3.0.dist-info → mindsdb-25.8.2.0.dist-info}/WHEEL +0 -0
  101. {mindsdb-25.7.3.0.dist-info → mindsdb-25.8.2.0.dist-info}/licenses/LICENSE +0 -0
  102. {mindsdb-25.7.3.0.dist-info → mindsdb-25.8.2.0.dist-info}/top_level.txt +0 -0
@@ -120,7 +120,7 @@ class DataCatalogLoader(BaseDataCatalog):
120
120
  Load the column metadata from the handler.
121
121
  """
122
122
  self.logger.info(f"Loading columns for {self.database_name}")
123
- response = self.data_handler.meta_get_columns(self.table_names)
123
+ response = self.data_handler.meta_get_columns([table.name for table in tables])
124
124
  if response.resp_type == RESPONSE_TYPE.ERROR:
125
125
  self.logger.error(f"Failed to load columns for {self.database_name}: {response.error_message}")
126
126
  return []
@@ -168,7 +168,7 @@ class DataCatalogLoader(BaseDataCatalog):
168
168
  Load the column statistics metadata from the handler.
169
169
  """
170
170
  self.logger.info(f"Loading column statistics for {self.database_name}")
171
- response = self.data_handler.meta_get_column_statistics(self.table_names)
171
+ response = self.data_handler.meta_get_column_statistics([table.name for table in tables])
172
172
  if response.resp_type == RESPONSE_TYPE.ERROR:
173
173
  self.logger.error(f"Failed to load column statistics for {self.database_name}: {response.error_message}")
174
174
  return
@@ -233,7 +233,7 @@ class DataCatalogLoader(BaseDataCatalog):
233
233
  Load the primary keys metadata from the handler.
234
234
  """
235
235
  self.logger.info(f"Loading primary keys for {self.database_name}")
236
- response = self.data_handler.meta_get_primary_keys(self.table_names)
236
+ response = self.data_handler.meta_get_primary_keys([table.name for table in tables])
237
237
  if response.resp_type == RESPONSE_TYPE.ERROR:
238
238
  self.logger.error(f"Failed to load primary keys for {self.database_name}: {response.error_message}")
239
239
  return
@@ -285,7 +285,7 @@ class DataCatalogLoader(BaseDataCatalog):
285
285
  Load the foreign keys metadata from the handler.
286
286
  """
287
287
  self.logger.info(f"Loading foreign keys for {self.database_name}")
288
- response = self.data_handler.meta_get_foreign_keys(self.table_names)
288
+ response = self.data_handler.meta_get_foreign_keys([table.name for table in tables])
289
289
  if response.resp_type == RESPONSE_TYPE.ERROR:
290
290
  self.logger.error(f"Failed to foreign keys for {self.database_name}: {response.error_message}")
291
291
  return
@@ -18,18 +18,30 @@ class DatabaseController:
18
18
  self.logs_db_controller = LogDBController()
19
19
  self.information_schema_controller = None
20
20
 
21
- def delete(self, name: str):
21
+ def delete(self, name: str, strict_case: bool = False) -> None:
22
+ """Delete a database (project or integration) by name.
23
+
24
+ Args:
25
+ name (str): The name of the database to delete.
26
+ strict_case (bool, optional): If True, the database name is case-sensitive. Defaults to False.
27
+
28
+ Raises:
29
+ EntityNotExistsError: If the database does not exist.
30
+ Exception: If the database cannot be deleted.
31
+
32
+ Returns:
33
+ None
34
+ """
22
35
  databases = self.get_dict()
23
- name = name.lower()
24
- if name not in databases:
36
+ if name.lower() not in databases:
25
37
  raise EntityNotExistsError("Database does not exists", name)
26
- db_type = databases[name]["type"]
38
+ db_type = databases[name.lower()]["type"]
27
39
  if db_type == "project":
28
- project = self.get_project(name)
40
+ project = self.get_project(name, strict_case)
29
41
  project.delete()
30
42
  return
31
43
  elif db_type == "data":
32
- self.integration_controller.delete(name)
44
+ self.integration_controller.delete(name, strict_case)
33
45
  return
34
46
  else:
35
47
  raise Exception(f"Database with type '{db_type}' cannot be deleted")
@@ -81,9 +93,9 @@ class DatabaseController:
81
93
 
82
94
  return result
83
95
 
84
- def get_dict(self, filter_type: Optional[str] = None):
96
+ def get_dict(self, filter_type: Optional[str] = None, lowercase: bool = True):
85
97
  return OrderedDict(
86
- (x["name"].lower(), {"type": x["type"], "engine": x["engine"], "id": x["id"]})
98
+ (x["name"].lower() if lowercase else x["name"], {"type": x["type"], "engine": x["engine"], "id": x["id"]})
87
99
  for x in self.get_list(filter_type=filter_type)
88
100
  )
89
101
 
@@ -98,8 +110,17 @@ class DatabaseController:
98
110
  def exists(self, db_name: str) -> bool:
99
111
  return db_name.lower() in self.get_dict()
100
112
 
101
- def get_project(self, name: str):
102
- return self.project_controller.get(name=name)
113
+ def get_project(self, name: str, strict_case: bool = False):
114
+ """Get a project by name.
115
+
116
+ Args:
117
+ name (str): The name of the project to retrieve.
118
+ strict_case (bool, optional): If True, the project name is case-sensitive. Defaults to False.
119
+
120
+ Returns:
121
+ Project: The project instance matching the given name.
122
+ """
123
+ return self.project_controller.get(name=name, strict_case=strict_case)
103
124
 
104
125
  def get_system_db(self, name: str):
105
126
  if name == "log":
@@ -112,19 +133,21 @@ class DatabaseController:
112
133
  else:
113
134
  raise Exception(f"Database '{name}' does not exists")
114
135
 
115
- def update(self, name: str, data: dict):
136
+ def update(self, name: str, data: dict, strict_case: bool = False):
116
137
  """
117
138
  Updates the database with the given name using the provided data.
118
139
 
119
140
  Parameters:
120
141
  name (str): The name of the database to update.
121
142
  data (dict): The data to update the database with.
143
+ strict_case (bool): if True, then name is case-sesitive
122
144
 
123
145
  Raises:
124
146
  EntityNotExistsError: If the database does not exist.
125
147
  """
126
- databases = self.get_dict()
127
- name = name.lower()
148
+ databases = self.get_dict(lowercase=(not strict_case))
149
+ if not strict_case:
150
+ name = name.lower()
128
151
  if name not in databases:
129
152
  raise EntityNotExistsError("Database does not exist.", name)
130
153
 
@@ -133,6 +156,8 @@ class DatabaseController:
133
156
  # Only the name of the project can be updated.
134
157
  if {"name"} != set(data):
135
158
  raise ValueError("Only the 'name' field can be updated for projects.")
159
+ if not data["name"].islower():
160
+ raise ValueError("New name must be in lower case.")
136
161
  self.project_controller.update(name=name, new_name=str(data["name"]))
137
162
  return
138
163
 
@@ -161,7 +161,7 @@ class IntegrationController:
161
161
  db.session.commit()
162
162
  return integration_record.id
163
163
 
164
- def add(self, name, engine, connection_args):
164
+ def add(self, name: str, engine, connection_args):
165
165
  logger.debug(
166
166
  "%s: add method calling name=%s, engine=%s, connection_args=%s, company_id=%s",
167
167
  self.__class__.__name__,
@@ -172,6 +172,9 @@ class IntegrationController:
172
172
  )
173
173
  handler_meta = self.get_handler_meta(engine)
174
174
 
175
+ if not name.islower():
176
+ raise ValueError(f"The name must be in lower case: {name}")
177
+
175
178
  accept_connection_args = handler_meta.get("connection_args")
176
179
  logger.debug("%s: accept_connection_args - %s", self.__class__.__name__, accept_connection_args)
177
180
 
@@ -210,20 +213,32 @@ class IntegrationController:
210
213
  integration_record.data = data
211
214
  db.session.commit()
212
215
 
213
- def delete(self, name):
214
- if name in ("files", "lightwood"):
216
+ def delete(self, name: str, strict_case: bool = False) -> None:
217
+ """Delete an integration by name.
218
+
219
+ Args:
220
+ name (str): The name of the integration to delete.
221
+ strict_case (bool, optional): If True, the integration name is case-sensitive. Defaults to False.
222
+
223
+ Raises:
224
+ Exception: If the integration cannot be deleted (system, permanent, demo, in use, or has active models).
225
+
226
+ Returns:
227
+ None
228
+ """
229
+ if name == "files":
215
230
  raise Exception("Unable to drop: is system database")
216
231
 
217
232
  self.handlers_cache.delete(name)
218
233
 
219
234
  # check permanent integration
220
- if name in self.handler_modules:
235
+ if name.lower() in self.handler_modules:
221
236
  handler = self.handler_modules[name]
222
237
 
223
238
  if getattr(handler, "permanent", False) is True:
224
239
  raise Exception("Unable to drop permanent integration")
225
240
 
226
- integration_record = self._get_integration_record(name)
241
+ integration_record = self._get_integration_record(name, case_sensitive=strict_case)
227
242
  if isinstance(integration_record.data, dict) and integration_record.data.get("is_demo") is True:
228
243
  raise Exception("Unable to drop demo object")
229
244
 
@@ -3,11 +3,12 @@ from copy import deepcopy
3
3
  from typing import List, Optional
4
4
  from collections import OrderedDict
5
5
 
6
+ import pandas as pd
6
7
  import sqlalchemy as sa
7
8
  import numpy as np
8
9
 
9
10
  from mindsdb_sql_parser.ast.base import ASTNode
10
- from mindsdb_sql_parser.ast import Select, Star, Constant, Identifier
11
+ from mindsdb_sql_parser.ast import Select, Star, Constant, Identifier, BinaryOperation
11
12
  from mindsdb_sql_parser import parse_sql
12
13
 
13
14
  from mindsdb.interfaces.storage import db
@@ -94,14 +95,38 @@ class Project:
94
95
  def drop_model(self, name: str):
95
96
  ModelController().delete_model(name, project_name=self.name)
96
97
 
97
- def drop_view(self, name: str):
98
- ViewController().delete(name, project_name=self.name)
98
+ def drop_view(self, name: str, strict_case: bool = False) -> None:
99
+ """Remove a view with the specified name from the current project.
100
+
101
+ Args:
102
+ name (str): The name of the view to remove.
103
+ strict_case (bool, optional): If True, the view name is case-sensitive. Defaults to False.
104
+
105
+ Raises:
106
+ EntityNotExistsError: If the view does not exist.
107
+
108
+ Returns:
109
+ None
110
+ """
111
+ ViewController().delete(name, project_name=self.name, strict_case=strict_case)
112
+
113
+ def create_view(self, name: str, query: str, session):
114
+ ast_query = parse_sql(query)
115
+
116
+ if isinstance(ast_query, Select):
117
+ # check create view sql
118
+ ast_query.limit = Constant(1)
119
+
120
+ query_context_controller.set_context(query_context_controller.IGNORE_CONTEXT)
121
+ try:
122
+ SQLQuery(ast_query, session=session, database=self.name)
123
+ finally:
124
+ query_context_controller.release_context(query_context_controller.IGNORE_CONTEXT)
99
125
 
100
- def create_view(self, name: str, query: str):
101
126
  ViewController().add(name, query=query, project_name=self.name)
102
127
 
103
- def update_view(self, name: str, query: str):
104
- ViewController().update(name, query=query, project_name=self.name)
128
+ def update_view(self, name: str, query: str, strict_case: bool = False):
129
+ ViewController().update(name, query=query, project_name=self.name, strict_case=strict_case)
105
130
 
106
131
  def delete_view(self, name: str):
107
132
  ViewController().delete(name, project_name=self.name)
@@ -112,21 +137,112 @@ class Project:
112
137
  view_meta["query_ast"] = parse_sql(view_meta["query"])
113
138
  return view_meta
114
139
 
115
- def query_view(self, query, session):
140
+ @staticmethod
141
+ def combine_view_select(view_query: Select, query: Select) -> Select:
142
+ """
143
+ Create a combined query from view's query and outer query.
144
+ """
145
+
146
+ # apply optimizations
147
+ if query.where is not None:
148
+ # Get conditions that can be duplicated into view's query
149
+ # It has to be simple condition with identifier and constant
150
+ # Also it shouldn't be under the OR condition
151
+
152
+ def get_conditions_to_move(node):
153
+ if not isinstance(node, BinaryOperation):
154
+ return []
155
+ op = node.op.upper()
156
+ if op == "AND":
157
+ conditions = []
158
+ conditions.extend(get_conditions_to_move(node.args[0]))
159
+ conditions.extend(get_conditions_to_move(node.args[1]))
160
+ return conditions
161
+
162
+ if op == "OR":
163
+ return []
164
+ if isinstance(node.args[0], (Identifier, Constant)) and isinstance(
165
+ node.args[1], (Identifier, Constant)
166
+ ):
167
+ return [node]
168
+
169
+ conditions = get_conditions_to_move(query.where)
170
+
171
+ if conditions:
172
+ # analyse targets
173
+ # if target element has alias
174
+ # if element is not identifier or the name is not equal to alias:
175
+ # add alias to black list
176
+ # white list:
177
+ # all targets that are identifiers with no alias or equal to its alias
178
+ # condition can be moved if
179
+ # column is not in black list AND (query has star(*) OR column in white list)
180
+
181
+ has_star = False
182
+ white_list, black_list = [], []
183
+ for target in view_query.targets:
184
+ if isinstance(target, Star):
185
+ has_star = True
186
+ if isinstance(target, Identifier):
187
+ name = target.parts[-1].lower()
188
+ if target.alias is None or target.alias.parts[-1].lower() == name:
189
+ white_list.append(name)
190
+ elif target.alias is not None:
191
+ black_list.append(target.alias.parts[-1].lower())
192
+
193
+ view_where = view_query.where
194
+ for condition in conditions:
195
+ arg1, arg2 = condition.args
196
+
197
+ if isinstance(arg1, Identifier):
198
+ name = arg1.parts[-1].lower()
199
+ if name in black_list or not (has_star or name in white_list):
200
+ continue
201
+ if isinstance(arg2, Identifier):
202
+ name = arg2.parts[-1].lower()
203
+ if name in black_list or not (has_star or name in white_list):
204
+ continue
205
+
206
+ # condition can be moved into view
207
+ condition2 = BinaryOperation(condition.op, [arg1, arg2])
208
+ if view_where is None:
209
+ view_where = condition2
210
+ else:
211
+ view_where = BinaryOperation("AND", args=[view_where, condition2])
212
+
213
+ # disable outer condition
214
+ condition.op = "="
215
+ condition.args = [Constant(0), Constant(0)]
216
+
217
+ view_query.where = view_where
218
+
219
+ # combine outer query with view's query
220
+ view_query.parentheses = True
221
+ query.from_table = view_query
222
+ return query
223
+
224
+ def query_view(self, query: Select, session) -> pd.DataFrame:
116
225
  view_meta = self.get_view_meta(query)
117
226
 
118
227
  query_context_controller.set_context("view", view_meta["id"])
119
-
228
+ query_applied = False
120
229
  try:
121
- sqlquery = SQLQuery(view_meta["query_ast"], session=session)
230
+ view_query = view_meta["query_ast"]
231
+ if isinstance(view_query, Select):
232
+ view_query = self.combine_view_select(view_query, query)
233
+ query_applied = True
234
+
235
+ sqlquery = SQLQuery(view_query, session=session)
122
236
  df = sqlquery.fetched_data.to_df()
123
237
  finally:
124
238
  query_context_controller.release_context("view", view_meta["id"])
125
239
 
126
240
  # remove duplicated columns
127
241
  df = df.loc[:, ~df.columns.duplicated()]
128
-
129
- return query_df(df, query, session=session)
242
+ if query_applied:
243
+ return df
244
+ else:
245
+ return query_df(df, query, session=session)
130
246
 
131
247
  @staticmethod
132
248
  def _get_model_data(predictor_record, integraion_record, with_secrets: bool = True):
@@ -279,18 +395,29 @@ class Project:
279
395
  ]
280
396
  return data
281
397
 
282
- def get_view(self, name):
283
- view_record = (
284
- db.session.query(db.View)
285
- .filter(
286
- db.View.project_id == self.id,
287
- db.View.company_id == ctx.company_id,
288
- sa.func.lower(db.View.name) == name.lower(),
289
- )
290
- .one_or_none()
398
+ def get_view(self, name: str, strict_case: bool = False) -> dict | None:
399
+ """Get a view by name from the current project.
400
+
401
+ Args:
402
+ name (str): The name of the view to retrieve.
403
+ strict_case (bool, optional): If True, the view name is case-sensitive. Defaults to False.
404
+
405
+ Returns:
406
+ dict | None: A dictionary with view information if found, otherwise None.
407
+ """
408
+ query = db.session.query(db.View).filter(
409
+ db.View.project_id == self.id,
410
+ db.View.company_id == ctx.company_id,
291
411
  )
412
+ if strict_case:
413
+ query = query.filter(db.View.name == name)
414
+ else:
415
+ query = query.filter(sa.func.lower(db.View.name) == name.lower())
416
+
417
+ view_record = query.one_or_none()
418
+
292
419
  if view_record is None:
293
- return view_record
420
+ return None
294
421
  return {
295
422
  "name": view_record.name,
296
423
  "query": view_record.query,
@@ -385,8 +512,29 @@ class ProjectController:
385
512
  return [Project.from_record(x) for x in records]
386
513
 
387
514
  def get(
388
- self, id: Optional[int] = None, name: Optional[str] = None, deleted: bool = False, is_default: bool = False
515
+ self,
516
+ id: int | None = None,
517
+ name: str | None = None,
518
+ deleted: bool = False,
519
+ is_default: bool = False,
520
+ strict_case: bool = False,
389
521
  ) -> Project:
522
+ """Get a project by id or name.
523
+
524
+ Args:
525
+ id (int | None, optional): The id of the project to retrieve. Cannot be used with 'name'.
526
+ name (str | None, optional): The name of the project to retrieve. Cannot be used with 'id'.
527
+ deleted (bool, optional): If True, include deleted projects. Defaults to False.
528
+ is_default (bool, optional): If True, only return the default project. Defaults to False.
529
+ strict_case (bool, optional): If True, the project name is case-sensitive. Defaults to False.
530
+
531
+ Raises:
532
+ ValueError: If both 'id' and 'name' are provided.
533
+ EntityNotExistsError: If the project is not found.
534
+
535
+ Returns:
536
+ Project: The project instance matching the given criteria.
537
+ """
390
538
  if id is not None and name is not None:
391
539
  raise ValueError("Both 'id' and 'name' can't be provided at the same time")
392
540
 
@@ -396,7 +544,10 @@ class ProjectController:
396
544
  if id is not None:
397
545
  q = q.filter_by(id=id)
398
546
  elif name is not None:
399
- q = q.filter((sa.func.lower(db.Project.name) == sa.func.lower(name)))
547
+ if strict_case:
548
+ q = q.filter((db.Project.name == name))
549
+ else:
550
+ q = q.filter((sa.func.lower(db.Project.name) == sa.func.lower(name)))
400
551
 
401
552
  if deleted is True:
402
553
  q = q.filter((db.Project.deleted_at != sa.null()))
@@ -12,64 +12,90 @@ class ViewController:
12
12
  from mindsdb.interfaces.database.database import DatabaseController
13
13
 
14
14
  database_controller = DatabaseController()
15
- project_databases_dict = database_controller.get_dict(filter_type='project')
15
+ project_databases_dict = database_controller.get_dict(filter_type="project")
16
16
 
17
17
  if project_name not in project_databases_dict:
18
- raise EntityNotExistsError('Can not find project', project_name)
18
+ raise EntityNotExistsError("Can not find project", project_name)
19
19
 
20
- project_id = project_databases_dict[project_name]['id']
20
+ project_id = project_databases_dict[project_name]["id"]
21
21
  view_record = (
22
22
  db.session.query(db.View.id)
23
23
  .filter(
24
- func.lower(db.View.name) == name,
25
- db.View.company_id == ctx.company_id,
26
- db.View.project_id == project_id
27
- ).first()
24
+ func.lower(db.View.name) == name, db.View.company_id == ctx.company_id, db.View.project_id == project_id
25
+ )
26
+ .first()
28
27
  )
29
28
  if view_record is not None:
30
- raise EntityExistsError('View already exists', name)
29
+ raise EntityExistsError("View already exists", name)
31
30
 
32
- view_record = db.View(
33
- name=name,
34
- company_id=ctx.company_id,
35
- query=query,
36
- project_id=project_id
37
- )
31
+ view_record = db.View(name=name, company_id=ctx.company_id, query=query, project_id=project_id)
38
32
  db.session.add(view_record)
39
33
  db.session.commit()
40
34
 
41
- def update(self, name, query, project_name):
42
- name = name.lower()
35
+ def update(self, name: str, query: str, project_name: str, strict_case: bool = False):
36
+ """Update the SQL query of an existing view in the specified project.
37
+
38
+ Args:
39
+ name (str): The name of the view to update.
40
+ query (str): The new SQL query for the view.
41
+ project_name (str): The name of the project containing the view.
42
+ strict_case (bool, optional): If True, the view name is case-sensitive. If False, the name comparison is case-insensitive. Defaults to False.
43
+
44
+ Raises:
45
+ EntityNotExistsError: If the view with the specified name does not exist in the given project.
46
+
47
+ Returns:
48
+ None
49
+ """
43
50
  project_record = get_project_record(project_name)
44
51
 
45
- rec = db.session.query(db.View).filter(
46
- func.lower(db.View.name) == name,
47
- db.View.company_id == ctx.company_id,
48
- db.View.project_id == project_record.id
49
- ).first()
52
+ q = db.session.query(db.View).filter(
53
+ db.View.company_id == ctx.company_id, db.View.project_id == project_record.id
54
+ )
55
+ if strict_case:
56
+ q = q.filter(db.View.name == name)
57
+ else:
58
+ q = q.filter(func.lower(db.View.name) == func.lower(name))
59
+
60
+ rec = q.first()
50
61
  if rec is None:
51
- raise EntityNotExistsError('View not found', name)
62
+ raise EntityNotExistsError("View not found", name)
52
63
  rec.query = query
53
64
  db.session.commit()
54
65
 
55
- def delete(self, name, project_name):
56
- name = name.lower()
66
+ def delete(self, name: str, project_name: str, strict_case: bool = False) -> None:
67
+ """Remove a view with the specified name from the given project.
68
+
69
+ Args:
70
+ name (str): The name of the view to remove.
71
+ project_name (str): The name of the project containing the view.
72
+ strict_case (bool, optional): If True, the view name is case-sensitive. Defaults to False.
73
+
74
+ Raises:
75
+ EntityNotExistsError: If the view does not exist.
76
+
77
+ Returns:
78
+ None
79
+ """
57
80
  project_record = get_project_record(project_name)
58
81
 
59
- rec = db.session.query(db.View).filter(
60
- func.lower(db.View.name) == name,
61
- db.View.company_id == ctx.company_id,
62
- db.View.project_id == project_record.id
63
- ).first()
64
- if rec is None:
65
- raise EntityNotExistsError('View not found', name)
66
- db.session.delete(rec)
82
+ query = db.session.query(db.View).filter(
83
+ db.View.company_id == ctx.company_id, db.View.project_id == project_record.id
84
+ )
85
+ if strict_case:
86
+ query = query.filter(db.View.name == name)
87
+ else:
88
+ query = query.filter(func.lower(db.View.name) == func.lower(name))
89
+
90
+ record = query.first()
91
+ if record is None:
92
+ raise EntityNotExistsError("View not found", name)
93
+ db.session.delete(record)
67
94
  db.session.commit()
68
95
 
69
- query_context_controller.drop_query_context('view', rec.id)
96
+ query_context_controller.drop_query_context("view", record.id)
70
97
 
71
98
  def list(self, project_name):
72
-
73
99
  project_names = {}
74
100
  for project in get_project_records():
75
101
  if project_name is not None and project.name != project_name:
@@ -77,49 +103,49 @@ class ViewController:
77
103
  project_names[project.id] = project.name
78
104
 
79
105
  query = db.session.query(db.View).filter(
80
- db.View.company_id == ctx.company_id,
81
- db.View.project_id.in_(list(project_names.keys()))
106
+ db.View.company_id == ctx.company_id, db.View.project_id.in_(list(project_names.keys()))
82
107
  )
83
108
 
84
109
  data = []
85
110
 
86
111
  for record in query:
87
-
88
- data.append({
89
- 'id': record.id,
90
- 'name': record.name,
91
- 'project': project_names[record.project_id],
92
- 'query': record.query,
93
- })
112
+ data.append(
113
+ {
114
+ "id": record.id,
115
+ "name": record.name,
116
+ "project": project_names[record.project_id],
117
+ "query": record.query,
118
+ }
119
+ )
94
120
 
95
121
  return data
96
122
 
97
123
  def _get_view_record_data(self, record):
98
- return {
99
- 'id': record.id,
100
- 'name': record.name,
101
- 'query': record.query
102
- }
124
+ return {"id": record.id, "name": record.name, "query": record.query}
103
125
 
104
126
  def get(self, id=None, name=None, project_name=None):
105
127
  project_record = get_project_record(project_name)
106
128
 
107
129
  if id is not None:
108
- records = db.session.query(db.View).filter_by(
109
- id=id,
110
- project_id=project_record.id,
111
- company_id=ctx.company_id
112
- ).all()
130
+ records = (
131
+ db.session.query(db.View)
132
+ .filter_by(id=id, project_id=project_record.id, company_id=ctx.company_id)
133
+ .all()
134
+ )
113
135
  elif name is not None:
114
- records = db.session.query(db.View).filter(
115
- func.lower(db.View.name) == name.lower(),
116
- db.View.project_id == project_record.id,
117
- db.View.company_id == ctx.company_id,
118
- ).all()
136
+ records = (
137
+ db.session.query(db.View)
138
+ .filter(
139
+ func.lower(db.View.name) == name.lower(),
140
+ db.View.project_id == project_record.id,
141
+ db.View.company_id == ctx.company_id,
142
+ )
143
+ .all()
144
+ )
119
145
  if len(records) == 0:
120
146
  if name is None:
121
- name = f'id={id}'
122
- raise EntityNotExistsError("Can't find view", f'{project_name}.{name}')
147
+ name = f"id={id}"
148
+ raise EntityNotExistsError("Can't find view", f"{project_name}.{name}")
123
149
  elif len(records) > 1:
124
150
  raise Exception(f"There are multiple views with name/id: {name}/{id}")
125
151
  record = records[0]