Files
imdd_silas/Classes/DataBaseHandler/DBHandler.m

1239 lines
52 KiB
Matlab
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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";
options.server = "";
options.port = 3306;
options.user = "";
options.password = "";
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
options.user, ... % Username
options.password, ... % Password (or getSecret)
"Vendor", "MySQL", ...
"Server", options.server, ...
"PortNumber", 3306, ...
"JDBCDriverLocation", "C:\Users\Silas\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<AGROW>
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 its 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<AGROW>
end
end
% Ensure main table is included
if ~ismember(mainTable, reqTables)
reqTables = unique([mainTable; reqTables]); %#ok<AGROW>
end
% Well build joins only for the required tables (minus the main)
otherTables = setdiff(reqTables, {mainTable});
% Keep track of whats 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 isnt 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