classdef DBHandler < handle % DBHANDLER Class to handle database queries % This class provides methods to interact with an SQLite database, including % inserting data, retrieving table names, and appending new rows. properties conn % Database connection object dataBase % Path to the SQLite database tableNames % Cell array containing names of all tables in the database tables = struct(); % Structure containing MATLAB tables for each database table distinctValues type end methods function obj = DBHandler(options) % DBHANDLER Constructor for the DBHandler class % Initializes the database connection and retrieves table and field names. % % Usage: % obj = DBHandler('pathToDB', 'path/to/database.db'); arguments options.dataBase = ""; % Default value for pathToDB if not provided options.type = "mysql"; end % Assign values to class properties based on input arguments fn = fieldnames(options); for n = 1:numel(fn) try obj.(fn{n}) = options.(fn{n}); end end % Establish a connection to the SQLite database try if options.type == "sqlite" obj.conn = sqlite(obj.dataBase); elseif options.type == "mysql" % datasource = "jdbc:mysql://134.245.243.254:3306/labor"; obj.conn = database( ... string(obj.dataBase), ... % Database name "silas", ... % Username "silas", ... % Password (or getSecret) "Vendor", "MySQL", ... "Server", "134.245.243.254", ... "PortNumber", 3306, ... "JDBCDriverLocation", "C:\Users\sioe\Documents\mysql-connector-j-9.3.0\mysql-connector-j-9.3.0.jar"); end catch e error('Failed to connect to the database: %s', e.message); end if obj.dbIsHealthy obj.refresh(); else error('DB seems to be corrupt') end end function obj = refresh(obj) % Get table names and the first rows of each table to understand the structure warning off obj.getTableNames(); obj.getTables(); warning on % obj.getDistinctValues(); end function obj = getTableNames(obj) % Get all table names from the database try if obj.type == "mysql" result = fetch(obj.conn, 'SHOW TABLES;'); obj.tableNames = result.Variables; elseif obj.type == "sqlite" result = fetch(obj.conn, 'SELECT name FROM sqlite_master WHERE type="table"'); obj.tableNames = result.name; end catch e error('Failed to retrieve table names: %s', e.message); end end function obj = getTables(obj) % Get a preview (first row) of each table to understand its structure for i = 1:numel(obj.tableNames) try tableName = obj.tableNames{i}; results = fetch(obj.conn, sprintf('SELECT * FROM %s WHERE 1 = 2', tableName)); % Matlab cant handle if there is a NULL in a returned % datarow... therefore do not return a row using the % above condition which is never true % results = sqlread(obj.conn, tableName, MaxRows=1); for l = 1:numel(results.Properties.VariableNames) varName = results.Properties.VariableNames{l}; obj.tables.(tableName).(varName) = []; % Store the preview as a reference end catch e warning('Failed to read the table %s: %s', tableName, e.message); end end end function obj = getDistinctValues(obj) % getDistinctValues Retrieves distinct values for each relevant field in all tables % excluding fields ending with "_id". Stores distinct values in the 'distinctValues' % property. % Initialize a structure to store distinct values for each table obj.distinctValues = struct(); % Iterate over each table in obj.tables tableNames_ = fieldnames(obj.tables); for i = 1:numel(tableNames_) tableName = tableNames_{i}; % Initialize a sub-struct to store distinct values for each field in the table obj.distinctValues.(tableName) = struct(); % Get all fields of the current table fieldNames = fieldnames(obj.tables.(tableName)); % Iterate over each field for j = 1:numel(fieldNames) fieldName = fieldNames{j}; % % Skip fields ending with '_id' as they don't contain useful distinct values % if endsWith(fieldName, '_id') % continue; % end % Construct SQL to get distinct values for the current field query = sprintf('SELECT DISTINCT %s FROM %s', fieldName, tableName); % Execute query and fetch distinct values try result = fetch(obj.conn, query); % Store the distinct values in the structure if ~isempty(result) distinctValues = table2array(result); else distinctValues = []; end obj.distinctValues.(tableName).(fieldName) = distinctValues; catch e % warning('Failed to retrieve distinct values for %s.%s: %s', tableName, fieldName, e.message); obj.distinctValues.(tableName).(fieldName) = []; end end end end function healthyDB = dbIsHealthy(obj) healthyDB = false; %Check for any duplicate paths duplictae_raw = obj.fetch("SELECT COALESCE(Runs.rx_raw_path,'NaN') AS rx_raw_path, COUNT(*) AS occurrences FROM Runs GROUP BY rx_raw_path HAVING COUNT(*) > 1"); duplictae_sync = obj.fetch("SELECT COALESCE(Runs.rx_sync_path,'NaN') AS rx_sync_path, COUNT(*) AS occurrences FROM Runs GROUP BY rx_sync_path HAVING COUNT(*) > 1"); if size(duplictae_raw,2) > 1 for i = 1:size(duplictae_raw,1) fprintf('Raw Rx Paths: Found %d duplictaes of %s \n',duplictae_raw.occurrences(i),duplictae_raw.rx_raw_path(i)); end end healthyDB = true; end function lastID = appendToTable(obj, tableName, newRow) % appendToTable Appends a new row to the specified table % % Usage: % appendToTable(tableName, newRow) % % Inputs: % tableName: The name of the table to append data to. % newRow: A MATLAB table or struct containing the new row to be appended. % Check if the table exists in the fetched tables if ~isfield(obj.tables, tableName) error('Table %s does not exist in the database or has not been fetched.', tableName); end % Handle struct preprocessing before table conversion if isstruct(newRow) fields = fieldnames(newRow); % % Handle empty fields % emptyFields = structfun(@isempty, newRow); % if sum(emptyFields) > 0 % emptyFieldNames = fields(emptyFields); % for idx = 1:numel(emptyFieldNames) % newRow.(emptyFieldNames{idx}) = NaN; % end % end % Convert any non-scalar numeric (including [] and vectors) into JSON for i = 1:numel(fields) name = fields{i}; value = newRow.(name); if isnumeric(value) && ~isscalar(value) % jsonencode([]) -> "[]" % jsonencode([a,b,c]) -> "[a,b,c]" newRow.(name) = jsonencode(value); end end % Convert to table newRow = struct2table(newRow); end % Ensure the new row matches the structure of the existing table existingTableStructure = obj.tables.(tableName); % Perform remaining data type checks and conversions for colName = newRow.Properties.VariableNames value = newRow.(colName{1}); if iscell(value) && ~isempty(value) if ischar(value{1}) || isstring(value{1}) newRow.(colName{1}) = value{1}; end elseif isobject(value) && ~isdatetime(value) && ~isa(value, "string") newRow.(colName{1}) = string(jsonencode(value)); elseif ischar(value) newRow.(colName{1}) = string(value); end end % Parameters for retry logic maxRetries = 50; basePause = 0.05; % seconds attempt = 0; success = false; while ~success && attempt <= maxRetries try if obj.type == "mysql" newRow_ = obj.convertTableToCellStrings(newRow); end sqlwrite(obj.conn, tableName, newRow,"Catalog",obj.dataBase); success = true; catch e if contains(e.message, 'database is locked') || contains(e.message, 'cannot rollback transaction') attempt = attempt + 1; pauseTime = basePause * (1 + rand()); fprintf('Database locked. Retry %d/%d after %.3f seconds...\n', ... attempt, maxRetries, pauseTime); pause(pauseTime); else fprintf('Error details:\n'); fprintf('Column values:\n'); disp(newRow); error('Failed to append to the table %s: %s', tableName, e.message); end end end if ~success error('Failed to append to table %s after %d retries due to database lock.', ... tableName, maxRetries); end % Retrieve the ID of the newly inserted row if obj.type == "mysql" query = "SELECT LAST_INSERT_ID()"; elseif obj.type == "sqlite" query = "SELECT last_insert_rowid()"; end result = fetch(obj.conn, query); lastID = result{1, 1}; end function [exists, count] = checkIfRunExists(obj, table2check, column2check, value2check) % checkIfRunExists Checks if a specific value exists in a specified column of a table % % Usage: % exists = checkIfRunExists(table2check, column2check, value2check) % % Inputs: % table2check: The name of the table to check for duplicates. % column2check: The name of the column to check within the specified table. % value2check: The value to check for in the specified column. % % Outputs: % exists: Boolean indicating whether the value already exists in the table. % Ensure the specified table and column exist in the database if ~isfield(obj.tables, table2check) error('Table %s does not exist in the database.', table2check); end % Ensure the specified column exists in the table structure if ~isfield(obj.tables.(table2check), column2check) error('Column %s does not exist in the table %s.', column2check, table2check); end % Construct the query to check for the value in the specified column if isnumeric(value2check) query = sprintf('SELECT COUNT(*) FROM %s WHERE %s = %d', table2check, column2check, value2check); else query = sprintf('SELECT COUNT(*) FROM %s WHERE %s = "%s"', table2check, column2check, value2check); end % Execute the query and pass the value2check to avoid SQL injection issues try result = fetch(obj.conn, query); count = result{1, 1}; % Extract the count from the result catch e error('Failed to execute the duplicate check query: %s', e.message); end % If count is greater than 0, then the value exists in the table exists = count > 0; if exists % disp(['The value "', num2str(value2check), '" already exists in the column "', column2check, '" of the table "', table2check, '".']); else % disp(['The value "', value2check, '" does not exist in the column "', column2check, '" of the table "', table2check, '".']); end end function hashStr = calcHash(~,object) jsonStr = jsonencode(object); md = java.security.MessageDigest.getInstance('MD5'); md.update(uint8(jsonStr)); hashBytes = typecast(md.digest, 'uint8'); hashStr = lower(dec2hex(hashBytes)'); hashStr = lower(strtrim(hashStr(:)')); % Convert to a lowercase string end function resultID = addProcessingResult(obj, run_id, resultData, eqParamsData) % addProcessingResult Adds a processing result and links it to an EqualizerParameters entry. % % Inputs: % run_id: A run_id from the main table to connect the BER with. % % resultData: A struct with fields corresponding to the ProcessingResults table. % % eqParamsData: A struct with fields corresponding to the EqualizerParameters table, % except 'eq_id' and 'config_hash'. These fields are used to compute % a hash and check for an existing configuration. % % Output: % resultID: The result_id of the newly inserted ProcessingResults entry. % 1. Compute hash for equalizer parameters % jsonStr = jsonencode(eqParamsData); % md = java.security.MessageDigest.getInstance('MD5'); % md.update(uint8(jsonStr)); % hashBytes = typecast(md.digest, 'uint8'); % hashStr = lower(dec2hex(hashBytes)'); % hashStr = lower(strtrim(hashStr(:)')); % Convert to a lowercase string hashStr = obj.calcHash(eqParamsData); % Add hash to equalizer parameters eqParamsData.hash = hashStr; % 2. Check if an equalizer configuration with the same hash exists queryStr = sprintf('SELECT eq_id FROM Equalizer WHERE hash = ''%s''', eqParamsData.hash); existingEntry = obj.fetch(queryStr); if ~isempty(existingEntry) % Use existing eq_id eq_id = existingEntry{1,1}; else % Insert the new equalizer configuration and get its eq_id eqParamsData = eqParamsData.toStruct; eq_id = obj.appendToTable('Equalizer', eqParamsData); end % 3. Add the equalizer configuration reference and run_id to resultData resultData = resultData.toStruct; resultData.eq_id = eq_id; resultData.run_id = run_id; % 4. Compute hash for the processing result tempResultData = rmfield(resultData, 'date_of_processing'); % resultJsonStr = jsonencode(tempResultData); % md2 = java.security.MessageDigest.getInstance('MD5'); % Create a new MD5 instance % md2.update(uint8(resultJsonStr)); % resultHashBytes = typecast(md2.digest, 'uint8'); % resultHashStr = lower(dec2hex(resultHashBytes)'); % resultHashStr = lower(strtrim(resultHashStr(:)')); % Convert to a lowercase string resultHashStr = obj.calcHash(tempResultData); % Add the result hash to resultData resultData.hash = resultHashStr; % 5. Check if an identical processing result already exists queryStr2 = sprintf('SELECT result_id FROM Results WHERE hash = ''%s''', resultData.hash); existingResult = obj.fetch(queryStr2); if ~isempty(existingResult) % If the result exists, return its result_id without inserting a new row resultID = existingResult{1,1}; warning(['Result already exists: ResultID: ',num2str(resultID),'| EQ ID: ',num2str(eq_id),' Run ID: ' num2str(run_id)]) return; end % 6. check if obj.tables.Results matches Metricstruct % Fields to exclude from comparison excludeFields = {'result_id', 'run_id', 'eq_id'}; % 7. Chack for new fields in Metric struct and append to % database if necessary ms=Metricstruct; metricFields = setdiff(fieldnames(ms), excludeFields); tableFields = setdiff(fieldnames(obj.tables.Results), excludeFields); % Check matches and find missing fields matches = all(ismember(metricFields, tableFields)); if ~matches missingFields = setdiff(metricFields, tableFields); % If there are missing fields, add them to the SQL table if ~isempty(missingFields) for i = 1:length(missingFields) fieldName = missingFields{i}; % Determine SQL data type based on MATLAB class fieldValue = ms.(fieldName); if isnumeric(fieldValue) if isinteger(fieldValue) sqlType = 'INTEGER'; else sqlType = 'REAL'; end elseif ischar(fieldValue) || isstring(fieldValue) sqlType = 'TEXT'; elseif isdatetime(fieldValue) sqlType = 'DATETIME'; elseif iscell(fieldValue) || isstruct(fieldValue) || islogical(fieldValue) sqlType = 'TEXT'; % Store as JSON else sqlType = 'TEXT'; % Default to TEXT for unknown types end % Create ALTER TABLE query queryStr = sprintf('ALTER TABLE Results ADD COLUMN %s %s', fieldName, sqlType); try % Execute the query obj.fetch(queryStr); fprintf('Added field "%s" of type %s to Results table\n', fieldName, sqlType); catch ME fprintf('Error adding field "%s": %s\n', fieldName, ME.message); end end else fprintf('No missing fields to add.\n'); end end % 8. Insert the processing result resultID = obj.appendToTable('Results', resultData); end function recalcHashes(obj) % recalcHashes Recalculate hashes for all rows in the Results and EqualizerParameters tables. % % For EqualizerParameters, the hash is computed from all fields except % 'eq_id' and 'config_hash'. % % For Results, the hash is computed from all fields except 'result_id', % 'result_hash', and 'date_of_processing'. % Recalculate hashes for EqualizerParameters eqParamsRows = obj.fetch('SELECT * FROM EqualizerParameters'); for i = 1:height(eqParamsRows) rowStruct = table2struct(eqParamsRows(i,:)); % Convert the table row to a struct % Remove fields not part of the hash computation if isfield(rowStruct, 'eq_id') rowStruct = rmfield(rowStruct, 'eq_id'); end if isfield(rowStruct, 'config_hash') rowStruct = rmfield(rowStruct, 'config_hash'); end % Compute MD5 hash from the JSON representation jsonStr = jsonencode(rowStruct); md = java.security.MessageDigest.getInstance('MD5'); md.update(uint8(jsonStr)); hashBytes = typecast(md.digest, 'uint8'); hashStr = lower(dec2hex(hashBytes)'); hashStr = lower(strtrim(hashStr(:)')); % Update the config_hash field using the eq_id from the table row eq_id = eqParamsRows.eq_id(i); updateQuery = sprintf('UPDATE EqualizerParameters SET config_hash = ''%s'' WHERE eq_id = %d', hashStr, eq_id); obj.executeSQL(updateQuery); end % Fetch all rows from the Results table using queryDB with no filters Results = obj.tables.Results; if isstruct(Results) newFields = {}; fNames = fieldnames(Results); for i = 1:numel(fNames) newFields{end+1} = ['Results.' fNames{i}]; end Results = newFields; end [resultsRows, ~] = obj.queryDB(obj.tables, Results); for i = 1:height(resultsRows) rowStruct = table2struct(resultsRows(i, :)); % Convert the row to a struct % Remove fields not used in the hash calculation if isfield(rowStruct, 'result_id') rowStruct = rmfield(rowStruct, 'result_id'); end if isfield(rowStruct, 'result_hash') rowStruct = rmfield(rowStruct, 'result_hash'); end if isfield(rowStruct, 'date_of_processing') rowStruct = rmfield(rowStruct, 'date_of_processing'); end % Compute MD5 hash from the JSON representation jsonStr = jsonencode(rowStruct); md = java.security.MessageDigest.getInstance('MD5'); md.update(uint8(jsonStr)); hashBytes = typecast(md.digest, 'uint8'); hashStr = lower(dec2hex(hashBytes)'); hashStr = lower(strtrim(hashStr(:)')); % Access the result_id from the table row and update the hash result_id = resultsRows.result_id(i); updateQuery = sprintf('UPDATE Results SET result_hash = ''%s'' WHERE result_id = %d', ... hashStr, result_id); obj.executeSQL(updateQuery); end end function executeSQL(obj, query) % This method executes an SQL statement using MATLAB's execute function. execute(obj.conn, query); end function answer = fetch(obj, query) maxFast = 20; maxSlow = 30; for attempt = 1:maxSlow try answer = fetch(obj.conn, query); return catch ME if attempt < maxFast pause(0.1) else pause(1) end lastErr = ME; end end error('Database fetch failed after %d attempts:\n%s', maxSlow, lastErr.getReport()) end function [result,query] = queryDB(obj, filterParams, selectedFields) % getPathsWithFlexibleFilter Retrieves values from Runs table with flexible filtering % and lets the user select which fields to include in the SELECT statement. % % Usage: % [rxRawPaths, filteredValues] = getPathsWithFlexibleFilter(filterParams) % % Inputs: % filterParams: A structure containing the parameters with their values. % If left empty, two popup windows will prompt the user for input. % % Outputs: % result: table with sql return % query: this was send to SQL DB arguments obj filterParams = []; selectedFields = []; end % Step 3: Construct the SQL query based on the inputs query = obj.constructSQLQuery(filterParams, selectedFields); % Step 4: Execute the query and handle results result = obj.fetch(query); result = obj.normalizeMySQLTable(result); end function cleanedTable = normalizeMySQLTable(~,result) cleanedTable = result; varNames = result.Properties.VariableNames; for i = 1:numel(varNames) col = result.(varNames{i}); % Only process if column is a cell array of strings or chars if iscell(col) && all(cellfun(@(x) ischar(x) || isstring(x), col)) % Try converting to numeric if possible numCol = str2double(col); if all(~isnan(numCol) | strcmpi(col, 'NaN')) % It's numeric (with possible NaNs) cleanedTable.(varNames{i}) = numCol; else % Clean double-quoted SQL literals (e.g., ""no_db"") cleanedTable.(varNames{i}) = strrep(string(col), '"', ''); end end end end function cellTable = convertTableToCellStrings(~,tbl) % Converts all variables in a MATLAB table to cell arrays of strings (like JDBC fetch from MySQL) % % Example: % jdbcFormatted = convertTableToCellStrings(myTable); cellTable = tbl; varNames = tbl.Properties.VariableNames; for i = 1:numel(varNames) col = tbl.(varNames{i}); if isnumeric(col) % Convert numeric values to strings cellTable.(varNames{i}) = arrayfun(@(x) num2str(x, '%.15g'), col, 'UniformOutput', false); elseif isstring(col) || ischar(col) % Ensure cell array of strings cellTable.(varNames{i}) = cellstr(col); elseif iscell(col) % Convert each cell entry to string cellTable.(varNames{i}) = cellfun(@convertToString, col, 'UniformOutput', false); elseif islogical(col) % Convert logicals to '0' or '1' cellTable.(varNames{i}) = cellstr(string(double(col))); elseif isdatetime(col) % Convert datetimes to formatted string cellTable.(varNames{i}) = cellstr(string(col)); else warning('Column "%s" has unsupported type. Converting using string().', varNames{i}); cellTable.(varNames{i}) = cellstr(string(col)); end end end function out = convertToString(~,val) if ischar(val) out = val; elseif isstring(val) out = char(val); elseif isnumeric(val) out = num2str(val, '%.15g'); elseif islogical(val) out = num2str(double(val)); elseif isdatetime(val) out = datestr(val, 'yyyy-mm-dd HH:MM:SS'); else out = char(string(val)); % fallback end end function query = constructSQLQuery(obj, filterParams, selectedFields) % constructSQLQuery Constructs the SQL query based on filter parameters and selected fields. arguments obj filterParams selectedFields end % -------- Step 1: Normalize selectedFields to {'Table.field', ...} -------- if isempty(selectedFields) || (ischar(selectedFields) && strcmpi(selectedFields, 'all')) selectedFields = obj.getTableFieldNames('Runs'); % default elseif isstruct(selectedFields) newFields = {}; tableNames = fieldnames(selectedFields); for t = 1:numel(tableNames) tableStruct = selectedFields.(tableNames{t}); fns = fieldnames(tableStruct); for f = 1:numel(fns) if isequal(tableStruct.(fns{f}), 1) newFields{end+1} = sprintf('%s.%s', tableNames{t}, fns{f}); %#ok end end end selectedFields = newFields; end % Parse the table names actually referenced by the SELECT reqTables = unique(cellfun(@(s) extractBefore(s, '.'), selectedFields, ... 'UniformOutput', false)); % -------- Step 2: Build SELECT with COALESCE wrapper as you already do ---- selectClause = obj.generateCoalesceString(selectedFields); % -------- Step 3: FROM and minimal JOIN plan ------------------------------ % Decide main table: prefer the first explicitly referenced table, else 'Runs' if ~isempty(reqTables) mainTable = reqTables{1}; else mainTable = 'Runs'; end % If WHERE references a table not in reqTables (e.g., Runs.*), make sure it’s present. whereClause = ''; if ~isempty(filterParams) whereClause = obj.generateWhereClause(filterParams); % Heuristic: add 'Runs' if WHERE clause mentions 'Runs.' if contains(whereClause, 'Runs.') reqTables = unique([reqTables; {'Runs'}]); %#ok end end % Ensure main table is included if ~ismember(mainTable, reqTables) reqTables = unique([mainTable; reqTables]); %#ok end % We’ll build joins only for the required tables (minus the main) otherTables = setdiff(reqTables, {mainTable}); % Keep track of what’s already in the FROM graph (start with main) present = string(mainTable); joins = strings(0,1); % Helper lambdas hasField = @(tbl, fld) isfield(obj.tables.(char(tbl)), char(fld)); canJoinBy = @(left, right, key) hasField(left, key) && hasField(right, key); % A small helper that adds a LEFT JOIN if the right table isn't present yet function addJoinByKey(rightTbl, key) if any(present == string(rightTbl)) return; % already joined end % Prefer to join against an already-present table that has the key anchor = ''; for k = 1:numel(present) if canJoinBy(char(present(k)), rightTbl, key) anchor = char(present(k)); break; end end if isempty(anchor) % No anchor in current graph; if the right table is 'Equalizer' and key is eq_id, % try to ensure a bridge table with eq_id exists (Results or a dashboard view). if strcmpi(rightTbl,'Equalizer') && strcmpi(key,'eq_id') % Bring in one eq_id-capable table if it is requested bridgeOrder = {'Results','dashboard_old','dashboard_new','dashboard_ungrouped'}; for b = 1:numel(bridgeOrder) br = bridgeOrder{b}; if ismember(br, reqTables) && ~any(present == string(br)) && hasField(obj.tables.(br),'eq_id') % Attach bridge by run_id if possible, otherwise leave for eq_id if any(present == "Runs") && hasField(obj.tables.(br),'run_id') && hasField(obj.tables.('Runs'),'run_id') joins(end+1,1) = "LEFT JOIN " + br + " ON Runs.run_id = " + br + ".run_id"; present(end+1,1) = string(br); anchor = br; % we can now anchor Equalizer on eq_id to this break; else % Fallback: anchor to main if it shares eq_id for k = 1:numel(present) pk = char(present(k)); if canJoinBy(pk, br, 'eq_id') joins(end+1,1) = "LEFT JOIN " + br + " ON " + pk + ".eq_id = " + br + ".eq_id"; present(end+1,1) = string(br); anchor = br; break; end end if ~isempty(anchor), break; end end end end end end % Re-scan for an anchor (maybe the bridge helped) if isempty(anchor) for k = 1:numel(present) if canJoinBy(char(present(k)), rightTbl, key) anchor = char(present(k)); break; end end end if isempty(anchor) % As a final fallback, if the main table is Runs and right has run_id, join by run_id if strcmpi(mainTable,'Runs') && hasField(obj.tables.(rightTbl),'run_id') && hasField(obj.tables.('Runs'),'run_id') anchor = 'Runs'; key = 'run_id'; end end if isempty(anchor) % Could not find a path; skip join silently (or throw if you prefer strict) return; end joins(end+1,1) = "LEFT JOIN " + rightTbl + " ON " + anchor + "." + key + " = " + rightTbl + "." + key; present(end+1,1) = string(rightTbl); end % First pass: if WHERE uses Runs.* and mainTable isn’t Runs, ensure Runs is in the graph if contains(string(whereClause), "Runs.") && ~any(present == "Runs") % Try to join Runs to whatever has run_id (mainTable ideally) if hasField(mainTable, 'run_id') && hasField('Runs', 'run_id') joins(end+1,1) = "LEFT JOIN Runs ON " + string(mainTable) + ".run_id = Runs.run_id"; present(end+1,1) = "Runs"; end end % Join the required tables with minimal edges for i = 1:numel(otherTables) tbl = otherTables{i}; % Prefer run_id join if possible, else eq_id, else skip if any(present == "Runs") && hasField(tbl,'run_id') addJoinByKey(tbl, 'run_id'); elseif hasField(tbl,'run_id') && hasField(mainTable,'run_id') addJoinByKey(tbl, 'run_id'); elseif hasField(tbl,'eq_id') addJoinByKey(tbl, 'eq_id'); else % no obvious key; skip end end % Build the FROM clause fromClause = "FROM " + string(mainTable) + " " + strjoin(joins, " "); % -------- Step 4: WHERE (unchanged logic) ------------------------------ if ~isempty(whereClause) query = char(strjoin([selectClause, fromClause, "WHERE " + string(whereClause)], " ")); else query = char(strjoin([selectClause, fromClause], " ")); end end function whereClause = generateWhereClause(obj, filterParams) filterClauses = []; % If filterParams is a DbFilterParameter object, get its internal structure if isa(filterParams, 'QueryFilter') filterParams = filterParams.toStruct(); end % Now proceed with the structure tableNames = fieldnames(filterParams); for t = 1:numel(tableNames) tableName = tableNames{t}; tableParams = filterParams.(tableName); fieldNames = fieldnames(tableParams); for i = 1:numel(fieldNames) fieldName = fieldNames{i}; value = tableParams.(fieldName); fullName = sprintf('%s.%s', tableName, fieldName); % Skip empty values if isempty(value) || (isa(value, 'QueryFilter') && isempty(value.value)) continue; end % Handle Filter class if isa(value, 'SqlFilter') if isnumeric(value.value) filterClause = sprintf('%s %s %f', ... fullName, value.operator, value.value); elseif ischar(value.value) || isstring(value.value) filterClause = sprintf('%s %s ''%s''', ... fullName, value.operator, char(value.value)); else continue; % Skip unsupported types end filterClauses = [filterClauses, filterClause, ' AND ']; else % Handle direct values (legacy support) if isnumeric(value) && isnan(value) filterClause = sprintf('%s IS NULL', fullName); elseif isnumeric(value) filterClause = sprintf('%s = %f', fullName, value); elseif ischar(value) || isstring(value) filterClause = sprintf('%s = ''%s''', fullName, char(value)); else continue; % Skip unsupported types end filterClauses = [filterClauses, filterClause, ' AND ']; end end end % Remove trailing ' AND ' if any filters were added if ~isempty(filterClauses) whereClause = filterClauses(1:end-5); else whereClause = ''; end end function selectedFields = promptSelectFields(obj) % promptSelectFields Prompts the user to select fields from multiple tables % using a custom checkbox GUI with scrolling. % % The function builds a list of all fields (formatted as 'TableName.fieldName') % and displays each as a checkbox inside an inner container panel. The container's % height is set to accommodate all checkboxes, so the scrollable panel shows scrollbars. % When the user clicks the "Select" button, the selected fields are returned. % If none are selected, all fields are returned. % Get all possible tables (excluding sqlite_sequence) tableNames = fieldnames(obj.tables); tableNames = setdiff(tableNames, {'sqlite_sequence'}); % Build a single cell array of all field names with table prefix. allFields = {}; for i = 1:numel(tableNames) fields = fieldnames(obj.tables.(tableNames{i})); for j = 1:numel(fields) allFields{end+1} = sprintf('%s.%s', tableNames{i}, fields{j}); end end numFields = numel(allFields); % Create the main figure. fig = uifigure('Name', 'Select Fields', 'Position', [100, 100, 400, 600]); % Create a scrollable panel. scrollPanel = uipanel(fig, 'Position', [10, 60, 380, 530], 'Scrollable', 'on'); % Define checkbox dimensions. checkboxHeight = 30; spacing = 5; totalHeight = numFields * (checkboxHeight + spacing); % Create an inner container panel with height larger than the scrollPanel's height. container = uipanel(scrollPanel, 'Position', [0, 0, scrollPanel.Position(3), totalHeight]); % Create checkboxes using absolute positioning in the container. checkboxes = gobjects(numFields, 1); for i = 1:numFields % Calculate the vertical position. % The origin (0,0) is at the bottom left of the container. yPos = totalHeight - i*(checkboxHeight + spacing) + spacing; checkboxes(i) = uicheckbox(container, ... 'Text', allFields{i}, ... 'Value', false, ... 'Position', [10, yPos, container.Position(3)-20, checkboxHeight]); end % Create a "Select" button in the main figure. btn = uibutton(fig, 'Text', 'Select', ... 'Position', [150, 10, 100, 30], ... 'ButtonPushedFcn', @(btn, event) uiresume(fig)); % Wait for the user to click the button. uiwait(fig); % Retrieve the selected fields. selectedFields = {}; for i = 1:numFields if checkboxes(i).Value selectedFields{end+1} = checkboxes(i).Text; end end % If no fields are selected, default to all fields. if isempty(selectedFields) selectedFields = allFields; end % Close the figure. delete(fig); end function filterParams = promptFilterParameters(obj) % promptFilterParameters Prompts the user to enter filter parameters using a % custom scrollable UI with dropdowns. % % For each table (excluding 'sqlite_sequence'), each field that has distinct % values is displayed as a label and a dropdown. The dropdown items are built % from the distinct values (with "All" prepended). The output is a struct where, % for each table, each field is set to the chosen value (or [] if "All" is selected). % Get all tables except 'sqlite_sequence' tableNames = fieldnames(obj.tables); tableNames = setdiff(tableNames, {'sqlite_sequence'}); % Precompute layout constants. heightPerTableLabel = 30; heightPerField = 40; % vertical space for a field (label + dropdown) spacing = 5; % Compute total required height. totalHeight = 0; for i = 1:numel(tableNames) totalHeight = totalHeight + heightPerTableLabel; tableName = tableNames{i}; tableFields = fieldnames(obj.tables.(tableName)); for j = 1:numel(tableFields) fieldName = tableFields{j}; % Only include fields that have distinct values stored. if isfield(obj.distinctValues.(tableName), fieldName) totalHeight = totalHeight + heightPerField; end end end % Create the main UI figure. fig = uifigure('Name', 'Input Parameters for Filtering', 'Position', [100, 100, 500, 600]); % Set a CloseRequestFcn so that closing the figure calls uiresume. fig.CloseRequestFcn = @(src, event) uiresume(src); % Create a scrollable panel inside the figure. scrollPanel = uipanel(fig, 'Position', [10, 60, 480, 530], 'Scrollable', 'on'); % Create an inner container panel with a height set to totalHeight. container = uipanel(scrollPanel, 'Position', [0, 0, scrollPanel.Position(3), totalHeight]); % Prepare cell arrays to store dropdown handles and corresponding table/field names. dropdownHandles = {}; dropdownTableNames = {}; dropdownFieldNames = {}; % Set the starting Y coordinate (filling from top to bottom). currentY = totalHeight; % Maximum number of dropdown items. maxItems = 100; for i = 1:numel(tableNames) % Create a label for the table name. uilabel(container, ... 'Text', tableNames{i}, ... 'FontWeight', 'bold', ... 'Position', [10, currentY - heightPerTableLabel + spacing, 200, heightPerTableLabel - spacing]); currentY = currentY - heightPerTableLabel; tableName = tableNames{i}; tableFields = fieldnames(obj.tables.(tableName)); for j = 1:numel(tableFields) fieldName = tableFields{j}; if ~isfield(obj.distinctValues.(tableName), fieldName) continue; % Skip if no distinct values are stored. end % Retrieve distinct values for the field. distinctValues_ = obj.distinctValues.(tableName).(fieldName); if ~isempty(distinctValues_) && numel(distinctValues_) > maxItems distinctValues_ = distinctValues_(1:maxItems); end if isempty(distinctValues_) items = {'All'}; else if isnumeric(distinctValues_) items = cellfun(@num2str, num2cell(distinctValues_), 'UniformOutput', false); elseif isstring(distinctValues_) items = cellstr(distinctValues_); elseif iscell(distinctValues_) && ~iscellstr(distinctValues_) items = cellfun(@num2str, distinctValues_, 'UniformOutput', false); else items = distinctValues_; end items = items(:)'; % Ensure row vector items = [{'All'}, items]; end % Create a label for the field. uilabel(container, ... 'Text', sprintf('%s:', fieldName), ... 'HorizontalAlignment', 'right', ... 'Position', [10, currentY - 25, 150, 25]); % Create a dropdown for the field. dd = uidropdown(container, ... 'Items', items, ... 'Value', 'All', ... 'Position', [170, currentY - 25, 200, 25]); % Store the dropdown handle and its associated table/field. dropdownHandles{end+1} = dd; dropdownTableNames{end+1} = tableName; dropdownFieldNames{end+1} = fieldName; currentY = currentY - heightPerField; end end % Create a "Submit" button at the bottom of the figure. btn = uibutton(fig, 'Text', 'Submit', ... 'Position', [200, 10, 100, 30], ... 'ButtonPushedFcn', @(btn, event) uiresume(fig)); % Wait until the user clicks "Submit" or closes the figure. uiwait(fig); % If the figure was closed (and thus no longer valid), return an empty struct. if ~isvalid(fig) filterParams = struct(); return; end % Build the filterParams struct from the dropdown selections. filterParams = struct(); for k = 1:numel(dropdownHandles) tableName = dropdownTableNames{k}; fieldName = dropdownFieldNames{k}; value = dropdownHandles{k}.Value; if ~isfield(filterParams, tableName) filterParams.(tableName) = struct(); end % If "All" is selected, assign empty; otherwise, try converting to numeric. if strcmp(value, 'All') filterParams.(tableName).(fieldName) = []; else numValue = str2double(value); if ~isnan(numValue) filterParams.(tableName).(fieldName) = numValue; else filterParams.(tableName).(fieldName) = value; end end end % Close the figure. delete(fig); end function fieldNames = getTableFieldNames(obj, tableName) % Returns all field names for a given table as a cell array in the format {'TableName.fieldName'} if isfield(obj.tables, tableName) % Get raw field names rawFields = fieldnames(obj.tables.(tableName)); % Create cell array with table name prefix fieldNames = cellfun(@(x) [tableName, '.', x], ... rawFields, ... 'UniformOutput', false); else error('Table "%s" not found in obj.tables.', tableName); end end function coalesceStr = generateCoalesceString(obj, selectedFields) % Generates a COALESCE string for selected fields % Input: % selectedFields: cell array of strings in format {'Table.field'} % e.g., {'Runs.run_id', 'Runs.bitrate'} % Output: % coalesceStr: string with COALESCE statements arguments obj selectedFields cell end % Initialize cell array to store each COALESCE statement coalesceStatements = cell(length(selectedFields), 1); % Generate COALESCE statement for each field for i = 1:length(selectedFields) % Split table and field name parts = strsplit(selectedFields{i}, '.'); if length(parts) ~= 2 error('Field name must be in format "Table.field": %s', selectedFields{i}); end tableName = parts{1}; fieldName = parts{2}; % Generate COALESCE statement coalesceStatements{i} = sprintf('COALESCE(%s.%s, ''NaN'') AS %s ', ... tableName, fieldName, fieldName); end % Join with comma, newline and MATLAB string continuation coalesceStr = strjoin(coalesceStatements, [', ' sprintf('\n ')]); % Add initial newline and spacing for formatting coalesceStr = [sprintf('SELECT DISTINCT \n ') coalesceStr]; end end end