github.com/pf-qiu/concourse/v6@v6.7.3-0.20201207032516-1f455d73275f/atc/db/versions_db.go (about)

     1  package db
     2  
     3  import (
     4  	"context"
     5  	"database/sql"
     6  	"encoding/json"
     7  	"fmt"
     8  	"sort"
     9  	"strconv"
    10  	"time"
    11  
    12  	sq "github.com/Masterminds/squirrel"
    13  	"github.com/pf-qiu/concourse/v6/atc"
    14  	"github.com/pf-qiu/concourse/v6/tracing"
    15  	gocache "github.com/patrickmn/go-cache"
    16  	"go.opentelemetry.io/otel/api/trace"
    17  	"go.opentelemetry.io/otel/label"
    18  )
    19  
    20  type VersionsDB struct {
    21  	conn      Conn
    22  	limitRows int
    23  
    24  	cache *gocache.Cache
    25  }
    26  
    27  func NewVersionsDB(conn Conn, limitRows int, cache *gocache.Cache) VersionsDB {
    28  	return VersionsDB{
    29  		conn:      conn,
    30  		limitRows: limitRows,
    31  		cache:     cache,
    32  	}
    33  }
    34  
    35  func (versions VersionsDB) IsFirstOccurrence(ctx context.Context, jobID int, inputName string, versionMD5 ResourceVersion, resourceId int) (bool, error) {
    36  	var exists bool
    37  	err := versions.conn.QueryRowContext(ctx, `
    38  		WITH builds_of_job AS (
    39  			SELECT id FROM builds WHERE job_id = $1
    40  		)
    41  		SELECT EXISTS (
    42  			SELECT 1
    43  			FROM build_resource_config_version_inputs i
    44  			JOIN builds_of_job b ON b.id = i.build_id
    45  			WHERE i.name = $2
    46  			AND i.version_md5 = $3
    47  			AND i.resource_id = $4
    48  		)`, jobID, inputName, versionMD5, resourceId).
    49  		Scan(&exists)
    50  	if err != nil {
    51  		return false, err
    52  	}
    53  
    54  	return !exists, nil
    55  }
    56  
    57  func (versions VersionsDB) VersionIsDisabled(ctx context.Context, resourceID int, versionMD5 ResourceVersion) (bool, error) {
    58  	var exists bool
    59  	err := versions.conn.QueryRow(`
    60  		SELECT EXISTS (
    61  			SELECT 1
    62  			FROM resource_disabled_versions
    63  			WHERE resource_id = $1
    64  			AND version_md5 = $2
    65  		)`, resourceID, versionMD5).
    66  		Scan(&exists)
    67  	if err != nil {
    68  		return false, err
    69  	}
    70  
    71  	return exists, nil
    72  }
    73  
    74  func (versions VersionsDB) LatestVersionOfResource(ctx context.Context, resourceID int) (ResourceVersion, bool, error) {
    75  	tx, err := versions.conn.Begin()
    76  	if err != nil {
    77  		return "", false, err
    78  	}
    79  
    80  	defer tx.Rollback()
    81  
    82  	version, found, err := versions.latestVersionOfResource(ctx, tx, resourceID)
    83  	if err != nil {
    84  		return "", false, err
    85  	}
    86  
    87  	if !found {
    88  		return "", false, nil
    89  	}
    90  
    91  	err = tx.Commit()
    92  	if err != nil {
    93  		return "", false, err
    94  	}
    95  
    96  	return version, true, nil
    97  }
    98  
    99  func (versions VersionsDB) SuccessfulBuilds(ctx context.Context, jobID int) PaginatedBuilds {
   100  	builder := psql.Select("id", "rerun_of").
   101  		From("builds").
   102  		Where(sq.Eq{
   103  			"job_id": jobID,
   104  			"status": "succeeded",
   105  		}).
   106  		OrderBy("COALESCE(rerun_of, id) DESC, id DESC")
   107  
   108  	return PaginatedBuilds{
   109  		builder: builder,
   110  		column:  "id",
   111  		jobID:   jobID,
   112  
   113  		limitRows: versions.limitRows,
   114  		conn:      versions.conn,
   115  	}
   116  }
   117  
   118  func (versions VersionsDB) SuccessfulBuildsVersionConstrained(
   119  	ctx context.Context,
   120  	jobID int,
   121  	constrainingCandidates map[string][]string,
   122  ) (PaginatedBuilds, error) {
   123  	versionsJSON, err := json.Marshal(constrainingCandidates)
   124  	if err != nil {
   125  		return PaginatedBuilds{}, err
   126  	}
   127  
   128  	builder := psql.Select("build_id", "rerun_of").
   129  		From("successful_build_outputs").
   130  		Where(sq.Expr("outputs @> ?::jsonb", versionsJSON)).
   131  		Where(sq.Eq{
   132  			"job_id": jobID,
   133  		}).
   134  		OrderBy("COALESCE(rerun_of, build_id) DESC, build_id DESC")
   135  
   136  	return PaginatedBuilds{
   137  		builder: builder,
   138  		column:  "build_id",
   139  		jobID:   jobID,
   140  
   141  		limitRows: versions.limitRows,
   142  		conn:      versions.conn,
   143  	}, nil
   144  }
   145  
   146  type resourceOutputs struct {
   147  	ResourceID int
   148  	Versions   []string
   149  }
   150  
   151  func (versions VersionsDB) SuccessfulBuildOutputs(ctx context.Context, buildID int) ([]AlgorithmVersion, error) {
   152  	cacheKey := fmt.Sprintf("o%d", buildID)
   153  
   154  	c, found := versions.cache.Get(cacheKey)
   155  	if found {
   156  		return c.([]AlgorithmVersion), nil
   157  	}
   158  
   159  	var outputsJSON string
   160  	err := psql.Select("outputs").
   161  		From("successful_build_outputs").
   162  		Where(sq.Eq{"build_id": buildID}).
   163  		RunWith(versions.conn).
   164  		QueryRowContext(ctx).
   165  		Scan(&outputsJSON)
   166  	if err != nil {
   167  		if err == sql.ErrNoRows {
   168  			outputsJSON, err = versions.migrateSingle(ctx, buildID)
   169  			if err != nil {
   170  				return nil, err
   171  			}
   172  		} else {
   173  			return nil, err
   174  		}
   175  	}
   176  
   177  	outputs := map[string][]string{}
   178  	err = json.Unmarshal([]byte(outputsJSON), &outputs)
   179  	if err != nil {
   180  		return nil, err
   181  	}
   182  
   183  	byResourceID := []resourceOutputs{}
   184  	for resourceIDStr, versions := range outputs {
   185  		resourceID, err := strconv.Atoi(resourceIDStr)
   186  		if err != nil {
   187  			return nil, err
   188  		}
   189  
   190  		byResourceID = append(byResourceID, resourceOutputs{
   191  			ResourceID: resourceID,
   192  			Versions:   versions,
   193  		})
   194  	}
   195  
   196  	sort.Slice(byResourceID, func(i, j int) bool {
   197  		return byResourceID[i].ResourceID < byResourceID[j].ResourceID
   198  	})
   199  
   200  	algorithmOutputs := []AlgorithmVersion{}
   201  	for _, outputs := range byResourceID {
   202  		for _, version := range outputs.Versions {
   203  			algorithmOutputs = append(algorithmOutputs, AlgorithmVersion{
   204  				ResourceID: outputs.ResourceID,
   205  				Version:    ResourceVersion(version),
   206  			})
   207  		}
   208  	}
   209  
   210  	versions.cache.Set(cacheKey, algorithmOutputs, time.Hour)
   211  
   212  	return algorithmOutputs, nil
   213  }
   214  
   215  func (versions VersionsDB) VersionExists(ctx context.Context, resourceID int, versionMD5 ResourceVersion) (bool, error) {
   216  	var exists bool
   217  	err := versions.conn.QueryRowContext(ctx, `
   218  		SELECT EXISTS (
   219  			SELECT 1
   220  			FROM resource_config_versions v
   221  			JOIN resources r ON r.resource_config_scope_id = v.resource_config_scope_id
   222  			WHERE r.id = $1
   223  			AND v.version_md5 = $2
   224  		)`, resourceID, versionMD5).
   225  		Scan(&exists)
   226  	if err != nil {
   227  		return false, err
   228  	}
   229  
   230  	return exists, nil
   231  }
   232  
   233  func (versions VersionsDB) FindVersionOfResource(ctx context.Context, resourceID int, v atc.Version) (ResourceVersion, bool, error) {
   234  	versionJSON, err := json.Marshal(v)
   235  	if err != nil {
   236  		return "", false, nil
   237  	}
   238  
   239  	cacheKey := fmt.Sprintf("v%d-%s", resourceID, versionJSON)
   240  
   241  	c, found := versions.cache.Get(cacheKey)
   242  	if found {
   243  		return c.(ResourceVersion), true, nil
   244  	}
   245  
   246  	var version ResourceVersion
   247  	err = psql.Select("rcv.version_md5").
   248  		From("resource_config_versions rcv").
   249  		Join("resources r ON r.resource_config_scope_id = rcv.resource_config_scope_id").
   250  		Where(sq.Eq{
   251  			"r.id": resourceID,
   252  		}).
   253  		Where(sq.Expr("rcv.version @> ?", versionJSON)).
   254  		RunWith(versions.conn).
   255  		QueryRowContext(ctx).
   256  		Scan(&version)
   257  	if err != nil {
   258  		if err == sql.ErrNoRows {
   259  			return "", false, nil
   260  		}
   261  		return "", false, err
   262  	}
   263  
   264  	versions.cache.Set(cacheKey, version, time.Hour)
   265  
   266  	return version, true, err
   267  }
   268  
   269  func (versions VersionsDB) NextEveryVersion(ctx context.Context, jobID int, resourceID int) (ResourceVersion, bool, bool, error) {
   270  	tx, err := versions.conn.Begin()
   271  	if err != nil {
   272  		return "", false, false, err
   273  	}
   274  
   275  	defer tx.Rollback()
   276  
   277  	var checkOrder int
   278  	err = tx.QueryRowContext(ctx, `
   279  		SELECT rcv.check_order
   280  		FROM resource_config_versions rcv
   281  		CROSS JOIN LATERAL (
   282  			SELECT i.build_id
   283  			FROM build_resource_config_version_inputs i
   284  			CROSS JOIN LATERAL (
   285  				SELECT b.id
   286  				FROM builds b
   287  				WHERE b.job_id = $1
   288  				AND i.build_id = b.id
   289  				LIMIT 1
   290  			) AS build
   291  			WHERE i.resource_id = $2
   292  			AND i.version_md5 = rcv.version_md5
   293  			LIMIT 1
   294  		) AS inputs
   295  		WHERE rcv.resource_config_scope_id = (SELECT resource_config_scope_id FROM resources WHERE id = $2)
   296  		ORDER BY rcv.check_order DESC
   297  		LIMIT 1;`, jobID, resourceID).Scan(&checkOrder)
   298  	if err != nil {
   299  		if err == sql.ErrNoRows {
   300  			version, found, err := versions.latestVersionOfResource(ctx, tx, resourceID)
   301  			if err != nil {
   302  				return "", false, false, err
   303  			}
   304  
   305  			if !found {
   306  				return "", false, false, nil
   307  			}
   308  
   309  			err = tx.Commit()
   310  			if err != nil {
   311  				return "", false, false, err
   312  			}
   313  
   314  			return version, false, true, nil
   315  		}
   316  
   317  		return "", false, false, err
   318  	}
   319  
   320  	var nextVersion ResourceVersion
   321  	rows, err := psql.Select("rcv.version_md5").
   322  		From("resource_config_versions rcv").
   323  		Where(sq.Expr("rcv.resource_config_scope_id = (SELECT resource_config_scope_id FROM resources WHERE id = ?)", resourceID)).
   324  		Where(sq.Expr("NOT EXISTS (SELECT 1 FROM resource_disabled_versions WHERE resource_id = ? AND version_md5 = rcv.version_md5)", resourceID)).
   325  		Where(sq.Gt{"rcv.check_order": checkOrder}).
   326  		OrderBy("rcv.check_order ASC").
   327  		Limit(2).
   328  		RunWith(tx).
   329  		QueryContext(ctx)
   330  	if err != nil {
   331  		return "", false, false, err
   332  	}
   333  
   334  	if rows.Next() {
   335  		err = rows.Scan(&nextVersion)
   336  		if err != nil {
   337  			return "", false, false, err
   338  		}
   339  
   340  		var hasNext bool
   341  		if rows.Next() {
   342  			hasNext = true
   343  		}
   344  
   345  		rows.Close()
   346  
   347  		err = tx.Commit()
   348  		if err != nil {
   349  			return "", false, false, err
   350  		}
   351  
   352  		return nextVersion, hasNext, true, nil
   353  	}
   354  
   355  	err = psql.Select("rcv.version_md5").
   356  		From("resource_config_versions rcv").
   357  		Where(sq.Expr("rcv.resource_config_scope_id = (SELECT resource_config_scope_id FROM resources WHERE id = ?)", resourceID)).
   358  		Where(sq.Expr("NOT EXISTS (SELECT 1 FROM resource_disabled_versions WHERE resource_id = ? AND version_md5 = rcv.version_md5)", resourceID)).
   359  		Where(sq.LtOrEq{"rcv.check_order": checkOrder}).
   360  		OrderBy("rcv.check_order DESC").
   361  		Limit(1).
   362  		RunWith(tx).
   363  		QueryRowContext(ctx).
   364  		Scan(&nextVersion)
   365  	if err != nil {
   366  		if err == sql.ErrNoRows {
   367  			return "", false, false, nil
   368  		}
   369  		return "", false, false, err
   370  	}
   371  
   372  	err = tx.Commit()
   373  	if err != nil {
   374  		return "", false, false, err
   375  	}
   376  
   377  	return nextVersion, false, true, nil
   378  }
   379  
   380  func (versions VersionsDB) LatestBuildPipes(ctx context.Context, buildID int) (map[int]BuildCursor, error) {
   381  	rows, err := psql.Select("p.from_build_id", "b.rerun_of", "b.job_id").
   382  		From("build_pipes p").
   383  		Join("builds b ON b.id = p.from_build_id").
   384  		Where(sq.Eq{
   385  			"p.to_build_id": buildID,
   386  		}).
   387  		RunWith(versions.conn).
   388  		QueryContext(ctx)
   389  	if err != nil {
   390  		return nil, err
   391  	}
   392  
   393  	jobToBuildPipes := map[int]BuildCursor{}
   394  	for rows.Next() {
   395  		var build BuildCursor
   396  		var jobID int
   397  
   398  		err = rows.Scan(&build.ID, &build.RerunOf, &jobID)
   399  		if err != nil {
   400  			return nil, err
   401  		}
   402  
   403  		jobToBuildPipes[jobID] = build
   404  	}
   405  
   406  	return jobToBuildPipes, nil
   407  }
   408  
   409  func (versions VersionsDB) LatestBuildUsingLatestVersion(ctx context.Context, jobID int, resourceID int) (int, bool, error) {
   410  	var buildID int
   411  	err := versions.conn.QueryRowContext(ctx, `
   412  		SELECT inputs.build_id
   413  		FROM resource_config_versions rcv
   414  		CROSS JOIN LATERAL (
   415  			SELECT i.build_id
   416  			FROM build_resource_config_version_inputs i
   417  			CROSS JOIN LATERAL (
   418  				SELECT b.id
   419  				FROM builds b
   420  				WHERE b.job_id = $1
   421  				AND i.build_id = b.id
   422  				ORDER BY b.id DESC
   423  				LIMIT 1
   424  			) AS build
   425  			WHERE i.resource_id = $2
   426  			AND i.version_md5 = rcv.version_md5
   427  			LIMIT 1
   428  		) AS inputs
   429  		WHERE rcv.resource_config_scope_id = (SELECT resource_config_scope_id FROM resources WHERE id = $2)
   430  		ORDER BY rcv.check_order DESC
   431  		LIMIT 1`, jobID, resourceID).Scan(&buildID)
   432  	if err != nil {
   433  		if err == sql.ErrNoRows {
   434  			return 0, false, nil
   435  		}
   436  		return 0, false, err
   437  	}
   438  
   439  	return buildID, true, nil
   440  }
   441  
   442  func (versions VersionsDB) UnusedBuilds(ctx context.Context, jobID int, lastUsedBuild BuildCursor) (PaginatedBuilds, error) {
   443  	builds, err := versions.newerBuilds(ctx, jobID, lastUsedBuild)
   444  	if err != nil {
   445  		return PaginatedBuilds{}, err
   446  	}
   447  
   448  	builder := psql.Select("id", "rerun_of").
   449  		From("builds").
   450  		Where(sq.And{
   451  			sq.Eq{
   452  				"job_id": jobID,
   453  				"status": "succeeded",
   454  			},
   455  			sq.Or{
   456  				sq.Eq{"id": lastUsedBuild.ID},
   457  				lastUsedBuild.OlderBuilds("id"),
   458  			},
   459  		}).
   460  		OrderBy("COALESCE(rerun_of, id) DESC, id DESC")
   461  
   462  	return PaginatedBuilds{
   463  		builder:      builder,
   464  		builds:       builds,
   465  		unusedBuilds: true,
   466  
   467  		column: "id",
   468  		jobID:  jobID,
   469  
   470  		limitRows: versions.limitRows,
   471  		conn:      versions.conn,
   472  	}, nil
   473  }
   474  
   475  func (versions VersionsDB) UnusedBuildsVersionConstrained(ctx context.Context, jobID int, lastUsedBuild BuildCursor, constrainingCandidates map[string][]string) (PaginatedBuilds, error) {
   476  	builds, err := versions.newerBuilds(ctx, jobID, lastUsedBuild)
   477  	if err != nil {
   478  		return PaginatedBuilds{}, err
   479  	}
   480  
   481  	versionsJSON, err := json.Marshal(constrainingCandidates)
   482  	if err != nil {
   483  		return PaginatedBuilds{}, err
   484  	}
   485  
   486  	builder := psql.Select("build_id", "rerun_of").
   487  		From("successful_build_outputs").
   488  		Where(sq.Expr("outputs @> ?::jsonb", versionsJSON)).
   489  		Where(sq.Eq{
   490  			"job_id": jobID,
   491  		}).
   492  		Where(sq.Or{
   493  			sq.Eq{"build_id": lastUsedBuild.ID},
   494  			lastUsedBuild.OlderBuilds("build_id"),
   495  		}).
   496  		OrderBy("COALESCE(rerun_of, build_id) DESC, build_id DESC")
   497  
   498  	return PaginatedBuilds{
   499  		builder:      builder,
   500  		builds:       builds,
   501  		unusedBuilds: true,
   502  
   503  		column: "build_id",
   504  		jobID:  jobID,
   505  
   506  		limitRows: versions.limitRows,
   507  		conn:      versions.conn,
   508  	}, nil
   509  
   510  }
   511  
   512  func (versions VersionsDB) newerBuilds(ctx context.Context, jobID int, lastUsedBuild BuildCursor) ([]BuildCursor, error) {
   513  	rows, err := psql.Select("id", "rerun_of").
   514  		From("builds").
   515  		Where(sq.And{
   516  			sq.Eq{
   517  				"job_id": jobID,
   518  				"status": "succeeded",
   519  			},
   520  			lastUsedBuild.NewerBuilds("id"),
   521  		}).
   522  		OrderBy("COALESCE(rerun_of, id) ASC, id ASC").
   523  		RunWith(versions.conn).
   524  		QueryContext(ctx)
   525  	if err != nil {
   526  		return nil, err
   527  	}
   528  
   529  	var builds []BuildCursor
   530  	for rows.Next() {
   531  		var build BuildCursor
   532  		err = rows.Scan(&build.ID, &build.RerunOf)
   533  		if err != nil {
   534  			return nil, err
   535  		}
   536  
   537  		builds = append(builds, build)
   538  	}
   539  
   540  	return builds, nil
   541  }
   542  
   543  func (versions VersionsDB) latestVersionOfResource(ctx context.Context, tx Tx, resourceID int) (ResourceVersion, bool, error) {
   544  	var scopeID sql.NullInt64
   545  	err := psql.Select("resource_config_scope_id").
   546  		From("resources").
   547  		Where(sq.Eq{"id": resourceID}).
   548  		RunWith(tx).
   549  		QueryRowContext(ctx).
   550  		Scan(&scopeID)
   551  	if err != nil {
   552  		if err == sql.ErrNoRows {
   553  			return "", false, nil
   554  		}
   555  		return "", false, err
   556  	}
   557  
   558  	if !scopeID.Valid {
   559  		return "", false, nil
   560  	}
   561  
   562  	var version ResourceVersion
   563  	err = psql.Select("version_md5").
   564  		From("resource_config_versions").
   565  		Where(sq.Eq{"resource_config_scope_id": scopeID}).
   566  		Where(sq.Expr("version_md5 NOT IN (SELECT version_md5 FROM resource_disabled_versions WHERE resource_id = ?)", resourceID)).
   567  		OrderBy("check_order DESC").
   568  		Limit(1).
   569  		RunWith(tx).
   570  		QueryRowContext(ctx).
   571  		Scan(&version)
   572  	if err != nil {
   573  		if err == sql.ErrNoRows {
   574  			return "", false, nil
   575  		}
   576  		return "", false, err
   577  	}
   578  
   579  	return version, true, nil
   580  }
   581  
   582  func (versions VersionsDB) migrateSingle(ctx context.Context, buildID int) (string, error) {
   583  	ctx, span := tracing.StartSpan(ctx, "VersionsDB.migrateSingle", tracing.Attrs{})
   584  	defer span.End()
   585  
   586  	span.SetAttributes(label.Int("buildID", buildID))
   587  
   588  	var outputs string
   589  	err := versions.conn.QueryRowContext(ctx, `
   590  		WITH builds_to_migrate AS (
   591  			UPDATE builds
   592  			SET needs_v6_migration = false
   593  			WHERE id = $1
   594  		)
   595  			INSERT INTO successful_build_outputs (
   596  				SELECT b.id, b.job_id, json_object_agg(sp.resource_id, sp.v), b.rerun_of
   597  				FROM builds b
   598  				JOIN (
   599  					SELECT build_id, resource_id, json_agg(version_md5) AS v
   600  					FROM (
   601  						(
   602  							SELECT build_id, resource_id, version_md5
   603  							FROM build_resource_config_version_outputs o
   604  							WHERE o.build_id = $1
   605  						)
   606  						UNION ALL
   607  						(
   608  							SELECT build_id, resource_id, version_md5
   609  							FROM build_resource_config_version_inputs i
   610  							WHERE i.build_id = $1
   611  						)
   612  				) AS agg GROUP BY build_id, resource_id) sp ON sp.build_id = b.id
   613  				WHERE b.id = $1
   614  				GROUP BY b.id, b.job_id, b.rerun_of
   615  			)
   616  			ON CONFLICT (build_id) DO UPDATE SET outputs = EXCLUDED.outputs
   617  			RETURNING outputs
   618  		`, buildID).
   619  		Scan(&outputs)
   620  	if err != nil {
   621  		tracing.End(span, err)
   622  		return "", err
   623  	}
   624  
   625  	span.AddEvent(ctx, "build migrated")
   626  
   627  	return outputs, nil
   628  }
   629  
   630  type BuildCursor struct {
   631  	ID      int
   632  	RerunOf sql.NullInt64
   633  }
   634  
   635  func (cursor BuildCursor) OlderBuilds(idCol string) sq.Sqlizer {
   636  	if cursor.RerunOf.Valid {
   637  		return sq.Or{
   638  			sq.Expr("COALESCE(rerun_of, "+idCol+") < ?", cursor.RerunOf.Int64),
   639  
   640  			// include original build of the rerun
   641  			sq.Eq{idCol: cursor.RerunOf.Int64},
   642  
   643  			// include earlier reruns of the same build
   644  			sq.And{
   645  				sq.Eq{"rerun_of": cursor.RerunOf.Int64},
   646  				sq.Lt{idCol: cursor.ID},
   647  			},
   648  		}
   649  	} else {
   650  		return sq.Expr("COALESCE(rerun_of, "+idCol+") < ?", cursor.ID)
   651  	}
   652  }
   653  
   654  func (cursor BuildCursor) NewerBuilds(idCol string) sq.Sqlizer {
   655  	if cursor.RerunOf.Valid {
   656  		return sq.Or{
   657  			sq.Expr("COALESCE(rerun_of, "+idCol+") > ?", cursor.RerunOf.Int64),
   658  			sq.And{
   659  				sq.Eq{"rerun_of": cursor.RerunOf.Int64},
   660  				sq.Gt{idCol: cursor.ID},
   661  			},
   662  		}
   663  	} else {
   664  		return sq.Or{
   665  			sq.Expr("COALESCE(rerun_of, "+idCol+") > ?", cursor.ID),
   666  
   667  			// include reruns of the build
   668  			sq.Eq{"rerun_of": cursor.ID},
   669  		}
   670  	}
   671  }
   672  
   673  type PaginatedBuilds struct {
   674  	builder sq.SelectBuilder
   675  	column  string
   676  
   677  	unusedBuilds bool
   678  	builds       []BuildCursor
   679  	offset       int
   680  
   681  	jobID int
   682  
   683  	limitRows int
   684  	conn      Conn
   685  }
   686  
   687  func (bs *PaginatedBuilds) Next(ctx context.Context) (int, bool, error) {
   688  	if bs.offset+1 > len(bs.builds) {
   689  		for {
   690  			builder := bs.builder
   691  
   692  			if len(bs.builds) > 0 {
   693  				pageBoundary := bs.builds[len(bs.builds)-1]
   694  				builder = builder.Where(pageBoundary.OlderBuilds(bs.column))
   695  			}
   696  
   697  			rows, err := builder.
   698  				Limit(uint64(bs.limitRows)).
   699  				RunWith(bs.conn).
   700  				QueryContext(ctx)
   701  			if err != nil {
   702  				return 0, false, err
   703  			}
   704  
   705  			builds := []BuildCursor{}
   706  			for rows.Next() {
   707  				var build BuildCursor
   708  				err = rows.Scan(&build.ID, &build.RerunOf)
   709  				if err != nil {
   710  					return 0, false, err
   711  				}
   712  
   713  				builds = append(builds, build)
   714  			}
   715  
   716  			if len(builds) == 0 {
   717  				migrated, err := bs.migrateLimit(ctx)
   718  				if err != nil {
   719  					return 0, false, err
   720  				}
   721  
   722  				if !migrated {
   723  					return 0, false, nil
   724  				}
   725  			} else {
   726  				bs.builds = builds
   727  				bs.offset = 0
   728  				bs.unusedBuilds = false
   729  				break
   730  			}
   731  		}
   732  	}
   733  
   734  	build := bs.builds[bs.offset]
   735  	bs.offset++
   736  
   737  	return build.ID, true, nil
   738  }
   739  
   740  func (bs *PaginatedBuilds) HasNext() bool {
   741  	return bs.unusedBuilds && len(bs.builds)-bs.offset+1 > 0
   742  }
   743  
   744  func (bs *PaginatedBuilds) migrateLimit(ctx context.Context) (bool, error) {
   745  	ctx, span := tracing.StartSpan(ctx, "PaginatedBuilds.migrateLimit", tracing.Attrs{})
   746  	defer span.End()
   747  
   748  	span.SetAttributes(label.Int("jobID", bs.jobID))
   749  
   750  	buildsToMigrateQueryBuilder := psql.Select("id", "job_id", "rerun_of").
   751  		From("builds").
   752  		Where(sq.Eq{
   753  			"job_id":             bs.jobID,
   754  			"needs_v6_migration": true,
   755  			"status":             "succeeded",
   756  		}).
   757  		OrderBy("COALESCE(rerun_of, id) DESC, id DESC").
   758  		Limit(uint64(bs.limitRows))
   759  
   760  	buildsToMigrateQuery, params, err := buildsToMigrateQueryBuilder.ToSql()
   761  	if err != nil {
   762  		tracing.End(span, err)
   763  		return false, err
   764  	}
   765  
   766  	results, err := bs.conn.ExecContext(ctx, `
   767  		WITH builds_to_migrate AS (`+buildsToMigrateQuery+`), migrated_outputs AS (
   768  			INSERT INTO successful_build_outputs (
   769  				SELECT bm.id, bm.job_id, json_object_agg(sp.resource_id, sp.v), bm.rerun_of
   770  				FROM builds_to_migrate bm
   771  				JOIN (
   772  					SELECT build_id, resource_id, json_agg(version_md5) AS v
   773  					FROM (
   774  						(
   775  							SELECT build_id, resource_id, version_md5
   776  							FROM build_resource_config_version_outputs o
   777  							JOIN builds_to_migrate bm ON bm.id = o.build_id
   778  						)
   779  						UNION ALL
   780  						(
   781  							SELECT build_id, resource_id, version_md5
   782  							FROM build_resource_config_version_inputs i
   783  							JOIN builds_to_migrate bm ON bm.id = i.build_id
   784  						)
   785  				) AS agg GROUP BY build_id, resource_id) sp ON sp.build_id = bm.id
   786  				GROUP BY bm.id, bm.job_id, bm.rerun_of
   787  			) ON CONFLICT (build_id) DO NOTHING
   788  		)
   789  		UPDATE builds
   790  		SET needs_v6_migration = false
   791  		WHERE id IN (SELECT id FROM builds_to_migrate)
   792  	`, params...)
   793  	if err != nil {
   794  		tracing.End(span, err)
   795  		return false, err
   796  	}
   797  
   798  	rowsAffected, err := results.RowsAffected()
   799  	if err != nil {
   800  		tracing.End(span, err)
   801  		return false, err
   802  	}
   803  
   804  	trace.SpanFromContext(ctx).AddEvent(
   805  		ctx,
   806  		"builds migrated",
   807  		label.Int64("rows", rowsAffected),
   808  	)
   809  
   810  	if rowsAffected == 0 {
   811  		return false, nil
   812  	}
   813  
   814  	return true, nil
   815  }