code.vegaprotocol.io/vega@v0.79.0/datanode/sqlstore/proposals.go (about) 1 // Copyright (C) 2023 Gobalsky Labs Limited 2 // 3 // This program is free software: you can redistribute it and/or modify 4 // it under the terms of the GNU Affero General Public License as 5 // published by the Free Software Foundation, either version 3 of the 6 // License, or (at your option) any later version. 7 // 8 // This program is distributed in the hope that it will be useful, 9 // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 // GNU Affero General Public License for more details. 12 // 13 // You should have received a copy of the GNU Affero General Public License 14 // along with this program. If not, see <http://www.gnu.org/licenses/>. 15 16 package sqlstore 17 18 import ( 19 "context" 20 "fmt" 21 "sort" 22 "strings" 23 24 "code.vegaprotocol.io/vega/datanode/entities" 25 "code.vegaprotocol.io/vega/datanode/metrics" 26 v2 "code.vegaprotocol.io/vega/protos/data-node/api/v2" 27 28 "github.com/georgysavva/scany/pgxscan" 29 "github.com/jackc/pgx/v4" 30 ) 31 32 type Proposals struct { 33 *ConnectionSource 34 } 35 36 var proposalsOrdering = TableOrdering{ 37 ColumnOrdering{Name: "vega_time", Sorting: ASC}, 38 ColumnOrdering{Name: "id", Sorting: ASC}, 39 } 40 41 func NewProposals(connectionSource *ConnectionSource) *Proposals { 42 p := &Proposals{ 43 ConnectionSource: connectionSource, 44 } 45 return p 46 } 47 48 func (ps *Proposals) Add(ctx context.Context, p entities.Proposal) error { 49 defer metrics.StartSQLQuery("Proposals", "Add")() 50 _, err := ps.Exec(ctx, 51 `INSERT INTO proposals( 52 id, 53 batch_id, 54 reference, 55 party_id, 56 state, 57 terms, 58 batch_terms, 59 rationale, 60 reason, 61 error_details, 62 proposal_time, 63 vega_time, 64 required_majority, 65 required_participation, 66 required_lp_majority, 67 required_lp_participation, 68 tx_hash) 69 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) 70 ON CONFLICT (id, vega_time) DO UPDATE SET 71 reference = EXCLUDED.reference, 72 party_id = EXCLUDED.party_id, 73 state = EXCLUDED.state, 74 terms = EXCLUDED.terms, 75 rationale = EXCLUDED.rationale, 76 reason = EXCLUDED.reason, 77 error_details = EXCLUDED.error_details, 78 proposal_time = EXCLUDED.proposal_time, 79 tx_hash = EXCLUDED.tx_hash 80 ; 81 `, 82 p.ID, p.BatchID, p.Reference, p.PartyID, p.State, p.Terms, p.BatchTerms, p.Rationale, p.Reason, 83 p.ErrorDetails, p.ProposalTime, p.VegaTime, p.RequiredMajority, p.RequiredParticipation, 84 p.RequiredLPMajority, p.RequiredLPParticipation, p.TxHash) 85 return err 86 } 87 88 func (ps *Proposals) getProposalsInBatch(ctx context.Context, batchID string) ([]entities.Proposal, error) { 89 var proposals []entities.Proposal 90 query := `SELECT * FROM proposals_current WHERE batch_id=$1` 91 92 rows, err := ps.Query(ctx, query, entities.ProposalID(batchID)) 93 if err != nil { 94 return proposals, fmt.Errorf("querying proposals: %w", err) 95 } 96 defer rows.Close() 97 98 if err = pgxscan.ScanAll(&proposals, rows); err != nil { 99 return proposals, fmt.Errorf("parsing proposals: %w", err) 100 } 101 102 sort.Slice(proposals, func(i, j int) bool { 103 return proposals[i].Terms.EnactmentTimestamp < proposals[j].Terms.EnactmentTimestamp 104 }) 105 106 return proposals, nil 107 } 108 109 // extendOrGetBatchProposal fetching sub proposals in case of batch proposal 110 // or fetches the whole batch if sub proposal is requested. 111 // If none of the above applies then proposal is returned without change. 112 func (ps *Proposals) extendOrGetBatchProposal(ctx context.Context, p entities.Proposal) (entities.Proposal, error) { 113 // if proposal is part of batch fetch to whole batch 114 if p.BelongsToBatch() { 115 return ps.GetByID(ctx, p.BatchID.String()) 116 } 117 118 // if it's batch fetch the sub proposals 119 if p.IsBatch() { 120 pps, err := ps.getProposalsInBatch(ctx, p.ID.String()) 121 if err != nil { 122 return p, ps.wrapE(err) 123 } 124 p.Proposals = pps 125 } 126 127 return p, nil 128 } 129 130 func (ps *Proposals) GetByID(ctx context.Context, id string) (entities.Proposal, error) { 131 defer metrics.StartSQLQuery("Proposals", "GetByID")() 132 var p entities.Proposal 133 query := `SELECT * FROM proposals_current WHERE id=$1` 134 135 if err := pgxscan.Get(ctx, ps.ConnectionSource, &p, query, entities.ProposalID(id)); err != nil { 136 return p, ps.wrapE(pgxscan.Get(ctx, ps.ConnectionSource, &p, query, entities.ProposalID(id))) 137 } 138 139 p, err := ps.extendOrGetBatchProposal(ctx, p) 140 if err != nil { 141 return p, err 142 } 143 144 return p, nil 145 } 146 147 // GetByIDWithoutBatch returns a proposal without extending single proposal by fetching batch proposal. 148 func (ps *Proposals) GetByIDWithoutBatch(ctx context.Context, id string) (entities.Proposal, error) { 149 defer metrics.StartSQLQuery("Proposals", "GetByIDWithoutBatch")() 150 var p entities.Proposal 151 query := `SELECT * FROM proposals_current WHERE id=$1` 152 153 if err := pgxscan.Get(ctx, ps.ConnectionSource, &p, query, entities.ProposalID(id)); err != nil { 154 return p, ps.wrapE(pgxscan.Get(ctx, ps.ConnectionSource, &p, query, entities.ProposalID(id))) 155 } 156 157 return p, nil 158 } 159 160 func (ps *Proposals) GetByReference(ctx context.Context, ref string) (entities.Proposal, error) { 161 defer metrics.StartSQLQuery("Proposals", "GetByReference")() 162 var p entities.Proposal 163 query := `SELECT * FROM proposals_current WHERE reference=$1 LIMIT 1` 164 165 if err := pgxscan.Get(ctx, ps.ConnectionSource, &p, query, ref); err != nil { 166 return p, ps.wrapE(err) 167 } 168 169 p, err := ps.extendOrGetBatchProposal(ctx, p) 170 if err != nil { 171 return p, err 172 } 173 174 return p, nil 175 } 176 177 func (ps *Proposals) GetByTxHash(ctx context.Context, txHash entities.TxHash) ([]entities.Proposal, error) { 178 defer metrics.StartSQLQuery("Proposals", "GetByTxHash")() 179 180 var proposals []entities.Proposal 181 query := `SELECT * FROM proposals WHERE tx_hash=$1` 182 err := pgxscan.Select(ctx, ps.ConnectionSource, &proposals, query, txHash) 183 if err != nil { 184 return nil, ps.wrapE(err) 185 } 186 187 for i, p := range proposals { 188 p, err := ps.extendOrGetBatchProposal(ctx, p) 189 if err != nil { 190 return proposals, err 191 } 192 193 proposals[i] = p 194 } 195 196 return proposals, nil 197 } 198 199 func getOpenStateProposalsQuery(inState *entities.ProposalState, conditions []string, pagination entities.CursorPagination, 200 pc *entities.ProposalCursor, pageForward bool, args ...interface{}, 201 ) (string, []interface{}, error) { 202 // if we're querying for a specific state and it's not the Open state, 203 // or if we are paging forward and the current state is not the open state 204 // then we do not need to query for any open state proposals 205 if (inState != nil && *inState != entities.ProposalStateOpen) || 206 (pageForward && pc.State != entities.ProposalStateUnspecified && pc.State != entities.ProposalStateOpen) { 207 // we aren't interested in open proposals so the query should be empty 208 return "", args, nil 209 } 210 211 if pc.State != entities.ProposalStateOpen { 212 if pagination.HasForward() { 213 pagination.Forward.Cursor = nil 214 } else if pagination.HasBackward() { 215 pagination.Backward.Cursor = nil 216 } 217 } 218 219 conditions = append([]string{ 220 fmt.Sprintf("state=%s", nextBindVar(&args, entities.ProposalStateOpen)), 221 }, conditions...) 222 223 query := `select * from proposals_current` 224 225 if len(conditions) > 0 { 226 query = fmt.Sprintf("%s WHERE %s", query, strings.Join(conditions, " AND ")) 227 } 228 229 var err error 230 query, args, err = PaginateQuery[entities.ProposalCursor](query, args, proposalsOrdering, pagination) 231 if err != nil { 232 return "", args, err 233 } 234 235 return query, args, nil 236 } 237 238 func getOtherStateProposalsQuery(inState *entities.ProposalState, conditions []string, pagination entities.CursorPagination, 239 pc *entities.ProposalCursor, pageForward bool, args ...interface{}, 240 ) (string, []interface{}, error) { 241 // if we're filtering for state and the state is open, 242 // or we're paging forward, and the cursor has reached the open proposals 243 // then we don't need to return any non-open proposal results 244 if (inState != nil && *inState == entities.ProposalStateOpen) || (!pageForward && pc.State == entities.ProposalStateOpen) { 245 // the open state query should already be providing the correct query for this 246 return "", args, nil 247 } 248 249 if pagination.HasForward() { 250 if pc.State == entities.ProposalStateOpen || pc.State == entities.ProposalStateUnspecified { 251 pagination.Forward.Cursor = nil 252 } 253 } else if pagination.HasBackward() { 254 if pc.State == entities.ProposalStateOpen || pc.State == entities.ProposalStateUnspecified { 255 pagination.Backward.Cursor = nil 256 } 257 } 258 259 if inState == nil { 260 conditions = append([]string{ 261 fmt.Sprintf("state!=%s", nextBindVar(&args, entities.ProposalStateOpen)), 262 }, conditions...) 263 } else { 264 conditions = append([]string{ 265 fmt.Sprintf("state=%s", nextBindVar(&args, *inState)), 266 }, conditions...) 267 } 268 query := `select * from proposals_current` 269 270 if len(conditions) > 0 { 271 query = fmt.Sprintf("%s WHERE %s", query, strings.Join(conditions, " AND ")) 272 } 273 274 var err error 275 query, args, err = PaginateQuery[entities.ProposalCursor](query, args, proposalsOrdering, pagination) 276 if err != nil { 277 return "", args, err 278 } 279 return query, args, nil 280 } 281 282 func clonePagination(p entities.CursorPagination) (entities.CursorPagination, error) { 283 var first, last int32 284 var after, before string 285 286 var pFirst, pLast *int32 287 var pAfter, pBefore *string 288 289 if p.HasForward() { 290 first = *p.Forward.Limit 291 pFirst = &first 292 if p.Forward.HasCursor() { 293 after = p.Forward.Cursor.Encode() 294 pAfter = &after 295 } 296 } 297 298 if p.HasBackward() { 299 last = *p.Backward.Limit 300 pLast = &last 301 if p.Backward.HasCursor() { 302 before = p.Backward.Cursor.Encode() 303 pBefore = &before 304 } 305 } 306 307 return entities.NewCursorPagination(pFirst, pAfter, pLast, pBefore, p.NewestFirst) 308 } 309 310 func (ps *Proposals) Get(ctx context.Context, 311 inState *entities.ProposalState, 312 partyIDStr *string, 313 proposalType *entities.ProposalType, 314 pagination entities.CursorPagination, 315 ) ([]entities.Proposal, entities.PageInfo, error) { 316 // This one is a bit tricky because we want all the open proposals listed at the top, sorted by date 317 // then other proposals in date order regardless of state. 318 319 // In order to do this, we need to construct a union of proposals where state = open, order by vega_time 320 // and state != open, order by vega_time 321 // If the cursor has been set, and we're traversing forward (newest-oldest), then we need to check if the 322 // state of the cursor is = open. If it is then we should append the open state proposals with the non-open state 323 // proposals. 324 // If the cursor state is != open, we have navigated passed the open state proposals and only need the non-open state proposals. 325 326 // If the cursor has been set and we're traversing backward (newest-oldest), then we need to check if the 327 // state of the cursor is = open. If it is then we should only return the proposals where state = open as we've already navigated 328 // passed all the non-open proposals. 329 // if the state of the cursor is != open, then we need to append all the proposals where the state = open after the proposals where 330 // state != open. 331 332 // This combined results of both queries is then wrapped with another select which should return the appropriate number of rows that 333 // are required for the pagination to determine whether or not there are any next/previous rows for the pageInfo. 334 var ( 335 pageInfo entities.PageInfo 336 stateOpenQuery string 337 stateOtherQuery string 338 stateOpenArgs []interface{} 339 stateOtherArgs []interface{} 340 ) 341 args := make([]interface{}, 0) 342 cursor := extractCursorFromPagination(pagination) 343 344 pc := &entities.ProposalCursor{} 345 346 if cursor != "" { 347 err := pc.Parse(cursor) 348 if err != nil { 349 return nil, pageInfo, err 350 } 351 } 352 353 pageForward := pagination.HasForward() || (!pagination.HasForward() && !pagination.HasBackward()) 354 var conditions []string 355 356 if partyIDStr != nil { 357 partyID := entities.PartyID(*partyIDStr) 358 conditions = append(conditions, fmt.Sprintf("party_id=%s", nextBindVar(&args, partyID))) 359 } 360 361 if proposalType != nil && *proposalType != entities.ProposalTypeAll { 362 conditions = append(conditions, fmt.Sprintf("terms ? %s", nextBindVar(&args, proposalType.String()))) 363 } 364 365 var err error 366 var openPagination, otherPagination entities.CursorPagination 367 // we need to clone the pagination objects because we need to alter the pagination data for the different states 368 // to support the required ordering of the data 369 openPagination, err = clonePagination(pagination) 370 if err != nil { 371 return nil, pageInfo, fmt.Errorf("invalid pagination: %w", err) 372 } 373 otherPagination, err = clonePagination(pagination) 374 if err != nil { 375 return nil, pageInfo, fmt.Errorf("invalid pagination: %w", err) 376 } 377 378 stateOpenQuery, stateOpenArgs, err = getOpenStateProposalsQuery(inState, conditions, openPagination, pc, pageForward, args...) 379 if err != nil { 380 return nil, pageInfo, err 381 } 382 stateOtherQuery, stateOtherArgs, err = getOtherStateProposalsQuery(inState, conditions, otherPagination, pc, pageForward, args...) 383 if err != nil { 384 return nil, pageInfo, err 385 } 386 387 batch := &pgx.Batch{} 388 389 if stateOpenQuery != "" { 390 batch.Queue(stateOpenQuery, stateOpenArgs...) 391 } 392 393 if stateOtherQuery != "" { 394 batch.Queue(stateOtherQuery, stateOtherArgs...) 395 } 396 397 defer metrics.StartSQLQuery("Proposals", "Get")() 398 // copy the store connection because we may need to make recursive calls when processing the from the batch 399 // causing the underlying connection to be busy and unusable 400 batchConn := ps.ConnectionSource 401 results := batchConn.SendBatch(ctx, batch) 402 defer results.Close() 403 404 proposals := make([]entities.Proposal, 0) 405 fetchedBatches := map[entities.ProposalID]struct{}{} 406 407 for { 408 rows, err := results.Query() 409 if err != nil { 410 break 411 } 412 413 var matchedProps []entities.Proposal 414 if err := pgxscan.ScanAll(&matchedProps, rows); err != nil { 415 return nil, pageInfo, fmt.Errorf("querying proposals: %w", err) 416 } 417 418 rows.Close() 419 420 var props []entities.Proposal 421 for _, p := range matchedProps { 422 var batchID entities.ProposalID 423 if p.BelongsToBatch() { 424 batchID = p.BatchID 425 } else if p.IsBatch() { 426 batchID = p.ID 427 } 428 429 if _, ok := fetchedBatches[batchID]; ok { 430 continue 431 } 432 433 p, err := ps.extendOrGetBatchProposal(ctx, p) 434 if err != nil { 435 return nil, pageInfo, err 436 } 437 props = append(props, p) 438 fetchedBatches[p.ID] = struct{}{} 439 } 440 441 if pageForward { 442 proposals = append(proposals, props...) 443 } else { 444 proposals = append(props, proposals...) 445 } 446 } 447 448 if limit := calculateLimit(pagination); limit > 0 && limit < len(proposals) { 449 proposals = proposals[:limit] 450 } 451 452 proposals, pageInfo = entities.PageEntities[*v2.GovernanceDataEdge](proposals, pagination) 453 return proposals, pageInfo, nil 454 }