201 lines
		
	
	
		
			4.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			201 lines
		
	
	
		
			4.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright (c) 2016 MariaDB Corporation Ab
 | |
| //
 | |
| // Use of this software is governed by the Business Source License included
 | |
| // in the LICENSE.TXT file and at www.mariadb.com/bsl11.
 | |
| //
 | |
| // Change Date: 2023-12-18
 | |
| //
 | |
| // On the date above, in accordance with the Business Source License, use
 | |
| // of this software will be governed by version 2 or later of the General
 | |
| // Public License.
 | |
| 
 | |
| package main
 | |
| 
 | |
| import (
 | |
| 	"database/sql"
 | |
| 	"encoding/json"
 | |
| 	"flag"
 | |
| 	"fmt"
 | |
| 	"log"
 | |
| 	"os"
 | |
| 	"regexp"
 | |
| 	"strconv"
 | |
| )
 | |
| 
 | |
| import _ "github.com/go-sql-driver/mysql"
 | |
| 
 | |
| var host = flag.String("host", "localhost", "Server address")
 | |
| var port = flag.Int("port", 3306, "Server port")
 | |
| var user = flag.String("user", "", "Server user")
 | |
| var passwd = flag.String("password", "", "Server password")
 | |
| var debug = flag.Bool("debug", false, "Debug output")
 | |
| 
 | |
| func PrintUsage() {
 | |
| 	fmt.Println(`Usage: cdc_schema [OPTIONS]
 | |
| 
 | |
| This program generates CDC schema files for all the tables in a database. The
 | |
| schema files need to be generated if the binary log files do not contain the
 | |
| CREATE TABLE events that define the table layout.
 | |
| 
 | |
| The "user" and "password" flags are required.
 | |
| `)
 | |
| 
 | |
| 	flag.PrintDefaults()
 | |
| }
 | |
| 
 | |
| // Avro field
 | |
| type Field struct {
 | |
| 	Name     string `json:"name"`
 | |
| 	Type     []string `json:"type"`
 | |
| 	RealType string `json:"real_type"`
 | |
| 	Length   int    `json:"length"`
 | |
| }
 | |
| 
 | |
| // Avro schema
 | |
| type Schema struct {
 | |
| 	Namespace string  `json:"namespace"`
 | |
| 	Type      string  `json:"type"`
 | |
| 	Name      string  `json:"name"`
 | |
| 	Fields    []Field `json:"fields"`
 | |
| }
 | |
| 
 | |
| // Debugging output helper function
 | |
| func LogObject(obj interface{}) {
 | |
| 	js, err := json.Marshal(obj)
 | |
| 	if err != nil {
 | |
| 		log.Fatal("Failed to parse object: ", err)
 | |
| 	} else {
 | |
| 		log.Println("Unsupported type: ", string(js))
 | |
| 	}
 | |
| }
 | |
| 
 | |
| var field_re *regexp.Regexp
 | |
| var length_re *regexp.Regexp
 | |
| 
 | |
| // Convert the SQL type to the appropriate Avro type
 | |
