mysql

package
v1.26.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 10, 2026 License: MIT Imports: 12 Imported by: 0

README

MySQL adapter

A high-throughput adapter for MySQL and MariaDB.

Status: fully working — 58/58 CLI integration tests pass (MySQL 8.4, 2026-04-21)

What it does

  • The full TDTP specification
  • Uses schema.Converter for strict typing
  • Every import strategy: Replace (ON DUPLICATE KEY UPDATE), Ignore (INSERT IGNORE), Fail, Copy
  • TDTQL filtering pushed down into SQL (WHERE, ORDER BY, LIMIT/OFFSET, IN)
  • Bracket-quoted names containing spaces and $ — the NAV, BC and ERP style
  • Errors handled properly, through the MySQL driver's own types
  • Transactional safety
  • Every MySQL data type
  • Compact format (v1.3.1), zstd and kanzi compression, hash verification
  • Cross-database round-trip: MySQL to MySQL, and MySQL to SQLite

Installing

go get github.com/go-sql-driver/mysql

Quick start

package main

import (
    "context"
    "fmt"

    "github.com/queuebridge/tdtp/pkg/adapters"
    _ "github.com/queuebridge/tdtp/pkg/adapters/mysql"
)

func main() {
    ctx := context.Background()

    // Connect to MySQL
    adapter, err := adapters.New("mysql", adapters.Config{
        DSN: "user:password@tcp(localhost:3306)/dbname?parseTime=true",
    })
    if err != nil {
        panic(err)
    }
    defer adapter.Close(ctx)

    // Export the data
    packets, err := adapter.ExportTable(ctx, "users")
    if err != nil {
        panic(err)
    }

    fmt.Printf("Exported %d packets\n", len(packets))
}

The DSN

Basic form
user:password@tcp(host:port)/dbname?parameters
user:password@tcp(localhost:3306)/mydb?parseTime=true&charset=utf8mb4&loc=UTC

The ones that matter:

  • parseTime=true — parses DATE, DATETIME and TIMESTAMP for you
  • charset=utf8mb4 — full Unicode
  • loc=UTC — the time zone for TIMESTAMP

Supported data types

TDTP to MySQL
TDTP type MySQL type Notes
INTEGER BIGINT INT when Length ≤ 4
REAL FLOAT -
DOUBLE DOUBLE -
DECIMAL(p,s) DECIMAL(p,s) Defaults to (18,2)
TEXT VARCHAR(n) / TEXT VARCHAR up to 65535
VARCHAR(n) VARCHAR(n) Defaults to 255
CHAR(n) CHAR(n) Defaults to 1
BOOLEAN TINYINT(1) 0/1
DATE DATE YYYY-MM-DD
DATETIME DATETIME With a time zone
TIMESTAMP TIMESTAMP UTC
BLOB BLOB Base64 inside TDTP

Import strategies

1. StrategyReplace (UPSERT)
// INSERT ... ON DUPLICATE KEY UPDATE
err := adapter.ImportPacket(ctx, pkt, adapters.StrategyReplace)
  • On a primary-key match → UPDATE the existing row
  • With no primary key → REPLACE INTO
2. StrategyIgnore
// INSERT IGNORE
err := adapter.ImportPacket(ctx, pkt, adapters.StrategyIgnore)
  • Duplicates are skipped without an error
  • The fastest option
3. StrategyFail
// INSERT
err := adapter.ImportPacket(ctx, pkt, adapters.StrategyFail)
  • Returns an error on a duplicate
  • Strict control over what lands
4. StrategyCopy
// Equivalent to INSERT — MySQL has no COPY
err := adapter.ImportPacket(ctx, pkt, adapters.StrategyCopy)

TDTQL filtering

The pushed-down SQL path
query := &packet.Query{
    Filters: &packet.Filters{
        Condition: &packet.Condition{
            Field:    "age",
            Operator: ">",
            Value:    "18",
        },
    },
    Limit: 100,
    Offset: 0,
}

