The phase (b) analysis engine: typed results (metrics with drill filters, charts as data), column and table profiles, distinct values and duplicates, AnalysisSecurity (analyze permission, table READ, personalized table and field security, analyzable/excluded fields), the effective per-table config, the minimum group size guard, data versions, async exact runs cached in data_analysis_run, and DataAnalysisService as the facade. The AnalysisBackend port has an RC.5 stand-in that pushes down what core supports today and streams the rest with row limits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
172 lines
8.1 KiB
Java
172 lines
8.1 KiB
Java
/*
|
|
* QQQ - Low-code Application Framework for Engineers.
|
|
* Copyright (C) 2021-2025. Kingsrook, LLC
|
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
|
* contact@kingsrook.com
|
|
* https://github.com/Kingsrook/
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* https://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package com.kingsrook.qbits.dataanalysis.engine;
|
|
|
|
import java.util.Arrays;
|
|
import java.util.List;
|
|
import java.util.Set;
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
import java.util.stream.Collectors;
|
|
import com.kingsrook.qbits.dataanalysis.DataAnalysisQBitConfig;
|
|
import com.kingsrook.qbits.dataanalysis.TableAnalysisDefaults;
|
|
import com.kingsrook.qbits.dataanalysis.model.DataAnalysisTables;
|
|
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionEnforcement;
|
|
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
|
import com.kingsrook.qqq.backend.core.context.QContext;
|
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
|
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
|
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
|
import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession;
|
|
|
|
/*******************************************************************************
|
|
** Instance-owned configuration resolver; only the settings lookup is privileged.
|
|
*******************************************************************************/
|
|
public class EffectiveConfigResolver
|
|
{
|
|
private final DataAnalysisQBitConfig defaults;
|
|
private final ConcurrentHashMap<String, CachedConfig> cache = new ConcurrentHashMap<>();
|
|
|
|
/*******************************************************************************
|
|
** A 60-second configuration snapshot, never a user-data cache.
|
|
*******************************************************************************/
|
|
private record CachedConfig(long expires, EffectiveConfig config)
|
|
{
|
|
}
|
|
|
|
|
|
|
|
/*******************************************************************************
|
|
** Bind code defaults to one host runtime.
|
|
*******************************************************************************/
|
|
public EffectiveConfigResolver(DataAnalysisQBitConfig defaults)
|
|
{
|
|
this.defaults = defaults;
|
|
}
|
|
|
|
|
|
|
|
/*******************************************************************************
|
|
** Return a detached snapshot so callers cannot change memoized policy.
|
|
*******************************************************************************/
|
|
public EffectiveConfig resolve(String table) throws QException
|
|
{
|
|
CachedConfig cached = cache.get(table);
|
|
if(cached == null || cached.expires() <= System.currentTimeMillis())
|
|
{
|
|
cached = new CachedConfig(System.currentTimeMillis() + 60_000, load(table));
|
|
cache.put(table, cached);
|
|
}
|
|
EffectiveConfig c = cached.config();
|
|
return (new EffectiveConfig().withEnabled(c.getEnabled()).withMaxRows(c.getMaxRows()).withSampleRows(c.getSampleRows())
|
|
.withSampleThresholdRows(c.getSampleThresholdRows()).withMinGroupSize(c.getMinGroupSize()).withCacheTtlMinutes(c.getCacheTtlMinutes())
|
|
.withAnalyzableFields(Set.copyOf(c.getAnalyzableFields())).withExcludedFields(Set.copyOf(c.getExcludedFields())));
|
|
}
|
|
|
|
|
|
|
|
/*******************************************************************************
|
|
** Settings editors may invalidate immediately instead of waiting for expiry.
|
|
*******************************************************************************/
|
|
public void invalidate()
|
|
{
|
|
cache.clear();
|
|
}
|
|
|
|
|
|
|
|
/*******************************************************************************
|
|
** Runtime row overrides per-table code defaults, then global defaults.
|
|
*******************************************************************************/
|
|
private EffectiveConfig load(String table) throws QException
|
|
{
|
|
TableAnalysisDefaults d = defaults.getTableDefaults().getOrDefault(table, new TableAnalysisDefaults());
|
|
EffectiveConfig c = new EffectiveConfig().withEnabled(!Boolean.FALSE.equals(d.getEnabled()))
|
|
.withMaxRows(or(d.getMaxRows(), defaults.getDefaultMaxRows())).withSampleRows(or(d.getSampleRows(), defaults.getDefaultSampleRows()))
|
|
.withSampleThresholdRows(or(d.getSampleThresholdRows(), defaults.getDefaultSampleThresholdRows()))
|
|
.withMinGroupSize(or(d.getMinGroupSize(), defaults.getDefaultMinGroupSize())).withCacheTtlMinutes(or(d.getCacheTtlMinutes(), defaults.getDefaultCacheTtlMinutes()))
|
|
.withAnalyzableFields(d.getAnalyzableFields() == null ? Set.of() : Set.copyOf(d.getAnalyzableFields()))
|
|
.withExcludedFields(d.getExcludedFields() == null ? Set.of() : Set.copyOf(d.getExcludedFields()));
|
|
QSession original = QContext.getQSession();
|
|
List<QRecord> rows;
|
|
try
|
|
{
|
|
QContext.setQSession(new QSystemUserSession());
|
|
QueryInput input = new QueryInput(DataAnalysisTables.TABLE_CONFIG)
|
|
.withFilter(DrillFilters.condition("table_name", QCriteriaOperator.EQUALS, table).withLimit(1))
|
|
.withFieldNamesToInclude(Set.of("enabled", "max_rows", "sample_rows", "sample_threshold_rows", "min_group_size", "cache_ttl_minutes", "analyzable_fields", "excluded_fields"))
|
|
.withShouldFetchHeavyFields(true);
|
|
input.setPermissionEnforcement(PermissionEnforcement.ENFORCE);
|
|
rows = new QueryAction().execute(input).getRecords();
|
|
}
|
|
finally
|
|
{
|
|
QContext.setQSession(original);
|
|
}
|
|
if(!rows.isEmpty())
|
|
{
|
|
QRecord r = rows.getFirst();
|
|
if(r.getValue("enabled") != null)
|
|
{
|
|
c.withEnabled(r.getValueBoolean("enabled"));
|
|
}
|
|
c.withMaxRows(or(r.getValueInteger("max_rows"), c.getMaxRows())).withSampleRows(or(r.getValueInteger("sample_rows"), c.getSampleRows()))
|
|
.withSampleThresholdRows(or(r.getValueInteger("sample_threshold_rows"), c.getSampleThresholdRows()))
|
|
.withMinGroupSize(or(r.getValueInteger("min_group_size"), c.getMinGroupSize())).withCacheTtlMinutes(or(r.getValueInteger("cache_ttl_minutes"), c.getCacheTtlMinutes()));
|
|
if(r.getValue("analyzable_fields") != null)
|
|
{
|
|
c.withAnalyzableFields(csv(r.getValueString("analyzable_fields")));
|
|
}
|
|
if(r.getValue("excluded_fields") != null)
|
|
{
|
|
c.withExcludedFields(csv(r.getValueString("excluded_fields")));
|
|
}
|
|
}
|
|
c.withEnabled(c.getEnabled() && (defaults.getIncludeTables().isEmpty() || defaults.getIncludeTables().contains(table)) && !defaults.getExcludeTables().contains(table));
|
|
if(List.of(c.getMaxRows(), c.getSampleRows(), c.getSampleThresholdRows(), c.getMinGroupSize(), c.getCacheTtlMinutes()).stream().anyMatch(n -> n < 0))
|
|
{
|
|
throw new QException("Analysis settings contain an invalid limit.");
|
|
}
|
|
return (c);
|
|
}
|
|
|
|
|
|
|
|
/*******************************************************************************
|
|
** Null inherits, zero is a real configured limit.
|
|
*******************************************************************************/
|
|
private static Integer or(Integer value, Integer fallback)
|
|
{
|
|
return (value == null ? fallback : value);
|
|
}
|
|
|
|
|
|
|
|
/*******************************************************************************
|
|
** Blank lists mean all readable fields or no exclusions.
|
|
*******************************************************************************/
|
|
private static Set<String> csv(String text)
|
|
{
|
|
return (Arrays.stream(text.split(",")).map(String::trim).filter(s -> !s.isEmpty()).collect(Collectors.toSet()));
|
|
}
|
|
}
|