839 lines
36 KiB
Matlab
839 lines
36 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
|
|
pathToDB % 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
|
|
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.pathToDB = ""; % Default value for pathToDB if not provided
|
|
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
|
|
obj.conn = sqlite(obj.pathToDB);
|
|
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
|
|
obj.getTableNames();
|
|
obj.getTables();
|
|
obj.getDistinctValues();
|
|
end
|
|
|
|
function obj = getTableNames(obj)
|
|
% Get all table names from the database
|
|
try
|
|
result = fetch(obj.conn, 'SELECT name FROM sqlite_master WHERE type="table"');
|
|
obj.tableNames = result.name;
|
|
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;
|
|
|
|
num_runs = obj.fetch('SELECT COUNT(*) AS total_runs FROM Runs');
|
|
|
|
num_configs = obj.fetch('SELECT COUNT(*) AS total_configurations FROM Configurations');
|
|
|
|
num_meas = obj.fetch('SELECT COUNT(*) AS total_measurements FROM Measurements');
|
|
|
|
assert((num_runs{1,1}==num_configs{1,1})&&(num_configs{1,1}==num_meas{1,1}),'Different num of entries per table');
|
|
|
|
%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
|
|
|
|
% Convert newRow to a table if it is a struct
|
|
if isstruct(newRow)
|
|
fields = fieldnames(newRow);
|
|
emptyFields = structfun(@isempty,newRow);
|
|
if sum(emptyFields) > 0
|
|
emptyFieldNames = fields(emptyFields); % use () to get a cell array
|
|
for idx = 1:numel(emptyFieldNames)
|
|
newRow.(emptyFieldNames{idx}) = NaN;
|
|
end
|
|
end
|
|
newRow = struct2table(newRow);
|
|
end
|
|
|
|
% Ensure the new row matches the structure of the existing table
|
|
existingTableStructure = obj.tables.(tableName);
|
|
|
|
% Perform data type checks and conversions
|
|
for colName = newRow.Properties.VariableNames
|
|
% Extract the value and its intended column type
|
|
value = newRow.(colName{1});
|
|
existingValue = existingTableStructure.(colName{1});
|
|
|
|
% If the value is a class object, convert it to JSON format
|
|
if isobject(value) && ~isdatetime(value) && ~isa(value,"string")
|
|
newRow.(colName{1}) = string(jsonencode(value));
|
|
|
|
% If the value is a character array, convert it to a string
|
|
elseif ischar(value)
|
|
newRow.(colName{1}) = string(value);
|
|
|
|
end
|
|
|
|
end
|
|
|
|
% Append the new row to the database table
|
|
try
|
|
sqlwrite(obj.conn, tableName, newRow);
|
|
% disp(['Successfully appended new row to the table ', tableName]);
|
|
catch e
|
|
error('Failed to append to the table %s: %s', tableName, e.message);
|
|
end
|
|
|
|
% Retrieve the measurement_id of the newly inserted row for linking other tables
|
|
result = fetch(obj.conn, 'SELECT last_insert_rowid()');
|
|
lastID = result{1, 1}; % Access the value directly from the table
|
|
end
|
|
|
|
function exists = 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
|
|
query = sprintf('SELECT COUNT(*) FROM %s WHERE %s = "%s"', table2check, column2check, value2check);
|
|
|
|
% 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 "', 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 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
|
|
|
|
% Add hash to equalizer parameters
|
|
eqParamsData.config_hash = hashStr;
|
|
|
|
% 2. Check if an equalizer configuration with the same hash exists
|
|
queryStr = sprintf('SELECT eq_id FROM EqualizerParameters WHERE config_hash = ''%s''', eqParamsData.config_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
|
|
eq_id = obj.appendToTable('EqualizerParameters', eqParamsData);
|
|
end
|
|
|
|
% 3. Add the equalizer configuration reference and run_id to resultData
|
|
resultData.eqParam_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
|
|
|
|
% Add the result hash to resultData
|
|
resultData.result_hash = resultHashStr;
|
|
|
|
% 5. Check if an identical processing result already exists
|
|
queryStr2 = sprintf('SELECT result_id FROM Results WHERE result_hash = ''%s''', resultData.result_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. 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)
|
|
answer = fetch(obj.conn,query);
|
|
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 1: Prompt the user to input filter parameters if not provided
|
|
if isempty(filterParams)
|
|
filterParams = obj.promptFilterParameters();
|
|
end
|
|
|
|
% Step 2: Prompt the user to select fields to include in the SELECT statement
|
|
if isempty(selectedFields)
|
|
selectedFields = obj.promptSelectFields();
|
|
else
|
|
if iscell(selectedFields)
|
|
|
|
elseif isstruct(selectedFields)
|
|
|
|
end
|
|
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);
|
|
end
|
|
|
|
function query = constructSQLQuery(obj, filterParams, selectedFields)
|
|
% constructSQLQuery Constructs the SQL query based on filter parameters and selected fields.
|
|
%
|
|
% If selectedFields is provided as a struct, it is converted to a cell array.
|
|
% The conversion takes the field names and creates entries like:
|
|
% {'selectedFields.fieldName'} for each field.
|
|
|
|
% Input check for selectedFields: if it's a struct, convert it to a cell array.
|
|
if isstruct(selectedFields)
|
|
newFields = {};
|
|
tableNames = fieldnames(selectedFields);
|
|
for t = 1:numel(tableNames)
|
|
tableStruct = selectedFields.(tableNames{t});
|
|
fieldNames = fieldnames(tableStruct);
|
|
for f = 1:numel(fieldNames)
|
|
if isequal(tableStruct.(fieldNames{f}), 1)
|
|
newFields{end+1} = sprintf('%s.%s', tableNames{t}, fieldNames{f});
|
|
end
|
|
end
|
|
end
|
|
selectedFields = newFields;
|
|
end
|
|
|
|
% Construct the SELECT clause dynamically based on user selection.
|
|
% (Assuming that when provided as a cell array, each entry is of the form
|
|
% 'TableName.fieldName' or, in our conversion case, 'selectedFields.fieldName'.)
|
|
selectClause = 'SELECT DISTINCT ';
|
|
for i = 1:numel(selectedFields)
|
|
fieldParts = strsplit(selectedFields{i}, '.');
|
|
% If the field comes from the struct conversion, its first part is 'selectedFields'
|
|
% and the actual field name is in the second part.
|
|
if strcmp(fieldParts{1}, 'selectedFields')
|
|
tableName = fieldParts{1}; % not used for type checking below
|
|
fieldName = fieldParts{2};
|
|
else
|
|
tableName = fieldParts{1};
|
|
fieldName = fieldParts{2};
|
|
end
|
|
|
|
% Decide on COALESCE depending on the field type.
|
|
% If the table is known in obj.tables and the field is numeric, use 'NaN'.
|
|
if isfield(obj.tables, tableName) && isfield(obj.tables.(tableName), fieldName) && isnumeric(obj.tables.(tableName).(fieldName))
|
|
selectClause = [selectClause, 'COALESCE(', selectedFields{i}, ', ''NaN'') AS ', fieldName];
|
|
else
|
|
selectClause = [selectClause, 'COALESCE(', selectedFields{i}, ', '''') AS ', fieldName];
|
|
end
|
|
|
|
if i < numel(selectedFields)
|
|
selectClause = [selectClause, ', '];
|
|
else
|
|
selectClause = [selectClause, ' '];
|
|
end
|
|
end
|
|
|
|
% --- Adaptive FROM Clause ---
|
|
% Use "Runs" as the main table and add LEFT JOINs for every other table in obj.tables
|
|
% (except "sqlite_sequence") that has a run_id field.
|
|
mainTable = 'Runs';
|
|
fromClause = ['FROM ', mainTable, ' '];
|
|
tableNamesAll = fieldnames(obj.tables);
|
|
for t = 1:numel(tableNamesAll)
|
|
tableName = tableNamesAll{t};
|
|
if strcmpi(tableName, mainTable) || strcmpi(tableName, 'sqlite_sequence')
|
|
continue;
|
|
end
|
|
|
|
if isfield(obj.tables.(tableName), 'run_id')
|
|
% most tables are directly linked to runs table
|
|
fromClause = [fromClause, 'LEFT JOIN ', tableName, ' ON ', mainTable, '.run_id = ', tableName, '.run_id '];
|
|
elseif isfield(obj.tables.(tableName), 'eq_id')
|
|
% equalizer is only linked to results table
|
|
fromClause = [fromClause, 'LEFT JOIN ', tableName, ' ON ', 'Results', '.eqParam_id = ', tableName, '.eq_id '];
|
|
end
|
|
end
|
|
|
|
% --- WHERE Clause Construction ---
|
|
baseQuery = [selectClause, ' ', fromClause, 'WHERE '];
|
|
filterClauses = [];
|
|
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);
|
|
|
|
% Handle various types of values for SQL query construction
|
|
if isempty(value)
|
|
continue;
|
|
elseif isnumeric(value) && isnan(value)
|
|
filterClause = sprintf('%s IS NULL', fullName);
|
|
elseif isnumeric(value) && ~isEnumeration(value)
|
|
filterClause = sprintf('%s = %f', fullName, value);
|
|
elseif islogical(value) || (isnumeric(value) && ismember(value, [0, 1])) && ~isEnumeration(value)
|
|
filterClause = sprintf('%s = %d', fullName, value);
|
|
elseif ischar(value) || isstring(value)
|
|
filterClause = sprintf('%s = ''%s''', fullName, char(value));
|
|
elseif isEnumeration(value)
|
|
filterClause = sprintf('%s = ''%s''', fullName, value);
|
|
else
|
|
error('Unsupported data type for field "%s".', fullName);
|
|
end
|
|
filterClauses = [filterClauses, filterClause, ' AND '];
|
|
end
|
|
end
|
|
|
|
% Remove trailing ' AND ' if any filters were added.
|
|
if ~isempty(filterClauses)
|
|
filterClauses = filterClauses(1:end-5);
|
|
query = [selectClause, ' ', fromClause, 'WHERE ', filterClauses];
|
|
else
|
|
query = [selectClause, ' ', fromClause];
|
|
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
|
|
|
|
|
|
end
|
|
end
|