packets, err := adapter.ExportTableWithQuery(ctx, "users", query, "", "")

TDTQL is translated into MySQL SQL automatically, with:

  • backticks around identifiers
  • native LIMIT and OFFSET
  • the database doing the optimising
The in-memory fallback

Where the translation is not possible, filtering falls back to memory automatically.

Performance

Batch Insert
packets := []*packet.DataPacket{pkt1, pkt2, pkt3}
err := adapter.ImportPackets(ctx, packets, adapters.StrategyReplace)
// Every packet in one transaction
Query handling
  • prepared statements throughout
  • multiple packets handled in one transaction
  • filtering in SQL, so the whole table is never loaded

Error handling

Duplicate Key
if err != nil {
    if mysqlErr, ok := err.(*mysql.MySQLError); ok {
        if mysqlErr.Number == 1062 {
            // Handle a duplicate key
        }
    }
}
Error kinds
  • 1062 - Duplicate entry (PRIMARY/UNIQUE KEY)
  • 1451 - Foreign key constraint
  • 1452 - Cannot add or update child row

Worked examples

Export with a filter
query := &packet.Query{
    Filters: &packet.Filters{
        Logic: "AND",
        Conditions: []*packet.Condition{
            {Field: "status", Operator: "=", Value: "active"},
            {Field: "created_at", Operator: ">", Value: "2024-01-01"},
        },
    },
    OrderBy: []packet.OrderField{
        {Field: "created_at", Direction: "DESC"},
    },
    Limit: 1000,
}

packets, _ := adapter.ExportTableWithQuery(ctx, "users", query, "sender", "recipient")
Import, creating the table
// The adapter creates the table if it is missing
err := adapter.ImportPacket(ctx, pkt, adapters.StrategyReplace)
Reading metadata
// The table's schema
schema, err := adapter.GetTableSchema(ctx, "users")

// Row count
count, err := adapter.GetTableRowCount(ctx, "users")

// Table size
size, err := adapter.GetTableSize(ctx, "users")

Implementation notes

Type Conversion

schema.Converter handles:

  • type validation
  • conversion between TDTP and MySQL
  • precision and scale for DECIMAL
  • formatting DATE, DATETIME and TIMESTAMP correctly
  • base64 for BLOB
Transaction Safety
  • every import runs in a transaction
  • ROLLBACK on any error
  • several packets can share one transaction
SQL Generation
  • identifiers are escaped automatically (with backticks)
  • parameterised queries, against SQL injection
  • indexes are used where they help

🎓 Best Practices

  1. Always set parseTime=true in the DSN if you touch temporal types
  2. Use charset=utf8mb4 for full Unicode
  3. Index the fields you filter on
  4. Use StrategyReplace where the operation must be idempotent
  5. Handle errors through the MySQL driver's types

Test status

CLI integration tests: 58 / 58 PASS (MySQL 8.4.9, 2026-04-21)

Group What it covers Tests
T1 Basic Export (rows, fields, list) 4
T2 TDTQL Filters (WHERE, IN, ORDER BY, LIMIT, bracket-quoted) 9
T3 Compression (zstd/kanzi, --hash, corruption) 6
T4 MySQL → MySQL Roundtrip (strategies, projection, ERP-style names) 8
T5 File Integrity (--test, --inspect) 3
T6 Edge Cases (empty result, errors) 3
T7 Compact Format v1.3.1 4
T8 MySQL → SQLite Roundtrip (cross-DB) 5
T9 Diff 7
T10 Merge (union, intersection, append, left/right priority) 9

Running them:

# 1. Start the container, from the repository root
docker compose up -d mysql

# 2. Run the tests
TDTPCLI_BIN=/tmp/tdtpcli.exe py -3 tests/cli/test_mysql.py

# 3. Just one group
TDTPCLI_BIN=/tmp/tdtpcli.exe py -3 tests/cli/test_mysql.py T4

Compatibility

  • ✅ MySQL 5.7+
  • ✅ MySQL 8.0+ (integration-tested on 8.4.9)
  • ✅ MariaDB 10.3+
  • ✅ Percona Server 5.7+

