tdengine_gorm

package module
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Aug 5, 2026 License: MIT Imports: 16 Imported by: 0

README

TDengine GORM WebSocket Dialect

English | 简体中文

A GORM dialect for TDengine 3.x that connects to taosAdapter through the official driver-go WebSocket driver. It does not require the local TDengine C client.

Compatibility

  • Go 1.18+
  • GORM 1.31.x
  • driver-go/v3 3.8.x
  • TDengine 3.3.6+ (CI covers 3.3.8.8, 3.4.1.6, and 3.4.2.2)

Transactions and regular SQL UPDATE statements are not supported. The dialect supports safe additive migrations, batch inserts, guarded time-range deletes, supertables, subtables, tag indexes, and TDengine-specific query clauses.

See MIGRATION.md when upgrading from v0.2.0.

Installation

go get github.com/FEINIAO233/tdengine-gorm-ws@latest

Connection

package main

import (
	"log"

	tdengine "github.com/FEINIAO233/tdengine-gorm-ws"
	"gorm.io/gorm"
)

func main() {
	dsn := "root:taosdata@ws(127.0.0.1:6041)/metrics?timezone=Asia%2FShanghai"
	db, err := gorm.Open(tdengine.Open(dsn), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}

	_ = db
}

Use wss(host:port) for TLS connections. URL-encode usernames or passwords containing special characters according to the driver-go DSN rules.

The default mode uses driver-go parameter interpolation. The following options preserve raw Go values and use prepared statements:

db, err := gorm.Open(tdengine.Open(dsn), &gorm.Config{PrepareStmt: true})

// Or let driver-go prepare parameters through the DSN.
dsn = dsn + "&interpolateParams=false"

You can also explicitly configure &tdengine.Dialect{DSN: dsn, BindMode: tdengine.BindModePrepared}.

For TDengine 3.3.x, keep the default interpolation mode. With the current driver-go Prepared/Stmt2 path, TDengine 3.4+ is the prepared-statement compatibility baseline. CI runs prepared-statement integration tests only on 3.4.x.

Supertables and Subtables

import (
	"github.com/FEINIAO233/tdengine-gorm-ws/clause/create"
	"github.com/FEINIAO233/tdengine-gorm-ws/clause/using"
)

stable := create.NewSTable("meters", true, []*create.Column{
	{Name: "ts", ColumnType: create.TimestampType},
	{Name: "val", ColumnType: create.DoubleType},
}, []*create.Column{
	{Name: "location", ColumnType: create.NCharType, Length: 64},
})

err := db.Table("meters").
	Clauses(create.NewCreateTableClause([]*create.Table{stable})).
	Create(map[string]interface{}{}).Error

err = db.Table("device-1").Clauses(using.SetUsingTags(
	"meters",
	using.Tag{Name: "location", Value: "Shanghai"},
)).Create(map[string]interface{}{
	"ts": time.Now(), "val": 12.5,
}).Error

SetUsing still accepts map[string]interface{} and sorts tags by name to produce deterministic SQL. Use SetUsingTags when tag order must be explicit.

The TDengine migrator can update several tags on one subtable or batch updates for several subtables:

migrator := db.Migrator().(tdengine.Migrator)

err = migrator.SetTableTags("device-1", map[string]interface{}{
	"location": "Beijing",
	"group":    8,
})

err = migrator.SetTableTagsBatch(
	tdengine.TableTagUpdate{Table: "device-1", Tags: map[string]interface{}{"group": 9}},
	tdengine.TableTagUpdate{Table: "device-2", Tags: map[string]interface{}{"group": 10}},
)

Multi-subtable batch updates require TDengine 3.4+. Tag names are sorted to produce deterministic SQL.

Batch Inserts

Pass a slice directly to GORM. The dialect emits TDengine's consecutive row syntax, VALUES (...) (...):

err := db.Table("device-1").Create([]map[string]interface{}{
	{"ts": time.Now(), "val": 12.5},
	{"ts": time.Now().Add(time.Second), "val": 13.5},
}).Error

Auto Migration

AutoMigrate is additive by design. It creates missing regular tables or supertables and adds missing columns and tags. It never drops columns or tags and never changes existing types automatically. The first data field must map to TIMESTAMP; mark tags with tdengine:"tag":

type Meter struct {
	TS       time.Time `gorm:"column:ts"`
	Value    float64   `gorm:"column:val"`
	Location string    `gorm:"type:NCHAR(64)" tdengine:"tag"`
}

err := db.Table("meters").AutoMigrate(&Meter{})

TDengine can combine the timestamp key with one additional integer or VARCHAR column. Mark that field explicitly with tdengine:"compositeKey":

type DeviceMetric struct {
	TS       time.Time
	DeviceID string `gorm:"type:VARCHAR;size:64" tdengine:"compositeKey"`
	Value    float64
}

