From 03a27801f36b455a6b432ff895dd49d4b2f90e85 Mon Sep 17 00:00:00 2001 From: rei Date: Sat, 14 Dec 2024 15:18:38 +0700 Subject: [PATCH 01/10] feat: extend config --- helpers/config.go | 16 +++++++++++--- models/models.go | 53 +++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/helpers/config.go b/helpers/config.go index 27a2c2a..edbfdad 100644 --- a/helpers/config.go +++ b/helpers/config.go @@ -13,15 +13,25 @@ type Config struct { Connections []models.Connection `toml:"database"` } -func LoadConfig() (config Config, err error) { +func LoadConfig() (Config, error) { + config := Config{} + file, err := os.ReadFile(filepath.Join(os.Getenv("HOME"), ".config", "lazysql", "config.toml")) if err != nil { - return + return config, err } err = toml.Unmarshal(file, &config) + if err != nil { + return config, err + } - return + for idx, cfg := range config.Connections { + cfg.ParseURL() + config.Connections[idx].URL = cfg.URL // can be better than this + } + + return config, nil } func LoadConnections() (connections []models.Connection, err error) { diff --git a/models/models.go b/models/models.go index 8aff0c8..cb65534 100644 --- a/models/models.go +++ b/models/models.go @@ -1,17 +1,62 @@ package models import ( + "fmt" + "net/url" + "github.com/rivo/tview" ) type Connection struct { - Name string - Provider string - DBName string - URL string + Name string + + // either use this directly + URL string + + // or parse manually + Provider string + Username string + Password string + Hostname string + Port string + DBName string + URLParams string + Commands []*Command } +// ParseURL will manually parse url if url empty +// +// for handling username & password with special characters +// +// only sqlserver for now +// +// need to refactor if wanted to reuse driver list in drivers/constants.go +func (c *Connection) ParseURL() { + if c.URL != "" { + return + } + + // only sqlserver for now + if c.Provider != "sqlserver" { + return + } + + user := url.QueryEscape(c.Username) + pass := url.QueryEscape(c.Password) + + c.URL = fmt.Sprintf( + "%s://%s:%s@%s:%s?database=%s%s", + c.Provider, + user, + pass, + c.Hostname, + c.Port, + c.DBName, + c.URLParams, + ) +} + type Command struct { Command string WaitForPort string From f74b4426b6b5defadbecbf89834cea614b81968d Mon Sep 17 00:00:00 2001 From: rei Date: Sat, 14 Dec 2024 15:18:57 +0700 Subject: [PATCH 02/10] feat: errorlist helper --- helpers/errorlist/errorlist.go | 41 ++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 helpers/errorlist/errorlist.go diff --git a/helpers/errorlist/errorlist.go b/helpers/errorlist/errorlist.go new file mode 100644 index 0000000..20471af --- /dev/null +++ b/helpers/errorlist/errorlist.go @@ -0,0 +1,41 @@ +package errorlist + +import ( + "fmt" +) + +type ErrorList []error + +func (e ErrorList) Error() string { + s := "" + + for idx, err := range e { + s = fmt.Sprintf("%d: %s\n", idx, err) + } + + return s +} + +// Append will append an error into [ErrorList] +func (e ErrorList) Append(err error) ErrorList { + return append(e, err) +} + +// New will create new instance of [ErrorList] +// +// use [nil] as parameter to create empty [ErrorList] +// +// use [Append] to add into [ErrorList] +func New(err error) ErrorList { + if err == nil { + return make(ErrorList, 0) + } + + if errl, ok := err.(ErrorList); ok { + return errl + } + + e := make(ErrorList, 0) + e = append(e, err) + return e +} From 609747b06cd4ac8658bf0dcf0e05e874d5490a24 Mon Sep 17 00:00:00 2001 From: rei Date: Sat, 14 Dec 2024 15:19:12 +0700 Subject: [PATCH 03/10] feat: added mssql driver --- components/connection_form.go | 2 + components/connection_selection.go | 2 + components/results_table.go | 29 +- drivers/constants.go | 1 + drivers/mssqql.go | 740 +++++++++++++++++++++++++++++ go.mod | 5 + go.sum | 35 ++ 7 files changed, 809 insertions(+), 5 deletions(-) create mode 100644 drivers/mssqql.go diff --git a/components/connection_form.go b/components/connection_form.go index 0047661..0a16aa9 100644 --- a/components/connection_form.go +++ b/components/connection_form.go @@ -180,6 +180,8 @@ func (form *ConnectionForm) testConnection(connectionString string) { db = &drivers.Postgres{} case drivers.DriverSqlite: db = &drivers.SQLite{} + case drivers.DriverMsSQL: + db = &drivers.MsSQL{} } err = db.TestConnection(connectionString) diff --git a/components/connection_selection.go b/components/connection_selection.go index 00657fe..a0c8e24 100644 --- a/components/connection_selection.go +++ b/components/connection_selection.go @@ -194,6 +194,8 @@ func (cs *ConnectionSelection) Connect(connection models.Connection) *tview.Appl newDBDriver = &drivers.Postgres{} case drivers.DriverSqlite: newDBDriver = &drivers.SQLite{} + case drivers.DriverMsSQL: + newDBDriver = &drivers.MsSQL{} } err := newDBDriver.Connect(connection.URL) diff --git a/components/results_table.go b/components/results_table.go index ffa11bd..c677b38 100644 --- a/components/results_table.go +++ b/components/results_table.go @@ -917,11 +917,30 @@ func (table *ResultsTable) FetchRecords(onError func()) [][]string { table.SetIsFiltering(false) } - columns, _ := table.DBDriver.GetTableColumns(databaseName, tableName) - constraints, _ := table.DBDriver.GetConstraints(databaseName, tableName) - foreignKeys, _ := table.DBDriver.GetForeignKeys(databaseName, tableName) - indexes, _ := table.DBDriver.GetIndexes(databaseName, tableName) - primaryKeyColumnNames, _ := table.DBDriver.GetPrimaryKeyColumnNames(databaseName, tableName) + columns, err := table.DBDriver.GetTableColumns(databaseName, tableName) + if err != nil { + table.SetError(err.Error(), nil) + } + + constraints, err := table.DBDriver.GetConstraints(databaseName, tableName) + if err != nil { + table.SetError(err.Error(), nil) + } + + foreignKeys, err := table.DBDriver.GetForeignKeys(databaseName, tableName) + if err != nil { + table.SetError(err.Error(), nil) + } + + indexes, err := table.DBDriver.GetIndexes(databaseName, tableName) + if err != nil { + table.SetError(err.Error(), nil) + } + + primaryKeyColumnNames, err := table.DBDriver.GetPrimaryKeyColumnNames(databaseName, tableName) + if err != nil { + table.SetError(err.Error(), nil) + } logger.Info("FetchRecords", map[string]any{"primaryKeyColumnNames": primaryKeyColumnNames}) diff --git a/drivers/constants.go b/drivers/constants.go index 31d8d6f..ebf246f 100644 --- a/drivers/constants.go +++ b/drivers/constants.go @@ -9,4 +9,5 @@ const ( DriverMySQL string = "mysql" DriverPostgres string = "postgres" DriverSqlite string = "sqlite3" + DriverMsSQL string = "sqlserver" ) diff --git a/drivers/mssqql.go b/drivers/mssqql.go new file mode 100644 index 0000000..2326132 --- /dev/null +++ b/drivers/mssqql.go @@ -0,0 +1,740 @@ +package drivers + +import ( + "database/sql" + "errors" + "fmt" + "strings" + + "github.com/jorgerojas26/lazysql/helpers/errorlist" + "github.com/jorgerojas26/lazysql/helpers/logger" + "github.com/jorgerojas26/lazysql/models" + "github.com/xo/dburl" + + _ "github.com/microsoft/go-mssqldb" +) + +type MsSQL struct { + Connection *sql.DB + Provider string +} + +const ( + mssqlDefaulPort = "1433" +) + +func (db *MsSQL) TestConnection(urlstr string) error { + return db.Connect(urlstr) +} + +func (db *MsSQL) Connect(urlstr string) error { + if urlstr == "" { + return errors.New("url string can not be empty") + } + + db.SetProvider(DriverMsSQL) + + var err error + + db.Connection, err = dburl.Open(urlstr) + if err != nil { + return err + } + + if err := db.Connection.Ping(); err != nil { + return err + } + + return nil +} + +func (db *MsSQL) GetDatabases() ([]string, error) { + databases := make([]string, 0) + + query := ` + SELECT + name + FROM + sys.databases + ` + rows, err := db.Connection.Query(query) + if err != nil { + return nil, err + } + + defer rows.Close() + + for rows.Next() { + var database string + if err := rows.Scan(&database); err != nil { + return nil, err + } + + databases = append(databases, database) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return databases, nil +} + +func (db *MsSQL) GetTables(database string) (map[string][]string, error) { + if database == "" { + return nil, errors.New("database name is required") + } + + tables := make(map[string][]string) + + query := ` + SELECT + CONCAT(schema_name(schema_id), '.', name) AS name + FROM + sys.tables + ` + rows, err := db.Connection.Query(query) + if err != nil { + return nil, err + } + + defer rows.Close() + + for rows.Next() { + var table string + if err := rows.Scan(&table); err != nil { + return nil, err + } + + tables[database] = append(tables[database], table) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return tables, nil +} + +func (db *MsSQL) GetTableColumns(database, table string) ([][]string, error) { + query := ` + SELECT + column_name, data_type, is_nullable, column_default + FROM + information_schema.columns + WHERE + table_catalog = @p1 + AND + table_schema = @p2 + AND + table_name = @p3 + ORDER BY + ordinal_position + ` + return db.getTableInformations(query, database, table) +} + +func (db *MsSQL) GetConstraints(database, table string) ([][]string, error) { + query := ` + SELECT + tc.constraint_name, + kcu.column_name, + tc.constraint_type + FROM + information_schema.table_constraints AS tc + JOIN + information_schema.key_column_usage AS kcu + ON + tc.constraint_name = kcu.constraint_name + AND + tc.table_schema = kcu.table_schema + JOIN + information_schema.constraint_column_usage AS ccu + ON + ccu.constraint_name = tc.constraint_name + AND + ccu.table_schema = tc.table_schema + WHERE NOT + tc.constraint_type = 'FOREIGN KEY' + AND + tc.table_catalog = @p1 + AND + tc.table_schema = @p2 + AND + tc.table_name = @p3 + ` + return db.getTableInformations(query, database, table) +} + +func (db *MsSQL) GetForeignKeys(database, table string) ([][]string, error) { + query := ` + SELECT + tc.constraint_name, + kcu.column_name, + tc.constraint_type + FROM + information_schema.table_constraints AS tc + JOIN + information_schema.key_column_usage AS kcu + ON + tc.constraint_name = kcu.constraint_name + AND + tc.table_schema = kcu.table_schema + JOIN + information_schema.constraint_column_usage AS ccu + ON + ccu.constraint_name = tc.constraint_name + AND + ccu.table_schema = tc.table_schema + WHERE + tc.constraint_type = 'FOREIGN KEY' + AND + tc.table_catalog = @p1 + AND + tc.table_schema = @p2 + AND + tc.table_name = @p3 + ` + return db.getTableInformations(query, database, table) +} + +func (db *MsSQL) GetIndexes(database, table string) ([][]string, error) { + query := ` + SELECT + t.name AS table_name, + i.name AS index_name, + i.is_unique AS is_unique, + i.is_primary_key AS is_primary_key, + i.type_desc AS index_type, + c.name AS column_name, + ic.key_ordinal AS seq_in_index, + ic.is_included_column AS is_included, + i.has_filter AS has_filter, + i.filter_definition AS filter_definition + FROM + sys.tables t + INNER JOIN + sys.schemas s + ON + t.schema_id = s.schema_id + INNER JOIN + sys.indexes i + ON + t.object_id = i.object_id + INNER JOIN + sys.index_columns ic + ON + i.object_id = ic.object_id + AND + i.index_id = ic.index_id + INNER JOIN + sys.columns c + ON + t.object_id = c.object_id + AND + ic.column_id = c.column_id + WHERE + DB_NAME() = @p1 + AND + s.name = @p2 + AND + t.name = @p3 + ORDER BY + i.type_desc + ` + return db.getTableInformations(query, database, table) +} + +func (db *MsSQL) GetRecords(database, table, where, sort string, offset, limit int) ([][]string, int, error) { + if database == "" { + return nil, 0, errors.New("database name is required") + } + + if table == "" { + return nil, 0, errors.New("table name is required") + } + + if limit == 0 { + limit = DefaultRowLimit + } + + results := make([][]string, 0) + + query := fmt.Sprintf("SELECT * FROM %s", table) + + if where != "" { + query += fmt.Sprintf(" %s", where) + } + + // since in mssql order is mandatory when using pagination + if sort == "" { + sort = "(SELECT NULL)" + } + query += fmt.Sprintf(" ORDER BY %s OFFSET @p1 ROWS FETCH NEXT @p2 ROWS ONLY", sort) + + rows, err := db.Connection.Query(query, offset, limit) + if err != nil { + return nil, 0, err + } + + defer rows.Close() + + columns, err := rows.Columns() + if err != nil { + return nil, 0, err + } + + results = append(results, columns) + + for rows.Next() { + rowValues := make([]any, len(columns)) + + for i := range columns { + rowValues[i] = new(sql.RawBytes) + } + + if err := rows.Scan(rowValues...); err != nil { + return nil, 0, err + } + + var row []string + for _, col := range rowValues { + if col == nil { + row = append(row, "NULL&") + continue + } + + colval := string(*col.(*sql.RawBytes)) + if colval == "" { + row = append(row, "EMPTY&") + } else { + row = append(row, string(*col.(*sql.RawBytes))) + } + } + + results = append(results, row) + } + + if err := rows.Err(); err != nil { + return nil, 0, err + } + + totalRecords := 0 + row := db.Connection.QueryRow(fmt.Sprintf("SELECT COUNT(*) FROM %s", table)) + if err := row.Scan(&totalRecords); err != nil { + return nil, 0, err + } + + return results, totalRecords, nil +} + +func (db *MsSQL) UpdateRecord(database, table, column, value, primaryKeyColumnName, primaryKeyValue string) error { + if database == "" { + return errors.New("database name is required") + } + + if table == "" { + return errors.New("table name is required") + } + + if column == "" { + return errors.New("table column is required") + } + + // if update into empty value is using "''" instead of "" + // then uncomment following condition + // + // if value == "" { + // return errors.New("column value can not be empty") + // } + + if primaryKeyColumnName == "" { + return errors.New("primary key column is required") + } + + if primaryKeyValue == "" { + return errors.New("primary key value is required") + } + + query := fmt.Sprintf( + "UPDATE %s SET %s = @p1 WHERE %s = @p2", + table, column, primaryKeyColumnName, + ) + _, err := db.Connection.Exec(query, value, primaryKeyValue) + + return err +} + +func (db *MsSQL) DeleteRecord(database, table, primaryKeyColumnName, primaryKeyValue string) error { + if database == "" { + return errors.New("database name is required") + } + + if table == "" { + return errors.New("table name is required") + } + + if primaryKeyColumnName == "" { + return errors.New("primary key column is required") + } + + if primaryKeyValue == "" { + return errors.New("primary key value is required") + } + + query := fmt.Sprintf( + "DELETE FROM %s WHERE %s = @p1", + table, primaryKeyColumnName, + ) + _, err := db.Connection.Exec(query, primaryKeyValue) + + return err +} + +func (db *MsSQL) ExecuteDMLStatement(query string) (string, error) { + if query == "" { + return "", errors.New("query is required") + } + + res, err := db.Connection.Exec(query) + if err != nil { + return "", err + } + + rowsAffected, err := res.RowsAffected() + if err != nil { + return "", err + } + + return fmt.Sprintf("%d rows affected", rowsAffected), nil +} + +func (db *MsSQL) ExecuteQuery(query string) ([][]string, error) { + if query == "" { + return nil, errors.New("query can not be empty") + } + + results := make([][]string, 0) + rows, err := db.Connection.Query(query) + if err != nil { + return nil, err + } + + defer rows.Close() + + columns, err := rows.Columns() + if err != nil { + return nil, err + } + + results = append(results, columns) + + for rows.Next() { + rowValues := make([]any, len(columns)) + + for i := range columns { + rowValues[i] = new(sql.RawBytes) + } + + if err := rows.Scan(rowValues...); err != nil { + return nil, err + } + + var row []string + for _, col := range rowValues { + row = append(row, string(*col.(*sql.RawBytes))) + } + + results = append(results, row) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return results, nil +} + +func (db *MsSQL) ExecutePendingChanges(changes []models.DBDMLChange) error { + if len(changes) <= 0 { + return nil + } + + queries := make([]models.Query, 0) + errlist := errorlist.New(nil) + + for _, change := range changes { + if change.Table == "" { + errlist = append(errlist, errors.New("table name is required")) + continue + } + + columnNames := make([]string, 0) + valuesPlaceholder := make([]string, 0) + values := make([]any, 0) + + for i, cell := range change.Values { + columnNames = append(columnNames, cell.Column) + + switch cell.Type { + case models.Default: + valuesPlaceholder = append(valuesPlaceholder, "DEFAULT") + case models.Null: + valuesPlaceholder = append(valuesPlaceholder, "NULL") + default: + valuesPlaceholder = append(valuesPlaceholder, fmt.Sprintf("@p%d", i+1)) + } + } + + for _, cell := range change.Values { + switch cell.Type { + case models.Empty: + values = append(values, "") + case models.String: + values = append(values, cell.Value) + } + } + + switch change.Type { + case models.DMLInsertType: + queryStr := fmt.Sprintf( + "INSERT INTO %s (%s) VALUES (%s)", + change.Table, + strings.Join(columnNames, ", "), + strings.Join(valuesPlaceholder, ", "), + ) + + newQuery := models.Query{ + Query: queryStr, + Args: values, + } + + queries = append(queries, newQuery) + case models.DMLUpdateType: + queryStr := fmt.Sprintf("UPDATE %s", change.Table) + + for i, column := range columnNames { + if i == 0 { + queryStr += fmt.Sprintf(" SET `%s` = %s", column, valuesPlaceholder[i]) + } else { + queryStr += fmt.Sprintf(", `%s` = %s", column, valuesPlaceholder[i]) + } + } + + args := make([]any, len(values)) + + copy(args, values) + + for i, pki := range change.PrimaryKeyInfo { + if i == 0 { + queryStr += fmt.Sprintf(" WHERE `%s` = @p%d", pki.Name, i+1) + } else { + queryStr += fmt.Sprintf(" AND `%s` = @p%d", pki.Name, i+1) + } + args = append(args, pki.Value) + } + + newQuery := models.Query{ + Query: queryStr, + Args: args, + } + + queries = append(queries, newQuery) + case models.DMLDeleteType: + queryStr := fmt.Sprintf("DELETE FROM %s", change.Table) + + deleteArgs := make([]any, len(change.PrimaryKeyInfo)) + + for i, pki := range change.PrimaryKeyInfo { + if i == 0 { + queryStr += fmt.Sprintf(" WHERE `%s` = @p%d", pki.Name, i+1) + } else { + queryStr += fmt.Sprintf(" AND `%s` = @p%d", pki.Name, i+1) + } + deleteArgs[i] = pki.Value + } + + newQuery := models.Query{ + Query: queryStr, + Args: deleteArgs, + } + + queries = append(queries, newQuery) + } + } + + // log loop errlist + if len(errlist) > 0 { + errmap := make(map[string]any) + for i, e := range errlist { + errmap[fmt.Sprintf("%d:", i+1)] = e + } + logger.Error("ExecutePendingChanges", errmap) + } + + return queriesInTransaction(db.Connection, queries) +} + +func (db *MsSQL) GetPrimaryKeyColumnNames(database, table string) ([]string, error) { + if database == "" { + return nil, errors.New("database name is required") + } + + if table == "" { + return nil, errors.New("table name is required") + } + + splitTableString := strings.Split(table, ".") + + if len(splitTableString) == 1 { + return nil, errors.New("table must be in the format schema.table") + } + + tableSchema := splitTableString[0] + tableName := splitTableString[1] + + pkColumnName := make([]string, 0) + query := ` + SELECT + c.name AS column_name + FROM + sys.tables t + INNER JOIN + sys.schemas s + ON + t.schema_id = s.schema_id + INNER JOIN + sys.key_constraints kc + ON + t.object_id = kc.parent_object_id + AND + kc.type = @p1 + INNER JOIN + sys.index_columns ic + ON + kc.unique_index_id = ic.index_id + AND + t.object_id = ic.object_id + INNER JOIN + sys.columns c + ON + ic.column_id = c.column_id + AND + t.object_id = c.object_id + WHERE + DB_NAME() = @p2 + AND + s.name = @p3 + AND + t.name = @p4 + ` + rows, err := db.Connection.Query(query, "PK", database, tableSchema, tableName) + if err != nil { + return nil, err + } + + defer rows.Close() + + for rows.Next() { + var colName string + err = rows.Scan(&colName) + if err != nil { + return nil, err + } + + if rows.Err() != nil { + return nil, rows.Err() + } + + pkColumnName = append(pkColumnName, colName) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return pkColumnName, nil +} + +func (db *MsSQL) SetProvider(provider string) { + db.Provider = provider +} + +func (db *MsSQL) GetProvider() string { + return db.Provider +} + +// getTableInformations is used for following func: +// +// - [GetTableColumns] +// - [GetConstraints] +// - [GetForeignKeys] +// - [GetIndexes] +// +// getTableInformations requires following parameter: +// +// - database name, used for filtering table_catalog +// - schema name, used for filtering table_schema +// - table name, used for filtering table_name +func (db *MsSQL) getTableInformations(query, database, table string) ([][]string, error) { + if database == "" { + return nil, errors.New("database name is required") + } + + if table == "" { + return nil, errors.New("table name is required") + } + + if query == "" { + return nil, errors.New("query can not be empty") + } + + splitTableString := strings.Split(table, ".") + + if len(splitTableString) == 1 { + return nil, errors.New("table must be in the format schema.table") + } + + tableSchema := splitTableString[0] + tableName := splitTableString[1] + + results := make([][]string, 0) + rows, err := db.Connection.Query(query, database, tableSchema, tableName) + if err != nil { + return nil, err + } + + defer rows.Close() + + columns, err := rows.Columns() + if err != nil { + return nil, err + } + + results = append(results, columns) + + for rows.Next() { + rowValues := make([]any, len(columns)) + + for i := range columns { + rowValues[i] = new(sql.RawBytes) + } + + if err := rows.Scan(rowValues...); err != nil { + return nil, err + } + + var row []string + for _, col := range rowValues { + row = append(row, string(*col.(*sql.RawBytes))) + } + + results = append(results, row) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return results, nil +} diff --git a/go.mod b/go.mod index 3d5afb1..95cd609 100644 --- a/go.mod +++ b/go.mod @@ -18,15 +18,20 @@ require ( require ( filippo.io/edwards25519 v1.1.0 // indirect + github.com/denisenkom/go-mssqldb v0.12.3 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/gdamore/encoding v1.0.1 // indirect + github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect + github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/microsoft/go-mssqldb v1.8.0 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/crypto v0.24.0 // indirect golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect golang.org/x/sys v0.27.0 // indirect golang.org/x/term v0.26.0 // indirect diff --git a/go.sum b/go.sum index 5feba0d..e400f1d 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,18 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0= +github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+8Y+8shw= +github.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= @@ -15,6 +22,12 @@ github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAY github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -31,12 +44,16 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/microsoft/go-mssqldb v1.8.0 h1:7cyZ/AT7ycDsEoWPIXibd+aVKFtteUNhDGf3aobP+tw= +github.com/microsoft/go-mssqldb v1.8.0/go.mod h1:6znkekS3T2vp0waiMhen4GPU1BiAsrP+iXHcE7a7rFo= github.com/mitchellh/go-linereader v0.0.0-20190213213312-1b945b3263eb h1:GRiLv4rgyqjqzxbhJke65IYUf4NCOOvrPOJbV/sPxkM= github.com/mitchellh/go-linereader v0.0.0-20190213213312-1b945b3263eb/go.mod h1:OaY7UOoTkkrX3wRwjpYRKafIkkyeD0UtweSHAWWiqQM= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= @@ -47,21 +64,31 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/xo/dburl v0.23.2 h1:Fl88cvayrgE56JA/sqhNMLljCW/b7RmG1mMkKMZUFgA= github.com/xo/dburl v0.23.2/go.mod h1:uazlaAQxj4gkshhfuuYyvwCBouOmNnG2aDxTCFZpmL4= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -70,7 +97,9 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -87,6 +116,7 @@ golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= @@ -99,6 +129,11 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= modernc.org/cc/v4 v4.23.1 h1:WqJoPL3x4cUufQVHkXpXX7ThFJ1C4ik80i2eXEXbhD8= From d9b5b5d99f26069d84f99ce5066eed5e2498ea8e Mon Sep 17 00:00:00 2001 From: rei Date: Sat, 14 Dec 2024 16:23:28 +0700 Subject: [PATCH 04/10] fix: remove schema from table name --- drivers/mssqql.go | 45 ++++++++------------------------------------- 1 file changed, 8 insertions(+), 37 deletions(-) diff --git a/drivers/mssqql.go b/drivers/mssqql.go index 2326132..3c66938 100644 --- a/drivers/mssqql.go +++ b/drivers/mssqql.go @@ -89,7 +89,7 @@ func (db *MsSQL) GetTables(database string) (map[string][]string, error) { query := ` SELECT - CONCAT(schema_name(schema_id), '.', name) AS name + name FROM sys.tables ` @@ -125,9 +125,7 @@ func (db *MsSQL) GetTableColumns(database, table string) ([][]string, error) { WHERE table_catalog = @p1 AND - table_schema = @p2 - AND - table_name = @p3 + table_name = @p2 ORDER BY ordinal_position ` @@ -159,9 +157,7 @@ func (db *MsSQL) GetConstraints(database, table string) ([][]string, error) { AND tc.table_catalog = @p1 AND - tc.table_schema = @p2 - AND - tc.table_name = @p3 + tc.table_name = @p2 ` return db.getTableInformations(query, database, table) } @@ -191,9 +187,7 @@ func (db *MsSQL) GetForeignKeys(database, table string) ([][]string, error) { AND tc.table_catalog = @p1 AND - tc.table_schema = @p2 - AND - tc.table_name = @p3 + tc.table_name = @p2 ` return db.getTableInformations(query, database, table) } @@ -236,9 +230,7 @@ func (db *MsSQL) GetIndexes(database, table string) ([][]string, error) { WHERE DB_NAME() = @p1 AND - s.name = @p2 - AND - t.name = @p3 + t.name = @p2 ORDER BY i.type_desc ` @@ -584,15 +576,6 @@ func (db *MsSQL) GetPrimaryKeyColumnNames(database, table string) ([]string, err return nil, errors.New("table name is required") } - splitTableString := strings.Split(table, ".") - - if len(splitTableString) == 1 { - return nil, errors.New("table must be in the format schema.table") - } - - tableSchema := splitTableString[0] - tableName := splitTableString[1] - pkColumnName := make([]string, 0) query := ` SELECT @@ -624,11 +607,9 @@ func (db *MsSQL) GetPrimaryKeyColumnNames(database, table string) ([]string, err WHERE DB_NAME() = @p2 AND - s.name = @p3 - AND - t.name = @p4 + t.name = @p3 ` - rows, err := db.Connection.Query(query, "PK", database, tableSchema, tableName) + rows, err := db.Connection.Query(query, "PK", database, table) if err != nil { return nil, err } @@ -674,7 +655,6 @@ func (db *MsSQL) GetProvider() string { // getTableInformations requires following parameter: // // - database name, used for filtering table_catalog -// - schema name, used for filtering table_schema // - table name, used for filtering table_name func (db *MsSQL) getTableInformations(query, database, table string) ([][]string, error) { if database == "" { @@ -689,17 +669,8 @@ func (db *MsSQL) getTableInformations(query, database, table string) ([][]string return nil, errors.New("query can not be empty") } - splitTableString := strings.Split(table, ".") - - if len(splitTableString) == 1 { - return nil, errors.New("table must be in the format schema.table") - } - - tableSchema := splitTableString[0] - tableName := splitTableString[1] - results := make([][]string, 0) - rows, err := db.Connection.Query(query, database, tableSchema, tableName) + rows, err := db.Connection.Query(query, database, table) if err != nil { return nil, err } From b62215aec11fab84eb88d98e440c9c16844072af Mon Sep 17 00:00:00 2001 From: rei Date: Sat, 14 Dec 2024 16:55:55 +0700 Subject: [PATCH 05/10] fix: Mssql ExecutePendingChanges syntax and parameter count --- drivers/mssqql.go | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/drivers/mssqql.go b/drivers/mssqql.go index 3c66938..636a1b0 100644 --- a/drivers/mssqql.go +++ b/drivers/mssqql.go @@ -507,9 +507,9 @@ func (db *MsSQL) ExecutePendingChanges(changes []models.DBDMLChange) error { for i, column := range columnNames { if i == 0 { - queryStr += fmt.Sprintf(" SET `%s` = %s", column, valuesPlaceholder[i]) + queryStr += fmt.Sprintf(" SET %s = %s", column, valuesPlaceholder[i]) } else { - queryStr += fmt.Sprintf(", `%s` = %s", column, valuesPlaceholder[i]) + queryStr += fmt.Sprintf(", %s = %s", column, valuesPlaceholder[i]) } } @@ -517,11 +517,15 @@ func (db *MsSQL) ExecutePendingChanges(changes []models.DBDMLChange) error { copy(args, values) + // start counting from valuesPlaceholder + // then add 1 by 1 on loop + updateCounterParams := len(valuesPlaceholder) for i, pki := range change.PrimaryKeyInfo { + updateCounterParams += 1 if i == 0 { - queryStr += fmt.Sprintf(" WHERE `%s` = @p%d", pki.Name, i+1) + queryStr += fmt.Sprintf(" WHERE %s = @p%d", pki.Name, updateCounterParams) } else { - queryStr += fmt.Sprintf(" AND `%s` = @p%d", pki.Name, i+1) + queryStr += fmt.Sprintf(" AND %s = @p%d", pki.Name, updateCounterParams) } args = append(args, pki.Value) } @@ -531,6 +535,8 @@ func (db *MsSQL) ExecutePendingChanges(changes []models.DBDMLChange) error { Args: args, } + // EZ way to log + // _ = os.WriteFile("/tmp/lazysql", []byte(queryStr+"\n"), 0644) queries = append(queries, newQuery) case models.DMLDeleteType: queryStr := fmt.Sprintf("DELETE FROM %s", change.Table) @@ -539,9 +545,9 @@ func (db *MsSQL) ExecutePendingChanges(changes []models.DBDMLChange) error { for i, pki := range change.PrimaryKeyInfo { if i == 0 { - queryStr += fmt.Sprintf(" WHERE `%s` = @p%d", pki.Name, i+1) + queryStr += fmt.Sprintf(" WHERE %s = @p%d", pki.Name, i+1) } else { - queryStr += fmt.Sprintf(" AND `%s` = @p%d", pki.Name, i+1) + queryStr += fmt.Sprintf(" AND %s = @p%d", pki.Name, i+1) } deleteArgs[i] = pki.Value } From 689ccb7a4ab777ae6bebadd1c783d0d3f597b0bc Mon Sep 17 00:00:00 2001 From: rei Date: Sat, 14 Dec 2024 19:42:06 +0700 Subject: [PATCH 06/10] adjust: MsSQL -> MSSQL --- components/connection_form.go | 4 +-- components/connection_selection.go | 4 +-- drivers/constants.go | 2 +- drivers/mssqql.go | 40 +++++++++++++++--------------- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/components/connection_form.go b/components/connection_form.go index 0a16aa9..b4db3cb 100644 --- a/components/connection_form.go +++ b/components/connection_form.go @@ -180,8 +180,8 @@ func (form *ConnectionForm) testConnection(connectionString string) { db = &drivers.Postgres{} case drivers.DriverSqlite: db = &drivers.SQLite{} - case drivers.DriverMsSQL: - db = &drivers.MsSQL{} + case drivers.DriverMSSQL: + db = &drivers.MSSQL{} } err = db.TestConnection(connectionString) diff --git a/components/connection_selection.go b/components/connection_selection.go index a0c8e24..ae2fae2 100644 --- a/components/connection_selection.go +++ b/components/connection_selection.go @@ -194,8 +194,8 @@ func (cs *ConnectionSelection) Connect(connection models.Connection) *tview.Appl newDBDriver = &drivers.Postgres{} case drivers.DriverSqlite: newDBDriver = &drivers.SQLite{} - case drivers.DriverMsSQL: - newDBDriver = &drivers.MsSQL{} + case drivers.DriverMSSQL: + newDBDriver = &drivers.MSSQL{} } err := newDBDriver.Connect(connection.URL) diff --git a/drivers/constants.go b/drivers/constants.go index ebf246f..c1d4a87 100644 --- a/drivers/constants.go +++ b/drivers/constants.go @@ -9,5 +9,5 @@ const ( DriverMySQL string = "mysql" DriverPostgres string = "postgres" DriverSqlite string = "sqlite3" - DriverMsSQL string = "sqlserver" + DriverMSSQL string = "sqlserver" ) diff --git a/drivers/mssqql.go b/drivers/mssqql.go index 636a1b0..ca27974 100644 --- a/drivers/mssqql.go +++ b/drivers/mssqql.go @@ -14,7 +14,7 @@ import ( _ "github.com/microsoft/go-mssqldb" ) -type MsSQL struct { +type MSSQL struct { Connection *sql.DB Provider string } @@ -23,16 +23,16 @@ const ( mssqlDefaulPort = "1433" ) -func (db *MsSQL) TestConnection(urlstr string) error { +func (db *MSSQL) TestConnection(urlstr string) error { return db.Connect(urlstr) } -func (db *MsSQL) Connect(urlstr string) error { +func (db *MSSQL) Connect(urlstr string) error { if urlstr == "" { return errors.New("url string can not be empty") } - db.SetProvider(DriverMsSQL) + db.SetProvider(DriverMSSQL) var err error @@ -48,7 +48,7 @@ func (db *MsSQL) Connect(urlstr string) error { return nil } -func (db *MsSQL) GetDatabases() ([]string, error) { +func (db *MSSQL) GetDatabases() ([]string, error) { databases := make([]string, 0) query := ` @@ -80,7 +80,7 @@ func (db *MsSQL) GetDatabases() ([]string, error) { return databases, nil } -func (db *MsSQL) GetTables(database string) (map[string][]string, error) { +func (db *MSSQL) GetTables(database string) (map[string][]string, error) { if database == "" { return nil, errors.New("database name is required") } @@ -116,7 +116,7 @@ func (db *MsSQL) GetTables(database string) (map[string][]string, error) { return tables, nil } -func (db *MsSQL) GetTableColumns(database, table string) ([][]string, error) { +func (db *MSSQL) GetTableColumns(database, table string) ([][]string, error) { query := ` SELECT column_name, data_type, is_nullable, column_default @@ -132,7 +132,7 @@ func (db *MsSQL) GetTableColumns(database, table string) ([][]string, error) { return db.getTableInformations(query, database, table) } -func (db *MsSQL) GetConstraints(database, table string) ([][]string, error) { +func (db *MSSQL) GetConstraints(database, table string) ([][]string, error) { query := ` SELECT tc.constraint_name, @@ -162,7 +162,7 @@ func (db *MsSQL) GetConstraints(database, table string) ([][]string, error) { return db.getTableInformations(query, database, table) } -func (db *MsSQL) GetForeignKeys(database, table string) ([][]string, error) { +func (db *MSSQL) GetForeignKeys(database, table string) ([][]string, error) { query := ` SELECT tc.constraint_name, @@ -192,7 +192,7 @@ func (db *MsSQL) GetForeignKeys(database, table string) ([][]string, error) { return db.getTableInformations(query, database, table) } -func (db *MsSQL) GetIndexes(database, table string) ([][]string, error) { +func (db *MSSQL) GetIndexes(database, table string) ([][]string, error) { query := ` SELECT t.name AS table_name, @@ -237,7 +237,7 @@ func (db *MsSQL) GetIndexes(database, table string) ([][]string, error) { return db.getTableInformations(query, database, table) } -func (db *MsSQL) GetRecords(database, table, where, sort string, offset, limit int) ([][]string, int, error) { +func (db *MSSQL) GetRecords(database, table, where, sort string, offset, limit int) ([][]string, int, error) { if database == "" { return nil, 0, errors.New("database name is required") } @@ -320,7 +320,7 @@ func (db *MsSQL) GetRecords(database, table, where, sort string, offset, limit i return results, totalRecords, nil } -func (db *MsSQL) UpdateRecord(database, table, column, value, primaryKeyColumnName, primaryKeyValue string) error { +func (db *MSSQL) UpdateRecord(database, table, column, value, primaryKeyColumnName, primaryKeyValue string) error { if database == "" { return errors.New("database name is required") } @@ -357,7 +357,7 @@ func (db *MsSQL) UpdateRecord(database, table, column, value, primaryKeyColumnNa return err } -func (db *MsSQL) DeleteRecord(database, table, primaryKeyColumnName, primaryKeyValue string) error { +func (db *MSSQL) DeleteRecord(database, table, primaryKeyColumnName, primaryKeyValue string) error { if database == "" { return errors.New("database name is required") } @@ -383,7 +383,7 @@ func (db *MsSQL) DeleteRecord(database, table, primaryKeyColumnName, primaryKeyV return err } -func (db *MsSQL) ExecuteDMLStatement(query string) (string, error) { +func (db *MSSQL) ExecuteDMLStatement(query string) (string, error) { if query == "" { return "", errors.New("query is required") } @@ -401,7 +401,7 @@ func (db *MsSQL) ExecuteDMLStatement(query string) (string, error) { return fmt.Sprintf("%d rows affected", rowsAffected), nil } -func (db *MsSQL) ExecuteQuery(query string) ([][]string, error) { +func (db *MSSQL) ExecuteQuery(query string) ([][]string, error) { if query == "" { return nil, errors.New("query can not be empty") } @@ -447,7 +447,7 @@ func (db *MsSQL) ExecuteQuery(query string) ([][]string, error) { return results, nil } -func (db *MsSQL) ExecutePendingChanges(changes []models.DBDMLChange) error { +func (db *MSSQL) ExecutePendingChanges(changes []models.DBDMLChange) error { if len(changes) <= 0 { return nil } @@ -573,7 +573,7 @@ func (db *MsSQL) ExecutePendingChanges(changes []models.DBDMLChange) error { return queriesInTransaction(db.Connection, queries) } -func (db *MsSQL) GetPrimaryKeyColumnNames(database, table string) ([]string, error) { +func (db *MSSQL) GetPrimaryKeyColumnNames(database, table string) ([]string, error) { if database == "" { return nil, errors.New("database name is required") } @@ -643,11 +643,11 @@ func (db *MsSQL) GetPrimaryKeyColumnNames(database, table string) ([]string, err return pkColumnName, nil } -func (db *MsSQL) SetProvider(provider string) { +func (db *MSSQL) SetProvider(provider string) { db.Provider = provider } -func (db *MsSQL) GetProvider() string { +func (db *MSSQL) GetProvider() string { return db.Provider } @@ -662,7 +662,7 @@ func (db *MsSQL) GetProvider() string { // // - database name, used for filtering table_catalog // - table name, used for filtering table_name -func (db *MsSQL) getTableInformations(query, database, table string) ([][]string, error) { +func (db *MSSQL) getTableInformations(query, database, table string) ([][]string, error) { if database == "" { return nil, errors.New("database name is required") } From b83eba8c0f0e89127a00ca9ed7983f5fb06862fa Mon Sep 17 00:00:00 2001 From: rei Date: Sat, 14 Dec 2024 19:46:05 +0700 Subject: [PATCH 07/10] adjust: remove errorlist pkg since the errors is not returned, but logged --- drivers/mssqql.go | 10 +-------- helpers/errorlist/errorlist.go | 41 ---------------------------------- 2 files changed, 1 insertion(+), 50 deletions(-) delete mode 100644 helpers/errorlist/errorlist.go diff --git a/drivers/mssqql.go b/drivers/mssqql.go index ca27974..f81fde5 100644 --- a/drivers/mssqql.go +++ b/drivers/mssqql.go @@ -6,7 +6,6 @@ import ( "fmt" "strings" - "github.com/jorgerojas26/lazysql/helpers/errorlist" "github.com/jorgerojas26/lazysql/helpers/logger" "github.com/jorgerojas26/lazysql/models" "github.com/xo/dburl" @@ -333,13 +332,6 @@ func (db *MSSQL) UpdateRecord(database, table, column, value, primaryKeyColumnNa return errors.New("table column is required") } - // if update into empty value is using "''" instead of "" - // then uncomment following condition - // - // if value == "" { - // return errors.New("column value can not be empty") - // } - if primaryKeyColumnName == "" { return errors.New("primary key column is required") } @@ -453,7 +445,7 @@ func (db *MSSQL) ExecutePendingChanges(changes []models.DBDMLChange) error { } queries := make([]models.Query, 0) - errlist := errorlist.New(nil) + errlist := make([]error, 0) for _, change := range changes { if change.Table == "" { diff --git a/helpers/errorlist/errorlist.go b/helpers/errorlist/errorlist.go deleted file mode 100644 index 20471af..0000000 --- a/helpers/errorlist/errorlist.go +++ /dev/null @@ -1,41 +0,0 @@ -package errorlist - -import ( - "fmt" -) - -type ErrorList []error - -func (e ErrorList) Error() string { - s := "" - - for idx, err := range e { - s = fmt.Sprintf("%d: %s\n", idx, err) - } - - return s -} - -// Append will append an error into [ErrorList] -func (e ErrorList) Append(err error) ErrorList { - return append(e, err) -} - -// New will create new instance of [ErrorList] -// -// use [nil] as parameter to create empty [ErrorList] -// -// use [Append] to add into [ErrorList] -func New(err error) ErrorList { - if err == nil { - return make(ErrorList, 0) - } - - if errl, ok := err.(ErrorList); ok { - return errl - } - - e := make(ErrorList, 0) - e = append(e, err) - return e -} From 4d9fd5b1285b41e35222fe6e7f69d5571c17b5aa Mon Sep 17 00:00:00 2001 From: rei Date: Sat, 14 Dec 2024 19:46:19 +0700 Subject: [PATCH 08/10] adjust: remove comment --- helpers/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/config.go b/helpers/config.go index edbfdad..4219d32 100644 --- a/helpers/config.go +++ b/helpers/config.go @@ -28,7 +28,7 @@ func LoadConfig() (Config, error) { for idx, cfg := range config.Connections { cfg.ParseURL() - config.Connections[idx].URL = cfg.URL // can be better than this + config.Connections[idx].URL = cfg.URL } return config, nil From cf12f9b3fee4298a9e1f35d71e8c1465c29c420e Mon Sep 17 00:00:00 2001 From: rei Date: Sun, 15 Dec 2024 20:10:35 +0700 Subject: [PATCH 09/10] adjust: move ParseURL from models into config, and rename it into ParseConfigURL --- helpers/config.go | 38 +++++++++++++++++++++++++++++++++++--- models/models.go | 35 ----------------------------------- 2 files changed, 35 insertions(+), 38 deletions(-) diff --git a/helpers/config.go b/helpers/config.go index 4219d32..4f28077 100644 --- a/helpers/config.go +++ b/helpers/config.go @@ -1,11 +1,14 @@ package helpers import ( + "fmt" + "net/url" "os" "path/filepath" "github.com/pelletier/go-toml/v2" + "github.com/jorgerojas26/lazysql/drivers" "github.com/jorgerojas26/lazysql/models" ) @@ -26,14 +29,43 @@ func LoadConfig() (Config, error) { return config, err } - for idx, cfg := range config.Connections { - cfg.ParseURL() - config.Connections[idx].URL = cfg.URL + for idx, conn := range config.Connections { + config.Connections[idx].URL = ParseConfigURL(&conn) } return config, nil } +// ParseConfigURL will manually parse config url if url empty +// +// it main purpose is for handling username & password with special characters +// +// only sqlserver for now +func ParseConfigURL(conn *models.Connection) string { + if conn.URL != "" { + return conn.URL + } + + // only sqlserver for now + if conn.Provider != drivers.DriverMSSQL { + return conn.URL + } + + user := url.QueryEscape(conn.Username) + pass := url.QueryEscape(conn.Password) + + return fmt.Sprintf( + "%s://%s:%s@%s:%s?database=%s%s", + conn.Provider, + user, + pass, + conn.Hostname, + conn.Port, + conn.DBName, + conn.URLParams, + ) +} + func LoadConnections() (connections []models.Connection, err error) { config, err := LoadConfig() if err != nil { diff --git a/models/models.go b/models/models.go index cb65534..a7bc9c9 100644 --- a/models/models.go +++ b/models/models.go @@ -1,9 +1,6 @@ package models import ( - "fmt" - "net/url" - "github.com/rivo/tview" ) @@ -25,38 +22,6 @@ type Connection struct { Commands []*Command } -// ParseURL will manually parse url if url empty -// -// for handling username & password with special characters -// -// only sqlserver for now -// -// need to refactor if wanted to reuse driver list in drivers/constants.go -func (c *Connection) ParseURL() { - if c.URL != "" { - return - } - - // only sqlserver for now - if c.Provider != "sqlserver" { - return - } - - user := url.QueryEscape(c.Username) - pass := url.QueryEscape(c.Password) - - c.URL = fmt.Sprintf( - "%s://%s:%s@%s:%s?database=%s%s", - c.Provider, - user, - pass, - c.Hostname, - c.Port, - c.DBName, - c.URLParams, - ) -} - type Command struct { Command string WaitForPort string From 682d8e780117c8c5457bc5e780cf98c8110f90a7 Mon Sep 17 00:00:00 2001 From: rei Date: Wed, 1 Jan 2025 21:19:02 +0700 Subject: [PATCH 10/10] adjust: G201: SQL string formatting (gosec), increment use ++, remove unused const --- drivers/mssqql.go | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/drivers/mssqql.go b/drivers/mssqql.go index f81fde5..c924944 100644 --- a/drivers/mssqql.go +++ b/drivers/mssqql.go @@ -18,10 +18,6 @@ type MSSQL struct { Provider string } -const ( - mssqlDefaulPort = "1433" -) - func (db *MSSQL) TestConnection(urlstr string) error { return db.Connect(urlstr) } @@ -251,7 +247,8 @@ func (db *MSSQL) GetRecords(database, table, where, sort string, offset, limit i results := make([][]string, 0) - query := fmt.Sprintf("SELECT * FROM %s", table) + query := "SELECT * FROM " + query += table if where != "" { query += fmt.Sprintf(" %s", where) @@ -340,10 +337,13 @@ func (db *MSSQL) UpdateRecord(database, table, column, value, primaryKeyColumnNa return errors.New("primary key value is required") } - query := fmt.Sprintf( - "UPDATE %s SET %s = @p1 WHERE %s = @p2", - table, column, primaryKeyColumnName, - ) + query := "UPDATE " + query += table + query += " SET " + query += column + query += " = @p1 WHERE " + query += primaryKeyColumnName + query += " = @p2" _, err := db.Connection.Exec(query, value, primaryKeyValue) return err @@ -366,10 +366,11 @@ func (db *MSSQL) DeleteRecord(database, table, primaryKeyColumnName, primaryKeyV return errors.New("primary key value is required") } - query := fmt.Sprintf( - "DELETE FROM %s WHERE %s = @p1", - table, primaryKeyColumnName, - ) + query := "DELETE FROM " + query += table + query += " WHERE " + query += primaryKeyColumnName + query += " = @p1" _, err := db.Connection.Exec(query, primaryKeyValue) return err @@ -513,7 +514,7 @@ func (db *MSSQL) ExecutePendingChanges(changes []models.DBDMLChange) error { // then add 1 by 1 on loop updateCounterParams := len(valuesPlaceholder) for i, pki := range change.PrimaryKeyInfo { - updateCounterParams += 1 + updateCounterParams++ if i == 0 { queryStr += fmt.Sprintf(" WHERE %s = @p%d", pki.Name, updateCounterParams) } else {