   650→	}
   651→	cclog.Debugf("Timer Partitions %s", time.Since(start))
   652→	return partitions.([]string), nil
   653→}
   654→
   655→// AllocatedNodes returns a map of all subclusters to a map of hostnames to the amount of jobs running on that host.
   656→// Hosts with zero jobs running on them will not show up!
   657→func (r *JobRepository) AllocatedNodes(cluster string) (map[string]map[string]int, error) {
   658→	start := time.Now()
   659→	subclusters := make(map[string]map[string]int)
   660→	rows, err := sq.Select("resources", "subcluster").From("job").
   661→		Where("job.job_state = 'running'").
   662→		Where("job.cluster = ?", cluster).
   663→		RunWith(r.stmtCache).Query()
   664→	if err != nil {
   665→		cclog.Errorf("Error while running AllocatedNodes query for cluster=%s: %v", cluster, err)
   666→		return nil, fmt.Errorf("failed to query allocated nodes for cluster %s: %w", cluster, err)
   667→	}
   668→
   669→	var raw []byte
   670→	defer rows.Close()
   671→	for rows.Next() {
   672→		raw = raw[0:0]
   673→		var resources []*schema.Resource
   674→		var subcluster string
   675→		if err := rows.Scan(&raw, &subcluster); err != nil {
   676→			cclog.Warnf("Error while scanning rows in AllocatedNodes: %v", err)
   677→			return nil, fmt.Errorf("failed to scan allocated nodes row: %w", err)
   678→		}
   679→		if err := json.Unmarshal(raw, &resources); err != nil {
   680→			cclog.Warnf("Error while unmarshaling raw resources json in AllocatedNodes: %v", err)
   681→			return nil, fmt.Errorf("failed to unmarshal resources in AllocatedNodes: %w", err)
   682→		}
   683→
   684→		hosts, ok := subclusters[subcluster]
   685→		if !ok {
   686→			hosts = make(map[string]int)
   687→			subclusters[subcluster] = hosts
   688→		}
   689→
   690→		for _, resource := range resources {
   691→			hosts[resource.Hostname] += 1
   692→		}
   693→	}
   694→
   695→	cclog.Debugf("Timer AllocatedNodes %s", time.Since(start))
   696→	return subclusters, nil
   697→}
   698→
   699→// StopJobsExceedingWalltimeBy marks running jobs as failed if they exceed their walltime limit.
   700→// This is typically called periodically to clean up stuck or orphaned jobs.
   701→//
   702→// Jobs are marked with:
   703→//   - monitoring_status: MonitoringStatusArchivingFailed
   704→//   - duration: 0
   705→//   - job_state: JobStateFailed
   706→//
   707→// Parameters:
   708→//   - seconds: Grace period beyond walltime before marking as failed
   709→//
   710→// Returns an error if the database update fails.
   711→// Logs the number of jobs marked as failed if any were affected.
   712→func (r *JobRepository) StopJobsExceedingWalltimeBy(seconds int) error {
   713→	start := time.Now()
   714→	currentTime := time.Now().Unix()
   715→	res, err := sq.Update("job").
   716→		Set("monitoring_status", schema.MonitoringStatusArchivingFailed).
   717→		Set("duration", 0).
   718→		Set("job_state", schema.JobStateFailed).
   719→		Where("job.job_state = 'running'").
   720→		Where("job.walltime > 0").
   721→		Where("job.start_time < (? - job.walltime)", currentTime-int64(seconds)).
   722→		RunWith(r.DB).Exec()
   723→	if err != nil {
   724→		cclog.Warnf("Error while stopping jobs exceeding walltime: %v", err)
   725→		return fmt.Errorf("failed to stop jobs exceeding walltime: %w", err)
   726→	}
   727→
   728→	rowsAffected, err := res.RowsAffected()
   729→	if err != nil {
   730→		cclog.Warnf("Error while fetching affected rows after stopping due to exceeded walltime: %v", err)
   731→		return fmt.Errorf("failed to get rows affected count: %w", err)
   732→	}
   733→
   734→	if rowsAffected > 0 {
   735→		cclog.Infof("%d jobs have been marked as failed due to running too long", rowsAffected)
   736→	}
   737→	cclog.Debugf("Timer StopJobsExceedingWalltimeBy %s", time.Since(start))
   738→	return nil
   739→}
   740→
   741→// FindJobIdsByTag returns all job database IDs associated with a specific tag.
   742→//
   743→// Parameters:
   744→//   - tagID: Database ID of the tag to search for
   745→//
   746→// Returns a slice of job IDs or an error if the query fails.
   747→func (r *JobRepository) FindJobIdsByTag(tagID int64) ([]int64, error) {
   748→	query := sq.Select("job.id").From("job").
   749→		Join("jobtag ON jobtag.job_id = job.id").
   750→		Where(sq.Eq{"jobtag.tag_id": tagID}).Distinct()
   751→	rows, err := query.RunWith(r.stmtCache).Query()
   752→	if err != nil {
   753→		cclog.Errorf("Error while running FindJobIdsByTag query for tagID=%d: %v", tagID, err)
   754→		return nil, fmt.Errorf("failed to find job IDs by tag %d: %w", tagID, err)
   755→	}
   756→	defer rows.Close()
   757→
   758→	jobIds := make([]int64, 0, 100)
   759→
   760→	for rows.Next() {
   761→		var jobID int64
   762→
   763→		if err := rows.Scan(&jobID); err != nil {
   764→			cclog.Warnf("Error while scanning rows in FindJobIdsByTag: %v", err)
   765→			return nil, fmt.Errorf("failed to scan job ID in FindJobIdsByTag: %w", err)
   766→		}
   767→
   768→		jobIds = append(jobIds, jobID)
   769→	}
   770→
   771→	return jobIds, nil
   772→}
   773→
   774→// FindRunningJobs returns all currently running jobs for a specific cluster.
   775→// Filters out short-running jobs based on repoConfig.MinRunningJobDuration threshold.
   776→//
   777→// Parameters:
   778→//   - cluster: Cluster name to filter jobs
   779→//
   780→// Returns a slice of running job objects or an error if the query fails.
   781→func (r *JobRepository) FindRunningJobs(cluster string) ([]*schema.Job, error) {
   782→	query := sq.Select(jobColumns...).From("job").
   783→		Where("job.cluster = ?", cluster).
   784→		Where("job.job_state = 'running'").
   785→		Where("job.duration > ?", repoConfig.MinRunningJobDuration)
   786→
   787→	rows, err := query.RunWith(r.stmtCache).Query()
   788→	if err != nil {
   789→		cclog.Errorf("Error while running FindRunningJobs query for cluster=%s: %v", cluster, err)
   790→		return nil, fmt.Errorf("failed to find running jobs for cluster %s: %w", cluster, err)
   791→	}
   792→	defer rows.Close()
   793→
   794→	jobs := make([]*schema.Job, 0, 50)
   795→	for rows.Next() {
   796→		job, err := scanJob(rows)
   797→		if err != nil {
   798→			cclog.Warnf("Error while scanning rows in FindRunningJobs: %v", err)
   799→			return nil, fmt.Errorf("failed to scan job in FindRunningJobs: %w", err)
   800→		}
   801→		jobs = append(jobs, job)
   802→	}
   803→
   804→	cclog.Debugf("JobRepository.FindRunningJobs(): Return job count %d (cluster: %s)", len(jobs), cluster)
   805→	return jobs, nil
   806→}
   807→
   808→// UpdateDuration recalculates and updates the duration field for all running jobs.
   809→// Called periodically to keep job durations current without querying individual jobs.
   810→//
   811→// Duration is calculated as: current_time - job.start_time
   812→//
   813→// Returns an error if the database update fails.
   814→func (r *JobRepository) UpdateDuration() error {
   815→	stmnt := sq.Update("job").
   816→		Set("duration", sq.Expr("? - job.start_time", time.Now().Unix())).
   817→		Where("job_state = 'running'")
   818→
   819→	_, err := stmnt.RunWith(r.stmtCache).Exec()
   820→	if err != nil {
   821→		cclog.Errorf("Error while updating duration for running jobs: %v", err)
   822→		return fmt.Errorf("failed to update duration for running jobs: %w", err)
   823→	}
   824→
   825→	return nil
   826→}
   827→
   828→// FindJobsBetween returns jobs within a specified time range.
   829→// If startTimeBegin is 0, returns all jobs before startTimeEnd.
   830→// Optionally excludes tagged jobs from results.
   831→//
   832→// Parameters:
   833→//   - startTimeBegin: Unix timestamp for range start (use 0 for unbounded start)
   834→//   - startTimeEnd: Unix timestamp for range end
   835→//   - omitTagged: "none" = include all jobs, "all" = exclude any tagged jobs,
   836→//     "user" = exclude jobs with user-created tags (not auto-tagger types "app"/"jobClass")
   837→//
   838→// Returns a slice of jobs or an error if the time range is invalid or query fails.
   839→func (r *JobRepository) FindJobsBetween(startTimeBegin int64, startTimeEnd int64, omitTagged string) ([]*schema.Job, error) {
   840→	var query sq.SelectBuilder
   841→
   842→	if startTimeBegin == startTimeEnd || startTimeBegin > startTimeEnd {
   843→		return nil, errors.New("startTimeBegin is equal or larger startTimeEnd")
   844→	}
   845→
   846→	if startTimeBegin == 0 {
   847→		cclog.Infof("Find jobs before %d", startTimeEnd)
   848→		query = sq.Select(jobColumns...).From("job").Where("job.start_time < ?", startTimeEnd)
   849→	} else {


   700→// This is typically called periodically to clean up stuck or orphaned jobs.
   701→//
   702→// Jobs are marked with:
   703→//   - monitoring_status: MonitoringStatusArchivingFailed
   704→//   - duration: 0
   705→//   - job_state: JobStateFailed
   706→//
   707→// Parameters:
   708→//   - seconds: Grace period beyond walltime before marking as failed
   709→//
   710→// Returns an error if the database update fails.
   711→// Logs the number of jobs marked as failed if any were affected.
   712→func (r *JobRepository) StopJobsExceedingWalltimeBy(seconds int) error {
   713→	start := time.Now()
   714→	currentTime := time.Now().Unix()
   715→	res, err := sq.Update("job").
   716→		Set("monitoring_status", schema.MonitoringStatusArchivingFailed).
   717→		Set("duration", 0).
   718→		Set("job_state", schema.JobStateFailed).
   719→		Where("job.job_state = 'running'").
   720→		Where("job.walltime > 0").
   721→		Where("job.start_time < (? - job.walltime)", currentTime-int64(seconds)).
   722→		RunWith(r.DB).Exec()
   723→	if err != nil {
   724→		cclog.Warnf("Error while stopping jobs exceeding walltime: %v", err)
   725→		return fmt.Errorf("failed to stop jobs exceeding walltime: %w", err)
   726→	}
   727→
   728→	rowsAffected, err := res.RowsAffected()
   729→	if err != nil {
   730→		cclog.Warnf("Error while fetching affected rows after stopping due to exceeded walltime: %v", err)
   731→		return fmt.Errorf("failed to get rows affected count: %w", err)
   732→	}
   733→
   734→	if rowsAffected > 0 {
   735→		cclog.Infof("%d jobs have been marked as failed due to running too long", rowsAffected)
   736→	}
   737→	cclog.Debugf("Timer StopJobsExceedingWalltimeBy %s", time.Since(start))
   738→	return nil
   739→}
   740→
   741→// FindJobIdsByTag returns all job database IDs associated with a specific tag.
   742→//
   743→// Parameters:
   744→//   - tagID: Database ID of the tag to search for
   745→//
   746→// Returns a slice of job IDs or an error if the query fails.
   747→func (r *JobRepository) FindJobIdsByTag(tagID int64) ([]int64, error) {
   748→	query := sq.Select("job.id").From("job").
   749→		Join("jobtag ON jobtag.job_id = job.id").
   750→		Where(sq.Eq{"jobtag.tag_id": tagID}).Distinct()
   751→	rows, err := query.RunWith(r.stmtCache).Query()
   752→	if err != nil {
   753→		cclog.Errorf("Error while running FindJobIdsByTag query for tagID=%d: %v", tagID, err)
   754→		return nil, fmt.Errorf("failed to find job IDs by tag %d: %w", tagID, err)
   755→	}
   756→	defer rows.Close()
   757→
   758→	jobIds := make([]int64, 0, 100)
   759→
   760→	for rows.Next() {
   761→		var jobID int64
   762→
   763→		if err := rows.Scan(&jobID); err != nil {
   764→			cclog.Warnf("Error while scanning rows in FindJobIdsByTag: %v", err)
   765→			return nil, fmt.Errorf("failed to scan job ID in FindJobIdsByTag: %w", err)
   766→		}
   767→
   768→		jobIds = append(jobIds, jobID)
   769→	}
   770→
   771→	return jobIds, nil
   772→}
   773→
   774→// FindRunningJobs returns all currently running jobs for a specific cluster.
   775→// Filters out short-running jobs based on repoConfig.MinRunningJobDuration threshold.
   776→//
   777→// Parameters:
   778→//   - cluster: Cluster name to filter jobs
   779→//
   780→// Returns a slice of running job objects or an error if the query fails.
   781→func (r *JobRepository) FindRunningJobs(cluster string) ([]*schema.Job, error) {
   782→	query := sq.Select(jobColumns...).From("job").
   783→		Where("job.cluster = ?", cluster).
   784→		Where("job.job_state = 'running'").
   785→		Where("job.duration > ?", repoConfig.MinRunningJobDuration)
   786→
   787→	rows, err := query.RunWith(r.stmtCache).Query()
   788→	if err != nil {
   789→		cclog.Errorf("Error while running FindRunningJobs query for cluster=%s: %v", cluster, err)
   790→		return nil, fmt.Errorf("failed to find running jobs for cluster %s: %w", cluster, err)
   791→	}
   792→	defer rows.Close()
   793→
   794→	jobs := make([]*schema.Job, 0, 50)
   795→	for rows.Next() {
   796→		job, err := scanJob(rows)
   797→		if err != nil {
   798→			cclog.Warnf("Error while scanning rows in FindRunningJobs: %v", err)
   799→			return nil, fmt.Errorf("failed to scan job in FindRunningJobs: %w", err)
   800→		}
   801→		jobs = append(jobs, job)
   802→	}
   803→
   804→	cclog.Debugf("JobRepository.FindRunningJobs(): Return job count %d (cluster: %s)", len(jobs), cluster)
   805→	return jobs, nil
   806→}
   807→
   808→// UpdateDuration recalculates and updates the duration field for all running jobs.
   809→// Called periodically to keep job durations current without querying individual jobs.
   810→//
   811→// Duration is calculated as: current_time - job.start_time
   812→//
   813→// Returns an error if the database update fails.
   814→func (r *JobRepository) UpdateDuration() error {
   815→	stmnt := sq.Update("job").
   816→		Set("duration", sq.Expr("? - job.start_time", time.Now().Unix())).
   817→		Where("job_state = 'running'")
   818→
   819→	_, err := stmnt.RunWith(r.stmtCache).Exec()
   820→	if err != nil {
   821→		cclog.Errorf("Error while updating duration for running jobs: %v", err)
   822→		return fmt.Errorf("failed to update duration for running jobs: %w", err)
   823→	}
   824→
   825→	return nil
   826→}
   827→
   828→// FindJobsBetween returns jobs within a specified time range.
   829→// If startTimeBegin is 0, returns all jobs before startTimeEnd.
   830→// Optionally excludes tagged jobs from results.
   831→//
   832→// Parameters:
   833→//   - startTimeBegin: Unix timestamp for range start (use 0 for unbounded start)
   834→//   - startTimeEnd: Unix timestamp for range end
   835→//   - omitTagged: "none" = include all jobs, "all" = exclude any tagged jobs,
   836→//     "user" = exclude jobs with user-created tags (not auto-tagger types "app"/"jobClass")
   837→//
   838→// Returns a slice of jobs or an error if the time range is invalid or query fails.
   839→func (r *JobRepository) FindJobsBetween(startTimeBegin int64, startTimeEnd int64, omitTagged string) ([]*schema.Job, error) {
   840→	var query sq.SelectBuilder
   841→
   842→	if startTimeBegin == startTimeEnd || startTimeBegin > startTimeEnd {
   843→		return nil, errors.New("startTimeBegin is equal or larger startTimeEnd")
   844→	}
   845→
   846→	if startTimeBegin == 0 {
   847→		cclog.Infof("Find jobs before %d", startTimeEnd)
   848→		query = sq.Select(jobColumns...).From("job").Where("job.start_time < ?", startTimeEnd)
   849→	} else {


   775→// Filters out short-running jobs based on repoConfig.MinRunningJobDuration threshold.
   776→//
   777→// Parameters:
   778→//   - cluster: Cluster name to filter jobs
   779→//
   780→// Returns a slice of running job objects or an error if the query fails.
   781→func (r *JobRepository) FindRunningJobs(cluster string) ([]*schema.Job, error) {
   782→	query := sq.Select(jobColumns...).From("job").
   783→		Where("job.cluster = ?", cluster).
   784→		Where("job.job_state = 'running'").
   785→		Where("job.duration > ?", repoConfig.MinRunningJobDuration)
   786→
   787→	rows, err := query.RunWith(r.stmtCache).Query()
   788→	if err != nil {
   789→		cclog.Errorf("Error while running FindRunningJobs query for cluster=%s: %v", cluster, err)
   790→		return nil, fmt.Errorf("failed to find running jobs for cluster %s: %w", cluster, err)
   791→	}
   792→	defer rows.Close()
   793→
   794→	jobs := make([]*schema.Job, 0, 50)
   795→	for rows.Next() {
   796→		job, err := scanJob(rows)
   797→		if err != nil {
   798→			cclog.Warnf("Error while scanning rows in FindRunningJobs: %v", err)
   799→			return nil, fmt.Errorf("failed to scan job in FindRunningJobs: %w", err)
   800→		}
   801→		jobs = append(jobs, job)
   802→	}
   803→
   804→	cclog.Debugf("JobRepository.FindRunningJobs(): Return job count %d (cluster: %s)", len(jobs), cluster)
   805→	return jobs, nil
   806→}
   807→
   808→// UpdateDuration recalculates and updates the duration field for all running jobs.
   809→// Called periodically to keep job durations current without querying individual jobs.
   810→//
   811→// Duration is calculated as: current_time - job.start_time
   812→//
   813→// Returns an error if the database update fails.
   814→func (r *JobRepository) UpdateDuration() error {
   815→	stmnt := sq.Update("job").
   816→		Set("duration", sq.Expr("? - job.start_time", time.Now().Unix())).
   817→		Where("job_state = 'running'")
   818→
   819→	_, err := stmnt.RunWith(r.stmtCache).Exec()
   820→	if err != nil {
   821→		cclog.Errorf("Error while updating duration for running jobs: %v", err)
   822→		return fmt.Errorf("failed to update duration for running jobs: %w", err)
   823→	}
   824→
   825→	return nil
   826→}
   827→
   828→// FindJobsBetween returns jobs within a specified time range.
   829→// If startTimeBegin is 0, returns all jobs before startTimeEnd.
   830→// Optionally excludes tagged jobs from results.
   831→//
   832→// Parameters:
   833→//   - startTimeBegin: Unix timestamp for range start (use 0 for unbounded start)
   834→//   - startTimeEnd: Unix timestamp for range end
   835→//   - omitTagged: "none" = include all jobs, "all" = exclude any tagged jobs,
   836→//     "user" = exclude jobs with user-created tags (not auto-tagger types "app"/"jobClass")
   837→//
   838→// Returns a slice of jobs or an error if the time range is invalid or query fails.
   839→func (r *JobRepository) FindJobsBetween(startTimeBegin int64, startTimeEnd int64, omitTagged string) ([]*schema.Job, error) {
   840→	var query sq.SelectBuilder
   841→
   842→	if startTimeBegin == startTimeEnd || startTimeBegin > startTimeEnd {
   843→		return nil, errors.New("startTimeBegin is equal or larger startTimeEnd")
   844→	}
   845→
   846→	if startTimeBegin == 0 {
   847→		cclog.Infof("Find jobs before %d", startTimeEnd)
   848→		query = sq.Select(jobColumns...).From("job").Where("job.start_time < ?", startTimeEnd)
   849→	} else {
   850→		cclog.Infof("Find jobs between %d and %d", startTimeBegin, startTimeEnd)
   851→		query = sq.Select(jobColumns...).From("job").Where("job.start_time BETWEEN ? AND ?", startTimeBegin, startTimeEnd)
   852→	}
   853→
   854→	switch omitTagged {


   810→//
   811→// Duration is calculated as: current_time - job.start_time
   812→//
   813→// Returns an error if the database update fails.
   814→func (r *JobRepository) UpdateDuration() error {
   815→	stmnt := sq.Update("job").
   816→		Set("duration", sq.Expr("? - job.start_time", time.Now().Unix())).
   817→		Where("job_state = 'running'")
   818→
   819→	_, err := stmnt.RunWith(r.stmtCache).Exec()
   820→	if err != nil {
   821→		cclog.Errorf("Error while updating duration for running jobs: %v", err)
   822→		return fmt.Errorf("failed to update duration for running jobs: %w", err)
   823→	}
   824→
   825→	return nil
   826→}
   827→
   828→// FindJobsBetween returns jobs within a specified time range.
   829→// If startTimeBegin is 0, returns all jobs before startTimeEnd.
   830→// Optionally excludes tagged jobs from results.
   831→//
   832→// Parameters:
   833→//   - startTimeBegin: Unix timestamp for range start (use 0 for unbounded start)
   834→//   - startTimeEnd: Unix timestamp for range end
   835→//   - omitTagged: "none" = include all jobs, "all" = exclude any tagged jobs,
   836→//     "user" = exclude jobs with user-created tags (not auto-tagger types "app"/"jobClass")
   837→//
   838→// Returns a slice of jobs or an error if the time range is invalid or query fails.
   839→func (r *JobRepository) FindJobsBetween(startTimeBegin int64, startTimeEnd int64, omitTagged string) ([]*schema.Job, error) {
   840→	var query sq.SelectBuilder
   841→
   842→	if startTimeBegin == startTimeEnd || startTimeBegin > startTimeEnd {
   843→		return nil, errors.New("startTimeBegin is equal or larger startTimeEnd")
   844→	}
   845→
   846→	if startTimeBegin == 0 {
   847→		cclog.Infof("Find jobs before %d", startTimeEnd)
   848→		query = sq.Select(jobColumns...).From("job").Where("job.start_time < ?", startTimeEnd)
   849→	} else {
   850→		cclog.Infof("Find jobs between %d and %d", startTimeBegin, startTimeEnd)
   851→		query = sq.Select(jobColumns...).From("job").Where("job.start_time BETWEEN ? AND ?", startTimeBegin, startTimeEnd)
   852→	}
   853→
   854→	switch omitTagged {
   855→	case "all":
   856→		query = query.Where("NOT EXISTS (SELECT 1 FROM jobtag WHERE jobtag.job_id = job.id)")
   857→	case "user":
   858→		query = query.Where("NOT EXISTS (SELECT 1 FROM jobtag JOIN tag ON tag.id = jobtag.tag_id WHERE jobtag.job_id = job.id AND tag.tag_type NOT IN ('app', 'jobClass'))")
   859→	}
   860→
   861→	query = query.OrderBy("job.cluster ASC", "job.subcluster ASC", "job.project ASC", "job.start_time ASC")
   862→
   863→	rows, err := query.RunWith(r.stmtCache).Query()
   864→	if err != nil {
   865→		cclog.Errorf("Error while running FindJobsBetween query: %v", err)
   866→		return nil, fmt.Errorf("failed to find jobs between %d and %d: %w", startTimeBegin, startTimeEnd, err)
   867→	}
   868→	defer rows.Close()
   869→
   870→	jobs := make([]*schema.Job, 0, 50)
   871→	for rows.Next() {
   872→		job, err := scanJob(rows)
   873→		if err != nil {
   874→			cclog.Warnf("Error while scanning rows in FindJobsBetween: %v", err)
   875→			return nil, fmt.Errorf("failed to scan job in FindJobsBetween: %w", err)
   876→		}
   877→		jobs = append(jobs, job)
   878→	}
   879→


  1060→//
  1061→// The timestamp parameter (ts) is compared against job.start_time to find
  1062→// relevant jobs. Returns an error if the database query fails or row iteration
  1063→// encounters errors. Individual row parsing errors are logged but don't fail
  1064→// the entire operation.
  1065→func (r *JobRepository) GetUsedNodes(ts int64) (map[string][]string, error) {
  1066→	// Note: Query expects index on (job_state, start_time) for optimal performance
  1067→	q := sq.Select("job.cluster", "job.resources").From("job").
  1068→		Where("job.start_time < ?", ts).
  1069→		Where(sq.Eq{"job.job_state": "running"})
  1070→
  1071→	rows, err := q.RunWith(r.stmtCache).Query()
  1072→	if err != nil {
  1073→		queryString, queryVars, _ := q.ToSql()
  1074→		return nil, fmt.Errorf("query failed [%s] %v: %w", queryString, queryVars, err)
  1075→	}
  1076→	defer rows.Close()
  1077→
  1078→	// Use a map of sets for efficient deduplication
  1079→	nodeSet := make(map[string]map[string]struct{})
  1080→
  1081→	var (
  1082→		cluster      string
  1083→		rawResources []byte
  1084→		resources    []*schema.Resource
  1085→		skippedRows  int
  1086→	)
  1087→
  1088→	for rows.Next() {
  1089→		if err := rows.Scan(&cluster, &rawResources); err != nil {


Perfect! Now I have all the information. Let me compile the comprehensive report:

## Summary of `job_state` SQL Query Usage

I've found **11 occurrences** of `job_state` in WHERE clauses across the repository layer. Here's the complete breakdown:

### 1. **stats.go - Line 116** (buildCountQuery)
- **Location**: `/Users/jan/prg/ClusterCockpit/cc-backend/internal/repository/stats.go:116`
- **Function**: `buildCountQuery()`
- **SQL Pattern**: Squirrel with parameterized placeholder
- **Code**: `query = query.Where("job.job_state = ?", "running")`
- **Parameterization**: YES - uses `?` placeholder with "running" passed as parameter
- **Context**: Counts jobs by kind; filters for running jobs when kind="running". Used by AddJobCount, AddJobCountGrouped
- **Source**: Validated - hardcoded constant "running" from switch statement

### 2. **stats.go - Line 148** (buildStatsQuery)
- **Location**: `/Users/jan/prg/ClusterCockpit/cc-backend/internal/repository/stats.go:148`
- **Function**: `buildStatsQuery()`
- **SQL Pattern**: Raw SQL string via fmt.Sprintf (inlined literal)
- **Code**: `durationExpr := fmt.Sprintf("(CASE WHEN job.job_state = 'running' THEN ...")`
- **Parameterization**: NO - hardcoded literal 'running' in string
- **Context**: Builds dynamic SQL for calculating duration. For running jobs: (now - start_time); for completed: duration field. Used in statistics aggregations
- **Source**: Validated - hardcoded constant in comparison expression

### 3. **stats.go - Line 208** (buildStatsQuery)
- **Location**: `/Users/jan/prg/ClusterCockpit/cc-backend/internal/repository/stats.go:208`
- **Function**: `buildStatsQuery()`
- **SQL Pattern**: Raw SQL string via SELECT clause
- **Code**: `columns = append(columns, "COUNT(CASE WHEN job.job_state = 'running' THEN 1 END) as runningJobs")`
- **Parameterization**: NO - hardcoded literal 'running'
- **Context**: Counts running jobs as part of statistics aggregation
- **Source**: Validated - hardcoded constant

### 4. **stats.go - Line 647** (AddHistograms)
- **Location**: `/Users/jan/prg/ClusterCockpit/cc-backend/internal/repository/stats.go:647`
- **Function**: `AddHistograms()`
- **SQL Pattern**: Raw SQL string via fmt.Sprintf (inlined literal)
- **Code**: `value := fmt.Sprintf("CAST(ROUND(((CASE WHEN job.job_state = 'running' THEN %d - job.start_time ELSE job.duration END) / %d) + 1) as int) as value"...)`
- **Parameterization**: NO - hardcoded literal 'running'
- **Context**: Duration histogram calculation; same as buildStatsQuery logic for running vs completed jobs
- **Source**: Validated - hardcoded constant

### 5. **jobQuery.go - Line 204** (BuildWhereClause)
- **Location**: `/Users/jan/prg/ClusterCockpit/cc-backend/internal/repository/jobQuery.go:204`
- **Function**: `BuildWhereClause()`
- **SQL Pattern**: Raw SQL string via fmt.Sprintf (inlined literal)
- **Code**: `query = query.Where(fmt.Sprintf("job.job_state = '%s'", singleStat))`
- **Parameterization**: NO - inlined literal using fmt.Sprintf
- **Context**: User filter for job state. When single state value requested, inlines for SQLite partial index matching. Comment explicitly notes this is safe because values come from GraphQL enum validation
- **Source**: Validated - values from `filter.State[0]` which is model.JobState enum (validated GraphQL type)
- **Security**: Comment at line 201-202 documents this is intentional for partial index optimization

### 6. **jobQuery.go - Line 210** (BuildWhereClause)
- **Location**: `/Users/jan/prg/ClusterCockpit/cc-backend/internal/repository/jobQuery.go:210`
- **Function**: `BuildWhereClause()`
- **SQL Pattern**: Squirrel sq.Eq
- **Code**: `query = query.Where(sq.Eq{"job.job_state": states})`
- **Parameterization**: YES - Squirrel parameterizes this with ? placeholders
- **Context**: Multiple state filter (IN clause); when filter has multiple state values
- **Source**: Validated - values from filter.State which is model.JobState enum array

### 7. **jobFind.go - Line 132** (GetJobList)
- **Location**: `/Users/jan/prg/ClusterCockpit/cc-backend/internal/repository/jobFind.go:132`
- **Function**: `GetJobList()`
- **SQL Pattern**: Raw SQL string (inlined literal)
- **Code**: `Where("job.job_state != 'running'")`
- **Parameterization**: NO - hardcoded literal 'running'
- **Context**: Gets list of non-running (completed) jobs for batch processing. Returns paginated IDs
- **Source**: Validated - hardcoded constant

### 8. **jobFind.go - Line 280** (FindConcurrentJobs)
- **Location**: `/Users/jan/prg/ClusterCockpit/cc-backend/internal/repository/jobFind.go:280`
- **Function**: `FindConcurrentJobs()`
- **SQL Pattern**: Squirrel with parameterized placeholder
- **Code**: `queryRunning := query.Where("job.job_state = ?", "running")`
- **Parameterization**: YES - uses ? placeholder with "running" as parameter
- **Context**: Finds running jobs overlapping with specified job's time range (for concurrent job analysis)
- **Source**: Validated - hardcoded constant "running"

### 9. **jobFind.go - Line 285** (FindConcurrentJobs)
- **Location**: `/Users/jan/prg/ClusterCockpit/cc-backend/internal/repository/jobFind.go:285`
- **Function**: `FindConcurrentJobs()`
- **SQL Pattern**: Squirrel with parameterized placeholder
- **Code**: `query = query.Where("job.job_state != ?", "running")`
- **Parameterization**: YES - uses ? placeholder with "running" as parameter
- **Context**: Finds completed jobs (not running) overlapping with specified job's time range
- **Source**: Validated - hardcoded constant "running"

### 10. **job.go - Line 661** (AllocatedNodes)
- **Location**: `/Users/jan/prg/ClusterCockpit/cc-backend/internal/repository/job.go:661`
- **Function**: `AllocatedNodes()`
- **SQL Pattern**: Raw SQL string (inlined literal)
- **Code**: `Where("job.job_state = 'running'").`
- **Parameterization**: NO - hardcoded literal 'running'
- **Context**: Gets all currently allocated nodes across running jobs on a cluster
- **Source**: Validated - hardcoded constant

### 11. **job.go - Line 719** (StopJobsExceedingWalltimeBy)
- **Location**: `/Users/jan/prg/ClusterCockpit/cc-backend/internal/repository/job.go:719`
- **Function**: `StopJobsExceedingWalltimeBy()`
- **SQL Pattern**: Raw SQL string (inlined literal)
- **Code**: `Where("job.job_state = 'running'").`
- **Parameterization**: NO - hardcoded literal 'running'
- **Context**: Marks stuck/orphaned jobs as failed if they exceed walltime. Filters to only running jobs
- **Source**: Validated - hardcoded constant

### 12. **job.go - Line 784** (FindRunningJobs)
- **Location**: `/Users/jan/prg/ClusterCockpit/cc-backend/internal/repository/job.go:784`
- **Function**: `FindRunningJobs()`
- **SQL Pattern**: Raw SQL string (inlined literal)
- **Code**: `Where("job.job_state = 'running'").`
- **Parameterization**: NO - hardcoded literal 'running'
- **Context**: Retrieves all running jobs for a cluster, filters by MinRunningJobDuration threshold
- **Source**: Validated - hardcoded constant

### 13. **job.go - Line 817** (UpdateDuration)
- **Location**: `/Users/jan/prg/ClusterCockpit/cc-backend/internal/repository/job.go:817`
- **Function**: `UpdateDuration()`
- **SQL Pattern**: Raw SQL string (inlined literal)
- **Code**: `Where("job_state = 'running'")` (note: missing "job." prefix)
- **Parameterization**: NO - hardcoded literal 'running'
- **Context**: Updates duration for all running jobs periodically (current_time - start_time)
- **Source**: Validated - hardcoded constant
- **Note**: Missing table prefix ("job." is omitted)

### 14. **job.go - Line 1069** (GetUsedNodes)
- **Location**: `/Users/jan/prg/ClusterCockpit/cc-backend/internal/repository/job.go:1069`
- **Function**: `GetUsedNodes()`
- **SQL Pattern**: Squirrel sq.Eq
- **Code**: `Where(sq.Eq{"job.job_state": "running"})`
- **Parameterization**: YES - Squirrel parameterizes this with ? placeholder
- **Context**: Gets nodes in use for running jobs before a timestamp. Comment notes performance requires index on (job_state, start_time)
- **Source**: Validated - hardcoded constant "running"

---

## Summary Table

| File | Line | Function | Parameterized | Value Source | Filter Type |
|------|------|----------|----------------|-------------|------------|
| stats.go | 116 | buildCountQuery | YES (?) | hardcoded "running" | Single known value |
| stats.go | 148 | buildStatsQuery | NO (inlined) | hardcoded 'running' | Single known value |
| stats.go | 208 | buildStatsQuery | NO (inlined) | hardcoded 'running' | Single known value |
| stats.go | 647 | AddHistograms | NO (inlined) | hardcoded 'running' | Single known value |
| jobQuery.go | 204 | BuildWhereClause | NO (fmt.Sprintf) | GraphQL enum validated | Single known value |
| jobQuery.go | 210 | BuildWhereClause | YES (sq.Eq) | GraphQL enum validated | Multiple values |
| jobFind.go | 132 | GetJobList | NO (inlined) | hardcoded 'running' | Single known value |
| jobFind.go | 280 | FindConcurrentJobs | YES (?) | hardcoded "running" | Single known value |
| jobFind.go | 285 | FindConcurrentJobs | YES (?) | hardcoded "running" | Single known value |
| job.go | 661 | AllocatedNodes | NO (inlined) | hardcoded 'running' | Single known value |
| job.go | 719 | StopJobsExceedingWalltimeBy | NO (inlined) | hardcoded 'running' | Single known value |
| job.go | 784 | FindRunningJobs | NO (inlined) | hardcoded 'running' | Single known value |
| job.go | 817 | UpdateDuration | NO (inlined) | hardcoded 'running' | Single known value |
| job.go | 1069 | GetUsedNodes | YES (sq.Eq) | hardcoded "running" | Single known value |

---

## Key Findings

1. **No user input flows directly into job_state filters** - All values come from either hardcoded constants or validated GraphQL enums
2. **Mixed parameterization approach** - 5 use parameterized queries (Squirrel with ?), 9 use inlined literals
3. **Intentional inlining in jobQuery.go:204** - Comment documents this is safe for SQLite partial index optimization
4. **Consistent "running" state usage** - Almost all filters target the hardcoded "running" state for specific queries
5. **One minor issue in job.go:817** - Missing "job." table prefix in UpdateDuration query (though still safe as it's unambiguous)

agentId: a7c36c0986477581c (for resuming to continue this agent's work if needed)
<usage>total_tokens: 66263
tool_uses: 17
duration_ms: 53390</usage>

Prompt:
Search the codebase for all places where `job_state` or `job.job_state` is used in SQL query construction (not in migration files). I need to find every place where a WHERE clause on job_state is built, whether via squirrel (sq.Where, query.Where) or raw SQL strings.

For each occurrence, note:
1. The file and line number
2. Whether the value is parameterized (using `?` placeholder) or inlined as a literal
3. The context — what function it's in and what it's used for (e.g., counting running jobs, building stats queries, etc.)
4. Whether it filters for a single known state value (like 'running') from a validated/hardcoded source, or from user input

Focus on:
- internal/repository/stats.go
- internal/repository/jobQuery.go
- internal/repository/job.go
- Any other files in internal/repository/
- internal/graph/ resolvers

Do NOT look at migration SQL files.


     1→# Usage Dashboard Query Optimization
     2→
     3→## Change 1: Partial indexes for running jobs
     4→
     5→- **New `internal/repository/migrations/sqlite3/14_running-partial-indexes.up.sql`** — partial covering indexes `WHERE job_state = 'running'` for user/project/subcluster groupings. Only running jobs in B-tree (tiny vs millions).
     6→- **New `internal/repository/migrations/sqlite3/14_running-partial-indexes.down.sql`** — drop them.
     7→- **`internal/repository/migration.go:30`** — `Version` 13 → 14.
     8→- **`internal/repository/jobQuery.go:200-202`** — inline literal state value (`fmt.Sprintf("job.job_state = '%s'", singleStat)`) so SQLite matches partial indexes. Safe: values from validated GraphQL enum.
     9→
    10→## Change 2: Request-scoped cache for grouped stats (6 SQL → 2)
    11→
    12→- **`internal/graph/stats_cache.go`** (new) — define `statsGroupCache` type (wraps `sync.Map`), context key, `getOrCompute` method using `sync.Once` per cache entry.
    13→- **`cmd/cc-backend/server.go:87-88`** — add `graphQLServer.AroundOperations` middleware to inject empty cache into context per request.
    14→- **`internal/graph/schema.resolvers.go:671-678`** — in `JobsStatistics`, when `groupBy != nil`: check cache for (filter+groupBy) key → on miss, call `JobsStatsGrouped` with nil sortBy/page (all results), cache them → sort+slice in Go per caller's sortBy/page.
    15→
    16→## Change 3: Parallelize histogram queries
    17→
    18→- **`internal/repository/stats.go:643-668`** — replace 4 sequential histogram calls in `AddHistograms` with `errgroup.WithContext`. Import `golang.org/x/sync/errgroup`. Each goroutine writes distinct struct field.
    19→
    20→## Change 4: Frontend uses consolidated results
    21→
    22→- **`web/frontend/src/status/dashdetails/UsageDash.svelte`** — replace 6-alias topStatsQuery with 2 aliases (USER, PROJECT) requesting all fields, no sort/page. Add 6 `$derived` vars to sort+slice top-10. Update template refs.
    23→
    24→## Verification
    25→```bash
    26→cd web/frontend && npm run build && cd ../.. && go build ./... && go test ./internal/repository/...
    27→```
    28→

# Usage Dashboard Query Optimization

## Change 1: Partial indexes for running jobs

- **New `internal/repository/migrations/sqlite3/14_running-partial-indexes.up.sql`** — partial covering indexes `WHERE job_state = 'running'` for user/project/subcluster groupings. Only running jobs in B-tree (tiny vs millions).
- **New `internal/repository/migrations/sqlite3/14_running-partial-indexes.down.sql`** — drop them.
- **`internal/repository/migration.go:30`** — `Version` 13 → 14.
- **`internal/repository/jobQuery.go:200-202`** — inline literal state value (`fmt.Sprintf("job.job_state = '%s'", singleStat)`) so SQLite matches partial indexes. Safe: values from validated GraphQL enum.

## Change 2: Request-scoped cache for grouped stats (6 SQL → 2)

- **`internal/graph/stats_cache.go`** (new) — define `statsGroupCache` type (wraps `sync.Map`), context key, `getOrCompute` method using `sync.Once` per cache entry.
- **`cmd/cc-backend/server.go:87-88`** — add `graphQLServer.AroundOperations` middleware to inject empty cache into context per request.
- **`internal/graph/schema.resolvers.go:671-678`** — in `JobsStatistics`, when `groupBy != nil`: check cache for (filter+groupBy) key → on miss, call `JobsStatsGrouped` with nil sortBy/page (all results), cache them → sort+slice in Go per caller's sortBy/page.

## Change 3: Parallelize histogram queries

- **`internal/repository/stats.go:643-668`** — replace 4 sequential histogram calls in `AddHistograms` with `errgroup.WithContext`. Import `golang.org/x/sync/errgroup`. Each goroutine writes distinct struct field.

## Change 4: Frontend uses consolidated results

- **`web/frontend/src/status/dashdetails/UsageDash.svelte`** — replace 6-alias topStatsQuery with 2 aliases (USER, PROJECT) requesting all fields, no sort/page. Add 6 `$derived` vars to sort+slice top-10. Update template refs.

## Verification
```bash
cd web/frontend && npm run build && cd ../.. && go build ./... && go test ./internal/repository/...
```


The exploration found **4 parameterized sites** that filter on a hardcoded `"running"` value where inlining could help SQLite use the partial indexes:

| File | Line | Function | Currently |
|------|------|----------|-----------|
| `stats.go` | 116 | `buildCountQuery` | `"job.job_state = ?", "running"` |
| `jobFind.go` | 280 | `FindConcurrentJobs` | `"job.job_state = ?", "running"` |
| `jobFind.go` | 285 | `FindConcurrentJobs` | `"job.job_state != ?", "running"` |
| `job.go` | 1069 | `GetUsedNodes` | `sq.Eq{"job.job_state": "running"}` |

However, the benefit is uneven:

- **`stats.go:116` (`buildCountQuery`)** — this is the strongest candidate. It's called by `AddJobCount`/`AddJobCountGrouped` on the dashboard. Inlining here lets SQLite use the partial indexes we just created.
- **`job.go:1069` (`GetUsedNodes`)** — also queries running jobs, could benefit.
- **`jobFind.go:280/285` (`FindConcurrentJobs`)** — these query by specific resources/time overlap, so the partial index is less likely to be chosen by the planner (the equality on `job_state` isn't the leading selectivity driver).

The remaining 8 sites already use inlined literals (`'running'` directly in SQL strings), so they're fine.


Write

# Inline literal job_state in remaining parameterized sites

## Files to change

- **`internal/repository/stats.go:116`** (`buildCountQuery`) — change `query.Where("job.job_state = ?", "running")` → `query.Where("job.job_state = 'running'")`. Highest impact: called by dashboard count queries.
- **`internal/repository/job.go:1069`** (`GetUsedNodes`) — change `sq.Eq{"job.job_state": "running"}` → `query.Where("job.job_state = 'running'")`. Benefits node allocation queries.

## Sites intentionally NOT changed

- **`jobFind.go:280,285`** (`FindConcurrentJobs`) — equality/inequality on job_state isn't the leading filter here (time overlap + resources dominate), so partial index unlikely to help. Also uses `!=` which doesn't match `WHERE job_state = 'running'` partial indexes.

## Verification
```bash
go build ./cmd/cc-backend && go test ./internal/repository/...
```


The file /Users/jan/.claude/plans/recursive-giggling-volcano.md has been updated successfully.

# Inline literal job_state in remaining parameterized sites

## Files to change

- **`internal/repository/stats.go:116`** (`buildCountQuery`) — change `query.Where("job.job_state = ?", "running")` → `query.Where("job.job_state = 'running'")`. Highest impact: called by dashboard count queries.
- **`internal/repository/job.go:1069`** (`GetUsedNodes`) — change `sq.Eq{"job.job_state": "running"}` → `query.Where("job.job_state = 'running'")`. Benefits node allocation queries.

## Sites intentionally NOT changed

- **`jobFind.go:280,285`** (`FindConcurrentJobs`) — equality/inequality on job_state isn't the leading filter here (time overlap + resources dominate), so partial index unlikely to help. Also uses `!=` which doesn't match `WHERE job_state = 'running'` partial indexes.

## Verification
```bash
go build ./cmd/cc-backend && go test ./internal/repository/...
```

+# Inline literal job_state in remaining parameterized sites

-## Change 1: Partial indexes for running jobs

-- **New `internal/repository/migrations/sqlite3/14_running-partial-indexes.up.sql`** — partial covering indexes `WHERE job_state = 'running'` for user/project/subcluster groupings. Only running jobs in B-tree (tiny vs millions).

-- **New `internal/repository/migrations/sqlite3/14_running-partial-indexes.down.sql`** — drop them.

-- **`internal/repository/migration.go:30`** — `Version` 13 → 14.

-- **`internal/repository/jobQuery.go:200-202`** — inline literal state value (`fmt.Sprintf("job.job_state = '%s'", singleStat)`) so SQLite matches partial indexes. Safe: values from validated GraphQL enum.

+- **`internal/repository/stats.go:116`** (`buildCountQuery`) — change `query.Where("job.job_state = ?", "running")` → `query.Where("job.job_state = 'running'")`. Highest impact: called by dashboard count queries.

+- **`internal/repository/job.go:1069`** (`GetUsedNodes`) — change `sq.Eq{"job.job_state": "running"}` → `query.Where("job.job_state = 'running'")`. Benefits node allocation queries.

-## Change 2: Request-scoped cache for grouped stats (6 SQL → 2)

-- **`internal/graph/stats_cache.go`** (new) — define `statsGroupCache` type (wraps `sync.Map`), context key, `getOrCompute` method using `sync.Once` per cache entry.

-- **`cmd/cc-backend/server.go:87-88`** — add `graphQLServer.AroundOperations` middleware to inject empty cache into context per request.

-- **`internal/graph/schema.resolvers.go:671-678`** — in `JobsStatistics`, when `groupBy != nil`: check cache for (filter+groupBy) key → on miss, call `JobsStatsGrouped` with nil sortBy/page (all results), cache them → sort+slice in Go per caller's sortBy/page.

+- **`jobFind.go:280,285`** (`FindConcurrentJobs`) — equality/inequality on job_state isn't the leading filter here (time overlap + resources dominate), so partial index unlikely to help. Also uses `!=` which doesn't match `WHERE job_state = 'running'` partial indexes.

-## Change 3: Parallelize histogram queries

-- **`internal/repository/stats.go:643-668`** — replace 4 sequential histogram calls in `AddHistograms` with `errgroup.WithContext`. Import `golang.org/x/sync/errgroup`. Each goroutine writes distinct struct field.

-## Change 4: Frontend uses consolidated results

-- **`web/frontend/src/status/dashdetails/UsageDash.svelte`** — replace 6-alias topStatsQuery with 2 aliases (USER, PROJECT) requesting all fields, no sort/page. Add 6 `$derived` vars to sort+slice top-10. Update template refs.

-cd web/frontend && npm run build && cd ../.. && go build ./... && go test ./internal/repository/...

+go build ./cmd/cc-backend && go test ./internal/repository/...

# Usage Dashboard Query Optimization

## Change 1: Partial indexes for running jobs

- **New `internal/repository/migrations/sqlite3/14_running-partial-indexes.up.sql`** — partial covering indexes `WHERE job_state = 'running'` for user/project/subcluster groupings. Only running jobs in B-tree (tiny vs millions).
- **New `internal/repository/migrations/sqlite3/14_running-partial-indexes.down.sql`** — drop them.
- **`internal/repository/migration.go:30`** — `Version` 13 → 14.
- **`internal/repository/jobQuery.go:200-202`** — inline literal state value (`fmt.Sprintf("job.job_state = '%s'", singleStat)`) so SQLite matches partial indexes. Safe: values from validated GraphQL enum.

## Change 2: Request-scoped cache for grouped stats (6 SQL → 2)

- **`internal/graph/stats_cache.go`** (new) — define `statsGroupCache` type (wraps `sync.Map`), context key, `getOrCompute` method using `sync.Once` per cache entry.
- **`cmd/cc-backend/server.go:87-88`** — add `graphQLServer.AroundOperations` middleware to inject empty cache into context per request.
- **`internal/graph/schema.resolvers.go:671-678`** — in `JobsStatistics`, when `groupBy != nil`: check cache for (filter+groupBy) key → on miss, call `JobsStatsGrouped` with nil sortBy/page (all results), cache them → sort+slice in Go per caller's sortBy/page.

## Change 3: Parallelize histogram queries

- **`internal/repository/stats.go:643-668`** — replace 4 sequential histogram calls in `AddHistograms` with `errgroup.WithContext`. Import `golang.org/x/sync/errgroup`. Each goroutine writes distinct struct field.

## Change 4: Frontend uses consolidated results

- **`web/frontend/src/status/dashdetails/UsageDash.svelte`** — replace 6-alias topStatsQuery with 2 aliases (USER, PROJECT) requesting all fields, no sort/page. Add 6 `$derived` vars to sort+slice top-10. Update template refs.

## Verification
```bash
cd web/frontend && npm run build && cd ../.. && go build ./... && go test ./internal/repository/...
```


ToolSearch

Question: select:ExitPlanMode


Question: select:ExitPlanMode


Two sites worth inlining (`stats.go:116` and `job.go:1069`), one intentionally skipped (`jobFind.go` — `!=` doesn't match partial indexes, and time overlap dominates selectivity there).


ExitPlanMode

# Inline literal job_state in remaining parameterized sites

## Files to change

- **`internal/repository/stats.go:116`** (`buildCountQuery`) — change `query.Where("job.job_state = ?", "running")` → `query.Where("job.job_state = 'running'")`. Highest impact: called by dashboard count queries.
- **`internal/repository/job.go:1069`** (`GetUsedNodes`) — change `sq.Eq{"job.job_state": "running"}` → `query.Where("job.job_state = 'running'")`. Benefits node allocation queries.

## Sites intentionally NOT changed

- **`jobFind.go:280,285`** (`FindConcurrentJobs`) — equality/inequality on job_state isn't the leading filter here (time overlap + resources dominate), so partial index unlikely to help. Also uses `!=` which doesn't match `WHERE job_state = 'running'` partial indexes.

## Verification
```bash
go build ./cmd/cc-backend && go test ./internal/repository/...
```


Error: The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.