A composite key can only be declared when the table is first created. AutoMigrate returns ErrCompositeKeyMigrationUnsupported instead of trying to add one to an existing table.

For explicit DDL changes, assert db.Migrator() to tdengine.Migrator and use methods such as AddStableColumn, AddStableTag, ModifyStableTag, and RenameStableTag.

TDengine virtual tables can be queried normally. Schema-changing Migrator operations detect virtual tables and return ErrVirtualTableUnsupported; use explicit TDengine VTABLE SQL when managing their definitions.

Table and Compression Options

The low-level create clause supports TDengine table options and per-column compression settings:

table := create.NewTable("metrics", true, []*create.Column{
	{
		Name: "ts", ColumnType: create.TimestampType,
		Encode: create.EncodeDeltaI, Compress: create.CompressLZ4,
		Level: create.CompressionMedium,
	},
	{
		Name: "value", ColumnType: create.DoubleType,
		Encode: create.EncodeBSS, Compress: create.CompressZstd,
		Level: create.CompressionHigh,
	},
}, "", nil).
	WithComment("device metrics").
	WithSMA("value").
	WithTTL(30)

Use WithTTL for regular tables and subtables. Supertables use WithKeep(value, unit), for example WithKeep(365, create.RetentionDays). Invalid option combinations and unsupported compression names return explicit errors while SQL is being built.

Tag Indexes

TDengine permits an index on a single supertable tag. Standard GORM index tags participate in AutoMigrate:

type Meter struct {
	TS       time.Time `gorm:"column:ts"`
	Location string    `gorm:"index:idx_meter_location" tdengine:"tag"`
}

CreateIndex, DropIndex, HasIndex, and GetIndexes use INFORMATION_SCHEMA.INS_INDEXES. Indexes on regular columns, multi-column indexes, unique indexes, and index renaming return explicit errors. TDengine automatically indexes the first tag of a supertable; migration does not create a duplicate when the same tag is already indexed.

Extended Data Types

Use GORM type tags for TDengine 3.x data types:

type ExtendedMetric struct {
	TS       time.Time
	Name     string `gorm:"type:VARCHAR;size:128"`
	Raw      []byte `gorm:"type:VARBINARY;size:256"`
	Price    string `gorm:"type:DECIMAL;precision:18;scale:2"`
	Geometry string `gorm:"type:GEOMETRY;size:512"`
	Payload  []byte `gorm:"type:BLOB"`
	Metadata string `gorm:"type:JSON" tdengine:"tag"`
}

The migrator enforces relevant TDengine restrictions: JSON is tag-only, DECIMAL and BLOB cannot be tags, and a table can contain at most one BLOB column.

DECIMAL requires TDengine 3.3.6+. BLOB and column-filtered COUNT_WINDOW require TDengine 3.3.7+. Enable PrepareStmt or BindModePrepared when writing raw []byte values to VARBINARY or BLOB columns so binary data is not handled as an interpolated string.

Updates and Deletes

TDengine does not provide regular row-level UPDATE. GORM Update and Updates return ErrUpdateNotSupported; update a row by inserting the same timestamp again.

GORM ON CONFLICT clauses return ErrOnConflictUnsupported. TDengine resolves duplicate rows through its timestamp or composite-primary-key insertion semantics instead of PostgreSQL-style conflict clauses.

Generic GORM Delete returns ErrDeleteNotSupported to guard against irreversible deletes. Use a bounded time range instead:

start := time.Now().Add(-time.Hour)
end := time.Now()
err := tdengine.DeleteTimeRange(db, "device-1", &start, &end).Error

The start is inclusive and the end is exclusive. Either bound may be nil, but they cannot both be nil.

TDengine Query Extensions

The library provides clauses for:

  • CREATE TABLE / CREATE STABLE
  • USING ... TAGS
  • INTERVAL, SESSION, STATE_WINDOW, EVENT_WINDOW, and COUNT_WINDOW
  • PARTITION BY
  • RANGE / EVERY for INTERP queries
  • FILL
  • SLIMIT / SOFFSET

See example/example.go for complete examples.

Integration Tests

Start TDengine and taosAdapter, then set a WebSocket endpoint without a database name:

$env:TDENGINE_GORM_TEST_ENDPOINT='root:taosdata@ws(127.0.0.1:6041)'
go test -v -count=1 -run 'Integration$' .

The tests create and remove temporary databases automatically. GitHub Actions runs the same suite against TDengine 3.3.8.8, 3.4.1.6, and 3.4.2.2.

Documentation

Index

Constants

View Source
const DriverName = "taosWS"

DriverName is the default driver name for TDengine.

Variables

