# Conflicts: # Classes/00_signals/Signal.m # Functions/EQ_structures/duobinary_signaling.m # Functions/EQ_structures/vnle_postfilter_mlse.m # Functions/EQ_visuals/showLevelScatter.m
1232 lines
52 KiB
Matlab
1232 lines
52 KiB
Matlab
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<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 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<AGROW>
|
||
end
|
||
end
|
||
|
||
% Ensure main table is included
|
||
if ~ismember(mainTable, reqTables)
|
||
reqTables = unique([mainTable; reqTables]); %#ok<AGROW>
|
||
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
|