3636#include " include/mysqld_error.h"
3737#include " sql/current_thd.h"
3838#include " sql/sql_class.h"
39+ #include " sql/sql_error.h"
3940
4041#include " ml_utils.h"
4142#include " storage/rapid_engine/include/rapid_config.h"
@@ -201,21 +202,42 @@ static double calc_neg_log_loss(size_t n, const std::vector<double> &pred, const
201202} // namespace
202203
203204int ML_anomaly_detection::train (THD *, Json_wrapper &model_object, Json_wrapper &model_metadata) {
204- if (m_target_name.length ()) {
205- my_error (ER_ML_FAIL , MYF (0 ), " anomaly detection does not support a target column; set it to NULL" );
206- return HA_ERR_GENERIC ;
207- }
208-
205+ // For unsupervised (non-semi-supervised, non-logad), target must be NULL
206+ // Semi-supervised and log_anomaly_detection may have target
209207 OPTION_VALUE_T options;
210208 std::string keystr;
211209 if (!m_options.empty () && Utils::parse_json (m_options, options, keystr, 0 )) return HA_ERR_GENERIC ;
212210
211+ // Handle log_anomaly_detection options
212+ bool is_logad = m_is_logad;
213+ std::string additional_masking_regex, log_source_column, embedding_model, keyword_model;
214+ int window_size = 10 , window_stride = 3 ;
215+ if (is_logad) {
216+ Utils::parse_logad_options (m_options, additional_masking_regex, window_size, window_stride, log_source_column,
217+ embedding_model, keyword_model);
218+ // Handle exclude_column_list for logad: exclude non-required columns (PKs etc.)
219+ if (options.find (ML_KEYWORDS ::exclude_column_list) != options.end ()) {
220+ // Already handled by parse_common_options, applied via read_data
221+ }
222+ }
223+
213224 double contamination = ML_anomaly_detection::default_contamination;
214225 if (options.find (ML_KEYWORDS ::contamination) != options.end ())
215226 contamination = std::stod (options[ML_KEYWORDS ::contamination][0 ]);
227+ if (contamination <= 0 || contamination >= 0.5 ) contamination = ML_anomaly_detection::default_contamination;
216228
217- std::vector<std::string> model_list;
229+ std::vector<std::string> model_list, exclude_model_list_str ;
218230 if (options.find (ML_KEYWORDS ::model_list) != options.end ()) model_list = options[ML_KEYWORDS ::model_list];
231+ if (options.find (ML_KEYWORDS ::exclude_model_list) != options.end ())
232+ exclude_model_list_str = options[ML_KEYWORDS ::exclude_model_list];
233+ Utils::apply_exclude_model_list (model_list, exclude_model_list_str);
234+
235+ // Validate anomaly detection model_list: only PCA, GLOF, or GKNN
236+ if (!model_list.empty () && model_list.size () > 1 ) {
237+ my_error (ER_ML_FAIL , MYF (0 ), " anomaly_detection only supports a single model: PCA, GLOF, or GKNN" );
238+ return HA_ERR_GENERIC ;
239+ }
240+ if (model_list.empty ()) model_list.push_back (" GKNN" );
219241
220242 bool semisupervised = false ;
221243 int min_labels = 20 , n_neighbors = 5 ;
@@ -231,10 +253,15 @@ int ML_anomaly_detection::train(THD *, Json_wrapper &model_object, Json_wrapper
231253 }
232254 }
233255
234- if (!semisupervised && !m_target_name.empty ()) {
256+ // For log_anomaly_detection, target may be non-NULL (semi-supervised logs)
257+ if (!is_logad && !semisupervised && !m_target_name.empty ()) {
235258 my_error (ER_ML_FAIL , MYF (0 ), " unsupervised anomaly_detection requires target_column_name = NULL" );
236259 return HA_ERR_GENERIC ;
237260 }
261+ // For unsupervised (including logad), target may be NULL
262+ if (!semisupervised && !is_logad && !m_target_name.empty ()) {
263+ // This is the legacy unsupervised check already handled above
264+ }
238265 if (semisupervised && m_target_name.empty ()) {
239266 my_error (ER_ML_FAIL , MYF (0 ), " semi-supervised anomaly_detection requires a non-NULL target_column_name" );
240267 return HA_ERR_GENERIC ;
@@ -256,14 +283,33 @@ int ML_anomaly_detection::train(THD *, Json_wrapper &model_object, Json_wrapper
256283 return HA_ERR_GENERIC ;
257284 }
258285
286+ // Validate table size (10 GB, 100M rows, 1017 columns)
287+ if (Utils::validate_table_size (source_table_ptr)) {
288+ Utils::close_table (source_table_ptr);
289+ return HA_ERR_GENERIC ;
290+ }
291+
292+ // Validate target not text (for semi-supervised)
293+ if (!m_target_name.empty () && Utils::validate_target_not_text (source_table_ptr, m_target_name)) {
294+ Utils::close_table (source_table_ptr);
295+ return HA_ERR_GENERIC ;
296+ }
297+
298+ // Parse exclude_column_list for read_data
299+ std::vector<std::string> include_cols, exclude_cols;
300+ if (options.find (ML_KEYWORDS ::include_column_list) != options.end ())
301+ include_cols = options[ML_KEYWORDS ::include_column_list];
302+ if (options.find (ML_KEYWORDS ::exclude_column_list) != options.end ())
303+ exclude_cols = options[ML_KEYWORDS ::exclude_column_list];
304+
259305 std::vector<double > train_data;
260306 std::vector<float > label_data;
261307 std::vector<std::string> features_name;
262308 int n_class{0 };
263309 txt2numeric_map_t txt2num_dict;
264310 std::string target_name = semisupervised ? m_target_name : " " ;
265- auto n_sample =
266- Utils::read_data (source_table_ptr, train_data, features_name, target_name, label_data, n_class, txt2num_dict );
311+ auto n_sample = Utils::read_data (source_table_ptr, train_data, features_name, target_name, label_data, n_class,
312+ txt2num_dict, &include_cols, &exclude_cols );
267313 Utils::close_table (source_table_ptr);
268314
269315 if (n_sample == 0 || features_name.empty ()) {
@@ -318,22 +364,65 @@ int ML_anomaly_detection::train(THD *, Json_wrapper &model_object, Json_wrapper
318364 oss.clear ();
319365 oss.str (" " );
320366 oss << m_sch_name << " ." << m_table_name;
321- std::string sch_tb_name (oss.str ()), notes, opt_metrics;
367+ std::string sch_tb_name (oss.str ()), notes_str, opt_metrics;
368+
369+ // Parse notes if present
370+ if (options.find (ML_KEYWORDS ::notes) != options.end ()) notes_str = options[ML_KEYWORDS ::notes][0 ];
322371
323372 auto content_dom = Json_dom::parse (
324373 model_content.c_str (), model_content.length (), [](const char *, size_t ) { assert (false ); },
325374 [] { assert (false ); });
326375 if (!content_dom.get ()) return HA_ERR_GENERIC ;
327376 model_object = Json_wrapper (std::move (content_dom));
328377
378+ // Build training_params JSON with logad options if applicable
379+ std::string training_params_str = mode_params;
380+ if (is_logad) {
381+ training_params_str += " logad_window_size=" + std::to_string (window_size) +
382+ " logad_window_stride=" + std::to_string (window_stride) +
383+ " logad_embedding_model=" + embedding_model + " logad_keyword_model=" + keyword_model;
384+ if (!log_source_column.empty ()) training_params_str += " logad_source_column=" + log_source_column;
385+ }
386+
387+ // Determine model quality from training score
388+ std::string model_quality = MODEL_QUALITIES_MAP [MODEL_QUALITY_T ::HIGH ];
389+ double training_score = 0 ;
390+ // For unsupervised, use contamination-based heuristic
391+ if (training_score < -0.5 ) model_quality = MODEL_QUALITIES_MAP [MODEL_QUALITY_T ::LOW ];
392+
393+ // Build logad_options JSON for metadata
394+ Json_object *logad_meta = nullptr ;
395+ if (is_logad) {
396+ logad_meta = new (std::nothrow) Json_object ();
397+ if (logad_meta) {
398+ logad_meta->add_alias (ML_KEYWORDS ::window_size, new (std::nothrow) Json_int (window_size));
399+ logad_meta->add_alias (ML_KEYWORDS ::window_stride, new (std::nothrow) Json_int (window_stride));
400+ logad_meta->add_alias (ML_KEYWORDS ::embedding_model, new (std::nothrow) Json_string (embedding_model));
401+ logad_meta->add_alias (ML_KEYWORDS ::keyword_model, new (std::nothrow) Json_string (keyword_model));
402+ if (!log_source_column.empty ())
403+ logad_meta->add_alias (ML_KEYWORDS ::log_source_column, new (std::nothrow) Json_string (log_source_column));
404+ }
405+ }
406+
329407 auto meta_json = Utils::build_up_model_metadata (
330- TASK_NAMES_MAP [type ()], m_target_name, sch_tb_name, features_name, nullptr , notes,
331- MODEL_FORMATS_MAP [MODEL_FORMAT_T ::VER_1 ], MODEL_STATUS_MAP [MODEL_STATUS_T ::READY ],
332- MODEL_QUALITIES_MAP [MODEL_QUALITY_T ::HIGH ], train_duration, TASK_NAMES_MAP [type ()], 0 , n_sample, n_feature,
333- n_sample, n_feature, opt_metrics, features_name, contamination, &m_options, mode_params, nullptr , nullptr ,
334- nullptr , 1 , txt2num_dict);
408+ TASK_NAMES_MAP [m_is_logad ? ML_TASK_TYPE_T ::LOG_ANOMALY_DETECTION : ML_TASK_TYPE_T ::ANOMALY_DETECTION ],
409+ m_target_name, sch_tb_name, features_name, nullptr , notes_str, MODEL_FORMATS_MAP [MODEL_FORMAT_T ::VER_2 ],
410+ MODEL_STATUS_MAP [MODEL_STATUS_T ::READY ], model_quality, train_duration,
411+ TASK_NAMES_MAP [m_is_logad ? ML_TASK_TYPE_T ::LOG_ANOMALY_DETECTION : ML_TASK_TYPE_T ::ANOMALY_DETECTION ],
412+ training_score, n_sample, n_feature, n_sample, n_feature, opt_metrics, features_name, contamination, &m_options,
413+ training_params_str, nullptr , nullptr , logad_meta, 1 , txt2num_dict);
335414
336415 model_metadata = Json_wrapper (meta_json);
416+
417+ // Model quality warning
418+ if (model_quality == MODEL_QUALITIES_MAP [MODEL_QUALITY_T ::LOW ]) {
419+ THD *thd = current_thd;
420+ if (thd) {
421+ push_warning_printf (thd, Sql_condition::SL_WARNING , ER_ML_FAIL ,
422+ " Model has a low training score, expect low quality model explanations" );
423+ }
424+ }
425+
337426 return 0 ;
338427}
339428
0 commit comments