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

     1  package sqlserver
     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  	"net/url"
    24  	"strings"
    25  
    26  	util "github.com/aldelo/common"
    27  	"github.com/jmoiron/sqlx"
    28  
    29  	// this package is used by database/sql as we are wrapping the sql access functionality in this utility package
    30  	_ "github.com/denisenkom/go-mssqldb"
    31  )
    32  
    33  // ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    34  // SQLServer struct Usage Guide
    35  // ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    36  
    37  /*
    38  	***************************************************************************************************************
    39  	First, Create "../model/global.go"
    40  	***************************************************************************************************************
    41  
    42  	package model
    43  
    44  	import (
    45  			"errors"
    46  			"time"
    47  			data "github.com/aldelo/common/wrapper/sqlserver"
    48  	)
    49  
    50  	// package level accessible to the sqlserver database object
    51  	var db *data.SQLServer
    52  
    53  	// SetDB allows code outside of package to set the sqlserver database reference
    54  	func SetDB(dbx *data.SQLServer) {
    55  		db = dbx
    56  	}
    57  
    58  	// BeginTran starts db transaction
    59  	func BeginTran() {
    60  		if db != nil {
    61  			db.Begin()
    62  		}
    63  	}
    64  
    65  	// CommitTran commits db transaction
    66  	func CommitTran() {
    67  		if db != nil {
    68  			db.Commit()
    69  		}
    70  	}
    71  
    72  	// RollbackTran rolls back db transaction
    73  	func RollbackTran() {
    74  		if db != nil {
    75  			db.Rollback()
    76  		}
    77  	}
    78  
    79  */
    80  
    81  /*
    82  	***************************************************************************************************************
    83  	Second, Prepare DB Object for Use in "../main.go"
    84  	***************************************************************************************************************
    85  
    86  	package main
    87  
    88  	import (
    89  			...
    90  			data "github.com/aldelo/common/wrapper/sqlserver"
    91  			"???/model" // ??? is path to the model package
    92  			...
    93  	)
    94  
    95  	...
    96  
    97  	func main() {
    98  		...
    99  
   100  		// ========================================
   101  		// setup database connection
   102  		// ========================================
   103  
   104  		//
   105  		// declare sqlserver object
   106  		//
   107  		s := new(data.SQLServer)
   108  
   109  		//
   110  		// set sqlserver dsn fields
   111  		//
   112  		s.AppName = ""		// application name from the calling agent
   113  		s.Host = "" 		// from aws aurora endpoint
   114  		s.Port = 0 			// custom port number if applicable (0 will ignore this field)
   115  		s.Database = ""		// database name
   116  		s.UserName = ""		// database server user name
   117  		s.Password = ""		// database server user password
   118  		s.Encrypted = false	// set to false to not use encryption
   119  		s.Instance = ""		// optional
   120  		s.Timeout = 15		// seconds
   121  
   122  		//
   123  		// open sqlserver database connection
   124  		//
   125  		if err := s.Open(); err != nil {
   126  			s.Close()
   127  		} else {
   128  			// add sqlserver object to model global
   129  			model.SetDB(&s)
   130  
   131  			// defer db clean up upon execution ends
   132  			defer model.SetDB(nil)
   133  			defer s.Close()
   134  		}
   135  
   136  		...
   137  	}
   138  
   139  */
   140  
   141  /*
   142  	***************************************************************************************************************
   143  	Third, Using SqlServer Struct
   144  	***************************************************************************************************************
   145  
   146  	package model
   147  
   148  	import (
   149  		"bytes"
   150  		"database/sql"	// this import is needed for db struct tags
   151  		"errors"
   152  		"time"
   153  		util "github.com/aldelo/common"
   154  	)
   155  
   156  	// create a struct, and use db struct tags to identify parameter names
   157  	// db struct tags can contain ,required ,size=# if string
   158  	type Customer struct {
   159  		CustomerID		int		`db:"customerID"`
   160  		CompanyName		string	`db:"companyName"`
   161  	}
   162  
   163  	// when composing sql statements, if statement is long, use bytes.Buffer (or use data/QueryBuilder.go)
   164  	var b bytes.Buffer
   165  
   166  	b.WriteString("xyz ")
   167  	b.WriteString("123")
   168  
   169  	v := b.String()		// v = xyz 123
   170  
   171  	// for insert, update, logical delete, physical delete
   172  	// use the appropriate functions from db struct, located in model/global.go
   173  	// the db struct is global in scope for code files within model package
   174  	db.GetStruct(...)
   175  	db.GetSliceStruct(...)
   176  	// etc
   177  
   178  */
   179  
   180  // ================================================================================================================
   181  // STRUCTS
   182  // ================================================================================================================
   183  
   184  // SQLServer struct encapsulates the SQLServer database access functionality (using sqlx package)
   185  type SQLServer struct {
   186  	// SQLServer connection properties
   187  	Host      string
   188  	Port      int
   189  	Instance  string
   190  	Database  string
   191  	UserName  string
   192  	Password  string
   193  	Timeout   int
   194  	Encrypted bool
   195  	AppName   string
   196  
   197  	// SQLSvr state object
   198  	db *sqlx.DB
   199  	tx *sqlx.Tx
   200  }
   201  
   202  // SQLResult defines sql action query result info
   203  type SQLResult struct {
   204  	RowsAffected    int64
   205  	NewlyInsertedID int64
   206  	Err             error
   207  }
   208  
   209  // ================================================================================================================
   210  // STRUCT FUNCTIONS
   211  // ================================================================================================================
   212  
   213  // ----------------------------------------------------------------------------------------------------------------
   214  // utility functions
   215  // ----------------------------------------------------------------------------------------------------------------
   216  
   217  // GetDsnADO serialize SQLServer dsn to ado style connection string, for use in database connectivity (dsn.Port is ignored)
   218  func (svr *SQLServer) GetDsnADO() (string, error) {
   219  	//
   220  	// first validate input
   221  	//
   222  	if len(svr.Host) == 0 {
   223  		return "", errors.New("SQL Server Host is Required")
   224  	}
   225  
   226  	if len(svr.Database) == 0 {
   227  		return "", errors.New("SQL Database is Required")
   228  	}
   229  
   230  	if len(svr.UserName) == 0 {
   231  		return "", errors.New("User ID is Required")
   232  	}
   233  
   234  	//
   235  	// now create ado style connection string
   236  	//
   237  	str := "server=" + svr.Host
   238  
   239  	if len(svr.Instance) > 0 {
   240  		str += "\\" + svr.Instance + ";"
   241  	} else {
   242  		str += ";"
   243  	}
   244  
   245  	str += "database=" + svr.Database + ";"
   246  
   247  	if len(svr.AppName) > 0 {
   248  		str += "app name=" + svr.AppName + ";"
   249  	}
   250  
   251  	str += "user id=" + svr.UserName + ";"
   252  
   253  	if len(svr.Password) > 0 {
   254  		str += "password=" + svr.Password + ";"
   255  	}
   256  
   257  	if svr.Timeout > 0 {
   258  		str += "connection timeout=" + util.Itoa(svr.Timeout) + ";"
   259  	} else {
   260  		str += "connection timeout=0;"
   261  	}
   262  
   263  	if !svr.Encrypted {
   264  		str += "encrypt=disable;"
   265  	} else {
   266  		str += "encrypt=true;"
   267  	}
   268  
   269  	// remove last semi-colon from str
   270  	str = str[:len(str)-1]
   271  
   272  	// return to caller
   273  	return str, nil
   274  }
   275  
   276  // GetDsnURL serialize sql server dsn to url style connection string, for use in database connectivity
   277  func (svr *SQLServer) GetDsnURL() (string, error) {
   278  	//
   279  	// first validate input
   280  	//
   281  	if len(svr.Host) == 0 {
   282  		return "", errors.New("SQL Server Host is Required")
   283  	}
   284  
   285  	if len(svr.Database) == 0 {
   286  		return "", errors.New("SQL Database is Required")
   287  	}
   288  
   289  	if len(svr.UserName) == 0 {
   290  		return "", errors.New("User ID is Required")
   291  	}
   292  
   293  	//
   294  	// now create url style connection string
   295  	//
   296  	query := url.Values{}
   297  	query.Add("app name", svr.AppName)
   298  	query.Add("database", svr.Database)
   299  
   300  	if svr.Timeout >= 0 && svr.Timeout <= 60 {
   301  		query.Add("connection timeout", util.Itoa(svr.Timeout))
   302  	} else {
   303  		query.Add("connection timeout", "0")
   304  	}
   305  
   306  	if !svr.Encrypted {
   307  		query.Add("encrypt", "disable")
   308  	} else {
   309  		query.Add("encrypt", "true")
   310  	}
   311  
   312  	var h string
   313  
   314  	if svr.Port > 0 {
   315  		h = fmt.Sprintf("%s:%d", svr.Host, svr.Port)
   316  	} else if len(svr.Instance) > 0 {
   317  		h = fmt.Sprintf("%s\\%s", svr.Host, svr.Instance)
   318  	} else {
   319  		h = svr.Host
   320  	}
   321  
   322  	u := url.URL{
   323  		Scheme:   "sqlserver",
   324  		User:     url.UserPassword(svr.UserName, svr.Password),
   325  		Host:     h,
   326  		RawQuery: query.Encode(),
   327  	}
   328  
   329  	// return to caller
   330  	return u.String(), nil
   331  }
   332  
   333  // Open a database by connecting to it, using the dsn properties defined in the struct fields
   334  //
   335  //	useADOConnectString = if ignored, default is true, to use URL connect string format, set parameter value to false explicitly
   336  func (svr *SQLServer) Open(useADOConnectString ...bool) error {
   337  	//
   338  	// get parameter value,
   339  	// default is expected
   340  	//
   341  	ado := true
   342  
   343  	if len(useADOConnectString) > 0 {
   344  		ado = useADOConnectString[0]
   345  	}
   346  
   347  	// declare
   348  	var str string
   349  	var err error
   350  
   351  	// get connect string
   352  	if ado {
   353  		str, err = svr.GetDsnADO()
   354  	} else {
   355  		str, err = svr.GetDsnURL()
   356  	}
   357  
   358  	if err != nil {
   359  		svr.tx = nil
   360  		svr.db = nil
   361  		return err
   362  	}
   363  
   364  	// validate connection string
   365  	if len(str) == 0 {
   366  		svr.tx = nil
   367  		svr.db = nil
   368  		return errors.New("SQL Server Connect String Generated Cannot Be Empty")
   369  	}
   370  
   371  	// now ready to open sql server database
   372  	svr.db, err = sqlx.Open("sqlserver", str)
   373  
   374  	if err != nil {
   375  		svr.tx = nil
   376  		svr.db = nil
   377  		return err
   378  	}
   379  
   380  	// test sql server state object
   381  	if err = svr.db.Ping(); err != nil {
   382  		svr.tx = nil
   383  		svr.db = nil
   384  		return err
   385  	}
   386  
   387  	// upon open, transaction object already nil
   388  	svr.tx = nil
   389  
   390  	// sql server state object successfully opened
   391  	return nil
   392  }
   393  
   394  // Close will close the database connection and set db to nil
   395  func (svr *SQLServer) Close() error {
   396  	if svr.db != nil {
   397  		if err := svr.db.Close(); err != nil {
   398  			return err
   399  		}
   400  
   401  		// clean up
   402  		svr.tx = nil
   403  		svr.db = nil
   404  		return nil
   405  	}
   406  
   407  	return nil
   408  }
   409  
   410  // Ping tests if current database connection is still active and ready
   411  func (svr *SQLServer) Ping() error {
   412  	if svr.db == nil {
   413  		return errors.New("SQL Server Not Connected")
   414  	}
   415  
   416  	if err := svr.db.Ping(); err != nil {
   417  		return err
   418  	}
   419  
   420  	// database ok
   421  	return nil
   422  }
   423  
   424  // Begin starts a database transaction, and stores the transaction object until commit or rollback
   425  func (svr *SQLServer) Begin() error {
   426  	// verify if the database connection is good
   427  	if err := svr.Ping(); err != nil {
   428  		return err
   429  	}
   430  
   431  	// does transaction already exist
   432  	if svr.tx != nil {
   433  		return errors.New("Transaction Already Started")
   434  	}
   435  
   436  	// begin transaction on database
   437  	tx, err := svr.db.Beginx()
   438  
   439  	if err != nil {
   440  		return err
   441  	}
   442  
   443  	// transaction begin successful,
   444  	// store tx into svr.tx field
   445  	svr.tx = tx
   446  
   447  	// return nil as success
   448  	return nil
   449  }
   450  
   451  // Commit finalizes a database transaction, and commits changes to database
   452  func (svr *SQLServer) Commit() error {
   453  	// verify if the database connection is good
   454  	if err := svr.Ping(); err != nil {
   455  		return err
   456  	}
   457  
   458  	// does transaction already exist
   459  	if svr.tx == nil {
   460  		return errors.New("Transaction Does Not Exist")
   461  	}
   462  
   463  	// perform tx commit
   464  	if err := svr.tx.Commit(); err != nil {
   465  		return err
   466  	}
   467  
   468  	// commit successful
   469  	svr.tx = nil
   470  	return nil
   471  }
   472  
   473  // Rollback cancels pending database changes for the current transaction and clears out transaction object
   474  func (svr *SQLServer) Rollback() error {
   475  	// verify if the database connection is good
   476  	if err := svr.Ping(); err != nil {
   477  		return err
   478  	}
   479  
   480  	// does transaction already exist
   481  	if svr.tx == nil {
   482  		return errors.New("Transaction Does Not Exist")
   483  	}
   484  
   485  	// perform tx commit
   486  	if err := svr.tx.Rollback(); err != nil {
   487  		svr.tx = nil
   488  		return err
   489  	}
   490  
   491  	// commit successful
   492  	svr.tx = nil
   493  	return nil
   494  }
   495  
   496  // ----------------------------------------------------------------------------------------------------------------
   497  // query and marshal to 'struct slice' or 'struct' helpers
   498  // ----------------------------------------------------------------------------------------------------------------
   499  
   500  // GetStructSlice performs query with optional variadic parameters, and unmarshal result rows into target struct slice,
   501  // in essence, each row of data is marshaled into the given struct, and multiple struct form the slice,
   502  // such as: []Customer where each row represent a customer, and multiple customers being part of the slice
   503  // [ Parameters ]
   504  //
   505  //	dest = pointer to the struct slice or address of struct slice, this is the result of rows to be marshaled into struct slice
   506  //	query = sql query, optionally having parameters marked as @p1, @p2, ... @pN, where each represents a parameter position
   507  //	args = conditionally required if positioned parameters are specified, must appear in the same order as the positional parameters
   508  //
   509  // [ Return Values ]
   510  //  1. notFound = indicates no rows found in query (aka sql.ErrNoRows), if error is detected, notFound is always false
   511  //  2. if error != nil, then error is encountered (if error == sql.ErrNoRows, then error is treated as nil, and dest is nil)
   512  //
   513  // [ Notes ]
   514  //  1. if error == nil, and len(dest struct slice) == 0 then zero struct slice result
   515  func (svr *SQLServer) GetStructSlice(dest interface{}, query string, args ...interface{}) (notFound bool, retErr error) {
   516  	// verify if the database connection is good
   517  	if err := svr.Ping(); err != nil {
   518  		return false, err
   519  	}
   520  
   521  	// perform select action, and unmarshal result rows into target struct slice
   522  	var err error
   523  
   524  	if svr.tx == nil {
   525  		// not in transaction mode
   526  		// query using db object
   527  		err = svr.db.Select(dest, query, args...)
   528  	} else {
   529  		// in transaction mode
   530  		// query using tx object
   531  		err = svr.tx.Select(dest, query, args...)
   532  	}
   533  
   534  	// if err is sql.ErrNoRows then treat as no error
   535  	if err != nil && err == sql.ErrNoRows {
   536  		notFound = true
   537  		dest = nil
   538  		err = nil
   539  	} else {
   540  		notFound = false
   541  	}
   542  
   543  	// return error
   544  	return notFound, err
   545  }
   546  
   547  // GetStruct performs query with optional variadic parameters, and unmarshal single result row into single target struct,
   548  // such as: Customer struct where one row of data represent a customer
   549  // [ Parameters ]
   550  //
   551  //	dest = pointer to struct or address of struct, this is the result of row to be marshaled into this struct
   552  //	query = sql query, optionally having parameters marked as @p1, @p2, ... @pN, where each represents a parameter position
   553  //	args = conditionally required if positioned parameters are specified, must appear in the same order as the positional parameters
   554  //
   555  // [ Return Values ]
   556  //  1. notFound = indicates no rows found in query (aka sql.ErrNoRows), if error is detected, notFound is always false
   557  //  2. if error != nil, then error is encountered (if error == sql.ErrNoRows, then error is treated as nil, and dest is nil)
   558  func (svr *SQLServer) GetStruct(dest interface{}, query string, args ...interface{}) (notFound bool, retErr error) {
   559  	// verify if the database connection is good
   560  	if err := svr.Ping(); err != nil {
   561  		return false, err
   562  	}
   563  
   564  	// perform select action, and unmarshal result row (single row) into target struct (single object)
   565  	var err error
   566  
   567  	if svr.tx == nil {
   568  		// not in transaction mode
   569  		// query using db object
   570  		err = svr.db.Get(dest, query, args...)
   571  	} else {
   572  		// in transaction mode
   573  		// query using tx object
   574  		err = svr.tx.Get(dest, query, args...)
   575  	}
   576  
   577  	// if err is sql.ErrNoRows then treat as no error
   578  	if err != nil && err == sql.ErrNoRows {
   579  		notFound = true
   580  		dest = nil
   581  		err = nil
   582  	} else {
   583  		notFound = false
   584  	}
   585  
   586  	// return error
   587  	return notFound, err
   588  }
   589  
   590  // ----------------------------------------------------------------------------------------------------------------
   591  // query and get rows helpers
   592  // ----------------------------------------------------------------------------------------------------------------
   593  
   594  // GetRowsByOrdinalParams performs query with optional variadic parameters to get ROWS of result, and returns *sqlx.Rows
   595  // [ Parameters ]
   596  //
   597  //	query = sql query, optionally having parameters marked as @p1, @p2, ... @pN, where each represents a parameter position
   598  //	args = conditionally required if positioned parameters are specified, must appear in the same order as the positional parameters
   599  //
   600  // [ Return Values ]
   601  //  1. *sqlx.Rows = pointer to sqlx.Rows; or nil if no rows yielded
   602  //  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)
   603  //
   604  // [ Ranged Loop & Scan ]
   605  //  1. to loop, use: for _, r := range rows
   606  //  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)
   607  //
   608  // [ Continuous Loop & Scan ]
   609  //  1. Continuous loop until endOfRows = true is yielded from ScanSlice() or ScanStruct()
   610  //  2. ScanSlice(): accepts *sqlx.Rows, scans rows result into target pointer slice (if no error, endOfRows = true is returned)
   611  //  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
   612  func (svr *SQLServer) GetRowsByOrdinalParams(query string, args ...interface{}) (*sqlx.Rows, error) {
   613  	// verify if the database connection is good
   614  	if err := svr.Ping(); err != nil {
   615  		return nil, err
   616  	}
   617  
   618  	// perform select action, and return sqlx rows
   619  	var rows *sqlx.Rows
   620  	var err error
   621  
   622  	if svr.tx == nil {
   623  		// not in transaction mode
   624  		// query using db object
   625  		rows, err = svr.db.Queryx(query, args...)
   626  	} else {
   627  		// in transaction mode
   628  		// query using tx object
   629  		rows, err = svr.tx.Queryx(query, args...)
   630  	}
   631  
   632  	// if err is sql.ErrNoRows then treat as no error
   633  	if err != nil && err == sql.ErrNoRows {
   634  		rows = nil
   635  		err = nil
   636  	}
   637  
   638  	// return result
   639  	return rows, err
   640  }
   641  
   642  // GetRowsByNamedMapParam performs query with named map containing parameters to get ROWS of result, and returns *sqlx.Rows
   643  // [ Syntax ]
   644  //  1. in sql = instead of defining ordinal parameters @p1..@pN, 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
   645  //  2. in go = setup a map variable: var p = make(map[string]interface{})
   646  //  3. in go = to set values into map variable: p["xyz"] = abc
   647  //     where xyz is the parameter name matching the sql :xyz (do not include : in go map "xyz")
   648  //     where abc is the value of the parameter value, whether string or other data types
   649  //     note: in using map, just add additional map elements using the p["xyz"] = abc syntax
   650  //     note: if parameter value can be a null, such as nullint, nullstring, use util.ToNullTime(), ToNullInt(), ToNullString(), etc.
   651  //  4. in go = when calling this function passing the map variable, simply pass the map variable p into the args parameter
   652  //
   653  // [ Parameters ]
   654  //
   655  //	query = sql query, optionally having parameters marked as :xyz for each parameter name, where each represents a named parameter
   656  //	args = required, the map variable of the named parameters
   657  //
   658  // [ Return Values ]
   659  //  1. *sqlx.Rows = pointer to sqlx.Rows; or nil if no rows yielded
   660  //  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)
   661  //
   662  // [ Ranged Loop & Scan ]
   663  //  1. to loop, use: for _, r := range rows
   664  //  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)
   665  //
   666  // [ Continuous Loop & Scan ]
   667  //  1. Continuous loop until endOfRows = true is yielded from ScanSlice() or ScanStruct()
   668  //  2. ScanSlice(): accepts *sqlx.Rows, scans rows result into target pointer slice (if no error, endOfRows = true is returned)
   669  //  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
   670  func (svr *SQLServer) GetRowsByNamedMapParam(query string, args map[string]interface{}) (*sqlx.Rows, error) {
   671  	// verify if the database connection is good
   672  	if err := svr.Ping(); err != nil {
   673  		return nil, err
   674  	}
   675  
   676  	// perform select action, and return sqlx rows
   677  	var rows *sqlx.Rows
   678  	var err error
   679  
   680  	if svr.tx == nil {
   681  		// not in transaction mode
   682  		// query using db object
   683  		rows, err = svr.db.NamedQuery(query, args)
   684  	} else {
   685  		// in transaction mode
   686  		// query using tx object
   687  		rows, err = svr.tx.NamedQuery(query, args)
   688  	}
   689  
   690  	if err != nil && err == sql.ErrNoRows {
   691  		// no rows
   692  		rows = nil
   693  		err = nil
   694  	}
   695  
   696  	// return result
   697  	return rows, err
   698  }
   699  
   700  // GetRowsByStructParam performs query with a struct as parameter input to get ROWS of result, and returns *sqlx.Rows
   701  // [ Syntax ]
   702  //  1. in sql = instead of defining ordinal parameters @p1..@pN, 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
   703  //  2. in sql = important: the :xyz defined where xyz portion of parameter name must batch the struct tag's `db:"xyz"`
   704  //  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
   705  //  4. in go = when calling this function passing the struct variable, simply pass the struct variable into the args parameter
   706  //
   707  // [ Parameters ]
   708  //
   709  //	query = sql query, optionally having parameters marked as :xyz for each parameter name, where each represents a named parameter
   710  //	args = required, the struct variable where struct fields' struct tags match to the named parameters
   711  //
   712  // [ Return Values ]
   713  //  1. *sqlx.Rows = pointer to sqlx.Rows; or nil if no rows yielded
   714  //  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)
   715  //
   716  // [ Ranged Loop & Scan ]
   717  //  1. to loop, use: for _, r := range rows
   718  //  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)
   719  //
   720  // [ Continuous Loop & Scan ]
   721  //  1. Continuous loop until endOfRows = true is yielded from ScanSlice() or ScanStruct()
   722  //  2. ScanSlice(): accepts *sqlx.Rows, scans rows result into target pointer slice (if no error, endOfRows = true is returned)
   723  //  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
   724  func (svr *SQLServer) GetRowsByStructParam(query string, args interface{}) (*sqlx.Rows, error) {
   725  	// verify if the database connection is good
   726  	if err := svr.Ping(); err != nil {
   727  		return nil, err
   728  	}
   729  
   730  	// perform select action, and return sqlx rows
   731  	var rows *sqlx.Rows
   732  	var err error
   733  
   734  	if svr.tx == nil {
   735  		// not in transaction mode
   736  		// query using db object
   737  		rows, err = svr.db.NamedQuery(query, args)
   738  	} else {
   739  		// in transaction mode
   740  		// query using tx object
   741  		rows, err = svr.tx.NamedQuery(query, args)
   742  	}
   743  
   744  	if err != nil && err == sql.ErrNoRows {
   745  		// no rows
   746  		rows = nil
   747  		err = nil
   748  	}
   749  
   750  	// return result
   751  	return rows, err
   752  }
   753  
   754  // ----------------------------------------------------------------------------------------------------------------
   755  // scan row data and marshal to 'slice' or 'struct' helpers
   756  // ----------------------------------------------------------------------------------------------------------------
   757  
   758  // ScanSlice takes in *sqlx.Rows as parameter, will invoke the rows.Next() to advance to next row position,
   759  // and marshals current row's column values into a pointer reference to a slice,
   760  // 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),
   761  // 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
   762  // [ Parameters ]
   763  //
   764  //	rows = *sqlx.Rows
   765  //	dest = pointer or address to slice, such as: variable to "*[]string", or variable to "&cList for declaration cList []string"
   766  //
   767  // [ Return Values ]
   768  //  1. endOfRows = true if this action call yielded end of rows, meaning stop further processing of current loop
   769  //  2. if error != nil, then error is encountered (if error == sql.ErrNoRows, then error is treated as nil, and dest is set as nil)
   770  func (svr *SQLServer) ScanSlice(rows *sqlx.Rows, dest []interface{}) (endOfRows bool, err error) {
   771  	// ensure rows pointer is set
   772  	if rows == nil {
   773  		return true, nil
   774  	}
   775  
   776  	// call rows.Next() first to position the row
   777  	if rows.Next() {
   778  		// now slice scan
   779  		dest, err = rows.SliceScan()
   780  
   781  		// if err is sql.ErrNoRows then treat as no error
   782  		if err != nil && err == sql.ErrNoRows {
   783  			endOfRows = true
   784  			dest = nil
   785  			err = nil
   786  			return
   787  		}
   788  
   789  		if err != nil {
   790  			// has error
   791  			endOfRows = false // although error but may not be at end of rows
   792  			dest = nil
   793  			return
   794  		}
   795  
   796  		// slice scan success, but may not be at end of rows
   797  		return false, nil
   798  	}
   799  
   800  	// no more rows
   801  	return true, nil
   802  }
   803  
   804  // ScanStruct takes in *sqlx.Rows, will invoke the rows.Next() to advance to next row position,
   805  // and marshals current row's column values into a pointer reference to a struct,
   806  // the struct fields and row columns must match for both name and sequence position,
   807  // this enables us to quickly convert the row's columns into a defined struct automatically,
   808  // 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
   809  // [ Parameters ]
   810  //
   811  //	rows = *sqlx.Rows
   812  //	dest = pointer or address to struct, such as: variable to "*Customer", or variable to "&c for declaration c Customer"
   813  //
   814  // [ Return Values ]
   815  //  1. endOfRows = true if this action call yielded end of rows, meaning stop further processing of current loop
   816  //  2. if error != nil, then error is encountered (if error == sql.ErrNoRows, then error is treated as nil, and dest is set as nil)
   817  func (svr *SQLServer) ScanStruct(rows *sqlx.Rows, dest interface{}) (endOfRows bool, err error) {
   818  	// ensure rows pointer is set
   819  	if rows == nil {
   820  		return true, nil
   821  	}
   822  
   823  	// call rows.Next() first to position the row
   824  	if rows.Next() {
   825  		// now struct scan
   826  		err = rows.StructScan(dest)
   827  
   828  		// if err is sql.ErrNoRows then treat as no error
   829  		if err != nil && err == sql.ErrNoRows {
   830  			endOfRows = true
   831  			dest = nil
   832  			err = nil
   833  			return
   834  		}
   835  
   836  		if err != nil {
   837  			// has error
   838  			endOfRows = false // although error but may not be at end of rows
   839  			dest = nil
   840  			return
   841  		}
   842  
   843  		// struct scan successful, but may not be at end of rows
   844  		return false, nil
   845  	}
   846  
   847  	// no more rows
   848  	return true, nil
   849  }
   850  
   851  // ----------------------------------------------------------------------------------------------------------------
   852  // query for single row helper
   853  // ----------------------------------------------------------------------------------------------------------------
   854  
   855  // GetSingleRow performs query with optional variadic parameters to get a single ROW of result, and returns *sqlx.Row (This function returns SINGLE ROW)
   856  // [ Parameters ]
   857  //
   858  //	query = sql query, optionally having parameters marked as @p1, @p2, ... @pN, where each represents a parameter position
   859  //	args = conditionally required if positioned parameters are specified, must appear in the same order as the positional parameters
   860  //
   861  // [ Return Values ]
   862  //  1. *sqlx.Row = pointer to sqlx.Row; or nil if no row yielded
   863  //  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)
   864  //
   865  // [ Scan Values ]
   866  //  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)
   867  //
   868  // [ WARNING !!! ]
   869  //
   870  //	WHEN USING Scan(), MUST CHECK Scan Result Error for sql.ErrNoRow status
   871  //	SUGGESTED TO USE ScanColumnsByRow() Instead of Scan()
   872  func (svr *SQLServer) GetSingleRow(query string, args ...interface{}) (*sqlx.Row, error) {
   873  	// verify if the database connection is good
   874  	if err := svr.Ping(); err != nil {
   875  		return nil, err
   876  	}
   877  
   878  	// perform select action, and return sqlx row
   879  	var row *sqlx.Row
   880  	var err error
   881  
   882  	if svr.tx == nil {
   883  		// not in transaction mode
   884  		// query using db object
   885  		row = svr.db.QueryRowx(query, args...)
   886  	} else {
   887  		// in transaction mode
   888  		// query using tx object
   889  		row = svr.tx.QueryRowx(query, args...)
   890  	}
   891  
   892  	if row == nil {
   893  		err = errors.New("No Row Data Found From Query")
   894  	} else {
   895  		err = row.Err()
   896  
   897  		if err != nil {
   898  			if err == sql.ErrNoRows {
   899  				// no rows
   900  				row = nil
   901  				err = nil
   902  			} else {
   903  				// has error
   904  				row = nil
   905  			}
   906  		}
   907  	}
   908  
   909  	// return result
   910  	return row, err
   911  }
   912  
   913  // ----------------------------------------------------------------------------------------------------------------
   914  // scan single row data and marshal to 'slice' or 'struct' or specific fields, or scan columns helpers
   915  // ----------------------------------------------------------------------------------------------------------------
   916  
   917  // ScanSliceByRow takes in *sqlx.Row as parameter, and marshals current row's column values into a pointer reference to a slice,
   918  // 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)
   919  // [ Parameters ]
   920  //
   921  //	row = *sqlx.Row
   922  //	dest = pointer or address to slice, such as: variable to "*[]string", or variable to "&cList for declaration cList []string"
   923  //
   924  // [ Return Values ]
   925  //  1. notFound = true if no row is found in current scan
   926  //  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)
   927  func (svr *SQLServer) ScanSliceByRow(row *sqlx.Row, dest []interface{}) (notFound bool, err error) {
   928  	// if row is nil, treat as no row and not an error
   929  	if row == nil {
   930  		dest = nil
   931  		return true, nil
   932  	}
   933  
   934  	// perform slice scan on the given row
   935  	dest, err = row.SliceScan()
   936  
   937  	// if err is sql.ErrNoRows then treat as no error
   938  	if err != nil && err == sql.ErrNoRows {
   939  		dest = nil
   940  		return true, nil
   941  	}
   942  
   943  	if err != nil {
   944  		// has error
   945  		dest = nil
   946  		return false, err // although error but may not be not found
   947  	}
   948  
   949  	// slice scan success
   950  	return false, nil
   951  }
   952  
   953  // ScanStructByRow takes in *sqlx.Row, and marshals current row's column values into a pointer reference to a struct,
   954  // the struct fields and row columns must match for both name and sequence position,
   955  // this enables us to quickly convert the row's columns into a defined struct automatically,
   956  // the dest is nil if no columns found; the dest is pointer of struct when mapping is complete
   957  // [ Parameters ]
   958  //
   959  //	row = *sqlx.Row
   960  //	dest = pointer or address to struct, such as: variable to "*Customer", or variable to "&c for declaration c Customer"
   961  //
   962  // [ Return Values ]
   963  //  1. notFound = true if no row is found in current scan
   964  //  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)
   965  func (svr *SQLServer) ScanStructByRow(row *sqlx.Row, dest interface{}) (notFound bool, err error) {
   966  	// if row is nil, treat as no row and not an error
   967  	if row == nil {
   968  		dest = nil
   969  		return true, nil
   970  	}
   971  
   972  	// now struct scan
   973  	err = row.StructScan(dest)
   974  
   975  	// if err is sql.ErrNoRows then treat as no error
   976  	if err != nil && err == sql.ErrNoRows {
   977  		dest = nil
   978  		return true, nil
   979  	}
   980  
   981  	if err != nil {
   982  		// has error
   983  		dest = nil
   984  		return false, err // although error but may not be not found
   985  	}
   986  
   987  	// struct scan successful
   988  	return false, nil
   989  }
   990  
   991  // ScanColumnsByRow accepts a *sqlx row, and scans specific columns into dest outputs,
   992  // this is different than ScanSliceByRow or ScanStructByRow because this function allows specific extraction of column values into target fields,
   993  // (note: this function must extra all row column values to dest variadic parameters as present in the row parameter)
   994  // [ Parameters ]
   995  //
   996  //	row = *sqlx.Row representing the row containing columns to extract, note that this function MUST extract all columns from this row
   997  //	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
   998  //
   999  // [ Return Values ]
  1000  //  1. notFound = true if no row is found in current scan
  1001  //  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)
  1002  //
  1003  // [ Example ]
  1004  //  1. assuming: Select CustomerID, CustomerName, Address FROM Customer Where CustomerPhone='123';
  1005  //  2. assuming: row // *sqlx.Row derived from GetSingleRow() or specific row from GetRowsByOrdinalParams() / GetRowsByNamedMapParam() / GetRowsByStructParam()
  1006  //  3. assuming: var CustomerID int64
  1007  //     var CustomerName string
  1008  //     var Address string
  1009  //  4. notFound, err := svr.ScanColumnsByRow(row, &CustomerID, &CustomerName, &Address)
  1010  func (svr *SQLServer) ScanColumnsByRow(row *sqlx.Row, dest ...interface{}) (notFound bool, err error) {
  1011  	// if row is nil, treat as no row and not an error
  1012  	if row == nil {
  1013  		return true, nil
  1014  	}
  1015  
  1016  	// now scan columns from row
  1017  	err = row.Scan(dest...)
  1018  
  1019  	// if err is sql.ErrNoRows then treat as no error
  1020  	if err != nil && err == sql.ErrNoRows {
  1021  		return true, nil
  1022  	}
  1023  
  1024  	if err != nil {
  1025  		// has error
  1026  		return false, err // although error but may not be not found
  1027  	}
  1028  
  1029  	// scan columns successful
  1030  	return false, nil
  1031  }
  1032  
  1033  // ----------------------------------------------------------------------------------------------------------------
  1034  // query for single value in single row helpers
  1035  // ----------------------------------------------------------------------------------------------------------------
  1036  
  1037  // GetScalarString performs query with optional variadic parameters, and returns the first row and first column value in string data type
  1038  // [ Parameters ]
  1039  //
  1040  //	query = sql query, optionally having parameters marked as @p1, @p2, ... @pN, where each represents a parameter position
  1041  //	args = conditionally required if positioned parameters are specified, must appear in the same order as the positional parameters
  1042  //
  1043  // [ Return Values ]
  1044  //  1. retVal = string value of scalar result, if no value, blank is returned
  1045  //  2. retNotFound = now row found
  1046  //  3. if error != nil, then error is encountered (if error == sql.ErrNoRows, then error is treated as nil, and retVal is returned as blank)
  1047  func (svr *SQLServer) GetScalarString(query string, args ...interface{}) (retVal string, retNotFound bool, retErr error) {
  1048  	// verify if the database connection is good
  1049  	if err := svr.Ping(); err != nil {
  1050  		return "", false, err
  1051  	}
  1052  
  1053  	// get row using query string and parameters
  1054  	var row *sqlx.Row
  1055  
  1056  	if svr.tx == nil {
  1057  		// not in transaction
  1058  		// use db object
  1059  		row = svr.db.QueryRowx(query, args...)
  1060  	} else {
  1061  		// in transaction
  1062  		// use tx object
  1063  		row = svr.tx.QueryRowx(query, args...)
  1064  	}
  1065  
  1066  	if row == nil {
  1067  		return "", false, errors.New("Scalar Query Yielded Empty Row")
  1068  	} else {
  1069  		retErr = row.Err()
  1070  
  1071  		if retErr != nil {
  1072  			if retErr == sql.ErrNoRows {
  1073  				// no rows
  1074  				return "", true, nil
  1075  			} else {
  1076  				// has error
  1077  				return "", false, retErr
  1078  			}
  1079  		}
  1080  	}
  1081  
  1082  	// get value via scan
  1083  	retErr = row.Scan(&retVal)
  1084  
  1085  	if retErr == sql.ErrNoRows {
  1086  		// no row
  1087  		return "", true, nil
  1088  	}
  1089  
  1090  	// return value
  1091  	return retVal, false, retErr
  1092  }
  1093  
  1094  // GetScalarNullString performs query with optional variadic parameters, and returns the first row and first column value in sql.NullString{} data type
  1095  // [ Parameters ]
  1096  //
  1097  //	query = sql query, optionally having parameters marked as @p1, @p2, ... @pN, where each represents a parameter position
  1098  //	args = conditionally required if positioned parameters are specified, must appear in the same order as the positional parameters
  1099  //
  1100  // [ Return Values ]
  1101  //  1. retVal = string value of scalar result, if no value, sql.NullString{} is returned
  1102  //  2. retNotFound = now row found
  1103  //  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{})
  1104  func (svr *SQLServer) GetScalarNullString(query string, args ...interface{}) (retVal sql.NullString, retNotFound bool, retErr error) {
  1105  	// verify if the database connection is good
  1106  	if err := svr.Ping(); err != nil {
  1107  		return sql.NullString{}, false, err
  1108  	}
  1109  
  1110  	// get row using query string and parameters
  1111  	var row *sqlx.Row
  1112  
  1113  	if svr.tx == nil {
  1114  		// not in transaction
  1115  		// use db object
  1116  		row = svr.db.QueryRowx(query, args...)
  1117  	} else {
  1118  		// in transaction
  1119  		// use tx object
  1120  		row = svr.tx.QueryRowx(query, args...)
  1121  	}
  1122  
  1123  	if row == nil {
  1124  		return sql.NullString{}, false, errors.New("Scalar Query Yielded Empty Row")
  1125  	} else {
  1126  		retErr = row.Err()
  1127  
  1128  		if retErr != nil {
  1129  			if retErr == sql.ErrNoRows {
  1130  				// no rows
  1131  				return sql.NullString{}, true, nil
  1132  			} else {
  1133  				// has error
  1134  				return sql.NullString{}, false, retErr
  1135  			}
  1136  		}
  1137  	}
  1138  
  1139  	// get value via scan
  1140  	retErr = row.Scan(&retVal)
  1141  
  1142  	if retErr == sql.ErrNoRows {
  1143  		// no row
  1144  		return sql.NullString{}, true, nil
  1145  	}
  1146  
  1147  	// return value
  1148  	return retVal, false, retErr
  1149  }
  1150  
  1151  // ----------------------------------------------------------------------------------------------------------------
  1152  // execute helpers
  1153  // ----------------------------------------------------------------------------------------------------------------
  1154  
  1155  // ExecByOrdinalParams executes action query string and parameters to return result, if error, returns error object within result
  1156  // [ Parameters ]
  1157  //
  1158  //	actionQuery = sql action query, optionally having parameters marked as @p1, @p2, ... @pN, where each represents a parameter position
  1159  //	args = conditionally required if positioned parameters are specified, must appear in the same order as the positional parameters
  1160  //
  1161  // [ Return Values ]
  1162  //  1. SQLResult = represents the sql action result received (including error info if applicable)
  1163  func (svr *SQLServer) ExecByOrdinalParams(query string, args ...interface{}) SQLResult {
  1164  	// verify if the database connection is good
  1165  	if err := svr.Ping(); err != nil {
  1166  		return SQLResult{RowsAffected: 0, NewlyInsertedID: 0, Err: err}
  1167  	}
  1168  
  1169  	// keep query trimmed
  1170  	query = util.Trim(query)
  1171  
  1172  	// is insert?
  1173  	isInsert := strings.ToUpper(query[:6]) == "INSERT"
  1174  
  1175  	// append action result query
  1176  	if query[len(query)-1:] != ";" {
  1177  		query += ";"
  1178  	}
  1179  
  1180  	if isInsert {
  1181  		query += ";SELECT PKID=SCOPE_IDENTITY(), RowsAffected=@@ROWCOUNT;"
  1182  	} else {
  1183  		query += ";SELECT RowsAffected=@@ROWCOUNT;"
  1184  	}
  1185  
  1186  	// perform exec action, and return to caller
  1187  	var rows *sqlx.Rows
  1188  	var err error
  1189  
  1190  	if svr.tx == nil {
  1191  		// not in transaction mode,
  1192  		// action using db object
  1193  		rows, err = svr.db.Queryx(query, args...)
  1194  	} else {
  1195  		// in transaction mode,
  1196  		// action using tx object
  1197  		rows, err = svr.tx.Queryx(query, args...)
  1198  	}
  1199  
  1200  	if err != nil {
  1201  		return SQLResult{RowsAffected: 0, NewlyInsertedID: 0, Err: err}
  1202  	}
  1203  
  1204  	// no row - error
  1205  	if rows == nil {
  1206  		return SQLResult{RowsAffected: 0, NewlyInsertedID: 0, Err: errors.New("ExecByOrdinalParams() Error: No Row Summary Returned")}
  1207  	}
  1208  
  1209  	if rows.Next() == false {
  1210  		return SQLResult{RowsAffected: 0, NewlyInsertedID: 0, Err: errors.New("ExecByOrdinalParams() Error: Rows.Next() Yielded No Data")}
  1211  	}
  1212  
  1213  	// evaluate rows affected
  1214  	var affected int64
  1215  	var newID int64
  1216  
  1217  	var returnErr error
  1218  	returnErr = nil
  1219  
  1220  	if isInsert {
  1221  		if err = rows.Scan(&newID, &affected); err != nil {
  1222  			if err != sql.ErrNoRows {
  1223  				returnErr = err
  1224  			}
  1225  		}
  1226  	} else {
  1227  		if err = rows.Scan(&affected); err != nil {
  1228  			if err != sql.ErrNoRows {
  1229  				returnErr = err
  1230  			}
  1231  		}
  1232  	}
  1233  
  1234  	// return result
  1235  	return SQLResult{RowsAffected: affected, NewlyInsertedID: newID, Err: returnErr}
  1236  }
  1237  
  1238  // ExecByNamedMapParam executes action query string with named map containing parameters to return result, if error, returns error object within result
  1239  // [ Syntax ]
  1240  //  1. in sql = instead of defining ordinal parameters @p1..@pN, 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
  1241  //  2. in go = setup a map variable: var p = make(map[string]interface{})
  1242  //  3. in go = to set values into map variable: p["xyz"] = abc
  1243  //     where xyz is the parameter name matching the sql :xyz (do not include : in go map "xyz")
  1244  //     where abc is the value of the parameter value, whether string or other data types
  1245  //     note: in using map, just add additional map elements using the p["xyz"] = abc syntax
  1246  //     note: if parameter value can be a null, such as nullint, nullstring, use util.ToNullTime(), ToNullInt(), ToNullString(), etc.
  1247  //  4. in go = when calling this function passing the map variable, simply pass the map variable p into the args parameter
  1248  //
  1249  // [ Parameters ]
  1250  //
  1251  //	actionQuery = sql action query, with named parameters using :xyz syntax
  1252  //	args = required, the map variable of the named parameters
  1253  //
  1254  // [ Return Values ]
  1255  //  1. SQLResult = represents the sql action result received (including error info if applicable)
  1256  func (svr *SQLServer) ExecByNamedMapParam(query string, args map[string]interface{}) SQLResult {
  1257  	// verify if the database connection is good
  1258  	if err := svr.Ping(); err != nil {
  1259  		return SQLResult{RowsAffected: 0, NewlyInsertedID: 0, Err: err}
  1260  	}
  1261  
  1262  	// keep query trimmed
  1263  	query = util.Trim(query)
  1264  
  1265  	// is insert?
  1266  	isInsert := strings.ToUpper(query[:6]) == "INSERT"
  1267  
  1268  	// append action result query
  1269  	if query[len(query)-1:] != ";" {
  1270  		query += ";"
  1271  	}
  1272  
  1273  	if isInsert {
  1274  		query += ";SELECT PKID=SCOPE_IDENTITY(), RowsAffected=@@ROWCOUNT;"
  1275  	} else {
  1276  		query += ";SELECT RowsAffected=@@ROWCOUNT;"
  1277  	}
  1278  
  1279  	// perform exec action, and return to caller
  1280  	var rows *sqlx.Rows
  1281  	var err error
  1282  
  1283  	if svr.tx == nil {
  1284  		// not in transaction mode,
  1285  		// action using db object
  1286  		rows, err = svr.db.NamedQuery(query, args)
  1287  	} else {
  1288  		// in transaction mode,
  1289  		// action using tx object
  1290  		rows, err = svr.tx.NamedQuery(query, args)
  1291  	}
  1292  
  1293  	if err != nil {
  1294  		return SQLResult{RowsAffected: 0, NewlyInsertedID: 0, Err: err}
  1295  	}
  1296  
  1297  	// no row - error
  1298  	if rows == nil {
  1299  		return SQLResult{RowsAffected: 0, NewlyInsertedID: 0, Err: errors.New("ExecByNamedMapParam() Error: No Row Summary Returned")}
  1300  	}
  1301  
  1302  	if rows.Next() == false {
  1303  		return SQLResult{RowsAffected: 0, NewlyInsertedID: 0, Err: errors.New("ExecByNamedMapParam() Error: Rows.Next() Yielded No Data")}
  1304  	}
  1305  
  1306  	// evaluate rows affected
  1307  	var affected int64
  1308  	var newID int64
  1309  
  1310  	var returnErr error
  1311  	returnErr = nil
  1312  
  1313  	if isInsert {
  1314  		if err = rows.Scan(&newID, &affected); err != nil {
  1315  			if err != sql.ErrNoRows {
  1316  				returnErr = err
  1317  			}
  1318  		}
  1319  	} else {
  1320  		if err = rows.Scan(&affected); err != nil {
  1321  			if err != sql.ErrNoRows {
  1322  				returnErr = err
  1323  			}
  1324  		}
  1325  	}
  1326  
  1327  	// return result
  1328  	return SQLResult{RowsAffected: affected, NewlyInsertedID: newID, Err: returnErr}
  1329  }
  1330  
  1331  // ExecByStructParam executes action query string with struct containing parameters to return result, if error, returns error object within result,
  1332  // the struct fields' struct tags must match the parameter names, such as: struct tag `db:"customerID"` must match parameter name in sql as ":customerID"
  1333  // [ Syntax ]
  1334  //  1. in sql = instead of defining ordinal parameters @p1..@pN, 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
  1335  //  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)
  1336  //
  1337  // [ Parameters ]
  1338  //
  1339  //	actionQuery = sql action query, with named parameters using :xyz syntax
  1340  //	args = required, the struct variable, whose fields having struct tags matching sql parameter names
  1341  //
  1342  // [ Return Values ]
  1343  //  1. SQLResult = represents the sql action result received (including error info if applicable)
  1344  func (svr *SQLServer) ExecByStructParam(query string, args interface{}) SQLResult {
  1345  	// verify if the database connection is good
  1346  	if err := svr.Ping(); err != nil {
  1347  		return SQLResult{RowsAffected: 0, NewlyInsertedID: 0, Err: err}
  1348  	}
  1349  
  1350  	// keep query trimmed
  1351  	query = util.Trim(query)
  1352  
  1353  	// is insert?
  1354  	isInsert := strings.ToUpper(query[:6]) == "INSERT"
  1355  
  1356  	// append action result query
  1357  	if query[len(query)-1:] != ";" {
  1358  		query += ";"
  1359  	}
  1360  
  1361  	if isInsert {
  1362  		query += ";SELECT PKID=SCOPE_IDENTITY(), RowsAffected=@@ROWCOUNT;"
  1363  	} else {
  1364  		query += ";SELECT RowsAffected=@@ROWCOUNT;"
  1365  	}
  1366  
  1367  	// perform exec action, and return to caller
  1368  	var rows *sqlx.Rows
  1369  	var err error
  1370  
  1371  	if svr.tx == nil {
  1372  		// not in transaction mode,
  1373  		// action using db object
  1374  		rows, err = svr.db.NamedQuery(query, args)
  1375  	} else {
  1376  		// in transaction mode,
  1377  		// action using tx object
  1378  		rows, err = svr.tx.NamedQuery(query, args)
  1379  	}
  1380  
  1381  	if err != nil {
  1382  		return SQLResult{RowsAffected: 0, NewlyInsertedID: 0, Err: err}
  1383  	}
  1384  
  1385  	// no row - error
  1386  	if rows == nil {
  1387  		return SQLResult{RowsAffected: 0, NewlyInsertedID: 0, Err: errors.New("ExecByStructParam() Error: No Row Summary Returned")}
  1388  	}
  1389  
  1390  	if rows.Next() == false {
  1391  		return SQLResult{RowsAffected: 0, NewlyInsertedID: 0, Err: errors.New("ExecByStructParam() Error: Rows.Next() Yielded No Data")}
  1392  	}
  1393  
  1394  	// evaluate rows affected
  1395  	var affected int64
  1396  	var newID int64
  1397  
  1398  	var returnErr error
  1399  	returnErr = nil
  1400  
  1401  	if isInsert {
  1402  		if err = rows.Scan(&newID, &affected); err != nil {
  1403  			if err != sql.ErrNoRows {
  1404  				returnErr = err
  1405  			}
  1406  		}
  1407  	} else {
  1408  		if err = rows.Scan(&affected); err != nil {
  1409  			if err != sql.ErrNoRows {
  1410  				returnErr = err
  1411  			}
  1412  		}
  1413  	}
  1414  
  1415  	// return result
  1416  	return SQLResult{RowsAffected: affected, NewlyInsertedID: newID, Err: returnErr}
  1417  }