2017-01-13 07:08:52 +00:00
|
|
|
// Copyright 2017 clair authors
|
2016-01-19 20:16:45 +00:00
|
|
|
//
|
|
|
|
// 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
|
|
|
|
//
|
|
|
|
// http://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.
|
|
|
|
|
2015-12-28 20:03:29 +00:00
|
|
|
package pgsql
|
|
|
|
|
|
|
|
import (
|
2016-01-12 15:40:46 +00:00
|
|
|
"database/sql"
|
2017-07-26 23:23:54 +00:00
|
|
|
"sort"
|
|
|
|
|
|
|
|
"github.com/lib/pq"
|
|
|
|
log "github.com/sirupsen/logrus"
|
2016-01-12 15:40:46 +00:00
|
|
|
|
2015-12-28 20:03:29 +00:00
|
|
|
"github.com/coreos/clair/database"
|
2016-12-28 01:45:11 +00:00
|
|
|
"github.com/coreos/clair/ext/versionfmt"
|
2017-01-13 07:08:52 +00:00
|
|
|
"github.com/coreos/clair/pkg/commonerr"
|
2015-12-28 20:03:29 +00:00
|
|
|
)
|
|
|
|
|
2018-09-19 19:38:07 +00:00
|
|
|
const (
|
|
|
|
soiNamespacedFeature = `
|
|
|
|
WITH new_feature_ns AS (
|
|
|
|
INSERT INTO namespaced_feature(feature_id, namespace_id)
|
|
|
|
SELECT CAST ($1 AS INTEGER), CAST ($2 AS INTEGER)
|
|
|
|
WHERE NOT EXISTS ( SELECT id FROM namespaced_feature WHERE namespaced_feature.feature_id = $1 AND namespaced_feature.namespace_id = $2)
|
|
|
|
RETURNING id
|
|
|
|
)
|
|
|
|
SELECT id FROM namespaced_feature WHERE namespaced_feature.feature_id = $1 AND namespaced_feature.namespace_id = $2
|
|
|
|
UNION
|
|
|
|
SELECT id FROM new_feature_ns`
|
|
|
|
|
|
|
|
searchPotentialAffectingVulneraibilities = `
|
|
|
|
SELECT nf.id, v.id, vaf.affected_version, vaf.id
|
|
|
|
FROM vulnerability_affected_feature AS vaf, vulnerability AS v,
|
|
|
|
namespaced_feature AS nf, feature AS f
|
|
|
|
WHERE nf.id = ANY($1)
|
|
|
|
AND nf.feature_id = f.id
|
|
|
|
AND nf.namespace_id = v.namespace_id
|
|
|
|
AND vaf.feature_name = f.name
|
|
|
|
AND vaf.vulnerability_id = v.id
|
|
|
|
AND v.deleted_at IS NULL`
|
|
|
|
|
|
|
|
searchNamespacedFeaturesVulnerabilities = `
|
|
|
|
SELECT vanf.namespaced_feature_id, v.name, v.description, v.link,
|
|
|
|
v.severity, v.metadata, vaf.fixedin, n.name, n.version_format
|
|
|
|
FROM vulnerability_affected_namespaced_feature AS vanf,
|
|
|
|
Vulnerability AS v,
|
|
|
|
vulnerability_affected_feature AS vaf,
|
|
|
|
namespace AS n
|
|
|
|
WHERE vanf.namespaced_feature_id = ANY($1)
|
|
|
|
AND vaf.id = vanf.added_by
|
|
|
|
AND v.id = vanf.vulnerability_id
|
|
|
|
AND n.id = v.namespace_id
|
|
|
|
AND v.deleted_at IS NULL`
|
|
|
|
)
|
|
|
|
|
2017-07-26 23:23:54 +00:00
|
|
|
func (tx *pgSession) PersistFeatures(features []database.Feature) error {
|
|
|
|
if len(features) == 0 {
|
|
|
|
return nil
|
2015-12-28 20:03:29 +00:00
|
|
|
}
|
|
|
|
|
2017-07-26 23:23:54 +00:00
|
|
|
// Sorting is needed before inserting into database to prevent deadlock.
|
|
|
|
sort.Slice(features, func(i, j int) bool {
|
|
|
|
return features[i].Name < features[j].Name ||
|
|
|
|
features[i].Version < features[j].Version ||
|
|
|
|
features[i].VersionFormat < features[j].VersionFormat
|
|
|
|
})
|
|
|
|
|
|
|
|
// TODO(Sida): A better interface for bulk insertion is needed.
|
2019-01-02 11:57:45 +00:00
|
|
|
keys := make([]interface{}, 0, len(features)*3)
|
|
|
|
for _, f := range features {
|
|
|
|
keys = append(keys, f.Name, f.Version, f.VersionFormat)
|
2017-07-26 23:23:54 +00:00
|
|
|
if f.Name == "" || f.Version == "" || f.VersionFormat == "" {
|
|
|
|
return commonerr.NewBadRequestError("Empty feature name, version or version format is not allowed")
|
2015-12-28 20:03:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-26 23:23:54 +00:00
|
|
|
_, err := tx.Exec(queryPersistFeature(len(features)), keys...)
|
|
|
|
return handleError("queryPersistFeature", err)
|
|
|
|
}
|
2016-01-24 03:02:34 +00:00
|
|
|
|
2017-07-26 23:23:54 +00:00
|
|
|
type namespacedFeatureWithID struct {
|
|
|
|
database.NamespacedFeature
|
|
|
|
|
|
|
|
ID int64
|
|
|
|
}
|
|
|
|
|
|
|
|
type vulnerabilityCache struct {
|
|
|
|
nsFeatureID int64
|
|
|
|
vulnID int64
|
|
|
|
vulnAffectingID int64
|
|
|
|
}
|
|
|
|
|
|
|
|
func (tx *pgSession) searchAffectingVulnerabilities(features []database.NamespacedFeature) ([]vulnerabilityCache, error) {
|
|
|
|
if len(features) == 0 {
|
|
|
|
return nil, nil
|
2015-12-28 20:03:29 +00:00
|
|
|
}
|
|
|
|
|
2017-07-26 23:23:54 +00:00
|
|
|
ids, err := tx.findNamespacedFeatureIDs(features)
|
2016-01-08 15:27:30 +00:00
|
|
|
if err != nil {
|
2017-07-26 23:23:54 +00:00
|
|
|
return nil, err
|
2016-01-08 15:27:30 +00:00
|
|
|
}
|
2015-12-28 20:03:29 +00:00
|
|
|
|
2017-07-26 23:23:54 +00:00
|
|
|
fMap := map[int64]database.NamespacedFeature{}
|
|
|
|
for i, f := range features {
|
|
|
|
if !ids[i].Valid {
|
2018-10-08 15:11:30 +00:00
|
|
|
return nil, database.ErrMissingEntities
|
2017-07-26 23:23:54 +00:00
|
|
|
}
|
|
|
|
fMap[ids[i].Int64] = f
|
2015-12-28 20:03:29 +00:00
|
|
|
}
|
|
|
|
|
2017-07-26 23:23:54 +00:00
|
|
|
cacheTable := []vulnerabilityCache{}
|
|
|
|
rows, err := tx.Query(searchPotentialAffectingVulneraibilities, pq.Array(ids))
|
2016-12-28 01:45:11 +00:00
|
|
|
if err != nil {
|
2017-07-26 23:23:54 +00:00
|
|
|
return nil, handleError("searchPotentialAffectingVulneraibilities", err)
|
2016-01-08 15:27:30 +00:00
|
|
|
}
|
2015-12-28 20:03:29 +00:00
|
|
|
|
2017-07-26 23:23:54 +00:00
|
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
|
|
var (
|
|
|
|
cache vulnerabilityCache
|
|
|
|
affected string
|
|
|
|
)
|
|
|
|
|
|
|
|
err := rows.Scan(&cache.nsFeatureID, &cache.vulnID, &affected, &cache.vulnAffectingID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if ok, err := versionfmt.InRange(fMap[cache.nsFeatureID].VersionFormat, fMap[cache.nsFeatureID].Version, affected); err != nil {
|
|
|
|
return nil, err
|
|
|
|
} else if ok {
|
|
|
|
cacheTable = append(cacheTable, cache)
|
2015-12-28 20:03:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-26 23:23:54 +00:00
|
|
|
return cacheTable, nil
|
|
|
|
}
|
2016-01-24 03:02:34 +00:00
|
|
|
|
2017-07-26 23:23:54 +00:00
|
|
|
func (tx *pgSession) CacheAffectedNamespacedFeatures(features []database.NamespacedFeature) error {
|
|
|
|
if len(features) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
2016-01-24 03:02:34 +00:00
|
|
|
|
2017-07-26 23:23:54 +00:00
|
|
|
_, err := tx.Exec(lockVulnerabilityAffects)
|
2015-12-28 20:03:29 +00:00
|
|
|
if err != nil {
|
2017-07-26 23:23:54 +00:00
|
|
|
return handleError("lockVulnerabilityAffects", err)
|
2015-12-28 20:03:29 +00:00
|
|
|
}
|
2016-01-24 03:02:34 +00:00
|
|
|
|
2017-07-26 23:23:54 +00:00
|
|
|
cache, err := tx.searchAffectingVulnerabilities(features)
|
2015-12-28 20:03:29 +00:00
|
|
|
|
2019-01-02 11:57:45 +00:00
|
|
|
keys := make([]interface{}, 0, len(cache)*3)
|
|
|
|
for _, c := range cache {
|
|
|
|
keys = append(keys, c.vulnID, c.nsFeatureID, c.vulnAffectingID)
|
2016-03-03 19:15:06 +00:00
|
|
|
}
|
|
|
|
|
2017-07-26 23:23:54 +00:00
|
|
|
if len(cache) == 0 {
|
|
|
|
return nil
|
2016-03-03 19:15:06 +00:00
|
|
|
}
|
|
|
|
|
2017-07-26 23:23:54 +00:00
|
|
|
affected, err := tx.Exec(queryPersistVulnerabilityAffectedNamespacedFeature(len(cache)), keys...)
|
2015-12-28 20:03:29 +00:00
|
|
|
if err != nil {
|
2017-07-26 23:23:54 +00:00
|
|
|
return handleError("persistVulnerabilityAffectedNamespacedFeature", err)
|
|
|
|
}
|
|
|
|
if count, err := affected.RowsAffected(); err != nil {
|
|
|
|
log.Debugf("Cached %d features in vulnerability_affected_namespaced_feature", count)
|
2015-12-28 20:03:29 +00:00
|
|
|
}
|
2017-07-26 23:23:54 +00:00
|
|
|
return nil
|
|
|
|
}
|
2015-12-28 20:03:29 +00:00
|
|
|
|
2017-07-26 23:23:54 +00:00
|
|
|
func (tx *pgSession) PersistNamespacedFeatures(features []database.NamespacedFeature) error {
|
|
|
|
if len(features) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
2016-01-24 03:02:34 +00:00
|
|
|
|
2017-07-26 23:23:54 +00:00
|
|
|
nsIDs := map[database.Namespace]sql.NullInt64{}
|
|
|
|
fIDs := map[database.Feature]sql.NullInt64{}
|
|
|
|
for _, f := range features {
|
|
|
|
nsIDs[f.Namespace] = sql.NullInt64{}
|
|
|
|
fIDs[f.Feature] = sql.NullInt64{}
|
|
|
|
}
|
|
|
|
|
|
|
|
fToFind := []database.Feature{}
|
|
|
|
for f := range fIDs {
|
|
|
|
fToFind = append(fToFind, f)
|
|
|
|
}
|
|
|
|
|
|
|
|
sort.Slice(fToFind, func(i, j int) bool {
|
|
|
|
return fToFind[i].Name < fToFind[j].Name ||
|
|
|
|
fToFind[i].Version < fToFind[j].Version ||
|
|
|
|
fToFind[i].VersionFormat < fToFind[j].VersionFormat
|
|
|
|
})
|
|
|
|
|
|
|
|
if ids, err := tx.findFeatureIDs(fToFind); err == nil {
|
|
|
|
for i, id := range ids {
|
|
|
|
if !id.Valid {
|
2018-10-08 15:11:30 +00:00
|
|
|
return database.ErrMissingEntities
|
2017-07-26 23:23:54 +00:00
|
|
|
}
|
|
|
|
fIDs[fToFind[i]] = id
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
nsToFind := []database.Namespace{}
|
|
|
|
for ns := range nsIDs {
|
|
|
|
nsToFind = append(nsToFind, ns)
|
2016-01-12 15:40:46 +00:00
|
|
|
}
|
|
|
|
|
2017-07-26 23:23:54 +00:00
|
|
|
if ids, err := tx.findNamespaceIDs(nsToFind); err == nil {
|
|
|
|
for i, id := range ids {
|
|
|
|
if !id.Valid {
|
2018-10-08 15:11:30 +00:00
|
|
|
return database.ErrMissingEntities
|
2017-07-26 23:23:54 +00:00
|
|
|
}
|
|
|
|
nsIDs[nsToFind[i]] = id
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return err
|
|
|
|
}
|
2016-01-24 03:02:34 +00:00
|
|
|
|
2019-01-02 11:57:45 +00:00
|
|
|
keys := make([]interface{}, 0, len(features)*2)
|
|
|
|
for _, f := range features {
|
|
|
|
keys = append(keys, fIDs[f.Feature], nsIDs[f.Namespace])
|
2017-07-26 23:23:54 +00:00
|
|
|
}
|
2016-01-24 03:02:34 +00:00
|
|
|
|
2017-07-26 23:23:54 +00:00
|
|
|
_, err := tx.Exec(queryPersistNamespacedFeature(len(features)), keys...)
|
2015-12-28 20:03:29 +00:00
|
|
|
if err != nil {
|
2017-07-26 23:23:54 +00:00
|
|
|
return err
|
2015-12-28 20:03:29 +00:00
|
|
|
}
|
2016-01-24 03:02:34 +00:00
|
|
|
|
2017-07-26 23:23:54 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// FindAffectedNamespacedFeatures looks up cache table and retrieves all
|
|
|
|
// vulnerabilities associated with the features.
|
|
|
|
func (tx *pgSession) FindAffectedNamespacedFeatures(features []database.NamespacedFeature) ([]database.NullableAffectedNamespacedFeature, error) {
|
|
|
|
if len(features) == 0 {
|
|
|
|
return nil, nil
|
|
|
|
}
|
2016-01-28 16:29:29 +00:00
|
|
|
|
2017-07-26 23:23:54 +00:00
|
|
|
returnFeatures := make([]database.NullableAffectedNamespacedFeature, len(features))
|
|
|
|
|
|
|
|
// featureMap is used to keep track of duplicated features.
|
|
|
|
featureMap := map[database.NamespacedFeature][]*database.NullableAffectedNamespacedFeature{}
|
|
|
|
// initialize return value and generate unique feature request queries.
|
|
|
|
for i, f := range features {
|
|
|
|
returnFeatures[i] = database.NullableAffectedNamespacedFeature{
|
|
|
|
AffectedNamespacedFeature: database.AffectedNamespacedFeature{
|
|
|
|
NamespacedFeature: f,
|
|
|
|
},
|
2016-01-28 16:29:29 +00:00
|
|
|
}
|
|
|
|
|
2017-07-26 23:23:54 +00:00
|
|
|
featureMap[f] = append(featureMap[f], &returnFeatures[i])
|
2015-12-28 20:03:29 +00:00
|
|
|
}
|
|
|
|
|
2017-07-26 23:23:54 +00:00
|
|
|
// query unique namespaced features
|
|
|
|
distinctFeatures := []database.NamespacedFeature{}
|
|
|
|
for f := range featureMap {
|
|
|
|
distinctFeatures = append(distinctFeatures, f)
|
2016-01-12 15:40:46 +00:00
|
|
|
}
|
2015-12-28 20:03:29 +00:00
|
|
|
|
2017-07-26 23:23:54 +00:00
|
|
|
nsFeatureIDs, err := tx.findNamespacedFeatureIDs(distinctFeatures)
|
2015-12-28 20:03:29 +00:00
|
|
|
if err != nil {
|
2017-07-26 23:23:54 +00:00
|
|
|
return nil, err
|
2016-01-12 15:40:46 +00:00
|
|
|
}
|
|
|
|
|
2017-07-26 23:23:54 +00:00
|
|
|
toQuery := []int64{}
|
|
|
|
featureIDMap := map[int64][]*database.NullableAffectedNamespacedFeature{}
|
|
|
|
for i, id := range nsFeatureIDs {
|
|
|
|
if id.Valid {
|
|
|
|
toQuery = append(toQuery, id.Int64)
|
|
|
|
for _, f := range featureMap[distinctFeatures[i]] {
|
|
|
|
f.Valid = id.Valid
|
|
|
|
featureIDMap[id.Int64] = append(featureIDMap[id.Int64], f)
|
|
|
|
}
|
|
|
|
}
|
2015-12-28 20:03:29 +00:00
|
|
|
}
|
|
|
|
|
2017-07-26 23:23:54 +00:00
|
|
|
rows, err := tx.Query(searchNamespacedFeaturesVulnerabilities, pq.Array(toQuery))
|
|
|
|
if err != nil {
|
|
|
|
return nil, handleError("searchNamespacedFeaturesVulnerabilities", err)
|
|
|
|
}
|
|
|
|
defer rows.Close()
|
2016-01-12 15:40:46 +00:00
|
|
|
|
2017-07-26 23:23:54 +00:00
|
|
|
for rows.Next() {
|
|
|
|
var (
|
|
|
|
featureID int64
|
|
|
|
vuln database.VulnerabilityWithFixedIn
|
|
|
|
)
|
|
|
|
err := rows.Scan(&featureID,
|
|
|
|
&vuln.Name,
|
|
|
|
&vuln.Description,
|
|
|
|
&vuln.Link,
|
|
|
|
&vuln.Severity,
|
|
|
|
&vuln.Metadata,
|
|
|
|
&vuln.FixedInVersion,
|
|
|
|
&vuln.Namespace.Name,
|
|
|
|
&vuln.Namespace.VersionFormat,
|
|
|
|
)
|
2016-01-12 15:40:46 +00:00
|
|
|
if err != nil {
|
2017-07-26 23:23:54 +00:00
|
|
|
return nil, handleError("searchNamespacedFeaturesVulnerabilities", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, f := range featureIDMap[featureID] {
|
|
|
|
f.AffectedBy = append(f.AffectedBy, vuln)
|
2016-01-12 15:40:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-26 23:23:54 +00:00
|
|
|
return returnFeatures, nil
|
2016-01-12 15:40:46 +00:00
|
|
|
}
|
|
|
|
|
2017-07-26 23:23:54 +00:00
|
|
|
func (tx *pgSession) findNamespacedFeatureIDs(nfs []database.NamespacedFeature) ([]sql.NullInt64, error) {
|
|
|
|
if len(nfs) == 0 {
|
|
|
|
return nil, nil
|
|
|
|
}
|
2016-01-15 20:22:52 +00:00
|
|
|
|
2017-07-26 23:23:54 +00:00
|
|
|
nfsMap := map[database.NamespacedFeature]sql.NullInt64{}
|
2019-01-02 11:57:45 +00:00
|
|
|
keys := make([]interface{}, 0, len(nfs)*4)
|
|
|
|
for _, nf := range nfs {
|
|
|
|
keys = append(keys, nf.Name, nf.Version, nf.VersionFormat, nf.Namespace.Name)
|
2017-07-26 23:23:54 +00:00
|
|
|
nfsMap[nf] = sql.NullInt64{}
|
|
|
|
}
|
|
|
|
|
|
|
|
rows, err := tx.Query(querySearchNamespacedFeature(len(nfs)), keys...)
|
2015-12-28 20:03:29 +00:00
|
|
|
if err != nil {
|
2017-07-26 23:23:54 +00:00
|
|
|
return nil, handleError("searchNamespacedFeature", err)
|
2015-12-28 20:03:29 +00:00
|
|
|
}
|
2017-07-26 23:23:54 +00:00
|
|
|
|
2015-12-28 20:03:29 +00:00
|
|
|
defer rows.Close()
|
2017-07-26 23:23:54 +00:00
|
|
|
var (
|
|
|
|
id sql.NullInt64
|
|
|
|
nf database.NamespacedFeature
|
|
|
|
)
|
2015-12-28 20:03:29 +00:00
|
|
|
|
|
|
|
for rows.Next() {
|
2017-07-26 23:23:54 +00:00
|
|
|
err := rows.Scan(&id, &nf.Name, &nf.Version, &nf.VersionFormat, &nf.Namespace.Name)
|
|
|
|
nf.Namespace.VersionFormat = nf.VersionFormat
|
2015-12-28 20:03:29 +00:00
|
|
|
if err != nil {
|
2017-07-26 23:23:54 +00:00
|
|
|
return nil, handleError("searchNamespacedFeature", err)
|
2015-12-28 20:03:29 +00:00
|
|
|
}
|
2017-07-26 23:23:54 +00:00
|
|
|
nfsMap[nf] = id
|
|
|
|
}
|
2015-12-28 20:03:29 +00:00
|
|
|
|
2017-07-26 23:23:54 +00:00
|
|
|
ids := make([]sql.NullInt64, len(nfs))
|
|
|
|
for i, nf := range nfs {
|
|
|
|
ids[i] = nfsMap[nf]
|
2015-12-28 20:03:29 +00:00
|
|
|
}
|
2017-07-26 23:23:54 +00:00
|
|
|
|
|
|
|
return ids, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (tx *pgSession) findFeatureIDs(fs []database.Feature) ([]sql.NullInt64, error) {
|
|
|
|
if len(fs) == 0 {
|
|
|
|
return nil, nil
|
2015-12-28 20:03:29 +00:00
|
|
|
}
|
2016-01-15 20:22:52 +00:00
|
|
|
|
2017-07-26 23:23:54 +00:00
|
|
|
fMap := map[database.Feature]sql.NullInt64{}
|
|
|
|
|
2019-01-02 11:57:45 +00:00
|
|
|
keys := make([]interface{}, 0, len(fs)*3)
|
|
|
|
for _, f := range fs {
|
|
|
|
keys = append(keys, f.Name, f.Version, f.VersionFormat)
|
2017-07-26 23:23:54 +00:00
|
|
|
fMap[f] = sql.NullInt64{}
|
|
|
|
}
|
|
|
|
|
|
|
|
rows, err := tx.Query(querySearchFeatureID(len(fs)), keys...)
|
|
|
|
if err != nil {
|
|
|
|
return nil, handleError("querySearchFeatureID", err)
|
|
|
|
}
|
|
|
|
defer rows.Close()
|
|
|
|
|
|
|
|
var (
|
|
|
|
id sql.NullInt64
|
|
|
|
f database.Feature
|
|
|
|
)
|
|
|
|
for rows.Next() {
|
|
|
|
err := rows.Scan(&id, &f.Name, &f.Version, &f.VersionFormat)
|
2016-01-15 20:22:52 +00:00
|
|
|
if err != nil {
|
2017-07-26 23:23:54 +00:00
|
|
|
return nil, handleError("querySearchFeatureID", err)
|
2016-01-15 20:22:52 +00:00
|
|
|
}
|
2017-07-26 23:23:54 +00:00
|
|
|
fMap[f] = id
|
2016-01-15 20:22:52 +00:00
|
|
|
}
|
2015-12-28 20:03:29 +00:00
|
|
|
|
2017-07-26 23:23:54 +00:00
|
|
|
ids := make([]sql.NullInt64, len(fs))
|
|
|
|
for i, f := range fs {
|
|
|
|
ids[i] = fMap[f]
|
|
|
|
}
|
|
|
|
|
|
|
|
return ids, nil
|
2015-12-28 20:03:29 +00:00
|
|
|
}
|