| func (f *Field) ToAvroType() {
 | |
| 	orig := f.Type
 | |
| 	f.Type = field_re.ReplaceAllString(f.Type, "")
 | |
| 	f.Length = -1
 | |
| 	f.RealType = f.Type
 | |
| 	switch f.Type {
 | |
| 	case "date", "datetime", "time", "timestamp", "year", "tinytext", "text",
 | |
| 		"mediumtext", "longtext", "char", "varchar":
 | |
| 		f.Type = ["null", "string"]
 | |
| 		f.Length, _ = strconv.Atoi(length_re.ReplaceAllString(orig, "$1"))
 | |
| 	case "enum", "set":
 | |
| 		f.Type = ["null", "string"]
 | |
| 	case "tinyblob", "blob", "mediumblob", "longblob", "binary", "varbinary":
 | |
| 		f.Type = ["null", "bytes"]
 | |
| 	case "int", "smallint", "mediumint", "integer", "tinyint", "short", "bit":
 | |
| 		f.Type = ["null", "int"]
 | |
| 	case "float":
 | |
| 		f.Type = ["null", "float"]
 | |
| 	case "double", "decimal":
 | |
| 		f.Type = ["null", "double"]
 | |
| 	case "long", "bigint":
 | |
| 		f.Type = ["null", "long"]
 | |
| 	default:
 | |
| 		LogObject(f)
 | |
| 		f.Type = ["null", "string"]
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // Create and store the Avro schema to disk
 | |
| func StoreSchema(db *sql.DB, schema, table string) {
 | |
| 	file, err := os.Create(schema + "." + table + ".000001.avsc")
 | |
| 	if err != nil {
 | |
| 		log.Fatal("Failed to create file:", err)
 | |
| 	}
 | |
| 	defer file.Close()
 | |
| 
 | |
| 	encoder := json.NewEncoder(file)
 | |
| 	fields, err := db.Query("DESCRIBE " + schema + "." + table)
 | |
| 	if err != nil {
 | |
| 		log.Fatal("Failed to query for description of "+schema+"."+table+": ", err)
 | |
| 	}
 | |
| 	defer fields.Close()
 | |
| 
 | |
| 	fieldlist := make([]Field, 0, 10)
 | |
| 
 | |
| 	for fields.Next() {
 | |
| 		var field Field
 | |
| 		var Null, Key, Default, Extra string
 | |
| 
 | |
| 		fields.Scan(&field.Name, &field.Type, &Null, &Key, &Default, &Extra)
 | |
| 		field.ToAvroType()
 | |
| 		fieldlist = append(fieldlist, field)
 | |
| 	}
 | |
| 
 | |
| 	encoder.Encode(Schema{Namespace: "MaxScaleChangeDataSchema.avro", Type: "record", Name: "ChangeRecord", Fields: fieldlist})
 | |
| }
 | |
| 
 | |
| // Main funtion that queries the database for table names
 | |
| func main() {
 | |
| 	var err error
 | |
| 	field_re, err = regexp.Compile("[(].*")
 | |
| 
 | |
| 	if err != nil {
 | |
| 		log.Fatal("Error: ", err)
 | |
| 	}
 | |
| 
 | |
| 	length_re, err = regexp.Compile(".*[(](.*)[)].*")
 | |
| 
 | |
| 	if err != nil {
 | |
| 		log.Fatal("Error: ", err)
 | |
| 	}
 | |
| 
 | |
| 	flag.Usage = PrintUsage
 | |
| 	flag.Parse()
 | |
| 
 | |
| 	if len(*user) == 0 || len(*passwd) == 0 {
 | |
| 		log.Fatal("Both the -user and -password flags are mandatory. See output of -help for more details.")
 | |
| 	}
 | |
| 
 | |
| 	var connect_str string = *user + ":" + *passwd + "@tcp(" + *host + ":" + strconv.Itoa(*port) + ")/"
 | |
| 
 | |
| 	if *debug {
 | |
| 		log.Println("Connect string: ", connect_str)
 | |
| 	}
 | |
| 
 | |
| 	db, err := sql.Open("mysql", connect_str)
 | |
| 	if err != nil {
 | |
| 		log.Fatal("Failed to open connection to", *host, *port, ":", err)
 | |
| 	}
 | |
| 	defer db.Close()
 | |
| 
 | |
| 	databases, err := db.Query("SHOW DATABASES")
 | |
| 	if err != nil {
 | |
| 		log.Fatal("Failed to query for databases: ", err)
 | |
| 	}
 | |
| 	defer databases.Close()
 | |
| 
 | |
| 	for databases.Next() {
 | |
| 		var schemaname string
 | |
| 		databases.Scan(&schemaname)
 | |
| 
 | |
| 		// Skip the system databases
 | |
| 		switch schemaname {
 | |
| 		case "mysql", "information_schema", "performance_schema":
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		tables, err := db.Query("SHOW TABLES FROM " + schemaname)
 | |
| 		if err != nil {
 | |
| 			log.Fatal("Failed to query for tables from "+schemaname+": ", err)
 | |
| 		}
 | |
| 		defer tables.Close()
 | |
| 
 | |
| 		for tables.Next() {
 | |
| 			var tablename string
 | |
| 			tables.Scan(&tablename)
 | |
| 
 | |
| 			if *debug {
 | |
| 				log.Println("Processing", schemaname, ".", tablename)
 | |
| 			}
 | |
| 
 | |
| 			StoreSchema(db, schemaname, tablename)
 | |
| 		}
 | |
| 	}
 | |
| }
 | 
