diff --git a/internal/repository/jobFind.go b/internal/repository/jobFind.go index ea5e1e9..1e2ccb8 100644 --- a/internal/repository/jobFind.go +++ b/internal/repository/jobFind.go @@ -194,10 +194,12 @@ func (r *JobRepository) FindConcurrentJobs( queryRunning := query.Where("job.job_state = ?").Where("(job.start_time BETWEEN ? AND ? OR job.start_time < ?)", "running", startTimeTail, stopTimeTail, startTime) + // Get At Least One Exact Hostname Match from JSON Resources Array in Database queryRunning = queryRunning.Where("EXISTS (SELECT 1 FROM json_each(job.resources) WHERE json_extract(value, '$.hostname') = ?)", hostname) query = query.Where("job.job_state != ?").Where("((job.start_time BETWEEN ? AND ?) OR (job.start_time + job.duration) BETWEEN ? AND ? OR (job.start_time < ?) AND (job.start_time + job.duration) > ?)", "running", startTimeTail, stopTimeTail, startTimeFront, stopTimeTail, startTime, stopTime) + // Get At Least One Exact Hostname Match from JSON Resources Array in Database query = query.Where("EXISTS (SELECT 1 FROM json_each(job.resources) WHERE json_extract(value, '$.hostname') = ?)", hostname) rows, err := query.RunWith(r.stmtCache).Query() diff --git a/internal/repository/jobQuery.go b/internal/repository/jobQuery.go index f081297..d169b6f 100644 --- a/internal/repository/jobQuery.go +++ b/internal/repository/jobQuery.go @@ -67,7 +67,8 @@ func (r *JobRepository) QueryJobs( rows, err := query.RunWith(r.stmtCache).Query() if err != nil { - log.Errorf("Error while running query: %v", err) + queryString, queryVars, _ := query.ToSql() + log.Errorf("Error while running query '%s' %v: %v", queryString, queryVars, err) return nil, err } @@ -197,14 +198,7 @@ func BuildWhereClause(filter *model.JobFilter, query sq.SelectBuilder) sq.Select query = buildIntCondition("job.num_hwthreads", filter.NumHWThreads, query) } if filter.Node != nil { - log.Infof("Applying node filter: %v", filter.Node) - if filter.Node.Eq != nil { - query = query.Where("EXISTS (SELECT 1 FROM json_each(job.resources) WHERE json_extract(value, '$.hostname') = ?)", *filter.Node.Eq) - } else if filter.Node.Contains != nil { - query = query.Where("EXISTS (SELECT 1 FROM json_each(job.resources) WHERE json_extract(value, '$.hostname') LIKE ?)", "%"+*filter.Node.Contains+"%") - } else { - query = buildStringCondition("job.resources", filter.Node, query) - } + query = buildResourceJsonCondition("hostname", filter.Node, query) } if filter.Energy != nil { query = buildFloatCondition("job.energy", filter.Energy, query) @@ -306,6 +300,28 @@ func buildMetaJsonCondition(jsonField string, cond *model.StringInput, query sq. return query } +func buildResourceJsonCondition(jsonField string, cond *model.StringInput, query sq.SelectBuilder) sq.SelectBuilder { + // Verify and Search Only in Valid Jsons + query = query.Where("JSON_VALID(resources)") + // add "AND" Sql query Block for field match + if cond.Eq != nil { + return query.Where("EXISTS (SELECT 1 FROM json_each(job.resources) WHERE json_extract(value, \"$."+jsonField+"\") = ?)", *cond.Eq) + } + if cond.Neq != nil { // Currently Unused + return query.Where("EXISTS (SELECT 1 FROM json_each(job.resources) WHERE json_extract(value, \"$."+jsonField+"\") != ?)", *cond.Neq) + } + if cond.StartsWith != nil { // Currently Unused + return query.Where("EXISTS (SELECT 1 FROM json_each(job.resources) WHERE json_extract(value, \"$."+jsonField+"\")) LIKE ?)", fmt.Sprint(*cond.StartsWith, "%")) + } + if cond.EndsWith != nil { // Currently Unused + return query.Where("EXISTS (SELECT 1 FROM json_each(job.resources) WHERE json_extract(value, \"$."+jsonField+"\") LIKE ?)", fmt.Sprint("%", *cond.EndsWith)) + } + if cond.Contains != nil { + return query.Where("EXISTS (SELECT 1 FROM json_each(job.resources) WHERE json_extract(value, \"$."+jsonField+"\") LIKE ?)", fmt.Sprint("%", *cond.Contains, "%")) + } + return query +} + var ( matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])") diff --git a/web/frontend/src/generic/Filters.svelte b/web/frontend/src/generic/Filters.svelte index 4a9be3e..74640ae 100644 --- a/web/frontend/src/generic/Filters.svelte +++ b/web/frontend/src/generic/Filters.svelte @@ -53,10 +53,16 @@ { range: "last30d", rangeLabel: "Last 30 days"} ]; + const nodeMatchLabels = { + eq: "", + contains: " Contains", + } + let filters = { projectMatch: filterPresets.projectMatch || "contains", userMatch: filterPresets.userMatch || "contains", jobIdMatch: filterPresets.jobIdMatch || "eq", + nodeMatch: filterPresets.nodeMatch || "eq", cluster: filterPresets.cluster || null, partition: filterPresets.partition || null, @@ -106,7 +112,7 @@ let items = []; if (filters.cluster) items.push({ cluster: { eq: filters.cluster } }); - if (filters.node) items.push({ node: { contains: filters.node } }); + if (filters.node) items.push({ node: { [filters.nodeMatch]: filters.node } }); if (filters.partition) items.push({ partition: { eq: filters.partition } }); if (filters.states.length != allJobStates.length) items.push({ state: filters.states }); @@ -178,6 +184,8 @@ let opts = []; if (filters.cluster) opts.push(`cluster=${filters.cluster}`); if (filters.node) opts.push(`node=${filters.node}`); + if (filters.node && filters.nodeMatch != "eq") // "eq" is default-case + opts.push(`nodeMatch=${filters.nodeMatch}`); if (filters.partition) opts.push(`partition=${filters.partition}`); if (filters.states.length != allJobStates.length) for (let state of filters.states) opts.push(`state=${state}`); @@ -196,7 +204,7 @@ opts.push(`jobId=${singleJobId}`); } if (filters.jobIdMatch != "eq") - opts.push(`jobIdMatch=${filters.jobIdMatch}`); + opts.push(`jobIdMatch=${filters.jobIdMatch}`); // "eq" is default-case for (let tag of filters.tags) opts.push(`tag=${tag}`); if (filters.duration.from && filters.duration.to) opts.push(`duration=${filters.duration.from}-${filters.duration.to}`); @@ -218,13 +226,13 @@ } else { for (let singleUser of filters.user) opts.push(`user=${singleUser}`); } - if (filters.userMatch != "contains") + if (filters.userMatch != "contains") // "contains" is default-case opts.push(`userMatch=${filters.userMatch}`); if (filters.project) opts.push(`project=${filters.project}`); + if (filters.project && filters.projectMatch != "contains") // "contains" is default-case + opts.push(`projectMatch=${filters.projectMatch}`); if (filters.jobName) opts.push(`jobName=${filters.jobName}`); if (filters.arrayJobId) opts.push(`arrayJobId=${filters.arrayJobId}`); - if (filters.project && filters.projectMatch != "contains") - opts.push(`projectMatch=${filters.projectMatch}`); if (filters.stats.length != 0) for (let stat of filters.stats) { opts.push(`stat=${stat.field}-${stat.from}-${stat.to}`); @@ -386,7 +394,7 @@ {#if filters.node != null} (isResourcesOpen = true)}> - Node: {filters.node} + Node{nodeMatchLabels[filters.nodeMatch]}: {filters.node} {/if} @@ -449,6 +457,7 @@ bind:numHWThreads={filters.numHWThreads} bind:numAccelerators={filters.numAccelerators} bind:namedNode={filters.node} + bind:nodeMatch={filters.nodeMatch} bind:isNodesModified bind:isHwthreadsModified bind:isAccsModified diff --git a/web/frontend/src/generic/filters/Resources.svelte b/web/frontend/src/generic/filters/Resources.svelte index 750c4a6..443dda7 100644 --- a/web/frontend/src/generic/filters/Resources.svelte +++ b/web/frontend/src/generic/filters/Resources.svelte @@ -24,6 +24,7 @@ ModalBody, ModalHeader, ModalFooter, + Input } from "@sveltestrap/sveltestrap"; import DoubleRangeSlider from "../select/DoubleRangeSlider.svelte"; @@ -40,11 +41,18 @@ export let isHwthreadsModified = false; export let isAccsModified = false; export let namedNode = null; + export let nodeMatch = "eq" let pendingNumNodes = numNodes, pendingNumHWThreads = numHWThreads, pendingNumAccelerators = numAccelerators, - pendingNamedNode = namedNode; + pendingNamedNode = namedNode, + pendingNodeMatch = nodeMatch; + + const nodeMatchLabels = { + eq: "Equal To", + contains: "Contains", + } const findMaxNumAccels = (clusters) => clusters.reduce( @@ -145,7 +153,17 @@ Select number of utilized Resources Named Node - + + + + + {#each Object.entries(nodeMatchLabels) as [nodeMatchKey, nodeMatchLabel]} + + {nodeMatchLabel} + + {/each} + + Number of Nodes { @@ -215,11 +233,13 @@ to: pendingNumAccelerators.to, }; namedNode = pendingNamedNode; + nodeMatch = pendingNodeMatch; dispatch("set-filter", { numNodes, numHWThreads, numAccelerators, namedNode, + nodeMatch }); }} > @@ -233,6 +253,7 @@ pendingNumHWThreads = { from: null, to: null }; pendingNumAccelerators = { from: null, to: null }; pendingNamedNode = null; + pendingNodeMatch = null; numNodes = { from: pendingNumNodes.from, to: pendingNumNodes.to }; numHWThreads = { from: pendingNumHWThreads.from, @@ -246,11 +267,13 @@ isHwthreadsModified = false; isAccsModified = false; namedNode = pendingNamedNode; + nodeMatch = pendingNodeMatch; dispatch("set-filter", { numNodes, numHWThreads, numAccelerators, namedNode, + nodeMatch }); }}>Reset