github.com/supabase/cli@v1.168.1/internal/utils/container_output.go (about)

     1  package utils
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"regexp"
    11  	"strconv"
    12  	"strings"
    13  
    14  	"github.com/docker/docker/pkg/jsonmessage"
    15  	"github.com/docker/docker/pkg/stdcopy"
    16  	"github.com/go-errors/errors"
    17  )
    18  
    19  func ProcessPullOutput(out io.ReadCloser, p Program) error {
    20  	dec := json.NewDecoder(out)
    21  
    22  	downloads := make(map[string]struct{ current, total int64 })
    23  
    24  	for {
    25  		var progress jsonmessage.JSONMessage
    26  
    27  		if err := dec.Decode(&progress); err == io.EOF {
    28  			break
    29  		} else if err != nil {
    30  			return err
    31  		}
    32  
    33  		if strings.HasPrefix(progress.Status, "Pulling from") {
    34  			p.Send(StatusMsg(progress.Status + "..."))
    35  		} else if progress.Status == "Pulling fs layer" || progress.Status == "Waiting" {
    36  			downloads[progress.ID] = struct{ current, total int64 }{
    37  				current: 0,
    38  				total:   0,
    39  			}
    40  		} else if progress.Status == "Downloading" {
    41  			downloads[progress.ID] = struct{ current, total int64 }{
    42  				current: progress.Progress.Current,
    43  				total:   progress.Progress.Total,
    44  			}
    45  
    46  			var overallProgress float64
    47  			for _, percentage := range downloads {
    48  				if percentage.total > 0 {
    49  					progress := float64(percentage.current) / float64(percentage.total)
    50  					overallProgress += progress / float64(len(downloads))
    51  				}
    52  			}
    53  
    54  			p.Send(ProgressMsg(&overallProgress))
    55  		}
    56  	}
    57  
    58  	p.Send(ProgressMsg(nil))
    59  
    60  	return nil
    61  }
    62  
    63  type DiffStream struct {
    64  	o bytes.Buffer
    65  	r *io.PipeReader
    66  	w *io.PipeWriter
    67  	p Program
    68  }
    69  
    70  func NewDiffStream(p Program) *DiffStream {
    71  	r, w := io.Pipe()
    72  	go func() {
    73  		if err := ProcessDiffProgress(p, r); err != nil {
    74  			fmt.Fprintln(os.Stderr, err)
    75  		}
    76  	}()
    77  	return &DiffStream{r: r, w: w, p: p}
    78  }
    79  
    80  func (c DiffStream) Stdout() io.Writer {
    81  	return &c.o
    82  }
    83  
    84  func (c DiffStream) Stderr() io.Writer {
    85  	return c.w
    86  }
    87  
    88  func (c DiffStream) Collect() ([]byte, error) {
    89  	if err := c.w.Close(); err != nil {
    90  		fmt.Fprintln(os.Stderr, "Failed to close stream:", err)
    91  	}
    92  	return ProcessDiffOutput(c.o.Bytes())
    93  }
    94  
    95  func ProcessDiffProgress(p Program, out io.Reader) error {
    96  	scanner := bufio.NewScanner(out)
    97  	re := regexp.MustCompile(`(.*)([[:digit:]]{2,3})%`)
    98  	for scanner.Scan() {
    99  		line := scanner.Text()
   100  
   101  		if line == "Starting schema diff..." {
   102  			percentage := 0.0
   103  			p.Send(ProgressMsg(&percentage))
   104  		}
   105  
   106  		matches := re.FindStringSubmatch(line)
   107  		if len(matches) != 3 {
   108  			// TODO: emit actual error statements
   109  			continue
   110  		}
   111  
   112  		p.Send(StatusMsg(matches[1]))
   113  		percentage, err := strconv.ParseFloat(matches[2], 64)
   114  		if err != nil {
   115  			continue
   116  		}
   117  		percentage = percentage / 100
   118  		p.Send(ProgressMsg(&percentage))
   119  	}
   120  	p.Send(ProgressMsg(nil))
   121  	return scanner.Err()
   122  }
   123  
   124  type DiffDependencies struct {
   125  	Type string `json:"type"`
   126  }
   127  
   128  type DiffEntry struct {
   129  	Type             string             `json:"type"`
   130  	Status           string             `json:"status"`
   131  	DiffDdl          string             `json:"diff_ddl"`
   132  	GroupName        string             `json:"group_name"`
   133  	Dependencies     []DiffDependencies `json:"dependencies"`
   134  	SourceSchemaName *string            `json:"source_schema_name"`
   135  }
   136  
   137  const diffHeader = `-- This script was generated by the Schema Diff utility in pgAdmin 4
   138  -- For the circular dependencies, the order in which Schema Diff writes the objects is not very sophisticated
   139  -- and may require manual changes to the script to ensure changes are applied in the correct order.
   140  -- Please report an issue for any failure with the reproduction steps.`
   141  
   142  func ProcessDiffOutput(diffBytes []byte) ([]byte, error) {
   143  	// TODO: Remove when https://github.com/supabase/pgadmin4/issues/24 is fixed.
   144  	diffBytes = bytes.TrimPrefix(diffBytes, []byte("NOTE: Configuring authentication for DESKTOP mode.\n"))
   145  
   146  	if len(diffBytes) == 0 {
   147  		return diffBytes, nil
   148  	}
   149  
   150  	var diffJson []DiffEntry
   151  	if err := json.Unmarshal(diffBytes, &diffJson); err != nil {
   152  		return nil, err
   153  	}
   154  
   155  	var filteredDiffDdls []string
   156  	for _, diffEntry := range diffJson {
   157  		if diffEntry.Status == "Identical" || diffEntry.DiffDdl == "" {
   158  			continue
   159  		}
   160  
   161  		switch diffEntry.Type {
   162  		case "extension", "function", "mview", "table", "trigger_function", "type", "view":
   163  			// skip
   164  		default:
   165  			continue
   166  		}
   167  
   168  		{
   169  			doContinue := false
   170  			for _, dep := range diffEntry.Dependencies {
   171  				if dep.Type == "extension" {
   172  					doContinue = true
   173  					break
   174  				}
   175  			}
   176  
   177  			if doContinue {
   178  				continue
   179  			}
   180  		}
   181  
   182  		isSchemaIgnored := func(schema string) bool {
   183  			for _, s := range InternalSchemas {
   184  				if s == schema {
   185  					return true
   186  				}
   187  			}
   188  			return false
   189  		}
   190  
   191  		if isSchemaIgnored(diffEntry.GroupName) ||
   192  			// Needed at least for trigger_function
   193  			(diffEntry.SourceSchemaName != nil && isSchemaIgnored(*diffEntry.SourceSchemaName)) {
   194  			continue
   195  		}
   196  
   197  		trimmed := strings.TrimSpace(diffEntry.DiffDdl)
   198  		if len(trimmed) > 0 {
   199  			filteredDiffDdls = append(filteredDiffDdls, trimmed)
   200  		}
   201  	}
   202  
   203  	if len(filteredDiffDdls) == 0 {
   204  		return nil, nil
   205  	}
   206  	return []byte(diffHeader + "\n\n" + strings.Join(filteredDiffDdls, "\n\n") + "\n"), nil
   207  }
   208  
   209  func ProcessPsqlOutput(out io.Reader, p Program) error {
   210  	r, w := io.Pipe()
   211  	doneCh := make(chan struct{}, 1)
   212  
   213  	go func() {
   214  		scanner := bufio.NewScanner(r)
   215  
   216  		for scanner.Scan() {
   217  			select {
   218  			case <-doneCh:
   219  				return
   220  			default:
   221  			}
   222  
   223  			line := scanner.Text()
   224  			p.Send(PsqlMsg(&line))
   225  		}
   226  	}()
   227  
   228  	var errBuf bytes.Buffer
   229  	if _, err := stdcopy.StdCopy(w, &errBuf, out); err != nil {
   230  		return err
   231  	}
   232  	if errBuf.Len() > 0 {
   233  		return errors.New("Error running SQL: " + errBuf.String())
   234  	}
   235  
   236  	doneCh <- struct{}{}
   237  	p.Send(PsqlMsg(nil))
   238  
   239  	return nil
   240  }