Documentation

Overview

Package mysql provides functionality for the TDTP framework.

Index

Constants

View Source
const AdapterType = "mysql"

AdapterType идентификатор MySQL адаптера

View Source
const MaxFractionalPrecision = 6

MaxFractionalPrecision — предел MySQL на дробную часть DATETIME/TIMESTAMP/TIME.

Variables

This section is empty.

Functions

func BuildFieldFromColumn

func BuildFieldFromColumn(columnName, dataType string, isPrimaryKey bool) (packet.Field, error)

BuildFieldFromColumn создает packet.Field из информации о колонке MySQL

func TDTPToMySQL

func TDTPToMySQL(field packet.Field) string

TDTPToMySQL конвертирует TDTP тип в MySQL тип

Types

type Adapter

type Adapter struct {
	// contains filtered or unexported fields
}

Adapter реализует adapters.Adapter для MySQL Написан с нуля с использованием base helpers для минимального дублирования

func (*Adapter) BeginTx

func (a *Adapter) BeginTx(ctx context.Context) (adapters.Tx, error)

BeginTx начинает транзакцию (для ImportHelper)

func (*Adapter) Close

func (a *Adapter) Close(ctx context.Context) error

Close закрывает соединение

func (*Adapter) Connect

func (a *Adapter) Connect(ctx context.Context, cfg adapters.Config) error

Connect подключается к MySQL и инициализирует base helpers

func (*Adapter) CreateTable

func (a *Adapter) CreateTable(ctx context.Context, tableName string, schema packet.Schema) error

CreateTable создает таблицу из TDTP схемы

func (*Adapter) DropTable

func (a *Adapter) DropTable(ctx context.Context, tableName string) error

DropTable удаляет таблицу

func (*Adapter) ExecuteRawQuery

func (a *Adapter) ExecuteRawQuery(ctx context.Context, query string) (*packet.DataPacket, error)

ExecuteRawQuery выполняет произвольный SQL запрос

func (*Adapter) ExportTable

func (a *Adapter) ExportTable(ctx context.Context, tableName string) ([]*packet.DataPacket, error)

ExportTable экспортирует всю таблицу - просто делегируем

func (*Adapter) ExportTableIncremental

func (a *Adapter) ExportTableIncremental(ctx context.Context, tableName string, incrementalConfig adapters.IncrementalConfig) ([]*packet.DataPacket, string, error)

ExportTableIncremental - пока не реализовано

func (*Adapter) ExportTableWithQuery

func (a *Adapter) ExportTableWithQuery(ctx context.Context, tableName string, query *packet.Query, sender, recipient string) ([]*packet.DataPacket, error)

ExportTableWithQuery экспортирует с TDTQL фильтрацией - просто делегируем

func (*Adapter) GetDatabaseType

func (a *Adapter) GetDatabaseType() string

GetDatabaseType возвращает тип базы данных

func (*Adapter) GetDatabaseVersion

func (a *Adapter) GetDatabaseVersion(ctx context.Context) (string, error)

GetDatabaseVersion возвращает версию MySQL

func (*Adapter) GetRowCount

func (a *Adapter) GetRowCount(ctx context.Context, tableName string) (int64, error)

GetRowCount возвращает количество строк в таблице

func (*Adapter) GetTableNames

func (a *Adapter) GetTableNames(ctx context.Context) ([]string, error)

GetTableNames возвращает список таблиц

func (*Adapter) GetTableSchema

func (a *Adapter) GetTableSchema(ctx context.Context, tableName string) (packet.Schema, error)

GetTableSchema читает схему таблицы из information_schema

func (*Adapter) GetViewNames

func (a *Adapter) GetViewNames(ctx context.Context) ([]adapters.ViewInfo, error)

GetViewNames возвращает список всех views с информацией об updatable/read-only

func (*Adapter) ImportPacket

func (a *Adapter) ImportPacket(ctx context.Context, pkt *packet.DataPacket, strategy adapters.ImportStrategy) error

