From a303a828105f384c6ffcf823aa05450c05386dc3 Mon Sep 17 00:00:00 2001 From: Stefan Pletka Date: Fri, 9 Aug 2024 15:20:17 +0200 Subject: [PATCH 1/5] Implemented the archived dashboards data access --- backend/pkg/api/data_access/user.go | 115 +++++++++++++----- backend/pkg/api/data_access/vdb_management.go | 112 ++++++++++++++++- .../api/enums/validator_dashboard_enums.go | 60 +++++++++ backend/pkg/api/handlers/internal.go | 1 + backend/pkg/api/types/dashboard.go | 2 +- backend/pkg/api/types/validator_dashboard.go | 4 +- .../migrations/postgres/TBD_is_archived.sql | 13 ++ frontend/types/api/dashboard.ts | 2 +- perfTesting/module_userdata/seed.go | 1 + 9 files changed, 272 insertions(+), 38 deletions(-) create mode 100644 backend/pkg/commons/db/migrations/postgres/TBD_is_archived.sql diff --git a/backend/pkg/api/data_access/user.go b/backend/pkg/api/data_access/user.go index f79844ba2..d52f18a6d 100644 --- a/backend/pkg/api/data_access/user.go +++ b/backend/pkg/api/data_access/user.go @@ -10,6 +10,7 @@ import ( t "github.com/gobitfly/beaconchain/pkg/api/types" "github.com/gobitfly/beaconchain/pkg/commons/utils" "github.com/pkg/errors" + "golang.org/x/sync/errgroup" ) type UserRepository interface { @@ -541,22 +542,26 @@ func (d *DataAccessService) GetFreeTierPerks(ctx context.Context) (*t.PremiumPer } func (d *DataAccessService) GetUserDashboards(ctx context.Context, userId uint64) (*t.UserDashboardsData, error) { - // TODO @DATA-ACCESS Adjust to api changes: return archival related fields result := &t.UserDashboardsData{} - dbReturn := []struct { - Id uint64 `db:"id"` - Name string `db:"name"` - PublicId sql.NullString `db:"public_id"` - PublicName sql.NullString `db:"public_name"` - SharedGroups sql.NullBool `db:"shared_groups"` - }{} + wg := errgroup.Group{} - // Get the validator dashboards including the public ones - err := d.alloyReader.SelectContext(ctx, &dbReturn, ` + validatorDashboardMap := make(map[uint64]*t.ValidatorDashboard, 0) + wg.Go(func() error { + dbReturn := []struct { + Id uint64 `db:"id"` + Name string `db:"name"` + IsArchived sql.NullString `db:"is_archived"` + PublicId sql.NullString `db:"public_id"` + PublicName sql.NullString `db:"public_name"` + SharedGroups sql.NullBool `db:"shared_groups"` + }{} + + err := d.alloyReader.SelectContext(ctx, &dbReturn, ` SELECT uvd.id, uvd.name, + uvd.is_archived, uvds.public_id, uvds.name AS public_name, uvds.shared_groups @@ -564,30 +569,80 @@ func (d *DataAccessService) GetUserDashboards(ctx context.Context, userId uint64 LEFT JOIN users_val_dashboards_sharing uvds ON uvd.id = uvds.dashboard_id WHERE uvd.user_id = $1 `, userId) - if err != nil { - return nil, err - } + if err != nil { + return err + } - // Fill the result - validatorDashboardMap := make(map[uint64]*t.ValidatorDashboard, 0) - for _, row := range dbReturn { - if _, ok := validatorDashboardMap[row.Id]; !ok { - validatorDashboardMap[row.Id] = &t.ValidatorDashboard{ - Id: row.Id, - Name: row.Name, - PublicIds: []t.VDBPublicId{}, + for _, row := range dbReturn { + if _, ok := validatorDashboardMap[row.Id]; !ok { + validatorDashboardMap[row.Id] = &t.ValidatorDashboard{ + Id: row.Id, + Name: row.Name, + PublicIds: []t.VDBPublicId{}, + IsArchived: row.IsArchived.Valid, + ArchivedReason: row.IsArchived.String, + } + } + if row.PublicId.Valid { + publicId := t.VDBPublicId{} + publicId.PublicId = row.PublicId.String + publicId.Name = row.PublicName.String + publicId.ShareSettings.ShareGroups = row.SharedGroups.Bool + + validatorDashboardMap[row.Id].PublicIds = append(validatorDashboardMap[row.Id].PublicIds, publicId) } } - if row.PublicId.Valid { - result := t.VDBPublicId{} - result.PublicId = row.PublicId.String - result.Name = row.PublicName.String - result.ShareSettings.ShareGroups = row.SharedGroups.Bool - validatorDashboardMap[row.Id].PublicIds = append(validatorDashboardMap[row.Id].PublicIds, result) + return nil + }) + + type DashboardCount struct { + Id uint64 `db:"id"` + GroupCount uint64 `db:"group_count"` + ValidatorCount uint64 `db:"validator_count"` + } + + validatorDashboardCountMap := make(map[uint64]DashboardCount, 0) + wg.Go(func() error { + dbReturn := []DashboardCount{} + + err := d.alloyReader.SelectContext(ctx, &dbReturn, ` + SELECT + uvd.id, + COUNT(DISTINCT(uvdg.id)) AS group_count, + COUNT(DISTINCT(uvdv.validator_index)) AS validator_count + FROM users_val_dashboards uvd + LEFT JOIN users_val_dashboards_groups uvdg ON uvd.id = uvdg.dashboard_id + LEFT JOIN users_val_dashboards_validators uvdv ON uvd.id = uvdv.dashboard_id + WHERE uvd.user_id = $1 + GROUP BY uvd.id + `, userId) + if err != nil { + return err + } + + for _, row := range dbReturn { + entry := DashboardCount{ + Id: row.Id, + GroupCount: row.GroupCount, + ValidatorCount: row.ValidatorCount, + } + validatorDashboardCountMap[row.Id] = entry } + + return nil + }) + + err := wg.Wait() + if err != nil { + return nil, fmt.Errorf("error retrieving user dashboards data: %v", err) } + + // Fill the result for _, validatorDashboard := range validatorDashboardMap { + validatorDashboard.GroupCount = validatorDashboardCountMap[validatorDashboard.Id].GroupCount + validatorDashboard.ValidatorCount = validatorDashboardCountMap[validatorDashboard.Id].ValidatorCount + result.ValidatorDashboards = append(result.ValidatorDashboards, *validatorDashboard) } @@ -608,11 +663,11 @@ func (d *DataAccessService) GetUserDashboards(ctx context.Context, userId uint64 // return number of active / archived dashboards func (d *DataAccessService) GetUserValidatorDashboardCount(ctx context.Context, userId uint64, active bool) (uint64, error) { - // @DATA-ACCESS return number of dashboards depending on archival status (see comment above) var count uint64 err := d.alloyReader.GetContext(ctx, &count, ` SELECT COUNT(*) FROM users_val_dashboards - WHERE user_id = $1 - `, userId) + WHERE user_id = $1 AND (($2 AND is_archived IS NULL) OR (NOT $2 AND is_archived IS NOT NULL)) + `, userId, active) + return count, err } diff --git a/backend/pkg/api/data_access/vdb_management.go b/backend/pkg/api/data_access/vdb_management.go index 28e9aa278..6fa1c2098 100644 --- a/backend/pkg/api/data_access/vdb_management.go +++ b/backend/pkg/api/data_access/vdb_management.go @@ -9,6 +9,7 @@ import ( "sort" "strconv" "strings" + "sync" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/gobitfly/beaconchain/pkg/api/enums" @@ -58,8 +59,96 @@ func (d *DataAccessService) GetValidatorDashboardInfoByPublicId(ctx context.Cont } func (d *DataAccessService) GetValidatorDashboard(ctx context.Context, dashboardId t.VDBId) (*t.ValidatorDashboard, error) { - // TODO @DATA-ACCESS - return d.dummy.GetValidatorDashboard(ctx, dashboardId) + result := &t.ValidatorDashboard{} + + wg := errgroup.Group{} + mutex := &sync.RWMutex{} + + wg.Go(func() error { + dbReturn := []struct { + Name string `db:"name"` + IsArchived sql.NullString `db:"is_archived"` + PublicId sql.NullString `db:"public_id"` + PublicName sql.NullString `db:"public_name"` + SharedGroups sql.NullBool `db:"shared_groups"` + }{} + + err := d.alloyReader.SelectContext(ctx, &dbReturn, ` + SELECT + uvd.name, + uvd.is_archived, + uvds.public_id, + uvds.name AS public_name, + uvds.shared_groups + FROM users_val_dashboards uvd + LEFT JOIN users_val_dashboards_sharing uvds ON uvd.id = uvds.dashboard_id + WHERE uvd.id = $1 + `, dashboardId.Id) + if err != nil { + return err + } + + if len(dbReturn) == 0 { + return fmt.Errorf("error dashboard with id %v not found", dashboardId) + } + + mutex.Lock() + result.Id = uint64(dashboardId.Id) + result.Name = dbReturn[0].Name + result.IsArchived = dbReturn[0].IsArchived.Valid + result.ArchivedReason = dbReturn[0].IsArchived.String + + for _, row := range dbReturn { + if row.PublicId.Valid { + publicId := t.VDBPublicId{} + publicId.PublicId = row.PublicId.String + publicId.Name = row.PublicName.String + publicId.ShareSettings.ShareGroups = row.SharedGroups.Bool + + result.PublicIds = append(result.PublicIds, publicId) + } + } + mutex.Unlock() + + return nil + }) + + wg.Go(func() error { + dbReturn := struct { + GroupCount uint64 `db:"group_count"` + ValidatorCount uint64 `db:"validator_count"` + }{} + + err := d.alloyReader.GetContext(ctx, &dbReturn, ` + WITH dashboards_groups AS + (SELECT COUNT(uvdg.id) AS group_count FROM users_val_dashboards_groups uvdg WHERE uvdg.dashboard_id = $1), + dashboards_validators AS + (SELECT COUNT(uvdv.validator_index) AS validator_count FROM users_val_dashboards_validators uvdv WHERE uvdv.dashboard_id = $1) + SELECT + dashboards_groups.group_count, + dashboards_validators.validator_count + FROM + dashboards_groups, + dashboards_validators + `, dashboardId.Id) + if err != nil { + return err + } + + mutex.Lock() + result.GroupCount = dbReturn.GroupCount + result.ValidatorCount = dbReturn.ValidatorCount + mutex.Unlock() + + return nil + }) + + err := wg.Wait() + if err != nil { + return nil, fmt.Errorf("error retrieving user dashboards data: %v", err) + } + + return result, nil } func (d *DataAccessService) GetValidatorDashboardName(ctx context.Context, dashboardId t.VDBIdPrimary) (string, error) { @@ -187,8 +276,23 @@ func (d *DataAccessService) RemoveValidatorDashboard(ctx context.Context, dashbo } func (d *DataAccessService) UpdateValidatorDashboardArchiving(ctx context.Context, dashboardId t.VDBIdPrimary, archived bool) (*t.VDBPostArchivingReturnData, error) { - // TODO @DATA-ACCESS - return d.dummy.UpdateValidatorDashboardArchiving(ctx, dashboardId, archived) + result := &t.VDBPostArchivingReturnData{} + + var archivedReason *string + if archived { + reason := enums.VDBArchivedReasons.User.ToString() + archivedReason = &reason + } + + err := d.alloyWriter.GetContext(ctx, result, ` + UPDATE users_val_dashboards SET is_archived = $1 WHERE id = $2 + RETURNING id, is_archived IS NOT NULL AS is_archived + `, archivedReason, dashboardId) + if err != nil { + return nil, err + } + + return result, nil } func (d *DataAccessService) UpdateValidatorDashboardName(ctx context.Context, dashboardId t.VDBIdPrimary, name string) (*t.VDBPostReturnData, error) { diff --git a/backend/pkg/api/enums/validator_dashboard_enums.go b/backend/pkg/api/enums/validator_dashboard_enums.go index d0e182b83..377e105e6 100644 --- a/backend/pkg/api/enums/validator_dashboard_enums.go +++ b/backend/pkg/api/enums/validator_dashboard_enums.go @@ -270,6 +270,66 @@ var VDBManageValidatorsColumns = struct { VDBManageValidatorsWithdrawalCredential, } +// ---------------- +// Validator Dashboard Archived Reasons + +type VDBArchivedReason int + +var _ EnumFactory[VDBArchivedReason] = VDBArchivedReason(0) + +const ( + VDBArchivedUser VDBArchivedReason = iota + VDBArchivedDashboards + VDBArchivedGroups + VDBArchivedValidators +) + +func (r VDBArchivedReason) Int() int { + return int(r) +} + +func (VDBArchivedReason) NewFromString(s string) VDBArchivedReason { + switch s { + case "user": + return VDBArchivedUser + case "dashboard_limit": + return VDBArchivedDashboards + case "group_limit": + return VDBArchivedGroups + case "validator_limit": + return VDBArchivedValidators + default: + return VDBArchivedReason(-1) + } +} + +func (r VDBArchivedReason) ToString() string { + switch r { + case VDBArchivedUser: + return "user" + case VDBArchivedDashboards: + return "dashboard_limit" + case VDBArchivedGroups: + return "group_limit" + case VDBArchivedValidators: + return "validator_limit" + default: + return "" + } +} + +var VDBArchivedReasons = struct { + User VDBArchivedReason + Dashboards VDBArchivedReason + Groups VDBArchivedReason + Validators VDBArchivedReason +}{ + VDBArchivedUser, + VDBArchivedDashboards, + VDBArchivedGroups, + VDBArchivedValidators, +} + // ---------------- // Validator Reward Chart Efficiency Filter diff --git a/backend/pkg/api/handlers/internal.go b/backend/pkg/api/handlers/internal.go index ef1086575..7802f6e0f 100644 --- a/backend/pkg/api/handlers/internal.go +++ b/backend/pkg/api/handlers/internal.go @@ -459,6 +459,7 @@ func (h *HandlerService) InternalPutValidatorDashboardArchiving(w http.ResponseW returnOk(w, types.ApiDataResponse[types.VDBPostArchivingReturnData]{ Data: types.VDBPostArchivingReturnData{Id: uint64(dashboardId), IsArchived: req.IsArchived}, }) + return } userId, ok := r.Context().Value(ctxUserIdKey).(uint64) diff --git a/backend/pkg/api/types/dashboard.go b/backend/pkg/api/types/dashboard.go index fdf5947be..eaf49a431 100644 --- a/backend/pkg/api/types/dashboard.go +++ b/backend/pkg/api/types/dashboard.go @@ -9,7 +9,7 @@ type ValidatorDashboard struct { Name string `json:"name"` PublicIds []VDBPublicId `json:"public_ids,omitempty"` IsArchived bool `json:"is_archived"` - ArchivedReason string `json:"archived_reason,omitempty" tstype:"'dashboard_limit' | 'validator_limit' | 'group_limit'"` + ArchivedReason string `json:"archived_reason,omitempty" tstype:"'user' | 'dashboard_limit' | 'validator_limit' | 'group_limit'"` ValidatorCount uint64 `json:"validator_count"` GroupCount uint64 `json:"group_count"` } diff --git a/backend/pkg/api/types/validator_dashboard.go b/backend/pkg/api/types/validator_dashboard.go index 9507fe689..131bb208f 100644 --- a/backend/pkg/api/types/validator_dashboard.go +++ b/backend/pkg/api/types/validator_dashboard.go @@ -33,8 +33,8 @@ type VDBOverviewData struct { type InternalGetValidatorDashboardResponse ApiDataResponse[VDBOverviewData] type VDBPostArchivingReturnData struct { - Id uint64 `json:"id"` - IsArchived bool `json:"is_archived"` + Id uint64 `db:"id" json:"id"` + IsArchived bool `db:"is_archived" json:"is_archived"` } // ------------------------------------------------------------ diff --git a/backend/pkg/commons/db/migrations/postgres/TBD_is_archived.sql b/backend/pkg/commons/db/migrations/postgres/TBD_is_archived.sql new file mode 100644 index 000000000..171b4b190 --- /dev/null +++ b/backend/pkg/commons/db/migrations/postgres/TBD_is_archived.sql @@ -0,0 +1,13 @@ +-- +goose Up +-- +goose StatementBegin + +-- TODO + +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin + +-- TODO + +-- +goose StatementEnd diff --git a/frontend/types/api/dashboard.ts b/frontend/types/api/dashboard.ts index 37fa19546..3ab7c3768 100644 --- a/frontend/types/api/dashboard.ts +++ b/frontend/types/api/dashboard.ts @@ -14,7 +14,7 @@ export interface ValidatorDashboard { name: string; public_ids?: VDBPublicId[]; is_archived: boolean; - archived_reason?: 'dashboard_limit' | 'validator_limit' | 'group_limit'; + archived_reason?: 'user' | 'dashboard_limit' | 'validator_limit' | 'group_limit'; validator_count: number /* uint64 */; group_count: number /* uint64 */; } diff --git a/perfTesting/module_userdata/seed.go b/perfTesting/module_userdata/seed.go index 460128a9c..c30a6d778 100644 --- a/perfTesting/module_userdata/seed.go +++ b/perfTesting/module_userdata/seed.go @@ -83,6 +83,7 @@ func (*Schemav1) CreateSchema(s *seeding.Seeder) error { network SMALLINT NOT NULL, -- indicate gnosis/eth mainnet and potentially testnets name VARCHAR(50) NOT NULL, created_at TIMESTAMP DEFAULT(NOW()), + is_archived TEXT, primary key (id) ); From bc5d0c61faffe6327563899a1dadfdfcfc92a4d0 Mon Sep 17 00:00:00 2001 From: Stefan Pletka Date: Mon, 12 Aug 2024 11:44:31 +0200 Subject: [PATCH 2/5] Updated migration file --- .../db/migrations/postgres/TBD_is_archived.sql | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/backend/pkg/commons/db/migrations/postgres/TBD_is_archived.sql b/backend/pkg/commons/db/migrations/postgres/TBD_is_archived.sql index 171b4b190..f569f9e29 100644 --- a/backend/pkg/commons/db/migrations/postgres/TBD_is_archived.sql +++ b/backend/pkg/commons/db/migrations/postgres/TBD_is_archived.sql @@ -1,13 +1,11 @@ -- +goose Up -- +goose StatementBegin - --- TODO - +SELECT 'up SQL query - add is_archived column'; +ALTER TABLE users_val_dashboards ADD COLUMN IF NOT EXISTS is_archived TEXT; -- +goose StatementEnd -- +goose Down -- +goose StatementBegin - --- TODO - --- +goose StatementEnd +SELECT 'down SQL query - drop is_archived column'; +ALTER TABLE users_val_dashboards DROP COLUMN IF EXISTS is_archived; +-- +goose StatementEnd \ No newline at end of file From 18d6c027b730653e98fc432ed27e73458cc78538 Mon Sep 17 00:00:00 2001 From: Stefan Pletka <124689083+Eisei24@users.noreply.github.com> Date: Wed, 14 Aug 2024 12:19:37 +0200 Subject: [PATCH 3/5] Avoid checks for admin users --- backend/pkg/api/handlers/internal.go | 47 +++++++++++++++------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/backend/pkg/api/handlers/internal.go b/backend/pkg/api/handlers/internal.go index ea0df52bc..29bc7fe08 100644 --- a/backend/pkg/api/handlers/internal.go +++ b/backend/pkg/api/handlers/internal.go @@ -472,28 +472,31 @@ func (h *HandlerService) InternalPutValidatorDashboardArchiving(w http.ResponseW handleErr(w, err) return } - if req.IsArchived { - if dashboardCount >= maxArchivedDashboardsCount { - returnConflict(w, errors.New("maximum number of archived validator dashboards reached")) - return - } - } else { - userInfo, err := h.dai.GetUserInfo(r.Context(), userId) - if err != nil { - handleErr(w, err) - return - } - if dashboardCount >= userInfo.PremiumPerks.ValidatorDasboards && !isUserAdmin(userInfo) { - returnConflict(w, errors.New("maximum number of active validator dashboards reached")) - return - } - if dashboardInfo.GroupCount >= userInfo.PremiumPerks.ValidatorGroupsPerDashboard && !isUserAdmin(userInfo) { - returnConflict(w, errors.New("maximum number of groups in dashboards reached")) - return - } - if dashboardInfo.ValidatorCount >= userInfo.PremiumPerks.ValidatorsPerDashboard && !isUserAdmin(userInfo) { - returnConflict(w, errors.New("maximum number of validators in dashboards reached")) - return + + userInfo, err := h.dai.GetUserInfo(r.Context(), userId) + if err != nil { + handleErr(w, err) + return + } + if !isUserAdmin(userInfo) { + if req.IsArchived { + if dashboardCount >= maxArchivedDashboardsCount { + returnConflict(w, errors.New("maximum number of archived validator dashboards reached")) + return + } + } else { + if dashboardCount >= userInfo.PremiumPerks.ValidatorDasboards { + returnConflict(w, errors.New("maximum number of active validator dashboards reached")) + return + } + if dashboardInfo.GroupCount >= userInfo.PremiumPerks.ValidatorGroupsPerDashboard { + returnConflict(w, errors.New("maximum number of groups in dashboards reached")) + return + } + if dashboardInfo.ValidatorCount >= userInfo.PremiumPerks.ValidatorsPerDashboard { + returnConflict(w, errors.New("maximum number of validators in dashboards reached")) + return + } } } From 03156c833e182acfd650e21c50c79bf1c84b317b Mon Sep 17 00:00:00 2001 From: Stefan Pletka <124689083+Eisei24@users.noreply.github.com> Date: Tue, 20 Aug 2024 11:02:31 +0200 Subject: [PATCH 4/5] Implemented review remarks --- backend/pkg/api/data_access/user.go | 7 +------ .../pkg/api/enums/validator_dashboard_enums.go | 17 +---------------- 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/backend/pkg/api/data_access/user.go b/backend/pkg/api/data_access/user.go index 58bb32a6a..209b13a4c 100644 --- a/backend/pkg/api/data_access/user.go +++ b/backend/pkg/api/data_access/user.go @@ -628,12 +628,7 @@ func (d *DataAccessService) GetUserDashboards(ctx context.Context, userId uint64 } for _, row := range dbReturn { - entry := DashboardCount{ - Id: row.Id, - GroupCount: row.GroupCount, - ValidatorCount: row.ValidatorCount, - } - validatorDashboardCountMap[row.Id] = entry + validatorDashboardCountMap[row.Id] = row } return nil diff --git a/backend/pkg/api/enums/validator_dashboard_enums.go b/backend/pkg/api/enums/validator_dashboard_enums.go index 377e105e6..928d5742c 100644 --- a/backend/pkg/api/enums/validator_dashboard_enums.go +++ b/backend/pkg/api/enums/validator_dashboard_enums.go @@ -275,7 +275,7 @@ var VDBManageValidatorsColumns = struct { type VDBArchivedReason int -var _ EnumFactory[VDBArchivedReason] = VDBArchivedReason(0) +var _ Enum = VDBArchivedReason(0) const ( VDBArchivedUser VDBArchivedReason = iota @@ -288,21 +288,6 @@ func (r VDBArchivedReason) Int() int { return int(r) } -func (VDBArchivedReason) NewFromString(s string) VDBArchivedReason { - switch s { - case "user": - return VDBArchivedUser - case "dashboard_limit": - return VDBArchivedDashboards - case "group_limit": - return VDBArchivedGroups - case "validator_limit": - return VDBArchivedValidators - default: - return VDBArchivedReason(-1) - } -} - func (r VDBArchivedReason) ToString() string { switch r { case VDBArchivedUser: From 5c0d48537ecca824c4a058bf41ed96648ed3bad7 Mon Sep 17 00:00:00 2001 From: Stefan Pletka <124689083+Eisei24@users.noreply.github.com> Date: Tue, 20 Aug 2024 14:12:46 +0200 Subject: [PATCH 5/5] Renamed migration file --- .../{TBD_is_archived.sql => 20240820141500_is_archived.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename backend/pkg/commons/db/migrations/postgres/{TBD_is_archived.sql => 20240820141500_is_archived.sql} (100%) diff --git a/backend/pkg/commons/db/migrations/postgres/TBD_is_archived.sql b/backend/pkg/commons/db/migrations/postgres/20240820141500_is_archived.sql similarity index 100% rename from backend/pkg/commons/db/migrations/postgres/TBD_is_archived.sql rename to backend/pkg/commons/db/migrations/postgres/20240820141500_is_archived.sql