View Source
var (
	// ErrUpdateNotSupported explains TDengine's insert-based update semantics.
	ErrUpdateNotSupported = errors.New("tdengine: GORM UPDATE is not supported; insert the same timestamp again to update a row")
	// ErrDeleteNotSupported directs callers to the time-range API.
	ErrDeleteNotSupported = errors.New("tdengine: GORM DELETE is not supported; use DeleteTimeRange")
	// ErrDeleteRangeRequired prevents an unbounded, irreversible deletion.
	ErrDeleteRangeRequired = errors.New("tdengine: at least one delete time bound is required")
)
View Source
var (
	ErrTagIndexOnly       = errors.New("tdengine: indexes are supported only on supertable tags")
	ErrSingleTagIndexOnly = errors.New("tdengine: a tag index must contain exactly one tag")
)
View Source
var (
	ErrTimestampFirst                   = errors.New("tdengine: the first data column must be TIMESTAMP")
	ErrNoDataColumns                    = errors.New("tdengine: a table requires at least one data column")
	ErrConstraintsUnsupported           = errors.New("tdengine: GORM constraints are not supported")
	ErrRenameTableUnsupported           = errors.New("tdengine: renaming tables through GORM is not supported")
	ErrCompositeKeyInvalid              = errors.New("tdengine: COMPOSITE KEY requires exactly one non-tag integer or VARCHAR column after the timestamp")
	ErrCompositeKeyMigrationUnsupported = errors.New("tdengine: COMPOSITE KEY can only be declared when creating a table")
	ErrVirtualTableUnsupported          = errors.New("tdengine: GORM migration operations on virtual tables are not supported")
)
View Source
var (
	ErrTagUpdateRequired       = errors.New("tdengine: at least one table tag update is required")
	ErrTagValueRequired        = errors.New("tdengine: at least one tag value is required")
	ErrDuplicateTagUpdateTable = errors.New("tdengine: a table may appear only once in a batch tag update")
)
View Source
var ErrOnConflictUnsupported = errors.New("tdengine: ON CONFLICT is not supported; insert the same primary key to replace a row")

Functions

func DeleteTimeRange added in v0.3.0

func DeleteTimeRange(db *gorm.DB, table string, start, end *time.Time) *gorm.DB

DeleteTimeRange deletes rows using TDengine's _rowts pseudocolumn. Start is inclusive and end is exclusive. A nil bound leaves that side open; at least one bound is required.

func Open

func Open(dsn string) gorm.Dialector

Types

type BindMode added in v0.3.0

type BindMode uint8

BindMode controls how values are passed to driver-go.

const (
	// BindModeAuto follows GORM PrepareStmt and the interpolateParams DSN option.
	BindModeAuto BindMode = iota
	// BindModeInterpolate encodes string values as TDengine SQL literals.
	BindModeInterpolate
	// BindModePrepared preserves Go values for driver-go prepared statements.
	BindModePrepared
)

type Dialect

type Dialect struct {
	DriverName string
	DSN        string
	Conn       gorm.ConnPool
	BindMode   BindMode
}

func (Dialect) BindVarTo

func (dialect Dialect) BindVarTo(writer clause.Writer, stmt *gorm.Statement, v interface{})

func (Dialect) ClauseBuilders

func (dialect Dialect) ClauseBuilders() map[string]clause.ClauseBuilder

func (*Dialect) Create

func (dialector *Dialect) Create(db *gorm.DB)

func (Dialect) DataTypeOf

func (dialect Dialect) DataTypeOf(field *schema.Field) string

func (Dialect) DefaultValueOf

func (dialect Dialect) DefaultValueOf(field *schema.Field) clause.Expression

func (Dialect) Delete added in v0.3.0

func (dialect Dialect) Delete(db *gorm.DB)

Delete blocks generic GORM deletes because TDengine only permits predicates on the first timestamp column.

func (Dialect) Explain

func (dialect Dialect) Explain(sql string, vars ...interface{}) string

func (Dialect) Initialize

func (dialect Dialect) Initialize(db *gorm.DB) (err error)

func (Dialect) Migrator

func (dialect Dialect) Migrator(db *gorm.DB) gorm.Migrator

func (Dialect) Name

func (dialect Dialect) Name() string

func (Dialect) QuoteTo

func (dialect Dialect) QuoteTo(writer clause.Writer, str string)

func (Dialect) RollbackTo

func (dialect Dialect) RollbackTo(tx *gorm.DB, name string) error

func (Dialect) SavePoint

func (dialect Dialect) SavePoint(tx *gorm.DB, name string) error

func (Dialect) Update added in v0.3.0

func (dialect Dialect) Update(db *gorm.DB)

Update blocks SQL UPDATE statements. TDengine updates time-series rows by inserting the same timestamp again.

type Migrator

type Migrator struct {
	migrator.Migrator
	// contains filtered or unexported fields
}

Migrator implements the subset of GORM migration operations that maps to TDengine tables and supertables. AutoMigrate is intentionally additive: it creates missing objects and adds missing columns/tags, but never drops or changes existing definitions.

func (Migrator) AddColumn added in v0.3.0

func (m Migrator) AddColumn(value interface{}, name string) error

func (Migrator) AddStableColumn added in v0.3.0

func (m Migrator) AddStableColumn(stable string, column *createclause.Column) error

func (Migrator) AddStableTag added in v0.3.0

func (m Migrator) AddStableTag(stable string, tag *createclause.Column) error

func (Migrator) AlterColumn

func (m Migrator) AlterColumn(value interface{}, name string) error

func (Migrator) AutoMigrate

func (m Migrator) AutoMigrate(values ...interface{}) error

func (Migrator) ColumnTypes added in v0.3.0

func (m Migrator) ColumnTypes(value interface{}) (result []gorm.ColumnType, err error)

ColumnTypes reads TDengine's native metadata instead of the MySQL-style information_schema tables used by GORM's default migrator.

func (Migrator) CreateConstraint added in v0.3.0

func (m Migrator) CreateConstraint(interface{}, string) error

func (Migrator) CreateIndex added in v0.3.0

func (m Migrator) CreateIndex(value interface{}, name string) error

func (Migrator) CreateTable added in v0.3.0

func (m Migrator) CreateTable(values ...interface{}) error

func (Migrator) DropColumn added in v0.3.0

func (m Migrator) DropColumn(value interface{}, name string) error

func (Migrator) DropConstraint

func (m Migrator) DropConstraint(interface{}, string) error

func (Migrator) DropIndex added in v0.3.0

func (m Migrator) DropIndex(value interface{}, name string) error

func (Migrator) DropStableColumn added in v0.3.0

func (m Migrator) DropStableColumn(stable, column string) error

func (Migrator) DropStableTag added in v0.3.0

func (m Migrator) DropStableTag(stable, tag string) error

func (Migrator) DropTable added in v0.3.0

func (m Migrator) DropTable(values ...interface{}) error

func (Migrator) FullDataTypeOf

func (m Migrator) FullDataTypeOf(field *schema.Field) clause.Expr

func (Migrator) GetIndexes added in v0.3.0

func (m Migrator) GetIndexes(value interface{}) (indexes []gorm.Index, err error)

func (Migrator) GetTables added in v0.3.0

func (m Migrator) GetTables() ([]string, error)

func (Migrator) HasColumn added in v0.3.0

func (m Migrator) HasColumn(value interface{}, name string) bool

func (Migrator) HasConstraint added in v0.3.0

func (m Migrator) HasConstraint(interface{}, string) bool

func (Migrator) HasIndex added in v0.3.0

func (m Migrator) HasIndex(value interface{}, name string) bool

func (Migrator) HasTable added in v0.3.0

func (m Migrator) HasTable(value interface{}) bool

func (Migrator) MigrateColumn added in v0.3.0

func (m Migrator) MigrateColumn(interface{}, *schema.Field, gorm.ColumnType) error

MigrateColumn is a no-op because automatic type changes can be destructive in TDengine. Call AlterColumn explicitly after reviewing the change.

func (Migrator) MigrateColumnUnique added in v0.3.0

func (m Migrator) MigrateColumnUnique(interface{}, *schema.Field, gorm.ColumnType) error

func (Migrator) ModifyStableColumn added in v0.3.0

func (m Migrator) ModifyStableColumn(stable string, column *createclause.Column) error

func (Migrator) ModifyStableTag added in v0.3.0

func (m Migrator) ModifyStableTag(stable string, tag *createclause.Column) error

func (Migrator) RenameColumn

func (m Migrator) RenameColumn(interface{}, string, string) error

func (Migrator) RenameIndex

func (m Migrator) RenameIndex(interface{}, string, string) error

func (Migrator) RenameStableTag added in v0.3.0

func (m Migrator) RenameStableTag(stable, oldName, newName string) error

func (Migrator) RenameTable added in v0.3.0

func (m Migrator) RenameTable(interface{}, interface{}) error

func (Migrator) SetTableTag added in v0.3.0

func (m Migrator) SetTableTag(table, tag string, value interface{}) error

func (Migrator) SetTableTags added in v0.3.0

func (m Migrator) SetTableTags(table string, tags map[string]interface{}) error

func (Migrator) SetTableTagsBatch added in v0.3.0

func (m Migrator) SetTableTagsBatch(updates ...TableTagUpdate) error

func (Migrator) TableType added in v0.3.0

func (m Migrator) TableType(value interface{}) (result gorm.TableType, err error)

type TableTagUpdate added in v0.3.0

type TableTagUpdate struct {
	Table string
	Tags  map[string]interface{}
}

Directories

Path Synopsis
clause

Jump to

Keyboard shortcuts

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