ImportPacket импортирует один пакет - просто делегируем

func (*Adapter) ImportPackets

func (a *Adapter) ImportPackets(ctx context.Context, packets []*packet.DataPacket, strategy adapters.ImportStrategy) error

ImportPackets импортирует несколько пакетов - просто делегируем

func (*Adapter) InsertRows

func (a *Adapter) InsertRows(ctx context.Context, tableName string, schema packet.Schema, rows []packet.Row, strategy adapters.ImportStrategy) error

InsertRows вставляет строки с учетом strategy Это ЕДИНСТВЕННОЕ место где MySQL-специфичная логика!

func (*Adapter) InspectTable

func (a *Adapter) InspectTable(ctx context.Context, tableName string) (*adapters.TableReport, error)

InspectTable returns extended metadata for a live MySQL table. Implements adapters.Adapter.

func (*Adapter) ParseFailures

func (a *Adapter) ParseFailures() []base.ParseFailure

ParseFailures implements base.ParseFailureReporter: values this adapter's exports could not convert to their declared type, since the previous call.

func (*Adapter) Ping

func (a *Adapter) Ping(ctx context.Context) error

Ping проверяет соединение

func (*Adapter) ReadAllRows

func (a *Adapter) ReadAllRows(ctx context.Context, tableName string, pkgSchema packet.Schema) ([][]string, error)

ReadAllRows читает все строки из таблицы

func (*Adapter) ReadAllRowsStream

func (a *Adapter) ReadAllRowsStream(ctx context.Context, tableName string, pkgSchema packet.Schema) (packet.Schema, <-chan []string, <-chan error, error)

ReadAllRowsStream читает таблицу потоком, реализуя base.StreamingDataReader.

Значения те же, что у ReadAllRows: обе дороги идут через base.cellToTDTP. Схему возвращает без изменений — MySQL, в отличие от MSSQL, ничего из неё не выкидывает (read-only колонки там режет PostProcessRows, которого здесь нет).

go-sql-driver действительно отдаёт строки по мере поступления, а не читает результат целиком. Это проверено, а не взято из документации: пик RSS экспорта одинаков на 524 288 и на 4 194 304 строках — 62 МБ в обоих случаях, при том что обычный путь на тех же данных вырос с 388 МБ до 2 823 МБ. Контейнер MySQL при этом держал те же ~617 МБ, что и до выборки, то есть проблема не переехала на сервер.

Признак настоящего потока — именно плоский пик при росте данных. Если он растёт линейно, драйвер материализует результат и поток декоративен.

func (*Adapter) ReadRowsWithSQL

func (a *Adapter) ReadRowsWithSQL(ctx context.Context, sqlQuery string, pkgSchema packet.Schema) ([][]string, error)

ReadRowsWithSQL выполняет SQL и возвращает строки

func (*Adapter) RenameTable

func (a *Adapter) RenameTable(ctx context.Context, oldName, newName string) error

RenameTable переименовывает таблицу

func (*Adapter) SetColumnarLayout

func (a *Adapter) SetColumnarLayout(on bool)

SetColumnarLayout включает колоночную раскладку Data (--columnar).

func (*Adapter) SetMaxFallbackRows

func (a *Adapter) SetMaxFallbackRows(n int64)

SetMaxFallbackRows задаёт лимит строк для in-memory fallback при провале SQL pushdown.

func (*Adapter) SetMaxMessageSize

func (a *Adapter) SetMaxMessageSize(size int)

SetMaxMessageSize задаёт максимальный размер одного TDTP пакета (в байтах). Вызывается из CLI при указании --packet-size.

func (*Adapter) SetSkipSpecialValues

func (a *Adapter) SetSkipSpecialValues(skip bool)

SetSkipSpecialValues включает режим --fast: DetectAndApply пропускается.

func (*Adapter) TableExists

func (a *Adapter) TableExists(ctx context.Context, tableName string) (bool, error)

TableExists проверяет существование таблицы

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL