github.com/aldelo/common@v1.5.1/wrapper/mysql/mysql.go (about)

     1  package mysql
     2  
     3  /*
     4   * Copyright 2020-2023 Aldelo, LP
     5   *
     6   * Licensed under the Apache License, Version 2.0 (the "License");
     7   * you may not use this file except in compliance with the License.
     8   * You may obtain a copy of the License at
     9   *
    10   *     http://www.apache.org/licenses/LICENSE-2.0
    11   *
    12   * Unless required by applicable law or agreed to in writing, software
    13   * distributed under the License is distributed on an "AS IS" BASIS,
    14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    15   * See the License for the specific language governing permissions and
    16   * limitations under the License.
    17   */
    18  
    19  import (
    20  	"database/sql"
    21  	"errors"
    22  	"fmt"
    23  	"github.com/aldelo/common/wrapper/xray"
    24  	awsxray "github.com/aws/aws-xray-sdk-go/xray"
    25  	"strings"
    26  	"sync"
    27  	"time"
    28  
    29  	util "github.com/aldelo/common"
    30  	"github.com/jmoiron/sqlx"
    31  
    32  	// this package is used by database/sql as we are wrapping the sql access functionality in this utility package
    33  	_ "github.com/go-sql-driver/mysql"
    34  )
    35  
    36  // ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    37  // MySql struct Usage Hint
    38  // ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    39  
    40  /*
    41  	***************************************************************************************************************
    42  	First, Create "../model/global.go"
    43  	***************************************************************************************************************
    44  
    45  	package model
    46  
    47  	import (
    48  			"errors"
    49  			"time"
    50  			data "github.com/aldelo/common/wrapper/mysql"
    51  	)
    52  
    53  	// package level accessible to the mysql server database object
    54  	var db *data.MySql
    55  
    56  	// SetDB allows code outside of package to set the mysql database reference
    57  	func SetDB(dbx *data.MySql) {
    58  		db = dbx
    59  	}
    60  
    61  	// BeginTran starts db transaction
    62  	func BeginTran() {
    63  		if db != nil {
    64  			db.Begin()
    65  		}
    66  	}
    67  
    68  	// CommitTran commits db transaction
    69  	func CommitTran() {
    70  		if db != nil {
    71  			db.Commit()
    72  		}
    73  	}
    74  
    75  	// RollbackTran rolls back db transaction
    76  	func RollbackTran() {
    77  		if db != nil {
    78  			db.Rollback()
    79  		}
    80  	}
    81  
    82  */
    83  
    84  /*
    85  	***************************************************************************************************************
    86  	Second, Prepare DB Object for Use in "../main.go"
    87  	***************************************************************************************************************
    88  
    89  	package main
    90  
    91  	import (
    92  			...
    93  			data "github.com/aldelo/common/wrapper/mysql"
    94  			"???/model" // ??? is path to model package
    95  			...
    96  	)
    97  
    98  	...
    99  
   100  	func main() {
   101  		...
   102  
   103  		// ========================================
   104  		// setup database connection
   105  		// ========================================
   106  
   107  		//
   108  		// declare mysql server object
   109  		//
   110  		s := new(data.MySql)
   111  
   112  		//
   113  		// set mysql dsn fields
   114  		//
   115  		s.Host = "" 	// from aws aurora endpoint
   116  		s.Port = 0 		// custom port number if applicable (0 will ignore this field)
   117  		s.Database = ""	// database name
   118  		s.UserName = ""	// database server user name
   119  		s.Password = ""	// database server user password
   120  
   121  		//
   122  		// open mysql server database connection
   123  		//
   124  		if err := s.Open(); err != nil {
   125  			s.Close()
   126  		} else {
   127  			// add mysql db object to model global
   128  			model.SetDB(&s)
   129  
   130  			// defer db clean up upon execution ends
   131  			defer model.SetDB(nil)
   132  			defer s.Close()
   133  		}
   134  
   135  		...
   136  	}
   137  
   138  */
   139  
   140  /*
   141  	***************************************************************************************************************
   142  	Third, Using MySql Struct
   143  	***************************************************************************************************************
   144  
   145  	package model
   146  
   147  	import (
   148  		"bytes"
   149  		"database/sql"	// this import is needed for db struct tags
   150  		"errors"
   151  		"time"
   152  		util "github.com/aldelo/common"
   153  	)
   154  
   155  	// create a struct, and use db struct tags to identify parameter names
   156  	// db struct tags can contain ,required ,size=# if string
   157  	type Customer struct {
   158  		CustomerID		int		`db:"customerID"`
   159  		CompanyName		string	`db:"companyName"`
   160  	}
   161  
   162  	// when composing sql statements, if statement is long, use bytes.Buffer (or use /QueryBuilder.go)
   163  	var b bytes.Buffer
   164  
   165  	b.WriteString("xyz ")
   166  	b.WriteString("123")
   167  
   168  	v := b.String()		// v = xyz 123
   169  
   170  	// for insert, update, logical delete, physical delete
   171  	// use the appropriate functions from db struct, located in model/global.go
   172  	// the db struct is global in scope for code files within model package
   173  	db.GetStruct(...)
   174  	db.GetSliceStruct(...)
   175  	// etc
   176  
   177  */
   178  
   179  // ================================================================================================================
   180  // STRUCTS
   181  // ================================================================================================================
   182  
   183  // MySql struct encapsulates the MySql server database access functionality by wrapping Sqlx package with top level methods
   184  //
   185  //	   	Charset = utf8, utf8mb4 (< Default)
   186  //			Collation = utf8mb4_general_ci (< Default), utf8_general_ci
   187  //	   	...Timeout = must be decimal number with unit suffix (ms, s, m, h), such as "30s", "0.5m", "1m30s"
   188  //
   189  //			MaxOpenConns = maximum open and idle connections for the connection pool, default is 0, which is unlimited (db *sqlx.DB internally is a connection pool object)
   190  //			MaxIdleConns = maximum number of idle connections to keep in the connection pool, default is 0 which is unlimited, this number should be equal or less than MaxOpenConns
   191  //			MaxConnIdleTime = maximum duration that an idle connection be kept in the connection pool, default is 0 which has no time limit, suggest 5 - 10 minutes if set
   192  type MySql struct {
   193  	// MySql server connection properties
   194  	UserName string
   195  	Password string
   196  
   197  	Host     string
   198  	Port     int
   199  	Database string
   200  
   201  	Charset        string // utf8, utf8mb4
   202  	Collation      string // utf8mb4_general_ci, utf8_general_ci
   203  	ConnectTimeout string // must be decimal number with unit suffix (ms, s, m, h), such as "30s", "0.5m", "1m30s"
   204  	ReadTimeout    string // must be decimal number with unit suffix (ms, s, m, h), such as "30s", "0.5m", "1m30s"
   205  	WriteTimeout   string // must be decimal number with unit suffix (ms, s, m, h), such as "30s", "0.5m", "1m30s"
   206  	RejectReadOnly bool
   207  
   208  	MaxOpenConns    int
   209  	MaxIdleConns    int
   210  	MaxConnIdleTime time.Duration
   211  
   212  	// mysql server state object
   213  	db    *sqlx.DB
   214  	txMap map[string]*MySqlTransaction
   215  	mux   sync.RWMutex
   216  
   217  	lastPing       time.Time
   218  	_parentSegment *xray.XRayParentSegment
   219  }
   220  
   221  // MySqlTransaction represents an instance of the sqlx.Tx.
   222  // Since sqlx.DB is actually a connection pool container, it may initiate multiple sql transactions,
   223  // therefore, independent sqlx.Tx must be managed individually throughout its lifecycle.
   224  //
   225  // each mysql transaction created will also register with MySql struct, so that during Close/Cleanup, all the related outstanding Transactions can rollback
   226  type MySqlTransaction struct {
   227  	id         string
   228  	parent     *MySql
   229  	tx         *sqlx.Tx
   230  	closed     bool
   231  	_xrayTxSeg *xray.XSegment
   232  }
   233  
   234  // Commit will commit the current mysql transaction and close off from further uses (if commit was successful).
   235  // on commit error, transaction will not close off
   236  func (t *MySqlTransaction) Commit() (err error) {
   237  	if t._xrayTxSeg != nil {
   238  		defer func() {
   239  			if err != nil {
   240  				_ = t._xrayTxSeg.Seg.AddError(err)
   241  			}
   242  			if t.closed {
   243  				t._xrayTxSeg.Close()
   244  			}
   245  		}()
   246  	}
   247  
   248  	if t.closed {
   249  		err = fmt.Errorf("MySql Commit Transaction Not Valid, Transaction Already Closed")
   250  		return err
   251  	} else if util.LenTrim(t.id) == 0 {
   252  		err = fmt.Errorf("MySql Commit Transaction Not Valid, ID is Missing")
   253  		return err
   254  	} else if t.parent == nil {
   255  		err = fmt.Errorf("MySql Commit Transaction Not Valid, Parent is Missing")
   256  		return err
   257  	} else if t.tx == nil {
   258  		err = fmt.Errorf("MySql Commit Transaction Not Valid, Transaction Object Nil")
   259  		return err
   260  	}
   261  
   262  	if err = t.parent.Ping(); err != nil {
   263  		// ping failed
   264  		if t._xrayTxSeg != nil {
   265  			_ = t._xrayTxSeg.Seg.AddError(err)
   266  			t._xrayTxSeg.Close()
   267  			t._xrayTxSeg = nil
   268  		}
   269  		return err
   270  	}
   271  
   272  	if t._xrayTxSeg == nil {
   273  		// not using xray
   274  		if e := t.tx.Commit(); e != nil {
   275  			// on commit error, return error, don't close off transaction
   276  			err = fmt.Errorf("MySql Commit Transaction Failed, %s", e)
   277  			return err
   278  		} else {
   279  			// transaction commit success
   280  			t.closed = true
   281  
   282  			// remove this transaction from mysql parent txMap
   283  			t.parent.mux.Lock()
   284  			delete(t.parent.txMap, t.id)
   285  			t.parent.mux.Unlock()
   286  
   287  			// success response
   288  			return nil
   289  		}
   290  	} else {
   291  		// using xray
   292  		subSeg := t._xrayTxSeg.NewSubSegment("Commit-Transaction")
   293  		defer subSeg.Close()
   294  
   295  		subSeg.Capture("Commit-Transaction-Do", func() error {
   296  			if e := t.tx.Commit(); e != nil {
   297  				// on commit error, return error, don't close off transaction
   298  				err = fmt.Errorf("MySql Commit Transaction Failed, %s", e)
   299  				_ = subSeg.Seg.AddError(err)
   300  				return err
   301  			} else {
   302  				// transaction commit success
   303  				t.closed = true
   304  
   305  				// remove this transaction from mysql parent txMap
   306  				t.parent.mux.Lock()
   307  				delete(t.parent.txMap, t.id)
   308  				t.parent.mux.Unlock()
   309  
   310  				// success response
   311  				return nil
   312  			}
   313  		})
   314  
   315  		return err
   316  	}
   317  }
   318  
   319  // Rollback will rollback the current mysql transaction and close off from further uses (whether rollback succeeds or failures)
   320  func (t *MySqlTransaction) Rollback() (err error) {
   321  	if t._xrayTxSeg != nil {
   322  		defer func() {
   323  			if err != nil {
   324  				_ = t._xrayTxSeg.Seg.AddError(err)
   325  			}
   326  			t._xrayTxSeg.Close()
   327  		}()
   328  	}
   329  
   330  	if t.closed {
   331  		err = fmt.Errorf("MySql Rollback Transaction Not Valid, Transaction Already Closed")
   332  		return err
   333  	} else if util.LenTrim(t.id) == 0 {
   334  		err = fmt.Errorf("MySql Rollback Transaction Not Valid, ID is Missing")
   335  		return err
   336  	} else if t.parent == nil {
   337  		err = fmt.Errorf("MySql Rollback Transaction Not Valid, Parent is Missing")
   338  		return err
   339  	} else if t.tx == nil {
   340  		err = fmt.Errorf("MySql Rollback Transaction Not Valid, Transaction Object Nil")
   341  		return err
   342  	}
   343  
   344  	if err = t.parent.Ping(); err != nil {
   345  		// ping failed
   346  		if t._xrayTxSeg != nil {
   347  			_ = t._xrayTxSeg.Seg.AddError(err)
   348  			t._xrayTxSeg.Close()
   349  			t._xrayTxSeg = nil
   350  		}
   351  		return err
   352  	}
   353  
   354  	// perform rollback
   355  	if t._xrayTxSeg == nil {
   356  		if e := t.tx.Rollback(); e != nil {
   357  			err = fmt.Errorf("MySql Rollback Transaction Failed, %s", e)
   358  		}
   359  	} else {
   360  		subSeg := t._xrayTxSeg.NewSubSegment("Rollback-Transaction")
   361  		defer subSeg.Close()
   362  
   363  		subSeg.Capture("Rollback-Transaction-Do", func() error {
   364  			if e := t.tx.Rollback(); e != nil {
   365  				err = fmt.Errorf("MySql Rollback Transaction Failed, %s", e)
   366  				_ = subSeg.Seg.AddError(err)
   367  				return err
   368  			} else {
   369  				return nil
   370  			}
   371  		})
   372  	}
   373  
   374  	// transaction rollback success or failure, always close off transaction
   375  	t.closed = true
   376  
   377  	// remove this transaction from mysql parent txMap
   378  	t.parent.mux.Lock()
   379  	delete(t.parent.txMap, t.id)
   380  	t.parent.mux.Unlock()
   381  
   382  	// return response
   383  	return err
   384  }
   385  
   386  // ready checks if MySqlTransaction
   387  func (t *MySqlTransaction) ready() error {
   388  	if t.closed {
   389  		return fmt.Errorf("MySql Transaction Not Valid, Transaction Already Closed")
   390  	} else if util.LenTrim(t.id) == 0 {
   391  		return fmt.Errorf("MySql Transaction Not Valid, ID is Missing")
   392  	} else if t.parent == nil {
   393  		return fmt.Errorf("MySql Transaction Not Valid, Parent is Missing")
   394  	} else if t.tx == nil {
   395  		return fmt.Errorf("MySql Transaction Not Valid, Transaction Object Nil")
   396  	}
   397  
   398  	return nil
   399  }
   400  
   401  // MySqlResult defines sql action query result info
   402  // [ Notes ]
   403  //  1. NewlyInsertedID = ONLY FOR INSERT, ONLY IF AUTO_INCREMENT PRIMARY KEY (Custom PK ID Will Have This Field as 0 Always)
   404  type MySqlResult struct {
   405  	RowsAffected    int64
   406  	NewlyInsertedID int64 // ONLY FOR INSERT, ONLY IF AUTO_INCREMENT PRIMARY KEY (Custom PK ID Will Have This Field as 0 Always)
   407  	Err             error
   408  }
   409  
   410  // ================================================================================================================
   411  // STRUCT FUNCTIONS
   412  // ================================================================================================================
   413  
   414  // ----------------------------------------------------------------------------------------------------------------
   415  // utility functions
   416  // ----------------------------------------------------------------------------------------------------------------
   417  
   418  // GetDsn serializes MySql server dsn to connection string, for use in database connectivity
   419  func (svr *MySql) GetDsn() (string, error) {
   420  	//
   421  	// first validate input
   422  	//
   423  	if len(svr.UserName) == 0 {
   424  		return "", errors.New("User Name is Required")
   425  	}
   426  
   427  	if len(svr.Password) == 0 {
   428  		return "", errors.New("Password is Required")
   429  	}
   430  
   431  	if len(svr.Host) == 0 {
   432  		return "", errors.New("MySQL Host Address is Required")
   433  	}
   434  
   435  	if len(svr.Database) == 0 {
   436  		return "", errors.New("MySQL Database Name is Required")
   437  	}
   438  
   439  	//
   440  	// now create mysql connection string
   441  	// format = [username[:password]@][protocol[(address)]]/dbname[?param1=value1&...&paramN=valueN]
   442  	//
   443  	str := svr.UserName + ":" + svr.Password
   444  	str += "@(" + svr.Host
   445  
   446  	if svr.Port > 0 {
   447  		str += ":" + util.Itoa(svr.Port)
   448  	}
   449  
   450  	str += ")/" + svr.Database
   451  
   452  	if util.LenTrim(svr.Charset) > 0 {
   453  		str += "?charset=" + svr.Charset
   454  	} else {
   455  		str += "?charset=utf8mb4"
   456  	}
   457  
   458  	if util.LenTrim(svr.Collation) > 0 {
   459  		str += "&collation=" + svr.Collation
   460  	} else {
   461  		str += "&collation=utf8mb4_general_ci"
   462  	}
   463  
   464  	str += "&parseTime=true"
   465  
   466  	if util.LenTrim(svr.ConnectTimeout) > 0 {
   467  		str += "&timeout=" + svr.ConnectTimeout
   468  	}
   469  
   470  	if util.LenTrim(svr.ReadTimeout) > 0 {
   471  		str += "&readTimeout=" + svr.ReadTimeout
   472  	}
   473  
   474  	if util.LenTrim(svr.WriteTimeout) > 0 {
   475  		str += "&writeTimeout=" + svr.WriteTimeout
   476  	}
   477  
   478  	if svr.RejectReadOnly {
   479  		str += "&rejectReadOnly=true"
   480  	}
   481  
   482  	// return to caller
   483  	return str, nil
   484  }
   485  
   486  // cleanUpAllSqlTransactions is a helper that cleans up (rolls back) any outstanding sql transactions in the txMap
   487  func (svr *MySql) cleanUpAllSqlTransactions() {
   488  	svr.mux.Lock()
   489  	if svr.txMap != nil && len(svr.txMap) > 0 {
   490  		for k, v := range svr.txMap {
   491  			if v != nil && !v.closed && v.tx != nil {
   492  				_ = v.tx.Rollback()
   493  				if v._xrayTxSeg != nil {
   494  					v._xrayTxSeg.Close()
   495  				}
   496  			}
   497  			delete(svr.txMap, k)
   498  		}
   499  		svr.txMap = make(map[string]*MySqlTransaction)
   500  	}
   501  	svr.mux.Unlock()
   502  }
   503  
   504  // Open will open a database either as normal or with xray tracing,
   505  // Open uses the dsn properties defined in the struct fields
   506  func (svr *MySql) Open(parentSegment ...*xray.XRayParentSegment) error {
   507  	if !xray.XRayServiceOn() {
   508  		return svr.openNormal()
   509  	} else {
   510  		if len(parentSegment) > 0 {
   511  			svr._parentSegment = parentSegment[0]
   512  		}
   513  
   514  		return svr.openWithXRay()
   515  	}
   516  }
   517  
   518  // openNormal opens a database by connecting to it, using the dsn properties defined in the struct fields
   519  func (svr *MySql) openNormal() error {
   520  	// clean up first
   521  	svr.db = nil
   522  	svr.cleanUpAllSqlTransactions()
   523  
   524  	// declare
   525  	var str string
   526  	var err error
   527  
   528  	// get connect string
   529  	str, err = svr.GetDsn()
   530  
   531  	if err != nil {
   532  		return err
   533  	}
   534  
   535  	// validate connection string
   536  	if len(str) == 0 {
   537  		return errors.New("MySQL Server Connect String Generated Cannot Be Empty")
   538  	}
   539  
   540  	// now ready to open mysql database
   541  	svr.db, err = sqlx.Open("mysql", str)
   542  
   543  	if err != nil {
   544  		return err
   545  	}
   546  
   547  	// test mysql server state object
   548  	if err = svr.db.Ping(); err != nil {
   549  		svr.db = nil
   550  		return err
   551  	}
   552  	svr.lastPing = time.Now()
   553  
   554  	if svr.MaxOpenConns > 0 {
   555  		svr.db.SetMaxOpenConns(svr.MaxOpenConns)
   556  	}
   557  
   558  	if svr.MaxIdleConns > 0 {
   559  		if svr.MaxIdleConns <= svr.MaxOpenConns || svr.MaxOpenConns == 0 {
   560  			svr.db.SetMaxIdleConns(svr.MaxIdleConns)
   561  		} else {
   562  			svr.db.SetMaxIdleConns(svr.MaxOpenConns)
   563  		}
   564  	}
   565  
   566  	if svr.MaxConnIdleTime > 0 {
   567  		svr.db.SetConnMaxIdleTime(svr.MaxConnIdleTime)
   568  	}
   569  
   570  	svr.db.SetConnMaxLifetime(0)
   571  
   572  	// mysql server state object successfully opened
   573  	return nil
   574  }
   575  
   576  // openWithXRay opens a database by connecting to it, wrap with XRay tracing, using the dsn properties defined in the struct fields
   577  func (svr *MySql) openWithXRay() (err error) {
   578  	trace := xray.NewSegment("MySql-Open-Entry", svr._parentSegment)
   579  	defer trace.Close()
   580  	defer func() {
   581  		_ = trace.Seg.AddMetadata("DB-Host", svr.Host)
   582  		_ = trace.Seg.AddMetadata("DB-Database", svr.Database)
   583  		_ = trace.Seg.AddMetadata("DB-UserName", svr.UserName)
   584  		_ = trace.Seg.AddMetadata("DB-Charset", svr.Charset)
   585  		_ = trace.Seg.AddMetadata("DB-Collation", svr.Collation)
   586  		_ = trace.Seg.AddMetadata("DB-ConnectTimeout", svr.ConnectTimeout)
   587  		_ = trace.Seg.AddMetadata("DB-ReadTimeout", svr.ReadTimeout)
   588  		_ = trace.Seg.AddMetadata("DB-WriteTimeout", svr.WriteTimeout)
   589  
   590  		if err != nil {
   591  			_ = trace.Seg.AddError(err)
   592  		}
   593  	}()
   594  
   595  	// clean up first
   596  	svr.db = nil
   597  	svr.cleanUpAllSqlTransactions()
   598  
   599  	// declare
   600  	var str string
   601  
   602  	trace.Capture("Get-DSN", func() error {
   603  		// get connect string
   604  		str, err = svr.GetDsn()
   605  		return err
   606  	})
   607  
   608  	if err != nil {
   609  		return err
   610  	}
   611  
   612  	// validate connection string
   613  	if len(str) == 0 {
   614  		err = errors.New("MySQL Server Connect String Generated Cannot Be Empty")
   615  		return err
   616  	}
   617  
   618  	trace.Capture("Get-SQL-Context", func() error {
   619  		// now ready to open mysql database
   620  		baseDb, e := awsxray.SQLContext("mysql", str)
   621  
   622  		if e != nil {
   623  			err = fmt.Errorf("openWithXRay() Failed During xray.SQLContext(): %s", e.Error())
   624  			return err
   625  		}
   626  
   627  		svr.db = sqlx.NewDb(baseDb, "mysql")
   628  		return nil
   629  	})
   630  
   631  	if err != nil {
   632  		return err
   633  	}
   634  
   635  	// test mysql server state object
   636  	subTrace := trace.NewSubSegment("MySql-Open-Ping")
   637  	defer subTrace.Close()
   638  	defer func() {
   639  		if err != nil {
   640  			_ = subTrace.Seg.AddError(err)
   641  		}
   642  	}()
   643  
   644  	subTrace.Capture("Ping", func() error {
   645  		if err = svr.db.PingContext(trace.Ctx); err != nil {
   646  			svr.db = nil
   647  			return err
   648  		}
   649  		svr.lastPing = time.Now()
   650  		return nil
   651  	})
   652  
   653  	if err != nil {
   654  		return err
   655  	}
   656  
   657  	if svr.MaxOpenConns > 0 {
   658  		svr.db.SetMaxOpenConns(svr.MaxOpenConns)
   659  	}
   660  
   661  	if svr.MaxIdleConns > 0 {
   662  		if svr.MaxIdleConns <= svr.MaxOpenConns || svr.MaxOpenConns == 0 {
   663  			svr.db.SetMaxIdleConns(svr.MaxIdleConns)
   664  		} else {
   665  			svr.db.SetMaxIdleConns(svr.MaxOpenConns)
   666  		}
   667  	}
   668  
   669  	if svr.MaxConnIdleTime > 0 {
   670  		svr.db.SetConnMaxIdleTime(svr.MaxConnIdleTime)
   671  	}
   672  
   673  	svr.db.SetConnMaxLifetime(0)
   674  
   675  	// mysql server state object successfully opened
   676  	return nil
   677  }
   678  
   679  // Close will close the database connection and set db to nil
   680  func (svr *MySql) Close() error {
   681  	svr.cleanUpAllSqlTransactions()
   682  
   683  	if svr.db != nil {
   684  		if err := svr.db.Close(); err != nil {
   685  			return err
   686  		}
   687  
   688  		// clean up
   689  		svr.db = nil
   690  		svr._parentSegment = nil
   691  		svr.lastPing = time.Time{}
   692  		return nil
   693  	}
   694  
   695  	return nil
   696  }
   697  
   698  // Ping tests if current database connection is still active and ready
   699  func (svr *MySql) Ping() (err error) {
   700  	if svr.db == nil {
   701  		return errors.New("MySQL Server Not Connected")
   702  	}
   703  
   704  	if time.Now().Sub(svr.lastPing) < 90*time.Second {
   705  		return nil
   706  	}
   707  
   708  	if !xray.XRayServiceOn() {
   709  		if err := svr.db.Ping(); err != nil {
   710  			return err
   711  		}
   712  	} else {
   713  		trace := xray.NewSegment("MySql-Ping", svr._parentSegment)
   714  		defer trace.Close()
   715  		defer func() {
   716  			_ = trace.Seg.AddMetadata("Ping-Timestamp-UTC", time.Now().UTC())
   717  
   718  			if err != nil {
   719  				_ = trace.Seg.AddError(err)
   720  			}
   721  		}()
   722  
   723  		if err = svr.db.PingContext(trace.Ctx); err != nil {
   724  			return err
   725  		}
   726  	}
   727  
   728  	// database ok
   729  	svr.lastPing = time.Now()
   730  	return nil
   731  }
   732  
   733  // Begin starts a mysql transaction, and stores the transaction object into txMap until commit or rollback.
   734  // ensure that transaction related actions are executed from the MySqlTransaction object.
   735  func (svr *MySql) Begin() (*MySqlTransaction, error) {
   736  	// verify if the database connection is good
   737  	if err := svr.Ping(); err != nil {
   738  		// begin failed
   739  		return nil, err
   740  	}
   741  
   742  	if !xray.XRayServiceOn() {
   743  		// begin transaction on database
   744  		if tx, err := svr.db.Beginx(); err != nil {
   745  			// begin failed
   746  			return nil, err
   747  		} else {
   748  			// begin succeeded, create MySqlTransaction and return result
   749  			myTx := &MySqlTransaction{
   750  				id:         util.NewULID(),
   751  				parent:     svr,
   752  				tx:         tx,
   753  				closed:     false,
   754  				_xrayTxSeg: nil,
   755  			}
   756  			if svr.txMap == nil {
   757  				svr.txMap = make(map[string]*MySqlTransaction)
   758  			}
   759  			svr.txMap[myTx.id] = myTx
   760  			return myTx, nil
   761  		}
   762  	} else {
   763  		// begin transaction on database
   764  		xseg := xray.NewSegment("MySql-Transaction", svr._parentSegment)
   765  		subXSeg := xseg.NewSubSegment("Begin-Transaction")
   766  
   767  		if tx, err := svr.db.BeginTxx(subXSeg.Ctx, &sql.TxOptions{Isolation: 0, ReadOnly: false}); err != nil {
   768  			// begin failed
   769  			_ = subXSeg.Seg.AddError(err)
   770  			_ = xseg.Seg.AddError(err)
   771  			subXSeg.Close()
   772  			xseg.Close()
   773  			return nil, err
   774  		} else {
   775  			// begin succeeded, create MySqlTransaction and return result
   776  			myTx := &MySqlTransaction{
   777  				id:         util.NewULID(),
   778  				parent:     svr,
   779  				tx:         tx,
   780  				closed:     false,
   781  				_xrayTxSeg: xseg,
   782  			}
   783  			if svr.txMap == nil {
   784  				svr.txMap = make(map[string]*MySqlTransaction)
   785  			}
   786  			svr.txMap[myTx.id] = myTx
   787  			subXSeg.Close()
   788  			return myTx, nil
   789  		}
   790  	}
   791  }
   792  
   793  // ----------------------------------------------------------------------------------------------------------------
   794  // query and marshal to 'struct slice' or 'struct' helpers
   795  // ----------------------------------------------------------------------------------------------------------------
   796  
   797  // GetStructSlice performs query with optional variadic parameters, and unmarshal result rows into target struct slice,
   798  // in essence, each row of data is marshaled into the given struct, and multiple struct form the slice,
   799  // such as: []Customer where each row represent a customer, and multiple customers being part of the slice
   800  // [ Parameters ]
   801  //
   802  //	dest = pointer to the struct slice or address of struct slice, this is the result of rows to be marshaled into struct slice
   803  //	query = sql query, optionally having parameters marked as ?, where each represents a parameter position
   804  //	args = conditionally required if positioned parameters are specified, must appear in the same order as the positional parameters
   805  //
   806  // [ Return Values ]
   807  //  1. notFound = indicates no rows found in query (aka sql.ErrNoRows), if error is detected, notFound is always false
   808  //  2. if error != nil, then error is encountered (if error == sql.ErrNoRows, then error is treated as nil, and dest is nil)
   809  //
   810  // [ Notes ]
   811  //  1. if error == nil, and len(dest struct slice) == 0 then zero struct slice result
   812  func (svr *MySql) GetStructSlice(dest interface{}, query string, args ...interface{}) (notFound bool, retErr error) {
   813  	// verify if the database connection is good
   814  	if err := svr.Ping(); err != nil {
   815  		return false, err
   816  	}
   817  
   818  	// perform select action, and unmarshal result rows into target struct slice
   819  	var err error
   820  
   821  	// not in transaction mode
   822  	// query using db object
   823  	if !xray.XRayServiceOn() {
   824  		err = svr.db.Select(dest, query, args...)
   825  	} else {
   826  		trace := xray.NewSegment("MySql-Select-GetStructSlice", svr._parentSegment)
   827  		defer trace.Close()
   828  		defer func() {
   829  			_ = trace.Seg.AddMetadata("SQL-Query", query)
   830  			_ = trace.Seg.AddMetadata("SQL-Param-Values", args)
   831  			_ = trace.Seg.AddMetadata("Struct-Slice-Result", dest)
   832  			if err != nil {
   833  				_ = trace.Seg.AddError(err)
   834  			}
   835  		}()
   836  
   837  		err = svr.db.SelectContext(trace.Ctx, dest, query, args...)
   838  	}
   839  
   840  	// if err is sql.ErrNoRows then treat as no error
   841  	if err != nil && err == sql.ErrNoRows {
   842  		notFound = true
   843  		dest = nil
   844  		err = nil
   845  	} else {
   846  		notFound = false
   847  	}
   848  
   849  	// return error
   850  	return notFound, err
   851  }
   852  
   853  // GetStructSlice performs query with optional variadic parameters, and unmarshal result rows into target struct slice,
   854  // in essence, each row of data is marshaled into the given struct, and multiple struct form the slice,
   855  // such as: []Customer where each row represent a customer, and multiple customers being part of the slice
   856  // [ Parameters ]
   857  //
   858  //	dest = pointer to the struct slice or address of struct slice, this is the result of rows to be marshaled into struct slice
   859  //	query = sql query, optionally having parameters marked as ?, where each represents a parameter position
   860  //	args = conditionally required if positioned parameters are specified, must appear in the same order as the positional parameters
   861  //
   862  // [ Return Values ]
   863  //  1. notFound = indicates no rows found in query (aka sql.ErrNoRows), if error is detected, notFound is always false
   864  //  2. if error != nil, then error is encountered (if error == sql.ErrNoRows, then error is treated as nil, and dest is nil)
   865  //
   866  // [ Notes ]
   867  //  1. if error == nil, and len(dest struct slice) == 0 then zero struct slice result
   868  func (t *MySqlTransaction) GetStructSlice(dest interface{}, query string, args ...interface{}) (notFound bool, retErr error) {
   869  	if retErr = t.ready(); retErr != nil {
   870  		return false, retErr
   871  	}
   872  
   873  	// verify if the database connection is good
   874  	if retErr = t.parent.Ping(); retErr != nil {
   875  		return false, retErr
   876  	}
   877  
   878  	// perform select action, and unmarshal result rows into target struct slice
   879  	var err error
   880  
   881  	// in transaction mode
   882  	// query using tx object
   883  	if t._xrayTxSeg == nil {
   884  		err = t.tx.Select(dest, query, args...)
   885  	} else {
   886  		subTrace := t._xrayTxSeg.NewSubSegment("Transaction-Select-GetStructSlice")
   887  		defer subTrace.Close()
   888  		defer func() {
   889  			_ = subTrace.Seg.AddMetadata("SQL-Query", query)
   890  			_ = subTrace.Seg.AddMetadata("SQL-Param-Values", args)
   891  			_ = subTrace.Seg.AddMetadata("Struct-Slice-Result", dest)
   892  			if err != nil {
   893  				_ = subTrace.Seg.AddError(err)
   894  			}
   895  		}()
   896  
   897  		err = t.tx.SelectContext(subTrace.Ctx, dest, query, args...)
   898  	}
   899  
   900  	// if err is sql.ErrNoRows then treat as no error
   901  	if err != nil && err == sql.ErrNoRows {
   902  		notFound = true
   903  		dest = nil
   904  		err = nil
   905  	} else {
   906  		notFound = false
   907  	}
   908  
   909  	// return error
   910  	return notFound, err
   911  }
   912  
   913  // GetStruct performs query with optional variadic parameters, and unmarshal single result row into single target struct,
   914  // such as: Customer struct where one row of data represent a customer
   915  // [ Parameters ]
   916  //
   917  //	dest = pointer to struct or address of struct, this is the result of row to be marshaled into this struct
   918  //	query = sql query, optionally having parameters marked as ?, where each represents a parameter position
   919  //	args = conditionally required if positioned parameters are specified, must appear in the same order as the positional parameters
   920  //
   921  // [ Return Values ]
   922  //  1. notFound = indicates no rows found in query (aka sql.ErrNoRows), if error is detected, notFound is always false
   923  //  2. if error != nil, then error is encountered (if error == sql.ErrNoRows, then error is treated as nil, and dest is nil)
   924  func (svr *MySql) GetStruct(dest interface{}, query string, args ...interface{}) (notFound bool, retErr error) {
   925  	// verify if the database connection is good
   926  	if err := svr.Ping(); err != nil {
   927  		return false, err
   928  	}
   929  
   930  	// perform select action, and unmarshal result row (single row) into target struct (single object)
   931  	var err error
   932  
   933  	// not in transaction mode
   934  	// query using db object
   935  	if !xray.XRayServiceOn() {
   936  		err = svr.db.Get(dest, query, args...)
   937  	} else {
   938  		trace := xray.NewSegment("MySql-Select-GetStruct", svr._parentSegment)
   939  		defer trace.Close()
   940  		defer func() {
   941  			_ = trace.Seg.AddMetadata("SQL-Query", query)
   942  			_ = trace.Seg.AddMetadata("SQL-Param-Values", args)
   943  			_ = trace.Seg.AddMetadata("Struct-Result", dest)
   944  			if err != nil {
   945  				_ = trace.Seg.AddError(err)
   946  			}
   947  		}()
   948  
   949  		err = svr.db.GetContext(trace.Ctx, dest, query, args...)
   950  	}
   951  
   952  	// if err is sql.ErrNoRows then treat as no error
   953  	if err != nil && err == sql.ErrNoRows {
   954  		notFound = true
   955  		dest = nil
   956  		err = nil
   957  	} else {
   958  		notFound = false
   959  	}
   960  
   961  	// return error
   962  	return notFound, err
   963  }
   964  
   965  // GetStruct performs query with optional variadic parameters, and unmarshal single result row into single target struct,
   966  // such as: Customer struct where one row of data represent a customer
   967  // [ Parameters ]
   968  //
   969  //	dest = pointer to struct or address of struct, this is the result of row to be marshaled into this struct
   970  //	query = sql query, optionally having parameters marked as ?, where each represents a parameter position
   971  //	args = conditionally required if positioned parameters are specified, must appear in the same order as the positional parameters
   972  //
   973  // [ Return Values ]
   974  //  1. notFound = indicates no rows found in query (aka sql.ErrNoRows), if error is detected, notFound is always false
   975  //  2. if error != nil, then error is encountered (if error == sql.ErrNoRows, then error is treated as nil, and dest is nil)
   976  func (t *MySqlTransaction) GetStruct(dest interface{}, query string, args ...interface{}) (notFound bool, retErr error) {
   977  	if retErr = t.ready(); retErr != nil {
   978  		return false, retErr
   979  	}
   980  
   981  	// verify if the database connection is good
   982  	if retErr = t.parent.Ping(); retErr != nil {
   983  		return false, retErr
   984  	}
   985  
   986  	// perform select action, and unmarshal result row (single row) into target struct (single object)
   987  	var err error
   988  
   989  	// in transaction mode
   990  	// query using tx object
   991  	if t._xrayTxSeg == nil {
   992  		err = t.tx.Get(dest, query, args...)
   993  	} else {
   994  		subTrace := t._xrayTxSeg.NewSubSegment("Transaction-Select-GetStruct")
   995  		defer subTrace.Close()
   996  		defer func() {
   997  			_ = subTrace.Seg.AddMetadata("SQL-Query", query)
   998  			_ = subTrace.Seg.AddMetadata("SQL-Param-Values", args)
   999  			_ = subTrace.Seg.AddMetadata("Struct-Result", dest)
  1000  			if err != nil {
  1001  				_ = subTrace.Seg.AddError(err)
  1002  			}
  1003  		}()
  1004  
  1005  		err = t.tx.GetContext(subTrace.Ctx, dest, query, args...)
  1006  	}
  1007  
  1008  	// if err is sql.ErrNoRows then treat as no error
  1009  	if err != nil && err == sql.ErrNoRows {
  1010  		notFound = true
  1011  		dest = nil
  1012  		err = nil
  1013  	} else {
  1014  		notFound = false
  1015  	}
  1016  
  1017  	// return error
  1018  	return notFound, err
  1019  }
  1020  
  1021  // ----------------------------------------------------------------------------------------------------------------
  1022  // query and get rows helpers
  1023  // ----------------------------------------------------------------------------------------------------------------
  1024  
  1025  // GetRowsByOrdinalParams performs query with optional variadic parameters to get ROWS of result, and returns *sqlx.Rows
  1026  // [ Parameters ]
  1027  //
  1028  //	query = sql query, optionally having parameters marked as ?, where each represents a parameter position
  1029  //	args = conditionally required if positioned parameters are specified, must appear in the same order as the positional parameters
  1030  //
  1031  // [ Return Values ]
  1032  //  1. *sqlx.Rows = pointer to sqlx.Rows; or nil if no rows yielded
  1033  //  2. if error != nil, then error is encountered (if error == sql.ErrNoRows, then error is treated as nil, and sqlx.Rows is returned as nil)
  1034  //
  1035  // [ Ranged Loop & Scan ]
  1036  //  1. to loop, use:
  1037  //     for {
  1038  //     if !rows.Next() { break }
  1039  //     rows.Scan(&x, &y, etc)
  1040  //     }
  1041  //  2. to scan, use: r.Scan(&x, &y, ...), where r is the row struct in loop, where &x &y etc are the scanned output value (scan in order of select columns sequence)
  1042  //
  1043  // [ Continuous Loop & Scan ]
  1044  //  1. Continuous loop until endOfRows = true is yielded from ScanSlice() or ScanStruct()
  1045  //  2. ScanSlice(): accepts *sqlx.Rows, scans rows result into target pointer slice (if no error, endOfRows = true is returned)
  1046  //  3. ScanStruct(): accepts *sqlx.Rows, scans current single row result into target pointer struct, returns endOfRows as true of false; if endOfRows = true, loop should stop
  1047  func (svr *MySql) GetRowsByOrdinalParams(query string, args ...interface{}) (*sqlx.Rows, error) {
  1048  	// verify if the database connection is good
  1049  	if err := svr.Ping(); err != nil {
  1050  		return nil, err
  1051  	}
  1052  
  1053  	// perform select action, and return sqlx rows
  1054  	var rows *sqlx.Rows
  1055  	var err error
  1056  
  1057  	// not in transaction mode
  1058  	// query using db object
  1059  	if !xray.XRayServiceOn() {
  1060  		rows, err = svr.db.Queryx(query, args...)
  1061  	} else {
  1062  		trace := xray.NewSegment("MySql-Select-GetRowsByOrdinalParams", svr._parentSegment)
  1063  		defer trace.Close()
  1064  		defer func() {
  1065  			_ = trace.Seg.AddMetadata("SQL-Query", query)
  1066  			_ = trace.Seg.AddMetadata("SQL-Param-Values", args)
  1067  			_ = trace.Seg.AddMetadata("Rows-Result", rows)
  1068  			if err != nil {
  1069  				_ = trace.Seg.AddError(err)
  1070  			}
  1071  		}()
  1072  
  1073  		rows, err = svr.db.QueryxContext(trace.Ctx, query, args...)
  1074  	}
  1075  
  1076  	// if err is sql.ErrNoRows then treat as no error
  1077  	if err != nil && err == sql.ErrNoRows {
  1078  		rows = nil
  1079  		err = nil
  1080  	}
  1081  
  1082  	// return result
  1083  	return rows, err
  1084  }
  1085  
  1086  // GetRowsByOrdinalParams performs query with optional variadic parameters to get ROWS of result, and returns *sqlx.Rows
  1087  // [ Parameters ]
  1088  //
  1089  //	query = sql query, optionally having parameters marked as ?, where each represents a parameter position
  1090  //	args = conditionally required if positioned parameters are specified, must appear in the same order as the positional parameters
  1091  //
  1092  // [ Return Values ]
  1093  //  1. *sqlx.Rows = pointer to sqlx.Rows; or nil if no rows yielded
  1094  //  2. if error != nil, then error is encountered (if error == sql.ErrNoRows, then error is treated as nil, and sqlx.Rows is returned as nil)
  1095  //
  1096  // [ Ranged Loop & Scan ]
  1097  //  1. to loop, use:
  1098  //     for {
  1099  //     if !rows.Next() { break }
  1100  //     rows.Scan(&x, &y, etc)
  1101  //     }
  1102  //  2. to scan, use: r.Scan(&x, &y, ...), where r is the row struct in loop, where &x &y etc are the scanned output value (scan in order of select columns sequence)
  1103  //
  1104  // [ Continuous Loop & Scan ]
  1105  //  1. Continuous loop until endOfRows = true is yielded from ScanSlice() or ScanStruct()
  1106  //  2. ScanSlice(): accepts *sqlx.Rows, scans rows result into target pointer slice (if no error, endOfRows = true is returned)
  1107  //  3. ScanStruct(): accepts *sqlx.Rows, scans current single row result into target pointer struct, returns endOfRows as true of false; if endOfRows = true, loop should stop
  1108  func (t *MySqlTransaction) GetRowsByOrdinalParams(query string, args ...interface{}) (*sqlx.Rows, error) {
  1109  	if err := t.ready(); err != nil {
  1110  		return nil, err
  1111  	}
  1112  
  1113  	// verify if the database connection is good
  1114  	if err := t.parent.Ping(); err != nil {
  1115  		return nil, err
  1116  	}
  1117  
  1118  	// perform select action, and return sqlx rows
  1119  	var rows *sqlx.Rows
  1120  	var err error
  1121  
  1122  	// in transaction mode
  1123  	// query using tx object
  1124  	if t._xrayTxSeg == nil {
  1125  		rows, err = t.tx.Queryx(query, args...)
  1126  	} else {
  1127  		subTrace := t._xrayTxSeg.NewSubSegment("Transaction-Select-GetRowsByOrdinalParams")
  1128  		defer subTrace.Close()
  1129  		defer func() {
  1130  			_ = subTrace.Seg.AddMetadata("SQL-Query", query)
  1131  			_ = subTrace.Seg.AddMetadata("SQL-Param-Values", args)
  1132  			_ = subTrace.Seg.AddMetadata("Rows-Result", rows)
  1133  			if err != nil {
  1134  				_ = subTrace.Seg.AddError(err)
  1135  			}
  1136  		}()
  1137  
  1138  		rows, err = t.tx.QueryxContext(subTrace.Ctx, query, args...)
  1139  	}
  1140  
  1141  	// if err is sql.ErrNoRows then treat as no error
  1142  	if err != nil && err == sql.ErrNoRows {
  1143  		rows = nil
  1144  		err = nil
  1145  	}
  1146  
  1147  	// return result
  1148  	return rows, err
  1149  }
  1150  
  1151  // GetRowsByNamedMapParam performs query with named map containing parameters to get ROWS of result, and returns *sqlx.Rows
  1152  // [ Syntax ]
  1153  //  1. in sql = instead of defining ordinal parameters ?, each parameter in sql does not need to be ordinal, rather define with :xyz (must have : in front of param name), where xyz is name of parameter, such as :customerID
  1154  //  2. in go = setup a map variable: var p = make(map[string]interface{})
  1155  //  3. in go = to set values into map variable: p["xyz"] = abc
  1156  //     where xyz is the parameter name matching the sql :xyz (do not include : in go map "xyz")
  1157  //     where abc is the value of the parameter value, whether string or other data types
  1158  //     note: in using map, just add additional map elements using the p["xyz"] = abc syntax
  1159  //     note: if parameter value can be a null, such as nullint, nullstring, use util.ToNullTime(), ToNullInt(), ToNullString(), etc.
  1160  //  4. in go = when calling this function passing the map variable, simply pass the map variable p into the args parameter
  1161  //
  1162  // [ Parameters ]
  1163  //
  1164  //	query = sql query, optionally having parameters marked as :xyz for each parameter name, where each represents a named parameter
  1165  //	args = required, the map variable of the named parameters
  1166  //
  1167  // [ Return Values ]
  1168  //  1. *sqlx.Rows = pointer to sqlx.Rows; or nil if no rows yielded
  1169  //  2. if error != nil, then error is encountered (if error == sql.ErrNoRows, then error is treated as nil, and sqlx.Rows is returned as nil)
  1170  //
  1171  // [ Ranged Loop & Scan ]
  1172  //  1. to loop, use:
  1173  //     for {
  1174  //     if !rows.Next() { break }
  1175  //     rows.Scan(&x, &y, etc)
  1176  //     }
  1177  //  2. to scan, use: r.Scan(&x, &y, ...), where r is the row struct in loop, where &x &y etc are the scanned output value (scan in order of select columns sequence)
  1178  //
  1179  // [ Continuous Loop & Scan ]
  1180  //  1. Continuous loop until endOfRows = true is yielded from ScanSlice() or ScanStruct()
  1181  //  2. ScanSlice(): accepts *sqlx.Rows, scans rows result into target pointer slice (if no error, endOfRows = true is returned)
  1182  //  3. ScanStruct(): accepts *sqlx.Rows, scans current single row result into target pointer struct, returns endOfRows as true of false; if endOfRows = true, loop should stop
  1183  func (svr *MySql) GetRowsByNamedMapParam(query string, args map[string]interface{}) (*sqlx.Rows, error) {
  1184  	// verify if the database connection is good
  1185  	if err := svr.Ping(); err != nil {
  1186  		return nil, err
  1187  	}
  1188  
  1189  	// perform select action, and return sqlx rows
  1190  	var rows *sqlx.Rows
  1191  	var err error
  1192  
  1193  	// not in transaction mode
  1194  	// query using db object
  1195  	if !xray.XRayServiceOn() {
  1196  		rows, err = svr.db.NamedQuery(query, args)
  1197  	} else {
  1198  		trace := xray.NewSegment("MySql-Select-GetRowsByNamedMapParam", svr._parentSegment)
  1199  		defer trace.Close()
  1200  		defer func() {
  1201  			_ = trace.Seg.AddMetadata("SQL-Query", query)
  1202  			_ = trace.Seg.AddMetadata("SQL-Param-Values", args)
  1203  			_ = trace.Seg.AddMetadata("Rows-Result", rows)
  1204  			if err != nil {
  1205  				_ = trace.Seg.AddError(err)
  1206  			}
  1207  		}()
  1208  
  1209  		rows, err = svr.db.NamedQueryContext(trace.Ctx, query, args)
  1210  	}
  1211  
  1212  	if err != nil && err == sql.ErrNoRows {
  1213  		// no rows
  1214  		rows = nil
  1215  		err = nil
  1216  	}
  1217  
  1218  	// return result
  1219  	return rows, err
  1220  }
  1221  
  1222  // GetRowsByNamedMapParam performs query with named map containing parameters to get ROWS of result, and returns *sqlx.Rows
  1223  // [ Syntax ]
  1224  //  1. in sql = instead of defining ordinal parameters ?, each parameter in sql does not need to be ordinal, rather define with :xyz (must have : in front of param name), where xyz is name of parameter, such as :customerID
  1225  //  2. in go = setup a map variable: var p = make(map[string]interface{})
  1226  //  3. in go = to set values into map variable: p["xyz"] = abc
  1227  //     where xyz is the parameter name matching the sql :xyz (do not include : in go map "xyz")
  1228  //     where abc is the value of the parameter value, whether string or other data types
  1229  //     note: in using map, just add additional map elements using the p["xyz"] = abc syntax
  1230  //     note: if parameter value can be a null, such as nullint, nullstring, use util.ToNullTime(), ToNullInt(), ToNullString(), etc.
  1231  //  4. in go = when calling this function passing the map variable, simply pass the map variable p into the args parameter
  1232  //
  1233  // [ Parameters ]
  1234  //
  1235  //	query = sql query, optionally having parameters marked as :xyz for each parameter name, where each represents a named parameter
  1236  //	args = required, the map variable of the named parameters
  1237  //
  1238  // [ Return Values ]
  1239  //  1. *sqlx.Rows = pointer to sqlx.Rows; or nil if no rows yielded
  1240  //  2. if error != nil, then error is encountered (if error == sql.ErrNoRows, then error is treated as nil, and sqlx.Rows is returned as nil)
  1241  //
  1242  // [ Ranged Loop & Scan ]
  1243  //  1. to loop, use:
  1244  //     for {
  1245  //     if !rows.Next() { break }
  1246  //     rows.Scan(&x, &y, etc)
  1247  //     }
  1248  //  2. to scan, use: r.Scan(&x, &y, ...), where r is the row struct in loop, where &x &y etc are the scanned output value (scan in order of select columns sequence)
  1249  //
  1250  // [ Continuous Loop & Scan ]
  1251  //  1. Continuous loop until endOfRows = true is yielded from ScanSlice() or ScanStruct()
  1252  //  2. ScanSlice(): accepts *sqlx.Rows, scans rows result into target pointer slice (if no error, endOfRows = true is returned)
  1253  //  3. ScanStruct(): accepts *sqlx.Rows, scans current single row result into target pointer struct, returns endOfRows as true of false; if endOfRows = true, loop should stop
  1254  func (t *MySqlTransaction) GetRowsByNamedMapParam(query string, args map[string]interface{}) (*sqlx.Rows, error) {
  1255  	if err := t.ready(); err != nil {
  1256  		return nil, err
  1257  	}
  1258  
  1259  	// verify if the database connection is good
  1260  	if err := t.parent.Ping(); err != nil {
  1261  		return nil, err
  1262  	}
  1263  
  1264  	// perform select action, and return sqlx rows
  1265  	var rows *sqlx.Rows
  1266  	var err error
  1267  
  1268  	// in transaction mode
  1269  	// query using tx object
  1270  	if t._xrayTxSeg == nil {
  1271  		rows, err = t.tx.NamedQuery(query, args)
  1272  	} else {
  1273  		subTrace := t._xrayTxSeg.NewSubSegment("Transaction-Select-GetRowsByNamedMapParam")
  1274  		defer subTrace.Close()
  1275  		defer func() {
  1276  			_ = subTrace.Seg.AddMetadata("SQL-Query", query)
  1277  			_ = subTrace.Seg.AddMetadata("SQL-Param-Values", args)
  1278  			_ = subTrace.Seg.AddMetadata("Rows-Result", rows)
  1279  			if err != nil {
  1280  				_ = subTrace.Seg.AddError(err)
  1281  			}
  1282  		}()
  1283  
  1284  		rows, err = sqlx.NamedQueryContext(subTrace.Ctx, t.tx, query, args)
  1285  	}
  1286  
  1287  	if err != nil && err == sql.ErrNoRows {
  1288  		// no rows
  1289  		rows = nil
  1290  		err = nil
  1291  	}
  1292  
  1293  	// return result
  1294  	return rows, err
  1295  }
  1296  
  1297  // GetRowsByStructParam performs query with a struct as parameter input to get ROWS of result, and returns *sqlx.Rows
  1298  // [ Syntax ]
  1299  //  1. in sql = instead of defining ordinal parameters ?, each parameter in sql does not need to be ordinal, rather define with :xyz (must have : in front of param name), where xyz is name of parameter, such as :customerID
  1300  //  2. in sql = important: the :xyz defined where xyz portion of parameter name must batch the struct tag's `db:"xyz"`
  1301  //  3. in go = a struct containing struct tags that matches the named parameters will be set with values, and passed into this function's args parameter input
  1302  //  4. in go = when calling this function passing the struct variable, simply pass the struct variable into the args parameter
  1303  //
  1304  // [ Parameters ]
  1305  //
  1306  //	query = sql query, optionally having parameters marked as :xyz for each parameter name, where each represents a named parameter
  1307  //	args = required, the struct variable where struct fields' struct tags match to the named parameters
  1308  //
  1309  // [ Return Values ]
  1310  //  1. *sqlx.Rows = pointer to sqlx.Rows; or nil if no rows yielded
  1311  //  2. if error != nil, then error is encountered (if error == sql.ErrNoRows, then error is treated as nil, and sqlx.Rows is returned as nil)
  1312  //
  1313  // [ Ranged Loop & Scan ]
  1314  //  1. to loop, use:
  1315  //     for {
  1316  //     if !rows.Next() { break }
  1317  //     rows.Scan(&x, &y, etc)
  1318  //     }
  1319  //  2. to scan, use: r.Scan(&x, &y, ...), where r is the row struct in loop, where &x &y etc are the scanned output value (scan in order of select columns sequence)
  1320  //
  1321  // [ Continuous Loop & Scan ]
  1322  //  1. Continuous loop until endOfRows = true is yielded from ScanSlice() or ScanStruct()
  1323  //  2. ScanSlice(): accepts *sqlx.Rows, scans rows result into target pointer slice (if no error, endOfRows = true is returned)
  1324  //  3. ScanStruct(): accepts *sqlx.Rows, scans current single row result into target pointer struct, returns endOfRows as true of false; if endOfRows = true, loop should stop
  1325  func (svr *MySql) GetRowsByStructParam(query string, args interface{}) (*sqlx.Rows, error) {
  1326  	// verify if the database connection is good
  1327  	if err := svr.Ping(); err != nil {
  1328  		return nil, err
  1329  	}
  1330  
  1331  	// perform select action, and return sqlx rows
  1332  	var rows *sqlx.Rows
  1333  	var err error
  1334  
  1335  	// not in transaction mode
  1336  	// query using db object
  1337  	if !xray.XRayServiceOn() {
  1338  		rows, err = svr.db.NamedQuery(query, args)
  1339  	} else {
  1340  		trace := xray.NewSegment("MySql-Select-GetRowsByStructParam", svr._parentSegment)
  1341  		defer trace.Close()
  1342  		defer func() {
  1343  			_ = trace.Seg.AddMetadata("SQL-Query", query)
  1344  			_ = trace.Seg.AddMetadata("SQL-Param-Values", args)
  1345  			_ = trace.Seg.AddMetadata("Rows-Result", rows)
  1346  			if err != nil {
  1347  				_ = trace.Seg.AddError(err)
  1348  			}
  1349  		}()
  1350  
  1351  		rows, err = svr.db.NamedQueryContext(trace.Ctx, query, args)
  1352  	}
  1353  
  1354  	if err != nil && err == sql.ErrNoRows {
  1355  		// no rows
  1356  		rows = nil
  1357  		err = nil
  1358  	}
  1359  
  1360  	// return result
  1361  	return rows, err
  1362  }
  1363  
  1364  // GetRowsByStructParam performs query with a struct as parameter input to get ROWS of result, and returns *sqlx.Rows
  1365  // [ Syntax ]
  1366  //  1. in sql = instead of defining ordinal parameters ?, each parameter in sql does not need to be ordinal, rather define with :xyz (must have : in front of param name), where xyz is name of parameter, such as :customerID
  1367  //  2. in sql = important: the :xyz defined where xyz portion of parameter name must batch the struct tag's `db:"xyz"`
  1368  //  3. in go = a struct containing struct tags that matches the named parameters will be set with values, and passed into this function's args parameter input
  1369  //  4. in go = when calling this function passing the struct variable, simply pass the struct variable into the args parameter
  1370  //
  1371  // [ Parameters ]
  1372  //
  1373  //	query = sql query, optionally having parameters marked as :xyz for each parameter name, where each represents a named parameter
  1374  //	args = required, the struct variable where struct fields' struct tags match to the named parameters
  1375  //
  1376  // [ Return Values ]
  1377  //  1. *sqlx.Rows = pointer to sqlx.Rows; or nil if no rows yielded
  1378  //  2. if error != nil, then error is encountered (if error == sql.ErrNoRows, then error is treated as nil, and sqlx.Rows is returned as nil)
  1379  //
  1380  // [ Ranged Loop & Scan ]
  1381  //  1. to loop, use:
  1382  //     for {
  1383  //     if !rows.Next() { break }
  1384  //     rows.Scan(&x, &y, etc)
  1385  //     }
  1386  //  2. to scan, use: r.Scan(&x, &y, ...), where r is the row struct in loop, where &x &y etc are the scanned output value (scan in order of select columns sequence)
  1387  //
  1388  // [ Continuous Loop & Scan ]
  1389  //  1. Continuous loop until endOfRows = true is yielded from ScanSlice() or ScanStruct()
  1390  //  2. ScanSlice(): accepts *sqlx.Rows, scans rows result into target pointer slice (if no error, endOfRows = true is returned)
  1391  //  3. ScanStruct(): accepts *sqlx.Rows, scans current single row result into target pointer struct, returns endOfRows as true of false; if endOfRows = true, loop should stop
  1392  func (t *MySqlTransaction) GetRowsByStructParam(query string, args interface{}) (*sqlx.Rows, error) {
  1393  	if err := t.ready(); err != nil {
  1394  		return nil, err
  1395  	}
  1396  
  1397  	// verify if the database connection is good
  1398  	if err := t.parent.Ping(); err != nil {
  1399  		return nil, err
  1400  	}
  1401  
  1402  	// perform select action, and return sqlx rows
  1403  	var rows *sqlx.Rows
  1404  	var err error
  1405  
  1406  	// in transaction mode
  1407  	// query using tx object
  1408  	if t._xrayTxSeg == nil {
  1409  		rows, err = t.tx.NamedQuery(query, args)
  1410  	} else {
  1411  		subTrace := t._xrayTxSeg.NewSubSegment("Transaction-Select-GetRowsByStructParam")
  1412  		defer subTrace.Close()
  1413  		defer func() {
  1414  			_ = subTrace.Seg.AddMetadata("SQL-Query", query)
  1415  			_ = subTrace.Seg.AddMetadata("SQL-Param-Values", args)
  1416  			_ = subTrace.Seg.AddMetadata("Rows-Result", rows)
  1417  			if err != nil {
  1418  				_ = subTrace.Seg.AddError(err)
  1419  			}
  1420  		}()
  1421  
  1422  		rows, err = sqlx.NamedQueryContext(subTrace.Ctx, t.tx, query, args)
  1423  	}
  1424  
  1425  	if err != nil && err == sql.ErrNoRows {
  1426  		// no rows
  1427  		rows = nil
  1428  		err = nil
  1429  	}
  1430  
  1431  	// return result
  1432  	return rows, err
  1433  }
  1434  
  1435  // ----------------------------------------------------------------------------------------------------------------
  1436  // scan row data and marshal to 'slice' or 'struct' helpers
  1437  // ----------------------------------------------------------------------------------------------------------------
  1438  
  1439  // ScanSlice takes in *sqlx.Rows as parameter, will invoke the rows.Next() to advance to next row position,
  1440  // and marshals current row's column values into a pointer reference to a slice,
  1441  // this enables us to quickly retrieve a slice of current row column values without knowing how many columns or names or columns (columns appear in select columns sequence),
  1442  // to loop thru all rows, use range, and loop until endOfRows = true; the dest is nil if no columns found; the dest is pointer of slice when columns exists
  1443  // [ Parameters ]
  1444  //
  1445  //	rows = *sqlx.Rows
  1446  //	dest = pointer or address to slice, such as: variable to "*[]string", or variable to "&cList for declaration cList []string"
  1447  //
  1448  // [ Return Values ]
  1449  //  1. endOfRows = true if this action call yielded end of rows, meaning stop further processing of current loop (rows will be closed automatically)
  1450  //  2. if error != nil, then error is encountered (if error == sql.ErrNoRows, then error is treated as nil, and dest is set as nil)
  1451  func (svr *MySql) ScanSlice(rows *sqlx.Rows, dest []interface{}) (endOfRows bool, err error) {
  1452  	// ensure rows pointer is set
  1453  	if rows == nil {
  1454  		return true, nil
  1455  	}
  1456  
  1457  	// call rows.Next() first to position the row
  1458  	if !xray.XRayServiceOn() {
  1459  		if rows.Next() {
  1460  			// now slice scan
  1461  			dest, err = rows.SliceScan()
  1462  
  1463  			// if err is sql.ErrNoRows then treat as no error
  1464  			if err != nil && err == sql.ErrNoRows {
  1465  				endOfRows = true
  1466  				dest = nil
  1467  				err = nil
  1468  				_ = rows.Close()
  1469  				return
  1470  			}
  1471  
  1472  			if err != nil {
  1473  				// has error
  1474  				endOfRows = false // although error but may not be at end of rows
  1475  				dest = nil
  1476  				return
  1477  			}
  1478  
  1479  			// slice scan success, but may not be at end of rows
  1480  			// exit function, and inform caller not endOfRows
  1481  			return false, nil
  1482  		} else {
  1483  			endOfRows = true
  1484  			dest = nil
  1485  			err = nil
  1486  			_ = rows.Close()
  1487  		}
  1488  	} else {
  1489  		trace := xray.NewSegment("MySql-InMemory-ScanSlice", svr._parentSegment)
  1490  		defer trace.Close()
  1491  		defer func() {
  1492  			if err != nil {
  1493  				_ = trace.Seg.AddError(err)
  1494  			}
  1495  		}()
  1496  
  1497  		trace.Capture("ScanSlice-Do", func() error {
  1498  			if rows.Next() {
  1499  				// now slice scan
  1500  				dest, err = rows.SliceScan()
  1501  
  1502  				// if err is sql.ErrNoRows then treat as no error
  1503  				if err != nil && err == sql.ErrNoRows {
  1504  					endOfRows = true
  1505  					dest = nil
  1506  					err = nil
  1507  					_ = rows.Close()
  1508  					return nil
  1509  				}
  1510  
  1511  				if err != nil {
  1512  					// has error
  1513  					endOfRows = false // although error but may not be at end of rows
  1514  					dest = nil
  1515  					return err
  1516  				}
  1517  
  1518  				// slice scan success, but may not be at end of rows
  1519  				// exit function, and inform caller not endOfRows
  1520  				endOfRows = false
  1521  				return nil
  1522  			} else {
  1523  				endOfRows = true
  1524  				dest = nil
  1525  				err = nil
  1526  				_ = rows.Close()
  1527  				return nil
  1528  			}
  1529  		}, &xray.XTraceData{
  1530  			Meta: map[string]interface{}{
  1531  				"Rows-To-Scan": rows,
  1532  				"Slice-Result": dest,
  1533  				"End-Of-Rows":  endOfRows,
  1534  			},
  1535  		})
  1536  	}
  1537  
  1538  	// no more rows
  1539  	return endOfRows, err
  1540  }
  1541  
  1542  // ScanStruct takes in *sqlx.Rows, will invoke the rows.Next() to advance to next row position,
  1543  // and marshals current row's column values into a pointer reference to a struct,
  1544  // the struct fields and row columns must match for both name and sequence position,
  1545  // this enables us to quickly convert the row's columns into a defined struct automatically,
  1546  // to loop thru all rows, use range, and loop until endOfRows = true; the dest is nil if no columns found; the dest is pointer of struct when mapping is complete
  1547  // [ Parameters ]
  1548  //
  1549  //	rows = *sqlx.Rows
  1550  //	dest = pointer or address to struct, such as: variable to "*Customer", or variable to "&c for declaration c Customer"
  1551  //
  1552  // [ Return Values ]
  1553  //  1. endOfRows = true if this action call yielded end of rows, meaning stop further processing of current loop (rows will be closed automatically)
  1554  //  2. if error != nil, then error is encountered (if error == sql.ErrNoRows, then error is treated as nil, and dest is set as nil)
  1555  func (svr *MySql) ScanStruct(rows *sqlx.Rows, dest interface{}) (endOfRows bool, err error) {
  1556  	// ensure rows pointer is set
  1557  	if rows == nil {
  1558  		return true, nil
  1559  	}
  1560  
  1561  	// call rows.Next() first to position the row
  1562  	if !xray.XRayServiceOn() {
  1563  		if rows.Next() {
  1564  			// now struct scan
  1565  			err = rows.StructScan(dest)
  1566  
  1567  			// if err is sql.ErrNoRows then treat as no error
  1568  			if err != nil && err == sql.ErrNoRows {
  1569  				endOfRows = true
  1570  				dest = nil
  1571  				err = nil
  1572  				_ = rows.Close()
  1573  				return
  1574  			}
  1575  
  1576  			if err != nil {
  1577  				// has error
  1578  				endOfRows = false // although error but may not be at end of rows
  1579  				dest = nil
  1580  				return
  1581  			}
  1582  
  1583  			// struct scan successful, but may not be at end of rows
  1584  			return false, nil
  1585  		} else {
  1586  			endOfRows = true
  1587  			dest = nil
  1588  			err = nil
  1589  			_ = rows.Close()
  1590  		}
  1591  	} else {
  1592  		trace := xray.NewSegment("MySql-InMemory-ScanStruct", svr._parentSegment)
  1593  		defer trace.Close()
  1594  		defer func() {
  1595  			if err != nil {
  1596  				_ = trace.Seg.AddError(err)
  1597  			}
  1598  		}()
  1599  
  1600  		trace.Capture("ScanStruct-Do", func() error {
  1601  			if rows.Next() {
  1602  				// now struct scan
  1603  				err = rows.StructScan(dest)
  1604  
  1605  				// if err is sql.ErrNoRows then treat as no error
  1606  				if err != nil && err == sql.ErrNoRows {
  1607  					endOfRows = true
  1608  					dest = nil
  1609  					err = nil
  1610  					_ = rows.Close()
  1611  					return nil
  1612  				}
  1613  
  1614  				if err != nil {
  1615  					// has error
  1616  					endOfRows = false // although error but may not be at end of rows
  1617  					dest = nil
  1618  					return err
  1619  				}
  1620  
  1621  				// struct scan successful, but may not be at end of rows
  1622  				endOfRows = false
  1623  				return nil
  1624  			} else {
  1625  				endOfRows = true
  1626  				dest = nil
  1627  				err = nil
  1628  				_ = rows.Close()
  1629  				return nil
  1630  			}
  1631  		}, &xray.XTraceData{
  1632  			Meta: map[string]interface{}{
  1633  				"Rows-To-Scan":  rows,
  1634  				"Struct-Result": dest,
  1635  				"End-Of-Rows":   endOfRows,
  1636  			},
  1637  		})
  1638  	}
  1639  
  1640  	// no more rows
  1641  	return endOfRows, err
  1642  }
  1643  
  1644  // ----------------------------------------------------------------------------------------------------------------
  1645  // query for single row helper
  1646  // ----------------------------------------------------------------------------------------------------------------
  1647  
  1648  // GetSingleRow performs query with optional variadic parameters to get a single ROW of result, and returns *sqlx.Row (This function returns SINGLE ROW)
  1649  // [ Parameters ]
  1650  //
  1651  //	query = sql query, optionally having parameters marked as ?, where each represents a parameter position
  1652  //	args = conditionally required if positioned parameters are specified, must appear in the same order as the positional parameters
  1653  //
  1654  // [ Return Values ]
  1655  //  1. *sqlx.Row = pointer to sqlx.Row; or nil if no row yielded
  1656  //  2. if error != nil, then error is encountered (if error = sql.ErrNoRows, then error is treated as nil, and sqlx.Row is returned as nil)
  1657  //
  1658  // [ Scan Values ]
  1659  //  1. Use row.Scan() and pass in pointer or address of variable to receive scanned value outputs (Scan is in the order of column sequences in select statement)
  1660  //
  1661  // [ WARNING !!! ]
  1662  //
  1663  //	WHEN USING Scan(), MUST CHECK Scan Result Error for sql.ErrNoRow status
  1664  //	SUGGESTED TO USE ScanColumnsByRow() Instead of Scan()
  1665  func (svr *MySql) GetSingleRow(query string, args ...interface{}) (*sqlx.Row, error) {
  1666  	// verify if the database connection is good
  1667  	if err := svr.Ping(); err != nil {
  1668  		return nil, err
  1669  	}
  1670  
  1671  	// perform select action, and return sqlx row
  1672  	var row *sqlx.Row
  1673  	var err error
  1674  
  1675  	// not in transaction mode
  1676  	// query using db object
  1677  	if !xray.XRayServiceOn() {
  1678  		row = svr.db.QueryRowx(query, args...)
  1679  	} else {
  1680  		trace := xray.NewSegment("MySql-Select-GetSingleRow", svr._parentSegment)
  1681  		defer trace.Close()
  1682  		defer func() {
  1683  			_ = trace.Seg.AddMetadata("SQL-Query", query)
  1684  			_ = trace.Seg.AddMetadata("SQL-Param-Values", args)
  1685  			_ = trace.Seg.AddMetadata("Row-Result", row)
  1686  			if err != nil {
  1687  				_ = trace.Seg.AddError(err)
  1688  			}
  1689  		}()
  1690  
  1691  		row = svr.db.QueryRowxContext(trace.Ctx, query, args...)
  1692  	}
  1693  
  1694  	if row == nil {
  1695  		err = errors.New("No Row Data Found From Query")
  1696  	} else {
  1697  		err = row.Err()
  1698  
  1699  		if err != nil {
  1700  			if err == sql.ErrNoRows {
  1701  				// no rows
  1702  				row = nil
  1703  				err = nil
  1704  			} else {
  1705  				// has error
  1706  				row = nil
  1707  			}
  1708  		}
  1709  	}
  1710  
  1711  	// return result
  1712  	return row, err
  1713  }
  1714  
  1715  // GetSingleRow performs query with optional variadic parameters to get a single ROW of result, and returns *sqlx.Row (This function returns SINGLE ROW)
  1716  // [ Parameters ]
  1717  //
  1718  //	query = sql query, optionally having parameters marked as ?, where each represents a parameter position
  1719  //	args = conditionally required if positioned parameters are specified, must appear in the same order as the positional parameters
  1720  //
  1721  // [ Return Values ]
  1722  //  1. *sqlx.Row = pointer to sqlx.Row; or nil if no row yielded
  1723  //  2. if error != nil, then error is encountered (if error = sql.ErrNoRows, then error is treated as nil, and sqlx.Row is returned as nil)
  1724  //
  1725  // [ Scan Values ]
  1726  //  1. Use row.Scan() and pass in pointer or address of variable to receive scanned value outputs (Scan is in the order of column sequences in select statement)
  1727  //
  1728  // [ WARNING !!! ]
  1729  //
  1730  //	WHEN USING Scan(), MUST CHECK Scan Result Error for sql.ErrNoRow status
  1731  //	SUGGESTED TO USE ScanColumnsByRow() Instead of Scan()
  1732  func (t *MySqlTransaction) GetSingleRow(query string, args ...interface{}) (*sqlx.Row, error) {
  1733  	if err := t.ready(); err != nil {
  1734  		return nil, err
  1735  	}
  1736  
  1737  	// verify if the database connection is good
  1738  	if err := t.parent.Ping(); err != nil {
  1739  		return nil, err
  1740  	}
  1741  
  1742  	// perform select action, and return sqlx row
  1743  	var row *sqlx.Row
  1744  	var err error
  1745  
  1746  	// in transaction mode
  1747  	// query using tx object
  1748  	if t._xrayTxSeg == nil {
  1749  		row = t.tx.QueryRowx(query, args...)
  1750  	} else {
  1751  		subTrace := t._xrayTxSeg.NewSubSegment("Transaction-Select-GetSingleRow")
  1752  		defer subTrace.Close()
  1753  		defer func() {
  1754  			_ = subTrace.Seg.AddMetadata("SQL-Query", query)
  1755  			_ = subTrace.Seg.AddMetadata("SQL-Param-Values", args)
  1756  			_ = subTrace.Seg.AddMetadata("Row-Result", row)
  1757  			if err != nil {
  1758  				_ = subTrace.Seg.AddError(err)
  1759  			}
  1760  		}()
  1761  
  1762  		row = t.tx.QueryRowxContext(subTrace.Ctx, query, args...)
  1763  	}
  1764  
  1765  	if row == nil {
  1766  		err = errors.New("No Row Data Found From Query")
  1767  	} else {
  1768  		err = row.Err()
  1769  
  1770  		if err != nil {
  1771  			if err == sql.ErrNoRows {
  1772  				// no rows
  1773  				row = nil
  1774  				err = nil
  1775  			} else {
  1776  				// has error
  1777  				row = nil
  1778  			}
  1779  		}
  1780  	}
  1781  
  1782  	// return result
  1783  	return row, err
  1784  }
  1785  
  1786  // ----------------------------------------------------------------------------------------------------------------
  1787  // scan single row data and marshal to 'slice' or 'struct' or specific fields, or scan columns helpers
  1788  // ----------------------------------------------------------------------------------------------------------------
  1789  
  1790  // ScanSliceByRow takes in *sqlx.Row as parameter, and marshals current row's column values into a pointer reference to a slice,
  1791  // this enables us to quickly retrieve a slice of current row column values without knowing how many columns or names or columns (columns appear in select columns sequence)
  1792  // [ Parameters ]
  1793  //
  1794  //	row = *sqlx.Row
  1795  //	dest = pointer or address to slice, such as: variable to "*[]string", or variable to "&cList for declaration cList []string"
  1796  //
  1797  // [ Return Values ]
  1798  //  1. notFound = true if no row is found in current scan
  1799  //  2. if error != nil, then error is encountered (if error == sql.ErrNoRows, then error is treated as nil, and dest is set as nil and notFound is true)
  1800  func (svr *MySql) ScanSliceByRow(row *sqlx.Row, dest []interface{}) (notFound bool, err error) {
  1801  	// if row is nil, treat as no row and not an error
  1802  	if row == nil {
  1803  		dest = nil
  1804  		return true, nil
  1805  	}
  1806  
  1807  	// perform slice scan on the given row
  1808  	if !xray.XRayServiceOn() {
  1809  		dest, err = row.SliceScan()
  1810  
  1811  		// if err is sql.ErrNoRows then treat as no error
  1812  		if err != nil && err == sql.ErrNoRows {
  1813  			dest = nil
  1814  			return true, nil
  1815  		}
  1816  
  1817  		if err != nil {
  1818  			// has error
  1819  			dest = nil
  1820  			return false, err // although error but may not be not found
  1821  		}
  1822  
  1823  		notFound = false
  1824  		err = nil
  1825  	} else {
  1826  		trace := xray.NewSegment("MySql-InMemory-ScanSliceByRow", svr._parentSegment)
  1827  		defer trace.Close()
  1828  		defer func() {
  1829  			if err != nil {
  1830  				_ = trace.Seg.AddError(err)
  1831  			}
  1832  		}()
  1833  
  1834  		trace.Capture("ScanSliceByRow-Do", func() error {
  1835  			dest, err = row.SliceScan()
  1836  
  1837  			// if err is sql.ErrNoRows then treat as no error
  1838  			if err != nil && err == sql.ErrNoRows {
  1839  				dest = nil
  1840  				notFound = true
  1841  				err = nil
  1842  				return nil
  1843  			}
  1844  
  1845  			if err != nil {
  1846  				// has error
  1847  				notFound = false
  1848  				dest = nil
  1849  				return err // although error but may not be not found
  1850  			}
  1851  
  1852  			notFound = false
  1853  			err = nil
  1854  			return nil
  1855  		}, &xray.XTraceData{
  1856  			Meta: map[string]interface{}{
  1857  				"Row-To-Scan":  row,
  1858  				"Slice-Result": dest,
  1859  			},
  1860  		})
  1861  	}
  1862  
  1863  	// slice scan success
  1864  	return notFound, err
  1865  }
  1866  
  1867  // ScanStructByRow takes in *sqlx.Row, and marshals current row's column values into a pointer reference to a struct,
  1868  // the struct fields and row columns must match for both name and sequence position,
  1869  // this enables us to quickly convert the row's columns into a defined struct automatically,
  1870  // the dest is nil if no columns found; the dest is pointer of struct when mapping is complete
  1871  // [ Parameters ]
  1872  //
  1873  //	row = *sqlx.Row
  1874  //	dest = pointer or address to struct, such as: variable to "*Customer", or variable to "&c for declaration c Customer"
  1875  //
  1876  // [ Return Values ]
  1877  //  1. notFound = true if no row is found in current scan
  1878  //  2. if error != nil, then error is encountered (if error == sql.ErrNoRows, then error is treated as nil, and dest is set as nil and notFound is true)
  1879  func (svr *MySql) ScanStructByRow(row *sqlx.Row, dest interface{}) (notFound bool, err error) {
  1880  	// if row is nil, treat as no row and not an error
  1881  	if row == nil {
  1882  		dest = nil
  1883  		return true, nil
  1884  	}
  1885  
  1886  	// now struct scan
  1887  	if !xray.XRayServiceOn() {
  1888  		err = row.StructScan(dest)
  1889  
  1890  		// if err is sql.ErrNoRows then treat as no error
  1891  		if err != nil && err == sql.ErrNoRows {
  1892  			dest = nil
  1893  			return true, nil
  1894  		}
  1895  
  1896  		if err != nil {
  1897  			// has error
  1898  			dest = nil
  1899  			return false, err // although error but may not be not found
  1900  		}
  1901  
  1902  		notFound = false
  1903  		err = nil
  1904  	} else {
  1905  		trace := xray.NewSegment("MySql-InMemory-ScanStructByRow", svr._parentSegment)
  1906  		defer trace.Close()
  1907  		defer func() {
  1908  			if err != nil {
  1909  				_ = trace.Seg.AddError(err)
  1910  			}
  1911  		}()
  1912  
  1913  		trace.Capture("ScanStructByRow-Do", func() error {
  1914  			err = row.StructScan(dest)
  1915  
  1916  			// if err is sql.ErrNoRows then treat as no error
  1917  			if err != nil && err == sql.ErrNoRows {
  1918  				dest = nil
  1919  				notFound = true
  1920  				err = nil
  1921  				return nil
  1922  			}
  1923  
  1924  			if err != nil {
  1925  				// has error
  1926  				dest = nil
  1927  				notFound = false
  1928  				return err // although error but may not be not found
  1929  			}
  1930  
  1931  			notFound = false
  1932  			err = nil
  1933  			return nil
  1934  		}, &xray.XTraceData{
  1935  			Meta: map[string]interface{}{
  1936  				"Row-To-Scan":   row,
  1937  				"Struct-Result": dest,
  1938  			},
  1939  		})
  1940  	}
  1941  
  1942  	// struct scan successful
  1943  	return notFound, err
  1944  }
  1945  
  1946  // ScanColumnsByRow accepts a *sqlx row, and scans specific columns into dest outputs,
  1947  // this is different than ScanSliceByRow or ScanStructByRow because this function allows specific extraction of column values into target fields,
  1948  // (note: this function must extra all row column values to dest variadic parameters as present in the row parameter)
  1949  // [ Parameters ]
  1950  //
  1951  //	row = *sqlx.Row representing the row containing columns to extract, note that this function MUST extract all columns from this row
  1952  //	dest = MUST BE pointer (or &variable) to target variable to receive the column value, data type must match column data type value, and sequence of dest must be in the order of columns sequence
  1953  //
  1954  // [ Return Values ]
  1955  //  1. notFound = true if no row is found in current scan
  1956  //  2. if error != nil, then error is encountered (if error == sql.ErrNoRows, then error is treated as nil, and dest is set as nil and notFound is true)
  1957  //
  1958  // [ Example ]
  1959  //  1. assuming: Select CustomerID, CustomerName, Address FROM Customer Where CustomerPhone='123';
  1960  //  2. assuming: row // *sqlx.Row derived from GetSingleRow() or specific row from GetRowsByOrdinalParams() / GetRowsByNamedMapParam() / GetRowsByStructParam()
  1961  //  3. assuming: var CustomerID int64
  1962  //     var CustomerName string
  1963  //     var Address string
  1964  //  4. notFound, err := svr.ScanColumnsByRow(row, &CustomerID, &CustomerName, &Address)
  1965  func (svr *MySql) ScanColumnsByRow(row *sqlx.Row, dest ...interface{}) (notFound bool, err error) {
  1966  	// if row is nil, treat as no row and not an error
  1967  	if row == nil {
  1968  		return true, nil
  1969  	}
  1970  
  1971  	// now scan columns from row
  1972  	if !xray.XRayServiceOn() {
  1973  		err = row.Scan(dest...)
  1974  
  1975  		// if err is sql.ErrNoRows then treat as no error
  1976  		if err != nil && err == sql.ErrNoRows {
  1977  			return true, nil
  1978  		}
  1979  
  1980  		if err != nil {
  1981  			// has error
  1982  			return false, err // although error but may not be not found
  1983  		}
  1984  
  1985  		notFound = false
  1986  		err = nil
  1987  	} else {
  1988  		trace := xray.NewSegment("MySql-InMemory-ScanColumnsByRow", svr._parentSegment)
  1989  		defer trace.Close()
  1990  		defer func() {
  1991  			if err != nil {
  1992  				_ = trace.Seg.AddError(err)
  1993  			}
  1994  		}()
  1995  
  1996  		trace.Capture("ScanColumnsByRow_Do", func() error {
  1997  			err = row.Scan(dest...)
  1998  
  1999  			// if err is sql.ErrNoRows then treat as no error
  2000  			if err != nil && err == sql.ErrNoRows {
  2001  				notFound = true
  2002  				err = nil
  2003  				return nil
  2004  			}
  2005  
  2006  			if err != nil {
  2007  				// has error
  2008  				notFound = false
  2009  				return err // although error but may not be not found
  2010  			}
  2011  
  2012  			notFound = false
  2013  			err = nil
  2014  			return nil
  2015  		}, &xray.XTraceData{
  2016  			Meta: map[string]interface{}{
  2017  				"Row-To-Scan":      row,
  2018  				"Dest-Vars-Result": dest,
  2019  			},
  2020  		})
  2021  	}
  2022  
  2023  	// scan columns successful
  2024  	return notFound, err
  2025  }
  2026  
  2027  // ----------------------------------------------------------------------------------------------------------------
  2028  // query for single value in single row helpers
  2029  // ----------------------------------------------------------------------------------------------------------------
  2030  
  2031  // GetScalarString performs query with optional variadic parameters, and returns the first row and first column value in string data type
  2032  // [ Parameters ]
  2033  //
  2034  //	query = sql query, optionally having parameters marked as ?, where each represents a parameter position
  2035  //	args = conditionally required if positioned parameters are specified, must appear in the same order as the positional parameters
  2036  //
  2037  // [ Return Values ]
  2038  //  1. retVal = string value of scalar result, if no value, blank is returned
  2039  //  2. retNotFound = now row found
  2040  //  3. if error != nil, then error is encountered (if error == sql.ErrNoRows, then error is treated as nil, and retVal is returned as blank)
  2041  func (svr *MySql) GetScalarString(query string, args ...interface{}) (retVal string, retNotFound bool, retErr error) {
  2042  	// verify if the database connection is good
  2043  	if err := svr.Ping(); err != nil {
  2044  		return "", false, err
  2045  	}
  2046  
  2047  	// get row using query string and parameters
  2048  	var row *sqlx.Row
  2049  
  2050  	// not in transaction
  2051  	// use db object
  2052  	if !xray.XRayServiceOn() {
  2053  		row = svr.db.QueryRowx(query, args...)
  2054  	} else {
  2055  		trace := xray.NewSegment("MySql-Select-GetScalarString", svr._parentSegment)
  2056  		defer trace.Close()
  2057  		defer func() {
  2058  			_ = trace.Seg.AddMetadata("SQL-Query", query)
  2059  			_ = trace.Seg.AddMetadata("SQL-Param-Values", args)
  2060  			_ = trace.Seg.AddMetadata("Row-Result", row)
  2061  			if retErr != nil {
  2062  				_ = trace.Seg.AddError(retErr)
  2063  			}
  2064  		}()
  2065  
  2066  		row = svr.db.QueryRowxContext(trace.Ctx, query, args...)
  2067  	}
  2068  
  2069  	if row == nil {
  2070  		return "", false, errors.New("Scalar Query Yielded Empty Row")
  2071  	} else {
  2072  		retErr = row.Err()
  2073  
  2074  		if retErr != nil {
  2075  			if retErr == sql.ErrNoRows {
  2076  				// no rows
  2077  				retErr = nil
  2078  				return "", true, nil
  2079  			} else {
  2080  				// has error
  2081  				return "", false, retErr
  2082  			}
  2083  		}
  2084  	}
  2085  
  2086  	// get value via scan
  2087  	retErr = row.Scan(&retVal)
  2088  
  2089  	if retErr == sql.ErrNoRows {
  2090  		// no rows
  2091  		retErr = nil
  2092  		return "", true, nil
  2093  	} else {
  2094  		// return value
  2095  		return retVal, false, retErr
  2096  	}
  2097  }
  2098  
  2099  // GetScalarString performs query with optional variadic parameters, and returns the first row and first column value in string data type
  2100  // [ Parameters ]
  2101  //
  2102  //	query = sql query, optionally having parameters marked as ?, where each represents a parameter position
  2103  //	args = conditionally required if positioned parameters are specified, must appear in the same order as the positional parameters
  2104  //
  2105  // [ Return Values ]
  2106  //  1. retVal = string value of scalar result, if no value, blank is returned
  2107  //  2. retNotFound = now row found
  2108  //  3. if error != nil, then error is encountered (if error == sql.ErrNoRows, then error is treated as nil, and retVal is returned as blank)
  2109  func (t *MySqlTransaction) GetScalarString(query string, args ...interface{}) (retVal string, retNotFound bool, retErr error) {
  2110  	if err := t.ready(); err != nil {
  2111  		return "", false, err
  2112  	}
  2113  
  2114  	// verify if the database connection is good
  2115  	if err := t.parent.Ping(); err != nil {
  2116  		return "", false, err
  2117  	}
  2118  
  2119  	// get row using query string and parameters
  2120  	var row *sqlx.Row
  2121  
  2122  	// in transaction
  2123  	// use tx object
  2124  	if t._xrayTxSeg == nil {
  2125  		row = t.tx.QueryRowx(query, args...)
  2126  	} else {
  2127  		subTrace := t._xrayTxSeg.NewSubSegment("Transaction-Select-GetScalarString")
  2128  		defer subTrace.Close()
  2129  		defer func() {
  2130  			_ = subTrace.Seg.AddMetadata("SQL-Query", query)
  2131  			_ = subTrace.Seg.AddMetadata("SQL-Param-Values", args)
  2132  			_ = subTrace.Seg.AddMetadata("Row-Result", row)
  2133  			if retErr != nil {
  2134  				_ = subTrace.Seg.AddError(retErr)
  2135  			}
  2136  		}()
  2137  
  2138  		row = t.tx.QueryRowxContext(subTrace.Ctx, query, args...)
  2139  	}
  2140  
  2141  	if row == nil {
  2142  		return "", false, errors.New("Scalar Query Yielded Empty Row")
  2143  	} else {
  2144  		retErr = row.Err()
  2145  
  2146  		if retErr != nil {
  2147  			if retErr == sql.ErrNoRows {
  2148  				// no rows
  2149  				retErr = nil
  2150  				return "", true, nil
  2151  			} else {
  2152  				// has error
  2153  				return "", false, retErr
  2154  			}
  2155  		}
  2156  	}
  2157  
  2158  	// get value via scan
  2159  	retErr = row.Scan(&retVal)
  2160  
  2161  	if retErr == sql.ErrNoRows {
  2162  		// no rows
  2163  		retErr = nil
  2164  		return "", true, nil
  2165  	} else {
  2166  		// return value
  2167  		return retVal, false, retErr
  2168  	}
  2169  }
  2170  
  2171  // GetScalarNullString performs query with optional variadic parameters, and returns the first row and first column value in sql.NullString{} data type
  2172  // [ Parameters ]
  2173  //
  2174  //	query = sql query, optionally having parameters marked as ?, where each represents a parameter position
  2175  //	args = conditionally required if positioned parameters are specified, must appear in the same order as the positional parameters
  2176  //
  2177  // [ Return Values ]
  2178  //  1. retVal = string value of scalar result, if no value, sql.NullString{} is returned
  2179  //  2. retNotFound = now row found
  2180  //  3. if error != nil, then error is encountered (if error == sql.ErrNoRows, then error is treated as nil, and retVal is returned as sql.NullString{})
  2181  func (svr *MySql) GetScalarNullString(query string, args ...interface{}) (retVal sql.NullString, retNotFound bool, retErr error) {
  2182  	// verify if the database connection is good
  2183  	if err := svr.Ping(); err != nil {
  2184  		return sql.NullString{}, false, err
  2185  	}
  2186  
  2187  	// get row using query string and parameters
  2188  	var row *sqlx.Row
  2189  
  2190  	// not in transaction
  2191  	// use db object
  2192  	if !xray.XRayServiceOn() {
  2193  		row = svr.db.QueryRowx(query, args...)
  2194  	} else {
  2195  		trace := xray.NewSegment("MySql-Select-GetScalarNullString", svr._parentSegment)
  2196  		defer trace.Close()
  2197  		defer func() {
  2198  			_ = trace.Seg.AddMetadata("SQL-Query", query)
  2199  			_ = trace.Seg.AddMetadata("SQL-Param-Values", args)
  2200  			_ = trace.Seg.AddMetadata("Row-Result", row)
  2201  			if retErr != nil {
  2202  				_ = trace.Seg.AddError(retErr)
  2203  			}
  2204  		}()
  2205  
  2206  		row = svr.db.QueryRowxContext(trace.Ctx, query, args...)
  2207  	}
  2208  
  2209  	if row == nil {
  2210  		retErr = errors.New("Scalar Query Yielded Empty Row")
  2211  		return sql.NullString{}, false, retErr
  2212  	} else {
  2213  		retErr = row.Err()
  2214  
  2215  		if retErr != nil {
  2216  			if retErr == sql.ErrNoRows {
  2217  				// no rows
  2218  				retErr = nil
  2219  				return sql.NullString{}, true, nil
  2220  			} else {
  2221  				// has error
  2222  				return sql.NullString{}, false, retErr
  2223  			}
  2224  		}
  2225  	}
  2226  
  2227  	// get value via scan
  2228  	retErr = row.Scan(&retVal)
  2229  
  2230  	if retErr == sql.ErrNoRows {
  2231  		// no rows
  2232  		retErr = nil
  2233  		return sql.NullString{}, true, nil
  2234  	} else {
  2235  		// return value
  2236  		return retVal, false, retErr
  2237  	}
  2238  }
  2239  
  2240  // GetScalarNullString performs query with optional variadic parameters, and returns the first row and first column value in sql.NullString{} data type
  2241  // [ Parameters ]
  2242  //
  2243  //	query = sql query, optionally having parameters marked as ?, where each represents a parameter position
  2244  //	args = conditionally required if positioned parameters are specified, must appear in the same order as the positional parameters
  2245  //
  2246  // [ Return Values ]
  2247  //  1. retVal = string value of scalar result, if no value, sql.NullString{} is returned
  2248  //  2. retNotFound = now row found
  2249  //  3. if error != nil, then error is encountered (if error == sql.ErrNoRows, then error is treated as nil, and retVal is returned as sql.NullString{})
  2250  func (t *MySqlTransaction) GetScalarNullString(query string, args ...interface{}) (retVal sql.NullString, retNotFound bool, retErr error) {
  2251  	if err := t.ready(); err != nil {
  2252  		return sql.NullString{}, false, err
  2253  	}
  2254  
  2255  	// verify if the database connection is good
  2256  	if err := t.parent.Ping(); err != nil {
  2257  		return sql.NullString{}, false, err
  2258  	}
  2259  
  2260  	// get row using query string and parameters
  2261  	var row *sqlx.Row
  2262  
  2263  	// in transaction
  2264  	// use tx object
  2265  	if t._xrayTxSeg == nil {
  2266  		row = t.tx.QueryRowx(query, args...)
  2267  	} else {
  2268  		subTrace := t._xrayTxSeg.NewSubSegment("Transaction-Select-GetScalarNullString")
  2269  		defer subTrace.Close()
  2270  		defer func() {
  2271  			_ = subTrace.Seg.AddMetadata("SQL-Query", query)
  2272  			_ = subTrace.Seg.AddMetadata("SQL-Param-Values", args)
  2273  			_ = subTrace.Seg.AddMetadata("Row-Result", row)
  2274  			if retErr != nil {
  2275  				_ = subTrace.Seg.AddError(retErr)
  2276  			}
  2277  		}()
  2278  
  2279  		row = t.tx.QueryRowxContext(subTrace.Ctx, query, args...)
  2280  	}
  2281  
  2282  	if row == nil {
  2283  		retErr = errors.New("Scalar Query Yielded Empty Row")
  2284  		return sql.NullString{}, false, retErr
  2285  	} else {
  2286  		retErr = row.Err()
  2287  
  2288  		if retErr != nil {
  2289  			if retErr == sql.ErrNoRows {
  2290  				// no rows
  2291  				retErr = nil
  2292  				return sql.NullString{}, true, nil
  2293  			} else {
  2294  				// has error
  2295  				return sql.NullString{}, false, retErr
  2296  			}
  2297  		}
  2298  	}
  2299  
  2300  	// get value via scan
  2301  	retErr = row.Scan(&retVal)
  2302  
  2303  	if retErr == sql.ErrNoRows {
  2304  		// no rows
  2305  		retErr = nil
  2306  		return sql.NullString{}, true, nil
  2307  	} else {
  2308  		// return value
  2309  		return retVal, false, retErr
  2310  	}
  2311  }
  2312  
  2313  // ----------------------------------------------------------------------------------------------------------------
  2314  // execute helpers
  2315  // ----------------------------------------------------------------------------------------------------------------
  2316  
  2317  // ExecByOrdinalParams executes action query string and parameters to return result, if error, returns error object within result
  2318  // [ Parameters ]
  2319  //
  2320  //	actionQuery = sql action query, optionally having parameters marked as ?1, ?2 .. ?N, where each represents a parameter position
  2321  //	args = conditionally required if positioned parameters are specified, must appear in the same order as the positional parameters
  2322  //
  2323  // [ Return Values ]
  2324  //  1. MySqlResult = represents the sql action result received (including error info if applicable)
  2325  func (svr *MySql) ExecByOrdinalParams(actionQuery string, args ...interface{}) MySqlResult {
  2326  	// verify if the database connection is good
  2327  	if err := svr.Ping(); err != nil {
  2328  		return MySqlResult{RowsAffected: 0, NewlyInsertedID: 0, Err: err}
  2329  	}
  2330  
  2331  	// is new insertion?
  2332  	var isInsert bool
  2333  
  2334  	if strings.ToUpper(util.Left(actionQuery, 6)) == "INSERT" {
  2335  		isInsert = true
  2336  	} else {
  2337  		isInsert = false
  2338  	}
  2339  
  2340  	// perform exec action, and return to caller
  2341  	var result sql.Result
  2342  	var err error
  2343  
  2344  	if !xray.XRayServiceOn() {
  2345  		// not in transaction mode,
  2346  		// action using db object
  2347  		result, err = svr.db.Exec(actionQuery, args...)
  2348  	} else {
  2349  		// not in transaction mode,
  2350  		// action using db object
  2351  		trace := xray.NewSegment("MySql-Exec-ExecByOrdinalParams", svr._parentSegment)
  2352  		defer trace.Close()
  2353  		defer func() {
  2354  			_ = trace.Seg.AddMetadata("SQL-Query", actionQuery)
  2355  			_ = trace.Seg.AddMetadata("SQL-Param-Values", args)
  2356  			_ = trace.Seg.AddMetadata("Exec-Result", result)
  2357  			if err != nil {
  2358  				_ = trace.Seg.AddError(err)
  2359  			}
  2360  		}()
  2361  
  2362  		result, err = svr.db.ExecContext(trace.Ctx, actionQuery, args...)
  2363  	}
  2364  
  2365  	if err != nil {
  2366  		err = errors.New("ExecByOrdinalParams() Error: " + err.Error())
  2367  		return MySqlResult{RowsAffected: 0, NewlyInsertedID: 0, Err: err}
  2368  	}
  2369  
  2370  	// if inserted, get last id if known
  2371  	var newID int64
  2372  	newID = 0
  2373  
  2374  	if isInsert {
  2375  		newID, err = result.LastInsertId()
  2376  
  2377  		if err != nil {
  2378  			err = errors.New("ExecByOrdinalParams() Get LastInsertId() Error: " + err.Error())
  2379  			return MySqlResult{RowsAffected: 0, NewlyInsertedID: 0, Err: err}
  2380  		}
  2381  	}
  2382  
  2383  	// get rows affected by this action
  2384  	var affected int64
  2385  	affected = 0
  2386  
  2387  	affected, err = result.RowsAffected()
  2388  
  2389  	if err != nil {
  2390  		err = errors.New("ExecByOrdinalParams() Get RowsAffected() Error: " + err.Error())
  2391  		return MySqlResult{RowsAffected: 0, NewlyInsertedID: 0, Err: err}
  2392  	}
  2393  
  2394  	// return result
  2395  	return MySqlResult{RowsAffected: affected, NewlyInsertedID: newID, Err: nil}
  2396  }
  2397  
  2398  // ExecByOrdinalParams executes action query string and parameters to return result, if error, returns error object within result
  2399  // [ Parameters ]
  2400  //
  2401  //	actionQuery = sql action query, optionally having parameters marked as ?1, ?2 .. ?N, where each represents a parameter position
  2402  //	args = conditionally required if positioned parameters are specified, must appear in the same order as the positional parameters
  2403  //
  2404  // [ Return Values ]
  2405  //  1. MySqlResult = represents the sql action result received (including error info if applicable)
  2406  func (t *MySqlTransaction) ExecByOrdinalParams(actionQuery string, args ...interface{}) MySqlResult {
  2407  	if err := t.ready(); err != nil {
  2408  		return MySqlResult{RowsAffected: 0, NewlyInsertedID: 0, Err: err}
  2409  	}
  2410  
  2411  	// verify if the database connection is good
  2412  	if err := t.parent.Ping(); err != nil {
  2413  		return MySqlResult{RowsAffected: 0, NewlyInsertedID: 0, Err: err}
  2414  	}
  2415  
  2416  	// is new insertion?
  2417  	var isInsert bool
  2418  
  2419  	if strings.ToUpper(util.Left(actionQuery, 6)) == "INSERT" {
  2420  		isInsert = true
  2421  	} else {
  2422  		isInsert = false
  2423  	}
  2424  
  2425  	// perform exec action, and return to caller
  2426  	var result sql.Result
  2427  	var err error
  2428  
  2429  	if t._xrayTxSeg == nil {
  2430  		// in transaction mode,
  2431  		// action using tx object
  2432  		result, err = t.tx.Exec(actionQuery, args...)
  2433  	} else {
  2434  		// in transaction mode,
  2435  		// action using tx object
  2436  		subTrace := t._xrayTxSeg.NewSubSegment("Transaction-Exec-ExecByOrdinalParams")
  2437  		defer subTrace.Close()
  2438  		defer func() {
  2439  			_ = subTrace.Seg.AddMetadata("SQL-Query", actionQuery)
  2440  			_ = subTrace.Seg.AddMetadata("SQL-Param-Values", args)
  2441  			_ = subTrace.Seg.AddMetadata("Exec-Result", result)
  2442  			if err != nil {
  2443  				_ = subTrace.Seg.AddError(err)
  2444  			}
  2445  		}()
  2446  
  2447  		result, err = t.tx.ExecContext(subTrace.Ctx, actionQuery, args...)
  2448  	}
  2449  
  2450  	if err != nil {
  2451  		err = errors.New("ExecByOrdinalParams() Error: " + err.Error())
  2452  		return MySqlResult{RowsAffected: 0, NewlyInsertedID: 0, Err: err}
  2453  	}
  2454  
  2455  	// if inserted, get last id if known
  2456  	var newID int64
  2457  	newID = 0
  2458  
  2459  	if isInsert {
  2460  		newID, err = result.LastInsertId()
  2461  
  2462  		if err != nil {
  2463  			err = errors.New("ExecByOrdinalParams() Get LastInsertId() Error: " + err.Error())
  2464  			return MySqlResult{RowsAffected: 0, NewlyInsertedID: 0, Err: err}
  2465  		}
  2466  	}
  2467  
  2468  	// get rows affected by this action
  2469  	var affected int64
  2470  	affected = 0
  2471  
  2472  	affected, err = result.RowsAffected()
  2473  
  2474  	if err != nil {
  2475  		err = errors.New("ExecByOrdinalParams() Get RowsAffected() Error: " + err.Error())
  2476  		return MySqlResult{RowsAffected: 0, NewlyInsertedID: 0, Err: err}
  2477  	}
  2478  
  2479  	// return result
  2480  	return MySqlResult{RowsAffected: affected, NewlyInsertedID: newID, Err: nil}
  2481  }
  2482  
  2483  // ExecByNamedMapParam executes action query string with named map containing parameters to return result, if error, returns error object within result
  2484  // [ Syntax ]
  2485  //  1. in sql = instead of defining ordinal parameters ?, each parameter in sql does not need to be ordinal, rather define with :xyz (must have : in front of param name), where xyz is name of parameter, such as :customerID
  2486  //  2. in go = setup a map variable: var p = make(map[string]interface{})
  2487  //  3. in go = to set values into map variable: p["xyz"] = abc
  2488  //     where xyz is the parameter name matching the sql :xyz (do not include : in go map "xyz")
  2489  //     where abc is the value of the parameter value, whether string or other data types
  2490  //     note: in using map, just add additional map elements using the p["xyz"] = abc syntax
  2491  //     note: if parameter value can be a null, such as nullint, nullstring, use util.ToNullTime(), ToNullInt(), ToNullString(), etc.
  2492  //  4. in go = when calling this function passing the map variable, simply pass the map variable p into the args parameter
  2493  //
  2494  // [ Parameters ]
  2495  //
  2496  //	actionQuery = sql action query, with named parameters using :xyz syntax
  2497  //	args = required, the map variable of the named parameters
  2498  //
  2499  // [ Return Values ]
  2500  //  1. MySqlResult = represents the sql action result received (including error info if applicable)
  2501  func (svr *MySql) ExecByNamedMapParam(actionQuery string, args map[string]interface{}) MySqlResult {
  2502  	// verify if the database connection is good
  2503  	if err := svr.Ping(); err != nil {
  2504  		return MySqlResult{RowsAffected: 0, NewlyInsertedID: 0, Err: err}
  2505  	}
  2506  
  2507  	// is new insertion?
  2508  	var isInsert bool
  2509  
  2510  	if strings.ToUpper(util.Left(actionQuery, 6)) == "INSERT" {
  2511  		isInsert = true
  2512  	} else {
  2513  		isInsert = false
  2514  	}
  2515  
  2516  	// perform exec action, and return to caller
  2517  	var result sql.Result
  2518  	var err error
  2519  
  2520  	if !xray.XRayServiceOn() {
  2521  		// not in transaction mode,
  2522  		// action using db object
  2523  		result, err = svr.db.NamedExec(actionQuery, args)
  2524  	} else {
  2525  		// not in transaction mode,
  2526  		// action using db object
  2527  		trace := xray.NewSegment("MySql-Exec-ExecByNamedMapParam", svr._parentSegment)
  2528  		defer trace.Close()
  2529  		defer func() {
  2530  			_ = trace.Seg.AddMetadata("SQL-Query", actionQuery)
  2531  			_ = trace.Seg.AddMetadata("SQL-Param-Values", args)
  2532  			_ = trace.Seg.AddMetadata("Exec-Result", result)
  2533  			if err != nil {
  2534  				_ = trace.Seg.AddError(err)
  2535  			}
  2536  		}()
  2537  
  2538  		result, err = svr.db.NamedExecContext(trace.Ctx, actionQuery, args)
  2539  	}
  2540  
  2541  	if err != nil {
  2542  		err = errors.New("ExecByNamedMapParam() Error: " + err.Error())
  2543  		return MySqlResult{RowsAffected: 0, NewlyInsertedID: 0, Err: err}
  2544  	}
  2545  
  2546  	// if inserted, get last id if known
  2547  	var newID int64
  2548  	newID = 0
  2549  
  2550  	if isInsert {
  2551  		newID, err = result.LastInsertId()
  2552  
  2553  		if err != nil {
  2554  			err = errors.New("ExecByNamedMapParam() Get LastInsertId() Error: " + err.Error())
  2555  			return MySqlResult{RowsAffected: 0, NewlyInsertedID: 0, Err: err}
  2556  		}
  2557  	}
  2558  
  2559  	// get rows affected by this action
  2560  	var affected int64
  2561  	affected = 0
  2562  
  2563  	affected, err = result.RowsAffected()
  2564  
  2565  	if err != nil {
  2566  		err = errors.New("ExecByNamedMapParam() Get RowsAffected() Error: " + err.Error())
  2567  		return MySqlResult{RowsAffected: 0, NewlyInsertedID: 0, Err: err}
  2568  	}
  2569  
  2570  	// return result
  2571  	return MySqlResult{RowsAffected: affected, NewlyInsertedID: newID, Err: nil}
  2572  }
  2573  
  2574  // ExecByNamedMapParam executes action query string with named map containing parameters to return result, if error, returns error object within result
  2575  // [ Syntax ]
  2576  //  1. in sql = instead of defining ordinal parameters ?, each parameter in sql does not need to be ordinal, rather define with :xyz (must have : in front of param name), where xyz is name of parameter, such as :customerID
  2577  //  2. in go = setup a map variable: var p = make(map[string]interface{})
  2578  //  3. in go = to set values into map variable: p["xyz"] = abc
  2579  //     where xyz is the parameter name matching the sql :xyz (do not include : in go map "xyz")
  2580  //     where abc is the value of the parameter value, whether string or other data types
  2581  //     note: in using map, just add additional map elements using the p["xyz"] = abc syntax
  2582  //     note: if parameter value can be a null, such as nullint, nullstring, use util.ToNullTime(), ToNullInt(), ToNullString(), etc.
  2583  //  4. in go = when calling this function passing the map variable, simply pass the map variable p into the args parameter
  2584  //
  2585  // [ Parameters ]
  2586  //
  2587  //	actionQuery = sql action query, with named parameters using :xyz syntax
  2588  //	args = required, the map variable of the named parameters
  2589  //
  2590  // [ Return Values ]
  2591  //  1. MySqlResult = represents the sql action result received (including error info if applicable)
  2592  func (t *MySqlTransaction) ExecByNamedMapParam(actionQuery string, args map[string]interface{}) MySqlResult {
  2593  	if err := t.ready(); err != nil {
  2594  		return MySqlResult{RowsAffected: 0, NewlyInsertedID: 0, Err: err}
  2595  	}
  2596  
  2597  	// verify if the database connection is good
  2598  	if err := t.parent.Ping(); err != nil {
  2599  		return MySqlResult{RowsAffected: 0, NewlyInsertedID: 0, Err: err}
  2600  	}
  2601  
  2602  	// is new insertion?
  2603  	var isInsert bool
  2604  
  2605  	if strings.ToUpper(util.Left(actionQuery, 6)) == "INSERT" {
  2606  		isInsert = true
  2607  	} else {
  2608  		isInsert = false
  2609  	}
  2610  
  2611  	// perform exec action, and return to caller
  2612  	var result sql.Result
  2613  	var err error
  2614  
  2615  	if t._xrayTxSeg == nil {
  2616  		// in transaction mode,
  2617  		// action using tx object
  2618  		result, err = t.tx.NamedExec(actionQuery, args)
  2619  	} else {
  2620  		// in transaction mode,
  2621  		// action using tx object
  2622  		subTrace := t._xrayTxSeg.NewSubSegment("Transaction-Exec-ExecByNamedMapParam")
  2623  		defer subTrace.Close()
  2624  		defer func() {
  2625  			_ = subTrace.Seg.AddMetadata("SQL-Query", actionQuery)
  2626  			_ = subTrace.Seg.AddMetadata("SQL-Param-Values", args)
  2627  			_ = subTrace.Seg.AddMetadata("Exec-Result", result)
  2628  			if err != nil {
  2629  				_ = subTrace.Seg.AddError(err)
  2630  			}
  2631  		}()
  2632  
  2633  		result, err = t.tx.NamedExecContext(subTrace.Ctx, actionQuery, args)
  2634  
  2635  	}
  2636  
  2637  	if err != nil {
  2638  		err = errors.New("ExecByNamedMapParam() Error: " + err.Error())
  2639  		return MySqlResult{RowsAffected: 0, NewlyInsertedID: 0, Err: err}
  2640  	}
  2641  
  2642  	// if inserted, get last id if known
  2643  	var newID int64
  2644  	newID = 0
  2645  
  2646  	if isInsert {
  2647  		newID, err = result.LastInsertId()
  2648  
  2649  		if err != nil {
  2650  			err = errors.New("ExecByNamedMapParam() Get LastInsertId() Error: " + err.Error())
  2651  			return MySqlResult{RowsAffected: 0, NewlyInsertedID: 0, Err: err}
  2652  		}
  2653  	}
  2654  
  2655  	// get rows affected by this action
  2656  	var affected int64
  2657  	affected = 0
  2658  
  2659  	affected, err = result.RowsAffected()
  2660  
  2661  	if err != nil {
  2662  		err = errors.New("ExecByNamedMapParam() Get RowsAffected() Error: " + err.Error())
  2663  		return MySqlResult{RowsAffected: 0, NewlyInsertedID: 0, Err: err}
  2664  	}
  2665  
  2666  	// return result
  2667  	return MySqlResult{RowsAffected: affected, NewlyInsertedID: newID, Err: nil}
  2668  }
  2669  
  2670  // ExecByStructParam executes action query string with struct containing parameters to return result, if error, returns error object within result,
  2671  // the struct fields' struct tags must match the parameter names, such as: struct tag `db:"customerID"` must match parameter name in sql as ":customerID"
  2672  // [ Syntax ]
  2673  //  1. in sql = instead of defining ordinal parameters ?, each parameter in sql does not need to be ordinal, rather define with :xyz (must have : in front of param name), where xyz is name of parameter, such as :customerID
  2674  //  2. in go = using a struct to contain fields to match parameters, make sure struct tags match to the sql parameter names, such as struct tag `db:"customerID"` must match parameter name in sql as ":customerID" (the : is not part of the match)
  2675  //
  2676  // [ Parameters ]
  2677  //
  2678  //	actionQuery = sql action query, with named parameters using :xyz syntax
  2679  //	args = required, the struct variable, whose fields having struct tags matching sql parameter names
  2680  //
  2681  // [ Return Values ]
  2682  //  1. MySqlResult = represents the sql action result received (including error info if applicable)
  2683  func (svr *MySql) ExecByStructParam(actionQuery string, args interface{}) MySqlResult {
  2684  	// verify if the database connection is good
  2685  	if err := svr.Ping(); err != nil {
  2686  		return MySqlResult{RowsAffected: 0, NewlyInsertedID: 0, Err: err}
  2687  	}
  2688  
  2689  	// is new insertion?
  2690  	var isInsert bool
  2691  
  2692  	if strings.ToUpper(util.Left(actionQuery, 6)) == "INSERT" {
  2693  		isInsert = true
  2694  	} else {
  2695  		isInsert = false
  2696  	}
  2697  
  2698  	// perform exec action, and return to caller
  2699  	var result sql.Result
  2700  	var err error
  2701  
  2702  	if !xray.XRayServiceOn() {
  2703  		// not in transaction mode,
  2704  		// action using db object
  2705  		result, err = svr.db.NamedExec(actionQuery, args)
  2706  	} else {
  2707  		// not in transaction mode,
  2708  		// action using db object
  2709  		trace := xray.NewSegment("MySql-Exec-ExecByStructParam", svr._parentSegment)
  2710  		defer trace.Close()
  2711  		defer func() {
  2712  			_ = trace.Seg.AddMetadata("SQL-Query", actionQuery)
  2713  			_ = trace.Seg.AddMetadata("SQL-Param-Values", args)
  2714  			_ = trace.Seg.AddMetadata("Exec-Result", result)
  2715  			if err != nil {
  2716  				_ = trace.Seg.AddError(err)
  2717  			}
  2718  		}()
  2719  
  2720  		result, err = svr.db.NamedExecContext(trace.Ctx, actionQuery, args)
  2721  	}
  2722  
  2723  	if err != nil {
  2724  		err = errors.New("ExecByStructParam() Error: " + err.Error())
  2725  		return MySqlResult{RowsAffected: 0, NewlyInsertedID: 0, Err: err}
  2726  	}
  2727  
  2728  	// if inserted, get last id if known
  2729  	var newID int64
  2730  	newID = 0
  2731  
  2732  	if isInsert {
  2733  		newID, err = result.LastInsertId()
  2734  
  2735  		if err != nil {
  2736  			err = errors.New("ExecByStructParam() Get LastInsertId() Error: " + err.Error())
  2737  			return MySqlResult{RowsAffected: 0, NewlyInsertedID: 0, Err: err}
  2738  		}
  2739  	}
  2740  
  2741  	// get rows affected by this action
  2742  	var affected int64
  2743  	affected = 0
  2744  
  2745  	affected, err = result.RowsAffected()
  2746  
  2747  	if err != nil {
  2748  		err = errors.New("ExecByStructParam() Get RowsAffected() Error: " + err.Error())
  2749  		return MySqlResult{RowsAffected: 0, NewlyInsertedID: 0, Err: err}
  2750  	}
  2751  
  2752  	// return result
  2753  	return MySqlResult{RowsAffected: affected, NewlyInsertedID: newID, Err: nil}
  2754  }
  2755  
  2756  // ExecByStructParam executes action query string with struct containing parameters to return result, if error, returns error object within result,
  2757  // the struct fields' struct tags must match the parameter names, such as: struct tag `db:"customerID"` must match parameter name in sql as ":customerID"
  2758  // [ Syntax ]
  2759  //  1. in sql = instead of defining ordinal parameters ?, each parameter in sql does not need to be ordinal, rather define with :xyz (must have : in front of param name), where xyz is name of parameter, such as :customerID
  2760  //  2. in go = using a struct to contain fields to match parameters, make sure struct tags match to the sql parameter names, such as struct tag `db:"customerID"` must match parameter name in sql as ":customerID" (the : is not part of the match)
  2761  //
  2762  // [ Parameters ]
  2763  //
  2764  //	actionQuery = sql action query, with named parameters using :xyz syntax
  2765  //	args = required, the struct variable, whose fields having struct tags matching sql parameter names
  2766  //
  2767  // [ Return Values ]
  2768  //  1. MySqlResult = represents the sql action result received (including error info if applicable)
  2769  func (t *MySqlTransaction) ExecByStructParam(actionQuery string, args interface{}) MySqlResult {
  2770  	if err := t.ready(); err != nil {
  2771  		return MySqlResult{RowsAffected: 0, NewlyInsertedID: 0, Err: err}
  2772  	}
  2773  
  2774  	// verify if the database connection is good
  2775  	if err := t.parent.Ping(); err != nil {
  2776  		return MySqlResult{RowsAffected: 0, NewlyInsertedID: 0, Err: err}
  2777  	}
  2778  
  2779  	// is new insertion?
  2780  	var isInsert bool
  2781  
  2782  	if strings.ToUpper(util.Left(actionQuery, 6)) == "INSERT" {
  2783  		isInsert = true
  2784  	} else {
  2785  		isInsert = false
  2786  	}
  2787  
  2788  	// perform exec action, and return to caller
  2789  	var result sql.Result
  2790  	var err error
  2791  
  2792  	if t._xrayTxSeg == nil {
  2793  		// in transaction mode,
  2794  		// action using tx object
  2795  		result, err = t.tx.NamedExec(actionQuery, args)
  2796  	} else {
  2797  		// in transaction mode,
  2798  		// action using tx object
  2799  		subTrace := t._xrayTxSeg.NewSubSegment("Transaction-Exec-ExecByStructParam")
  2800  		defer subTrace.Close()
  2801  		defer func() {
  2802  			_ = subTrace.Seg.AddMetadata("SQL-Query", actionQuery)
  2803  			_ = subTrace.Seg.AddMetadata("SQL-Param-Values", args)
  2804  			_ = subTrace.Seg.AddMetadata("Exec-Result", result)
  2805  			if err != nil {
  2806  				_ = subTrace.Seg.AddError(err)
  2807  			}
  2808  		}()
  2809  
  2810  		result, err = t.tx.NamedExecContext(subTrace.Ctx, actionQuery, args)
  2811  	}
  2812  
  2813  	if err != nil {
  2814  		err = errors.New("ExecByStructParam() Error: " + err.Error())
  2815  		return MySqlResult{RowsAffected: 0, NewlyInsertedID: 0, Err: err}
  2816  	}
  2817  
  2818  	// if inserted, get last id if known
  2819  	var newID int64
  2820  	newID = 0
  2821  
  2822  	if isInsert {
  2823  		newID, err = result.LastInsertId()
  2824  
  2825  		if err != nil {
  2826  			err = errors.New("ExecByStructParam() Get LastInsertId() Error: " + err.Error())
  2827  			return MySqlResult{RowsAffected: 0, NewlyInsertedID: 0, Err: err}
  2828  		}
  2829  	}
  2830  
  2831  	// get rows affected by this action
  2832  	var affected int64
  2833  	affected = 0
  2834  
  2835  	affected, err = result.RowsAffected()
  2836  
  2837  	if err != nil {
  2838  		err = errors.New("ExecByStructParam() Get RowsAffected() Error: " + err.Error())
  2839  		return MySqlResult{RowsAffected: 0, NewlyInsertedID: 0, Err: err}
  2840  	}
  2841  
  2842  	// return result
  2843  	return MySqlResult{RowsAffected: affected, NewlyInsertedID: newID, Err: nil}
  2844  }