|
@@ -20,6 +20,7 @@ import (
|
|
"fmt"
|
|
"fmt"
|
|
"github.com/lf-edge/ekuiper/extensions/sqldatabase/driver"
|
|
"github.com/lf-edge/ekuiper/extensions/sqldatabase/driver"
|
|
"github.com/lf-edge/ekuiper/pkg/api"
|
|
"github.com/lf-edge/ekuiper/pkg/api"
|
|
|
|
+ "github.com/lf-edge/ekuiper/pkg/ast"
|
|
"github.com/lf-edge/ekuiper/pkg/cast"
|
|
"github.com/lf-edge/ekuiper/pkg/cast"
|
|
"github.com/lf-edge/ekuiper/pkg/errorx"
|
|
"github.com/lf-edge/ekuiper/pkg/errorx"
|
|
"github.com/xo/dburl"
|
|
"github.com/xo/dburl"
|
|
@@ -33,11 +34,22 @@ type sqlConfig struct {
|
|
Fields []string `json:"fields"`
|
|
Fields []string `json:"fields"`
|
|
DataTemplate string `json:"dataTemplate"`
|
|
DataTemplate string `json:"dataTemplate"`
|
|
TableDataField string `json:"tableDataField"`
|
|
TableDataField string `json:"tableDataField"`
|
|
|
|
+ RowkindField string `json:"rowkindField"`
|
|
|
|
+ KeyField string `json:"keyField"`
|
|
}
|
|
}
|
|
|
|
|
|
-func (t *sqlConfig) buildSql(ctx api.StreamContext, mapData map[string]interface{}) ([]string, string, error) {
|
|
|
|
|
|
+func (t *sqlConfig) buildInsertSql(ctx api.StreamContext, mapData map[string]interface{}) ([]string, string, error) {
|
|
|
|
+ keys, vals, err := t.getKeyValues(ctx, mapData)
|
|
|
|
+ if err != nil {
|
|
|
|
+ return keys, "", err
|
|
|
|
+ }
|
|
|
|
+ sqlStr := "(" + strings.Join(vals, ",") + ")"
|
|
|
|
+ return keys, sqlStr, nil
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+func (t *sqlConfig) getKeyValues(ctx api.StreamContext, mapData map[string]interface{}) ([]string, []string, error) {
|
|
if 0 == len(mapData) {
|
|
if 0 == len(mapData) {
|
|
- return nil, "", fmt.Errorf("data is empty.")
|
|
|
|
|
|
+ return nil, nil, fmt.Errorf("data is empty.")
|
|
}
|
|
}
|
|
logger := ctx.GetLogger()
|
|
logger := ctx.GetLogger()
|
|
var keys, vals []string
|
|
var keys, vals []string
|
|
@@ -66,9 +78,7 @@ func (t *sqlConfig) buildSql(ctx api.StreamContext, mapData map[string]interface
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
-
|
|
|
|
- sqlStr := "(" + strings.Join(vals, ",") + ")"
|
|
|
|
- return keys, sqlStr, nil
|
|
|
|
|
|
+ return keys, vals, nil
|
|
}
|
|
}
|
|
|
|
|
|
type sqlSink struct {
|
|
type sqlSink struct {
|
|
@@ -89,6 +99,9 @@ func (m *sqlSink) Configure(props map[string]interface{}) error {
|
|
if cfg.Table == "" {
|
|
if cfg.Table == "" {
|
|
return fmt.Errorf("property Table is required")
|
|
return fmt.Errorf("property Table is required")
|
|
}
|
|
}
|
|
|
|
+ if cfg.RowkindField != "" && cfg.KeyField == "" {
|
|
|
|
+ return fmt.Errorf("keyField is required when rowkindField is set")
|
|
|
|
+ }
|
|
m.conf = cfg
|
|
m.conf = cfg
|
|
return nil
|
|
return nil
|
|
}
|
|
}
|
|
@@ -108,11 +121,16 @@ func (m *sqlSink) Open(ctx api.StreamContext) (err error) {
|
|
|
|
|
|
func (m *sqlSink) writeToDB(ctx api.StreamContext, sqlStr *string) error {
|
|
func (m *sqlSink) writeToDB(ctx api.StreamContext, sqlStr *string) error {
|
|
ctx.GetLogger().Debugf(*sqlStr)
|
|
ctx.GetLogger().Debugf(*sqlStr)
|
|
- rows, err := m.db.Query(*sqlStr)
|
|
|
|
|
|
+ r, err := m.db.Exec(*sqlStr)
|
|
if err != nil {
|
|
if err != nil {
|
|
return fmt.Errorf("%s: %s", errorx.IOErr, err.Error())
|
|
return fmt.Errorf("%s: %s", errorx.IOErr, err.Error())
|
|
}
|
|
}
|
|
- return rows.Close()
|
|
|
|
|
|
+ d, err := r.RowsAffected()
|
|
|
|
+ if err != nil {
|
|
|
|
+ ctx.GetLogger().Errorf("get rows affected error: %s", err.Error())
|
|
|
|
+ }
|
|
|
|
+ ctx.GetLogger().Debugf("Rows affected: %d", d)
|
|
|
|
+ return nil
|
|
}
|
|
}
|
|
|
|
|
|
func (m *sqlSink) Collect(ctx api.StreamContext, item interface{}) error {
|
|
func (m *sqlSink) Collect(ctx api.StreamContext, item interface{}) error {
|
|
@@ -130,10 +148,12 @@ func (m *sqlSink) Collect(ctx api.StreamContext, item interface{}) error {
|
|
item = tm
|
|
item = tm
|
|
}
|
|
}
|
|
|
|
|
|
- var table string
|
|
|
|
- var err error
|
|
|
|
- v, ok := item.(map[string]interface{})
|
|
|
|
- if ok {
|
|
|
|
|
|
+ var (
|
|
|
|
+ table string
|
|
|
|
+ err error
|
|
|
|
+ )
|
|
|
|
+ switch v := item.(type) {
|
|
|
|
+ case map[string]interface{}:
|
|
table, err = ctx.ParseTemplate(m.conf.Table, v)
|
|
table, err = ctx.ParseTemplate(m.conf.Table, v)
|
|
if err != nil {
|
|
if err != nil {
|
|
ctx.GetLogger().Errorf("parse template for table %s error: %v", m.conf.Table, err)
|
|
ctx.GetLogger().Errorf("parse template for table %s error: %v", m.conf.Table, err)
|
|
@@ -142,60 +162,102 @@ func (m *sqlSink) Collect(ctx api.StreamContext, item interface{}) error {
|
|
if m.conf.TableDataField != "" {
|
|
if m.conf.TableDataField != "" {
|
|
item = v[m.conf.TableDataField]
|
|
item = v[m.conf.TableDataField]
|
|
}
|
|
}
|
|
|
|
+ case []map[string]interface{}:
|
|
|
|
+ if len(v) == 0 {
|
|
|
|
+ ctx.GetLogger().Warnf("empty data array")
|
|
|
|
+ return nil
|
|
|
|
+ }
|
|
|
|
+ table, err = ctx.ParseTemplate(m.conf.Table, v[0])
|
|
|
|
+ if err != nil {
|
|
|
|
+ ctx.GetLogger().Errorf("parse template for table %s error: %v", m.conf.Table, err)
|
|
|
|
+ return err
|
|
|
|
+ }
|
|
}
|
|
}
|
|
|
|
|
|
var keys []string = nil
|
|
var keys []string = nil
|
|
var values []string = nil
|
|
var values []string = nil
|
|
var vars string
|
|
var vars string
|
|
|
|
|
|
- switch v := item.(type) {
|
|
|
|
- case []map[string]interface{}:
|
|
|
|
- for _, mapData := range v {
|
|
|
|
- keys, vars, err = m.conf.buildSql(ctx, mapData)
|
|
|
|
|
|
+ if m.conf.RowkindField == "" {
|
|
|
|
+ switch v := item.(type) {
|
|
|
|
+ case []map[string]interface{}:
|
|
|
|
+ for _, mapData := range v {
|
|
|
|
+ keys, vars, err = m.conf.buildInsertSql(ctx, mapData)
|
|
|
|
+ if err != nil {
|
|
|
|
+ return err
|
|
|
|
+ }
|
|
|
|
+ values = append(values, vars)
|
|
|
|
+ }
|
|
|
|
+ if keys != nil {
|
|
|
|
+ sqlStr := fmt.Sprintf("INSERT INTO %s (%s) values ", table, strings.Join(keys, ",")) + strings.Join(values, ",") + ";"
|
|
|
|
+ return m.writeToDB(ctx, &sqlStr)
|
|
|
|
+ }
|
|
|
|
+ return nil
|
|
|
|
+ case map[string]interface{}:
|
|
|
|
+ keys, vars, err = m.conf.buildInsertSql(ctx, v)
|
|
if err != nil {
|
|
if err != nil {
|
|
return err
|
|
return err
|
|
}
|
|
}
|
|
values = append(values, vars)
|
|
values = append(values, vars)
|
|
- }
|
|
|
|
- if keys != nil {
|
|
|
|
- sqlStr := fmt.Sprintf("INSERT INTO %s (%s) values ", table, strings.Join(keys, ",")) + strings.Join(values, ",") + ";"
|
|
|
|
- return m.writeToDB(ctx, &sqlStr)
|
|
|
|
- }
|
|
|
|
- return nil
|
|
|
|
- case map[string]interface{}:
|
|
|
|
- keys, vars, err = m.conf.buildSql(ctx, v)
|
|
|
|
- if err != nil {
|
|
|
|
- return err
|
|
|
|
- }
|
|
|
|
- values = append(values, vars)
|
|
|
|
- if keys != nil {
|
|
|
|
- sqlStr := fmt.Sprintf("INSERT INTO %s (%s) values ", table, strings.Join(keys, ",")) + strings.Join(values, ",") + ";"
|
|
|
|
- return m.writeToDB(ctx, &sqlStr)
|
|
|
|
- }
|
|
|
|
- return nil
|
|
|
|
- case []interface{}:
|
|
|
|
- for _, data := range v {
|
|
|
|
- mapData, ok := data.(map[string]interface{})
|
|
|
|
- if !ok {
|
|
|
|
- ctx.GetLogger().Errorf("unsupported type: %T", data)
|
|
|
|
- return fmt.Errorf("unsupported type: %T", data)
|
|
|
|
|
|
+ if keys != nil {
|
|
|
|
+ sqlStr := fmt.Sprintf("INSERT INTO %s (%s) values ", table, strings.Join(keys, ",")) + strings.Join(values, ",") + ";"
|
|
|
|
+ return m.writeToDB(ctx, &sqlStr)
|
|
}
|
|
}
|
|
|
|
+ return nil
|
|
|
|
+ case []interface{}:
|
|
|
|
+ for _, data := range v {
|
|
|
|
+ mapData, ok := data.(map[string]interface{})
|
|
|
|
+ if !ok {
|
|
|
|
+ ctx.GetLogger().Errorf("unsupported type: %T", data)
|
|
|
|
+ return fmt.Errorf("unsupported type: %T", data)
|
|
|
|
+ }
|
|
|
|
|
|
- keys, vars, err = m.conf.buildSql(ctx, mapData)
|
|
|
|
|
|
+ keys, vars, err = m.conf.buildInsertSql(ctx, mapData)
|
|
|
|
+ if err != nil {
|
|
|
|
+ ctx.GetLogger().Errorf("sql sink build sql error %v for data", err, mapData)
|
|
|
|
+ return err
|
|
|
|
+ }
|
|
|
|
+ values = append(values, vars)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if keys != nil {
|
|
|
|
+ sqlStr := fmt.Sprintf("INSERT INTO %s (%s) values ", table, strings.Join(keys, ",")) + strings.Join(values, ",") + ";"
|
|
|
|
+ return m.writeToDB(ctx, &sqlStr)
|
|
|
|
+ }
|
|
|
|
+ return nil
|
|
|
|
+ default: // never happen
|
|
|
|
+ return fmt.Errorf("unsupported type: %T", item)
|
|
|
|
+ }
|
|
|
|
+ } else {
|
|
|
|
+ switch d := item.(type) {
|
|
|
|
+ case []map[string]interface{}:
|
|
|
|
+ for _, el := range d {
|
|
|
|
+ err := m.save(ctx, table, el)
|
|
|
|
+ if err != nil {
|
|
|
|
+ ctx.GetLogger().Error(err)
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ case map[string]interface{}:
|
|
|
|
+ err := m.save(ctx, table, d)
|
|
if err != nil {
|
|
if err != nil {
|
|
- ctx.GetLogger().Errorf("sql sink build sql error %v for data", err, mapData)
|
|
|
|
return err
|
|
return err
|
|
}
|
|
}
|
|
- values = append(values, vars)
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- if keys != nil {
|
|
|
|
- sqlStr := fmt.Sprintf("INSERT INTO %s (%s) values ", table, strings.Join(keys, ",")) + strings.Join(values, ",") + ";"
|
|
|
|
- return m.writeToDB(ctx, &sqlStr)
|
|
|
|
|
|
+ case []interface{}:
|
|
|
|
+ for _, vv := range d {
|
|
|
|
+ el, ok := vv.(map[string]interface{})
|
|
|
|
+ if !ok {
|
|
|
|
+ ctx.GetLogger().Errorf("unsupported type: %T", vv)
|
|
|
|
+ return fmt.Errorf("unsupported type: %T", vv)
|
|
|
|
+ }
|
|
|
|
+ err := m.save(ctx, table, el)
|
|
|
|
+ if err != nil {
|
|
|
|
+ ctx.GetLogger().Error(err)
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ default:
|
|
|
|
+ return fmt.Errorf("unrecognized format of %s", item)
|
|
}
|
|
}
|
|
return nil
|
|
return nil
|
|
- default: // never happen
|
|
|
|
- return fmt.Errorf("unsupported type: %T", item)
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
@@ -206,6 +268,67 @@ func (m *sqlSink) Close(_ api.StreamContext) error {
|
|
return nil
|
|
return nil
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+// save save updatable data only to db
|
|
|
|
+func (m *sqlSink) save(ctx api.StreamContext, table string, data map[string]interface{}) error {
|
|
|
|
+ rowkind := ast.RowkindInsert
|
|
|
|
+ c, ok := data[m.conf.RowkindField]
|
|
|
|
+ if ok {
|
|
|
|
+ rowkind, ok = c.(string)
|
|
|
|
+ if !ok {
|
|
|
|
+ return fmt.Errorf("rowkind field %s is not a string in data %v", m.conf.RowkindField, data)
|
|
|
|
+ }
|
|
|
|
+ if rowkind != ast.RowkindInsert && rowkind != ast.RowkindUpdate && rowkind != ast.RowkindDelete {
|
|
|
|
+ return fmt.Errorf("invalid rowkind %s", rowkind)
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ var sqlStr string
|
|
|
|
+ switch rowkind {
|
|
|
|
+ case ast.RowkindInsert:
|
|
|
|
+ keys, vars, err := m.conf.buildInsertSql(ctx, data)
|
|
|
|
+ if err != nil {
|
|
|
|
+ return err
|
|
|
|
+ }
|
|
|
|
+ values := []string{vars}
|
|
|
|
+ if keys != nil {
|
|
|
|
+ sqlStr = fmt.Sprintf("INSERT INTO %s (%s) values ", table, strings.Join(keys, ",")) + strings.Join(values, ",") + ";"
|
|
|
|
+ }
|
|
|
|
+ case ast.RowkindUpdate:
|
|
|
|
+ keyval, ok := data[m.conf.KeyField]
|
|
|
|
+ if !ok {
|
|
|
|
+ return fmt.Errorf("field %s does not exist in data %v", m.conf.KeyField, data)
|
|
|
|
+ }
|
|
|
|
+ keys, vals, err := m.conf.getKeyValues(ctx, data)
|
|
|
|
+ if err != nil {
|
|
|
|
+ return err
|
|
|
|
+ }
|
|
|
|
+ sqlStr = fmt.Sprintf("UPDATE %s SET ", table)
|
|
|
|
+ for i, key := range keys {
|
|
|
|
+ if i != 0 {
|
|
|
|
+ sqlStr += ","
|
|
|
|
+ }
|
|
|
|
+ sqlStr += fmt.Sprintf("%s=%s", key, vals[i])
|
|
|
|
+ }
|
|
|
|
+ if _, ok := keyval.(string); ok {
|
|
|
|
+ sqlStr += fmt.Sprintf(" WHERE %s = \"%s\";", m.conf.KeyField, keyval)
|
|
|
|
+ } else {
|
|
|
|
+ sqlStr += fmt.Sprintf(" WHERE %s = %v;", m.conf.KeyField, keyval)
|
|
|
|
+ }
|
|
|
|
+ case ast.RowkindDelete:
|
|
|
|
+ keyval, ok := data[m.conf.KeyField]
|
|
|
|
+ if !ok {
|
|
|
|
+ return fmt.Errorf("field %s does not exist in data %v", m.conf.KeyField, data)
|
|
|
|
+ }
|
|
|
|
+ if _, ok := keyval.(string); ok {
|
|
|
|
+ sqlStr = fmt.Sprintf("DELETE FROM %s WHERE %s = \"%s\";", table, m.conf.KeyField, keyval)
|
|
|
|
+ } else {
|
|
|
|
+ sqlStr = fmt.Sprintf("DELETE FROM %s WHERE %s = %v;", table, m.conf.KeyField, keyval)
|
|
|
|
+ }
|
|
|
|
+ default:
|
|
|
|
+ return fmt.Errorf("invalid rowkind %s", rowkind)
|
|
|
|
+ }
|
|
|
|
+ return m.writeToDB(ctx, &sqlStr)
|
|
|
|
+}
|
|
|
|
+
|
|
func Sql() api.Sink {
|
|
func Sql() api.Sink {
|
|
return &sqlSink{}
|
|
return &sqlSink{}
|
|
}
|
|
}
|