mirror of
https://github.com/hanzo-io/cloud.git
synced 2026-10-30 18:57:42 +00:00
356 lines
9.3 KiB
Go
356 lines
9.3 KiB
Go
package main
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/go-mysql-org/go-mysql/replication"
|
|
_ "github.com/marcboeker/go-duckdb/v2"
|
|
"vitess.io/vitess/go/vt/sqlparser"
|
|
|
|
"github.com/xitongsys/parquet-go-source/local"
|
|
"github.com/xitongsys/parquet-go/parquet"
|
|
"github.com/xitongsys/parquet-go/source"
|
|
"github.com/xitongsys/parquet-go/writer"
|
|
)
|
|
|
|
const CREATE_TABLE_SQL string = `
|
|
CREATE TABLE IF NOT EXISTS query (
|
|
binlog VARCHAR,
|
|
db_name VARCHAR,
|
|
table_name VARCHAR,
|
|
timestamp INTEGER,
|
|
type VARCHAR,
|
|
row_id INTEGER,
|
|
event_size INTEGER
|
|
)
|
|
`
|
|
|
|
const INSERT_QUERY_SQL string = "INSERT INTO query (binlog, db_name, table_name, timestamp, type, row_id, event_size) VALUES %s"
|
|
|
|
type Query struct {
|
|
Timestamp uint32
|
|
Metadata SQLSourceMetadata
|
|
RowId int32
|
|
EventSize uint32
|
|
SQL string
|
|
}
|
|
|
|
type BinlogIndexer struct {
|
|
BatchSize int
|
|
binlogName string
|
|
binlogPath string
|
|
|
|
// State
|
|
queries []Query
|
|
currentRowId int32
|
|
|
|
// Internal
|
|
db *sql.DB
|
|
fw source.ParquetFile
|
|
pw *writer.ParquetWriter
|
|
parser *replication.BinlogParser
|
|
sqlParser *sqlparser.Parser
|
|
isClosed bool
|
|
|
|
// AnnotateRowsEvent parsing related
|
|
annotateRowsEvent *replication.MariadbAnnotateRowsEvent
|
|
annotateRowsEventTimestamp uint32
|
|
tableMapEvents [][]string // list of [database, table]
|
|
annotateRowsEventSize uint32 // actual event + related events (table map + write rows)
|
|
}
|
|
|
|
type ParquetRow struct {
|
|
Id int32 `parquet:"name=id, type=INT32"`
|
|
Query string `parquet:"name=query, type=BYTE_ARRAY, convertedtype=UTF8, encoding=PLAIN_DICTIONARY"`
|
|
}
|
|
|
|
//export NewBinlogIndexer
|
|
func NewBinlogIndexer(basePath string, binlogPath string, databaseFilename string, batchSize int) (*BinlogIndexer, error) {
|
|
if _, err := os.Stat(basePath); os.IsNotExist(err) {
|
|
return nil, fmt.Errorf("base path does not exist: %w", err)
|
|
}
|
|
binlogFilename := filepath.Base(binlogPath)
|
|
db, err := sql.Open("duckdb", filepath.Join(basePath, databaseFilename))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open database: %w", err)
|
|
}
|
|
// Create SQL sql_parser
|
|
sql_parser, err := sqlparser.New(sqlparser.Options{})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create sql parser: %w", err)
|
|
}
|
|
|
|
// Create table
|
|
_, err = db.Exec(CREATE_TABLE_SQL)
|
|
if err != nil {
|
|
_ = db.Close()
|
|
return nil, fmt.Errorf("failed to create table: %w", err)
|
|
}
|
|
|
|
/*
|
|
* Delete current binlog data (if exists)
|
|
* Because, parquet file doesn't support appending data
|
|
* So we need to delete the current binlog data before we start index it again
|
|
*/
|
|
|
|
_, err = db.Exec("DELETE FROM query WHERE binlog = ?", binlogFilename)
|
|
if err != nil {
|
|
_ = db.Close()
|
|
return nil, fmt.Errorf("failed to delete binlog data: %w", err)
|
|
}
|
|
|
|
parquet_filepath := filepath.Join(basePath, fmt.Sprintf("queries_%s.parquet", binlogFilename))
|
|
_ = os.Remove(parquet_filepath)
|
|
|
|
// Estimate out row group size
|
|
pageSize, err := EstimateParquetPageSize(binlogPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to estimate row group size: %w", err)
|
|
}
|
|
// Create parquet writer
|
|
fw, err := local.NewLocalFileWriter(parquet_filepath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create local file: %w", err)
|
|
}
|
|
|
|
parquetWriter, err := writer.NewParquetWriter(fw, new(ParquetRow), 1)
|
|
parquetWriter.RowGroupSize = 128 * 1024 * 1024 // 128MB
|
|
|
|
parquetWriter.PageSize = pageSize
|
|
parquetWriter.CompressionType = parquet.CompressionCodec_ZSTD
|
|
|
|
return &BinlogIndexer{
|
|
BatchSize: batchSize,
|
|
binlogName: binlogFilename,
|
|
binlogPath: binlogPath,
|
|
queries: make([]Query, 0),
|
|
currentRowId: 1,
|
|
db: db,
|
|
fw: fw,
|
|
pw: parquetWriter,
|
|
parser: replication.NewBinlogParser(),
|
|
sqlParser: sql_parser,
|
|
isClosed: false,
|
|
tableMapEvents: make([][]string, 0),
|
|
}, nil
|
|
}
|
|
|
|
func (p *BinlogIndexer) Start() error {
|
|
err := p.parser.ParseFile(p.binlogPath, 0, p.onBinlogEvent)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse binlog: %w", err)
|
|
}
|
|
// Check for any pending annotate rows event
|
|
p.commitAnnotateRowsEvent()
|
|
// Flush the last batch
|
|
err = p.flush()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to flush: %w", err)
|
|
}
|
|
// Close everything
|
|
p.Close()
|
|
return nil
|
|
}
|
|
|
|
func (p *BinlogIndexer) onBinlogEvent(e *replication.BinlogEvent) error {
|
|
commitAnnotateRowsEvent := false
|
|
switch e.Header.EventType {
|
|
case replication.MARIADB_ANNOTATE_ROWS_EVENT:
|
|
if event, ok := e.Event.(*replication.MariadbAnnotateRowsEvent); ok {
|
|
p.annotateRowsEvent = event
|
|
p.annotateRowsEventSize = e.Header.EventSize
|
|
p.annotateRowsEventTimestamp = e.Header.Timestamp
|
|
}
|
|
case replication.TABLE_MAP_EVENT:
|
|
if event, ok := e.Event.(*replication.TableMapEvent); ok {
|
|
p.tableMapEvents = append(p.tableMapEvents, []string{string(event.Schema), string(event.Table)})
|
|
p.annotateRowsEventSize += e.Header.EventSize
|
|
}
|
|
case replication.WRITE_ROWS_EVENTv1, replication.UPDATE_ROWS_EVENTv1, replication.DELETE_ROWS_EVENTv1:
|
|
if _, ok := e.Event.(*replication.RowsEvent); ok {
|
|
p.annotateRowsEventSize += e.Header.EventSize
|
|
}
|
|
case replication.QUERY_EVENT:
|
|
if event, ok := e.Event.(*replication.QueryEvent); ok {
|
|
p.addQuery(string(event.Query), string(event.Schema), e.Header.Timestamp, e.Header.EventSize)
|
|
}
|
|
commitAnnotateRowsEvent = true
|
|
default:
|
|
commitAnnotateRowsEvent = true
|
|
}
|
|
|
|
if commitAnnotateRowsEvent {
|
|
p.commitAnnotateRowsEvent()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *BinlogIndexer) commitAnnotateRowsEvent() {
|
|
if p.annotateRowsEvent == nil {
|
|
return
|
|
}
|
|
|
|
if len(p.tableMapEvents) == 0 {
|
|
return
|
|
}
|
|
|
|
metadata := ExtractSQLMetadata(string(p.annotateRowsEvent.Query), p.sqlParser, "")
|
|
metadata.Tables = make([]*SQLTable, 0)
|
|
for _, table := range p.tableMapEvents {
|
|
metadata.Tables = append(metadata.Tables, &SQLTable{
|
|
Database: table[0],
|
|
Table: table[1],
|
|
})
|
|
}
|
|
|
|
p.queries = append(p.queries, Query{
|
|
Timestamp: p.annotateRowsEventTimestamp,
|
|
Metadata: metadata,
|
|
RowId: p.currentRowId,
|
|
EventSize: p.annotateRowsEventSize,
|
|
SQL: string(p.annotateRowsEvent.Query),
|
|
})
|
|
p.currentRowId += 1
|
|
|
|
// reset
|
|
p.annotateRowsEvent = nil
|
|
p.annotateRowsEventSize = 0
|
|
p.annotateRowsEventTimestamp = 0
|
|
p.tableMapEvents = make([][]string, 0)
|
|
|
|
p.flushIfNeeded()
|
|
}
|
|
|
|
func (p *BinlogIndexer) addQuery(query string, schema string, timestamp uint32, eventSize uint32) {
|
|
metadata := ExtractSQLMetadata(query, p.sqlParser, string(schema))
|
|
|
|
p.queries = append(p.queries, Query{
|
|
Timestamp: timestamp,
|
|
Metadata: metadata,
|
|
RowId: p.currentRowId,
|
|
EventSize: eventSize,
|
|
SQL: query,
|
|
})
|
|
p.currentRowId += 1
|
|
p.flushIfNeeded()
|
|
}
|
|
|
|
func (p *BinlogIndexer) flushIfNeeded() {
|
|
if len(p.queries) >= p.BatchSize {
|
|
err := p.flush()
|
|
if err != nil {
|
|
fmt.Printf("[WARN] failed to flush: %v\n", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *BinlogIndexer) flush() error {
|
|
if len(p.queries) == 0 {
|
|
return nil
|
|
}
|
|
// Create transaction
|
|
tx, err := p.db.Begin()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to begin db transaction: %w", err)
|
|
}
|
|
|
|
defer func() {
|
|
// In case of error rollback changes
|
|
_ = tx.Rollback()
|
|
}()
|
|
|
|
// Write the queries to db
|
|
batch := make([]string, 0, len(p.queries))
|
|
for _, query := range p.queries {
|
|
for _, table := range query.Metadata.Tables {
|
|
batch = append(batch, fmt.Sprintf("('%s', '%s', '%s', %d, '%s', %d, %d)",
|
|
p.binlogName, table.Database, table.Table, query.Timestamp, query.Metadata.Type, query.RowId, query.EventSize))
|
|
}
|
|
}
|
|
|
|
// Insert the queries
|
|
_, err = tx.Exec(fmt.Sprintf(INSERT_QUERY_SQL, strings.Join(batch, ",")))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to insert queries: %w", err)
|
|
}
|
|
|
|
// Release memory of batch
|
|
batch = nil
|
|
|
|
// Insert all query in parquet file
|
|
for _, query := range p.queries {
|
|
if err = p.pw.Write(ParquetRow{
|
|
Id: query.RowId,
|
|
Query: query.SQL,
|
|
}); err != nil {
|
|
return fmt.Errorf("failed to write query to parquet file: %w", err)
|
|
}
|
|
}
|
|
|
|
// Commit the transaction
|
|
err = tx.Commit()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to commit db transaction: %w", err)
|
|
}
|
|
|
|
// Clear the queries
|
|
p.queries = make([]Query, 0)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *BinlogIndexer) Close() {
|
|
if p.isClosed {
|
|
return
|
|
}
|
|
// Do a final flush
|
|
if err := p.flush(); err != nil {
|
|
fmt.Printf("[WARN] failed to flush: %v\n", err)
|
|
}
|
|
// try to stop the parquet writer
|
|
if err := p.pw.WriteStop(); err != nil {
|
|
fmt.Printf("[WARN] failed to stop parquet writer: %v\n", err)
|
|
}
|
|
|
|
// try to close the parquet file
|
|
if err := p.fw.Close(); err != nil {
|
|
fmt.Printf("[WARN] failed to close parquet file: %v\n", err)
|
|
}
|
|
// try to close the db
|
|
if err := p.db.Close(); err != nil {
|
|
fmt.Printf("[WARN] failed to close db: %v\n", err)
|
|
}
|
|
|
|
p.isClosed = true
|
|
}
|
|
|
|
//export RemoveBinlogIndex
|
|
func RemoveBinlogIndex(basePath string, binlogPath string, databaseFilename string) error {
|
|
binlogFilename := filepath.Base(binlogPath)
|
|
|
|
db, err := sql.Open("duckdb", filepath.Join(basePath, databaseFilename))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open database: %w", err)
|
|
}
|
|
|
|
defer func() {
|
|
_ = db.Close()
|
|
}()
|
|
|
|
// drop binlog
|
|
_, err = db.Exec("DELETE FROM query WHERE binlog = ?", binlogFilename)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete binlog data: %w", err)
|
|
}
|
|
|
|
// drop parquet file
|
|
parquetFilepath := filepath.Join(basePath, fmt.Sprintf("queries_%s.parquet", binlogFilename))
|
|
_ = os.Remove(parquetFilepath)
|
|
return nil
|
|
}
|