175 lines
5.3 KiB
Go
175 lines
5.3 KiB
Go
//go:build ignore
|
|
|
|
// Read-only probe of the live `logistics` database for the customer-app work.
|
|
//
|
|
// STRICTLY READ-ONLY. It runs SELECTs against information_schema and a few
|
|
// COUNTs, and nothing else — no DDL, no INSERT, no UPDATE. It exists to answer
|
|
// one question before anybody boots the service against this database:
|
|
//
|
|
// is migrations.Migrate() actually additive here, or would it collide with
|
|
// something that already exists?
|
|
//
|
|
// main.go calls Migrate() on startup, so "just run the server and see" is a
|
|
// schema change to a database the Miler app and both consoles are live against.
|
|
// This is the safe half of that check.
|
|
//
|
|
// go run scratch/cx_readonly_probe.go
|
|
package main
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"strings"
|
|
|
|
_ "github.com/lib/pq"
|
|
)
|
|
|
|
// The tables migrations.Migrate() would create for the customer app.
|
|
var newTables = []string{
|
|
"serviceablestates",
|
|
"serviceabledistricts",
|
|
"pickupslottemplates",
|
|
"customerbookinglimits",
|
|
"bookingdestinations",
|
|
"bookingparcelphotos",
|
|
"bookingstageevents",
|
|
"customerrefreshtokens",
|
|
"customerdevices",
|
|
}
|
|
|
|
// Columns it would add to existing tables. These are the ones that touch rows
|
|
// the live system already reads.
|
|
var newColumns = map[string][]string{
|
|
"pickupbookings": {
|
|
"slotid", "customerstage", "customerstatus",
|
|
"estimateminrupees", "estimatemaxrupees", "routekm",
|
|
"pickuptitle", "pickupsub", "cancelreason",
|
|
},
|
|
"bookingparcels": {"bookingdestinationid"},
|
|
}
|
|
|
|
var newSequences = []string{"cx_booking_reference_seq", "cx_tracking_seq"}
|
|
|
|
func main() {
|
|
// Credentials come from the environment only. The other probes in this
|
|
// folder hardcode the production password; that is one more copy of a live
|
|
// credential in the repository than there needs to be, and this file does
|
|
// not add another.
|
|
//
|
|
// set -a && . ./.env && set +a && go run scratch/cx_readonly_probe.go
|
|
host := env("DB_HOST", "")
|
|
port := env("DB_PORT", "5433")
|
|
user := env("DB_USER", "")
|
|
pass := env("DB_PASSWORD", "")
|
|
name := env("DB_NAME", "logistics")
|
|
|
|
if host == "" || user == "" || pass == "" {
|
|
log.Fatal("set DB_HOST, DB_USER and DB_PASSWORD first — " +
|
|
"e.g. `set -a && . ./.env && set +a`")
|
|
}
|
|
|
|
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable",
|
|
host, user, pass, name, port)
|
|
|
|
db, err := sql.Open("postgres", dsn)
|
|
if err != nil {
|
|
log.Fatalf("open: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
if err := db.Ping(); err != nil {
|
|
log.Fatalf("ping: %v", err)
|
|
}
|
|
fmt.Printf("connected read-only to %s@%s:%s/%s\n\n", user, host, port, name)
|
|
|
|
var total int
|
|
must(db.QueryRow(`SELECT count(*) FROM information_schema.tables
|
|
WHERE table_schema = 'public'`).Scan(&total))
|
|
fmt.Printf("existing public tables: %d\n\n", total)
|
|
|
|
fmt.Println("── tables the migration would CREATE ──")
|
|
existing := 0
|
|
for _, t := range newTables {
|
|
var n int
|
|
must(db.QueryRow(`SELECT count(*) FROM information_schema.tables
|
|
WHERE table_schema = 'public' AND table_name = $1`, t).Scan(&n))
|
|
if n > 0 {
|
|
existing++
|
|
var rows int
|
|
if err := db.QueryRow(fmt.Sprintf(`SELECT count(*) FROM %q`, t)).Scan(&rows); err != nil {
|
|
rows = -1
|
|
}
|
|
fmt.Printf(" %-24s ALREADY EXISTS (%d rows)\n", t, rows)
|
|
} else {
|
|
fmt.Printf(" %-24s absent — would be created\n", t)
|
|
}
|
|
}
|
|
|
|
fmt.Println("\n── columns the migration would ADD to live tables ──")
|
|
for table, cols := range newColumns {
|
|
var tableRows int
|
|
if err := db.QueryRow(fmt.Sprintf(`SELECT count(*) FROM %q`, table)).Scan(&tableRows); err != nil {
|
|
fmt.Printf(" %s: could not count rows: %v\n", table, err)
|
|
continue
|
|
}
|
|
fmt.Printf(" %s (%d rows):\n", table, tableRows)
|
|
for _, col := range cols {
|
|
var n int
|
|
must(db.QueryRow(`SELECT count(*) FROM information_schema.columns
|
|
WHERE table_schema = 'public' AND table_name = $1 AND column_name = $2`,
|
|
table, col).Scan(&n))
|
|
state := "absent — would be added"
|
|
if n > 0 {
|
|
state = "ALREADY EXISTS"
|
|
}
|
|
fmt.Printf(" %-24s %s\n", col, state)
|
|
}
|
|
}
|
|
|
|
fmt.Println("\n── sequences ──")
|
|
for _, s := range newSequences {
|
|
var n int
|
|
must(db.QueryRow(`SELECT count(*) FROM information_schema.sequences
|
|
WHERE sequence_schema = 'public' AND sequence_name = $1`, s).Scan(&n))
|
|
state := "absent — would be created"
|
|
if n > 0 {
|
|
state = "ALREADY EXISTS"
|
|
}
|
|
fmt.Printf(" %-30s %s\n", s, state)
|
|
}
|
|
|
|
// Anything that would make the migration NOT additive: a name collision on
|
|
// a table that is not ours, or a column of an incompatible type.
|
|
fmt.Println("\n── collision check ──")
|
|
if existing == 0 {
|
|
fmt.Println(" none — every customer-app table is new to this database")
|
|
} else {
|
|
fmt.Printf(" %d of %d target tables already exist; inspect them before migrating\n",
|
|
existing, len(newTables))
|
|
}
|
|
|
|
// How much live data the additive columns would land on.
|
|
var bookings, consignments int
|
|
_ = db.QueryRow(`SELECT count(*) FROM pickupbookings`).Scan(&bookings)
|
|
_ = db.QueryRow(`SELECT count(*) FROM consignments`).Scan(&consignments)
|
|
fmt.Printf("\nlive rows that would gain nullable columns: pickupbookings=%d consignments=%d\n",
|
|
bookings, consignments)
|
|
|
|
fmt.Println("\nNOTHING WAS WRITTEN. This probe issues SELECTs only.")
|
|
}
|
|
|
|
func env(key, fallback string) string {
|
|
if v := strings.TrimSpace(os.Getenv(key)); v != "" {
|
|
return v
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func must(err error) {
|
|
if err != nil {
|
|
log.Fatalf("query: %v", err)
|
|
}
|
|
}
|