| // Package schema enables checking the schema, both columns and indexes, of a database. |
| package schema |
| |
| import ( |
| "context" |
| "reflect" |
| "strings" |
| "time" |
| |
| "go.skia.org/infra/go/skerr" |
| "go.skia.org/infra/go/sql/pool" |
| ) |
| |
| const ( |
| // Timeout used on Contexts when making SQL requests. |
| sqlTimeout = time.Minute |
| SpannerDBType = "spanner" |
| CockroachDBType = "cockroachdb" |
| ) |
| |
| // TableNames takes in a "table type", that is a table whose fields are slices. |
| // Each field will be interpreted as a table. TableNames returns all expected |
| // table names in a string slice with table names lowercased. |
| // |
| // For example: |
| // |
| // "description", "taskresult" |
| func TableNames(tables interface{}) []string { |
| ret := []string{} |
| for _, structField := range reflect.VisibleFields(reflect.TypeOf(tables)) { |
| ret = append(ret, strings.ToLower(structField.Name)) |
| } |
| return ret |
| } |
| |
| // Description describes the schema for all tables. |
| type Description struct { |
| ColumnNameAndType map[string]string |
| IndexNames []string |
| } |
| |
| // Query to return the typesQuery for each column in all tables. |
| const TypesQuery = ` |
| SELECT |
| column_name, |
| CONCAT(data_type, ' def:', column_default, ' nullable:', is_nullable) |
| FROM |
| information_schema.columns |
| WHERE |
| table_name = $1; |
| ` |
| |
| const TypesQuerySpanner = ` |
| SELECT |
| column_name, |
| data_type || ' def:' || COALESCE(column_default, '') || ' nullable:' || COALESCE(is_nullable, '') |
| FROM |
| information_schema.columns |
| WHERE |
| table_name = $1; |
| ` |
| |
| // Query to return the index names for each table. |
| const IndexNameQuery = ` |
| SELECT DISTINCT |
| index_name |
| FROM |
| information_schema.statistics |
| WHERE |
| table_name = $1 |
| ORDER BY |
| index_name DESC |
| ` |
| |
| const SpannerIndexNameQuery = ` |
| SELECT DISTINCT |
| index_name |
| FROM |
| information_schema.indexes |
| WHERE |
| table_name = $1 |
| ORDER BY |
| index_name DESC |
| ` |
| |
| // GetDescription returns a Description populated for every table listed in |
| // `tables`. |
| func GetDescription(ctx context.Context, db pool.Pool, tables interface{}, databaseType string) (*Description, error) { |
| ctx, cancel := context.WithTimeout(ctx, sqlTimeout) |
| defer cancel() |
| colNameAndType := map[string]string{} |
| indexNames := []string{} |
| for _, tableName := range TableNames(tables) { |
| // Fill in colNameAndType. |
| typeQuery := TypesQuery |
| if databaseType == SpannerDBType { |
| typeQuery = TypesQuerySpanner |
| } |
| rows, err := db.Query(ctx, typeQuery, tableName) |
| if err != nil { |
| return nil, skerr.Wrap(err) |
| } |
| for rows.Next() { |
| var colName string |
| var colType string |
| err := rows.Scan(&colName, &colType) |
| if err != nil { |
| return nil, skerr.Wrap(err) |
| } |
| // Temporarily exclude TraceParams generated columns |
| if tableName == "traceparams" && colName != "trace_id" && |
| colName != "params" && colName != "createdat" { |
| continue |
| } |
| colNameAndType[tableName+"."+colName] = colType |
| } |
| |
| // Fill in indexNames. |
| indexQuery := IndexNameQuery |
| if databaseType == SpannerDBType { |
| indexQuery = SpannerIndexNameQuery |
| } |
| rows, err = db.Query(ctx, indexQuery, tableName) |
| if err != nil { |
| return nil, skerr.Wrap(err) |
| } |
| for rows.Next() { |
| var indexName string |
| err := rows.Scan(&indexName) |
| if err != nil { |
| return nil, skerr.Wrap(err) |
| } |
| // In version 22.1 CDB changed the name of the primary key from |
| // "primary" to <table_name>_pkey: |
| // https://github.com/cockroachdb/cockroach/pull/70604. |
| // |
| // But if you created a table in a version before 22.1 then the name |
| // of the key is preserved as "primary" going forward. Since we know |
| // every table will have a primary key the actual name isn't giving |
| // us any useful information so we just ignore it regardless of the |
| // name. |
| if indexName == "primary" || indexName == tableName+"_pkey" { |
| continue |
| } |
| // Temporarily exclude TraceParams generated indexes |
| if tableName == "traceparams" && indexName != "PRIMARY_KEY" { |
| continue |
| } |
| indexNames = append(indexNames, tableName+"."+indexName) |
| } |
| } |
| |
| return &Description{ |
| ColumnNameAndType: colNameAndType, |
| IndexNames: indexNames, |
| }, nil |
| } |