-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapply.go
More file actions
187 lines (165 loc) · 6.98 KB
/
Copy pathapply.go
File metadata and controls
187 lines (165 loc) · 6.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
package pistachio
import (
"context"
"fmt"
"io"
"strings"
"time"
"github.com/winebarrel/pistachio/model"
"github.com/winebarrel/pistachio/parser"
)
type ApplyOptions struct {
FilterOptions
DropPolicy
Files []string `arg:"" help:"Path to the desired schema SQL file(s)."`
PreSQL string `xor:"pre-sql" env:"PISTA_PRE_SQL" help:"SQL to execute before applying changes."`
PreSQLFile string `type:"path" xor:"pre-sql" env:"PISTA_PRE_SQL_FILE" help:"Path to a SQL file to execute before applying changes."`
ConcurrentlyPreSQL string `xor:"concurrently-pre-sql" env:"PISTA_CONCURRENTLY_PRE_SQL" help:"SQL to execute before CONCURRENTLY index DDL (e.g. SET lock_timeout). Runs outside any transaction, only when the diff contains CONCURRENTLY index DDL."`
ConcurrentlyPreSQLFile string `type:"path" xor:"concurrently-pre-sql" env:"PISTA_CONCURRENTLY_PRE_SQL_FILE" help:"Path to a SQL file to execute before CONCURRENTLY index DDL."`
WithTx bool `xor:"tx-mode" env:"PISTA_WITH_TX" help:"Execute pre-SQL and schema changes in a transaction."`
DisableIndexConcurrently bool `xor:"index-concurrently" env:"PISTA_DISABLE_INDEX_CONCURRENTLY" help:"Ignore CONCURRENTLY opt-ins (directive and inline) and emit plain CREATE/DROP INDEX."`
ForceIndexConcurrently bool `xor:"index-concurrently,tx-mode" env:"PISTA_FORCE_INDEX_CONCURRENTLY" help:"Force CONCURRENTLY on every CREATE/DROP INDEX, including pure drops. Cannot be combined with --with-tx."`
BulkAlter bool `env:"PISTA_BULK_ALTER" help:"Combine consecutive ALTER TABLE actions on the same table into a single statement. FK changes, RENAME, VALIDATE CONSTRAINT, RLS toggles, and skipped DROPs stay separate."`
}
// ApplyResult holds the result of an Apply operation.
type ApplyResult struct {
Count ObjectCount
DisallowedDrops string
// Applied reports whether any schema change was actually applied: schema
// DDL or an executed -- pista:execute statement. Pre-SQL,
// concurrently-pre-SQL, transaction control, search_path setup, and
// -- pista:execute directives skipped by their check SQL do not count.
Applied bool
// Duration is the elapsed time of the apply phase: every statement sent to
// the database (transaction BEGIN/COMMIT, pre-SQL, schema DDL, search_path
// setup, -- pista:execute check SQL, and execute statements) plus the time
// writing them to the output writer. It excludes connection setup and diff
// computation, and is zero unless Applied is true. With a fast writer it is
// dominated by database execution time.
Duration time.Duration
}
func (client *Client) Apply(ctx context.Context, options *ApplyOptions, w io.Writer) (*ApplyResult, error) {
if err := client.validateSchemas(); err != nil {
return nil, err
}
conn, err := client.connect(ctx)
if err != nil {
return nil, err
}
defer conn.Close(ctx) //nolint:errcheck
result, err := client.diffAll(ctx, conn, &diffAllOptions{
FilterOptions: options.FilterOptions,
DropPolicy: options.DropPolicy,
Files: options.Files,
PreSQL: options.PreSQL,
PreSQLFile: options.PreSQLFile,
ConcurrentlyPreSQL: options.ConcurrentlyPreSQL,
ConcurrentlyPreSQLFile: options.ConcurrentlyPreSQLFile,
DisableIndexConcurrently: options.DisableIndexConcurrently,
ForceIndexConcurrently: options.ForceIndexConcurrently,
BulkAlter: options.BulkAlter,
})
if err != nil {
return nil, err
}
if options.WithTx && result.HasConcurrentlyIndex {
return nil, fmt.Errorf("--with-tx cannot be used with CONCURRENTLY index operations")
}
applyResult := &ApplyResult{
Count: result.Count,
DisallowedDrops: strings.Join(result.DisallowedDrops, "\n"),
}
if len(result.Stmts) == 0 && len(result.ExecuteStmts) == 0 {
return applyResult, nil
}
start := time.Now()
applied := false
exec := conn.Exec
queryRow := conn.QueryRow
commit := func(context.Context) error { return nil }
if options.WithTx {
tx, err := conn.Begin(ctx)
if err != nil {
return nil, fmt.Errorf("failed to begin transaction: %w", err)
}
fmt.Fprintln(w, "-- Transaction started") //nolint:errcheck
committed := false
defer func() {
tx.Rollback(ctx) //nolint:errcheck
if !committed {
fmt.Fprintln(w, "-- Transaction rolled back") //nolint:errcheck
}
}()
exec = tx.Exec
queryRow = tx.QueryRow
commit = func(ctx context.Context) error {
if err := tx.Commit(ctx); err != nil {
return err
}
committed = true
fmt.Fprintln(w, "-- Transaction committed") //nolint:errcheck
return nil
}
}
// Pre-SQL and concurrently-pre-SQL are setup steps (e.g. SET lock_timeout),
// not schema changes, so they do not mark the apply as applied. Whether
// "-- No changes" is reported depends only on actual schema DDL and
// executed -- pista:execute statements.
if result.PreSQL != "" {
fmt.Fprintln(w, result.PreSQL) //nolint:errcheck
if _, err := exec(ctx, result.PreSQL); err != nil {
return nil, fmt.Errorf("failed to execute pre-SQL: %w", err)
}
}
// concurrently-pre-SQL is gated on HasConcurrentlyIndex so it only runs
// when there is CONCURRENTLY index DDL to apply. WithTx + HasConcurrentlyIndex
// is rejected above, so this always runs outside a transaction.
if result.ConcurrentlyPreSQL != "" && result.HasConcurrentlyIndex {
fmt.Fprintln(w, result.ConcurrentlyPreSQL) //nolint:errcheck
if _, err := exec(ctx, result.ConcurrentlyPreSQL); err != nil {
return nil, fmt.Errorf("failed to execute concurrently-pre-SQL: %w", err)
}
}
for _, stmt := range result.Stmts {
fmt.Fprintln(w, stmt) //nolint:errcheck
if _, err := exec(ctx, stmt); err != nil {
return nil, fmt.Errorf("failed to execute SQL: %s: %w", stmt, err)
}
applied = true
}
// Execute -- pista:execute statements after schema changes.
// Set search_path so unqualified names resolve to the configured schemas.
if len(result.ExecuteStmts) > 0 && len(client.Schemas) > 0 {
quoted := make([]string, len(client.Schemas))
for i, s := range client.Schemas {
quoted[i] = model.Ident(s)
}
searchPath := "SET search_path TO " + strings.Join(quoted, ", ")
if _, err := exec(ctx, searchPath); err != nil {
return nil, fmt.Errorf("failed to set search_path: %w", err)
}
}
for _, es := range result.ExecuteStmts {
shouldExecute := true
if es.CheckSQL != "" {
if err := queryRow(ctx, es.CheckSQL).Scan(&shouldExecute); err != nil {
return nil, fmt.Errorf("failed to evaluate check SQL: %s: %w", es.CheckSQL, err)
}
}
if shouldExecute {
fmt.Fprintln(w, parser.FormatExecuteStmt(es)) //nolint:errcheck
if _, err := exec(ctx, es.SQL); err != nil {
return nil, fmt.Errorf("failed to execute SQL: %s: %w", es.SQL, err)
}
applied = true
}
}
if err := commit(ctx); err != nil {
return nil, fmt.Errorf("failed to commit transaction: %w", err)
}
applyResult.Applied = applied
if applied {
applyResult.Duration = time.Since(start)
}
return applyResult, nil
}