blob: a29c16b1f9dd1fed158d8565099bcf1c187757d8 [file] [log] [blame]
Serge Bazanski424e2012023-02-15 23:31:49 +01001package reflection
2
3import (
4 "context"
5 "database/sql"
6 "fmt"
7 "strings"
8
9 "github.com/iancoleman/strcase"
10 "k8s.io/klog/v2"
11)
12
13// Schema contains information about the tag types in a BMDB. It also contains an
14// active connection to the BMDB, allowing retrieval of data based on the
15// detected schema.
16//
17// It also contains an embedded connection to the CockroachDB database backing
18// this BMDB which is then used to retrieve data described by this schema.
19type Schema struct {
20 // TagTypes is the list of tag types extracted from the BMDB.
21 TagTypes []TagType
22 // Version is the go-migrate schema version of the BMDB this schema was extracted
23 // from. By convention, it is a stringified base-10 number representing the number
24 // of seconds since UNIX epoch of when the migration version was created, but
25 // this is not guaranteed.
26 Version string
27
28 db *sql.DB
29}
30
31// TagType describes the type of a BMDB Tag. Each tag in turn corresponds to a
32// CockroachDB database.
33type TagType struct {
34 // NativeName is the name of the table that holds tags of this type.
35 NativeName string
36 // Fields are the types of fields contained in this tag type.
37 Fields []TagFieldType
38}
39
40// Name returns the canonical name of this tag type. For example, a table named
41// machine_agent_started will have a canonical name AgentStarted.
42func (r *TagType) Name() string {
43 tableSuffix := strings.TrimPrefix(r.NativeName, "machine_")
44 parts := strings.Split(tableSuffix, "_")
45 // Capitalize some known acronyms.
46 for i, p := range parts {
47 parts[i] = strings.ReplaceAll(p, "os", "OS")
48 }
49 return strcase.ToCamel(strings.Join(parts, "_"))
50}
51
52// TagFieldType is the type of a field within a BMDB Tag. Each tag field in turn
53// corresponds to a column inside its Tag table.
54type TagFieldType struct {
55 // NativeName is the name of the column that holds this field type. It is also
56 // the canonical name of the field type.
57 NativeName string
58 // NativeType is the CockroachDB type name of this field.
59 NativeType string
60 // NativeUDTName is the CockroachDB user-defined-type name of this field. This is
61 // only valid if NativeType is 'USER-DEFINED'.
62 NativeUDTName string
63}
64
65// HumanType returns a human-readable representation of the field's type. This is
66// not well-defined, and should be used only informatively.
67func (r *TagFieldType) HumanType() string {
68 switch r.NativeType {
69 case "USER-DEFINED":
70 return r.NativeUDTName
71 case "timestamp with time zone":
72 return "timestamp"
73 case "bytea":
74 return "bytes"
75 default:
76 return r.NativeType
77 }
78}
79
80// Reflect builds a runtime BMDB schema from a raw SQL connection to the BMDB
81// database. You're probably looking for bmdb.Connection.Reflect.
82func Reflect(ctx context.Context, db *sql.DB) (*Schema, error) {
83 // Get all tables in the currently connected to database.
84 rows, err := db.QueryContext(ctx, `
85 SELECT table_name
86 FROM information_schema.tables
87 WHERE table_catalog = current_database()
88 AND table_schema = 'public'
89 AND table_name LIKE 'machine\_%'
90 `)
91 if err != nil {
92 return nil, fmt.Errorf("could not query table names: %w", err)
93 }
94 defer rows.Close()
95
96 // Collect all table names for further processing.
97 var tableNames []string
98 for rows.Next() {
99 var name string
100 if err := rows.Scan(&name); err != nil {
101 return nil, fmt.Errorf("table name scan failed: %w", err)
102 }
103 tableNames = append(tableNames, name)
104 }
105
106 // Start processing each table into a TagType.
107 tags := make([]TagType, 0, len(tableNames))
108 for _, tagName := range tableNames {
109 // Get all columns of the table.
110 rows, err := db.QueryContext(ctx, `
111 SELECT column_name, data_type, udt_name
112 FROM information_schema.columns
113 WHERE table_catalog = current_database()
114 AND table_schema = 'public'
115 AND table_name = $1
116 `, tagName)
117 if err != nil {
118 return nil, fmt.Errorf("could not query columns: %w", err)
119 }
120
121 tag := TagType{
122 NativeName: tagName,
123 }
124
125 // Build field types from columns.
126 foundMachineID := false
127 for rows.Next() {
128 var column_name, data_type, udt_name string
129 if err := rows.Scan(&column_name, &data_type, &udt_name); err != nil {
130 rows.Close()
131 return nil, fmt.Errorf("column scan failed: %w", err)
132 }
133 if column_name == "machine_id" {
134 foundMachineID = true
135 continue
136 }
137 tag.Fields = append(tag.Fields, TagFieldType{
138 NativeName: column_name,
139 NativeType: data_type,
140 NativeUDTName: udt_name,
141 })
142 }
143
144 // Make sure there's a machine_id key in the table, then remove it.
145 if !foundMachineID {
146 klog.Warningf("Table %q has no machine_id column, skipping", tag.NativeName)
147 continue
148 }
149
150 tags = append(tags, tag)
151 }
152
153 // Retrieve version information from go-migrate's schema_migrations table.
154 var version string
155 var dirty bool
156 if err := db.QueryRowContext(ctx, "SELECT version, dirty FROM schema_migrations").Scan(&version, &dirty); err != nil {
157 return nil, fmt.Errorf("could not select schema version: %w", err)
158 }
159 if dirty {
160 version += " DIRTY!!!"
161 }
162
163 return &Schema{
164 TagTypes: tags,
165 Version: version,
166
167 db: db,
168 }, nil
169}