Compare commits
	
		
			19 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 4b55dbaacf | |||
| c399fa42ae | |||
| 9e586f7706 | |||
| 3cc8dccc63 | |||
| 7fedfbca81 | |||
| 3c439ba428 | |||
| ad24f6db44 | |||
| 1869ff3d75 | |||
| 30ce8c4b60 | |||
| 885bb53244 | |||
| 1c7dc1820a | |||
| 7e16e799e4 | |||
| 890e16241d | |||
| b9d0348735 | |||
| b9e9575b9b | |||
| 295a098eb4 | |||
| b69a082bb1 | |||
| a4a8c83d17 | |||
| e952176bb0 | 
							
								
								
									
										263
									
								
								cryptext/pronouncablePassword.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										263
									
								
								cryptext/pronouncablePassword.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,263 @@ | |||||||
|  | package cryptext | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"crypto/rand" | ||||||
|  | 	"io" | ||||||
|  | 	"math/big" | ||||||
|  | 	mathrand "math/rand" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	ppStartChar            = "BCDFGHJKLMNPQRSTVWXZ" | ||||||
|  | 	ppEndChar              = "ABDEFIKMNORSTUXYZ" | ||||||
|  | 	ppVowel                = "AEIOUY" | ||||||
|  | 	ppConsonant            = "BCDFGHJKLMNPQRSTVWXZ" | ||||||
|  | 	ppSegmentLenMin        = 3 | ||||||
|  | 	ppSegmentLenMax        = 7 | ||||||
|  | 	ppMaxRepeatedVowel     = 2 | ||||||
|  | 	ppMaxRepeatedConsonant = 2 | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ppContinuation = map[uint8]string{ | ||||||
|  | 	'A': "BCDFGHJKLMNPRSTVWXYZ", | ||||||
|  | 	'B': "ADFIKLMNORSTUY", | ||||||
|  | 	'C': "AEIKOUY", | ||||||
|  | 	'D': "AEILORSUYZ", | ||||||
|  | 	'E': "BCDFGHJKLMNPRSTVWXYZ", | ||||||
|  | 	'F': "ADEGIKLOPRTUY", | ||||||
|  | 	'G': "ABDEFHILMNORSTUY", | ||||||
|  | 	'H': "AEIOUY", | ||||||
|  | 	'I': "BCDFGHJKLMNPRSTVWXZ", | ||||||
|  | 	'J': "AEIOUY", | ||||||
|  | 	'K': "ADEFHILMNORSTUY", | ||||||
|  | 	'L': "ADEFGIJKMNOPSTUVWYZ", | ||||||
|  | 	'M': "ABEFIKOPSTUY", | ||||||
|  | 	'N': "ABEFIKOPSTUY", | ||||||
|  | 	'O': "BCDFGHJKLMNPRSTVWXYZ", | ||||||
|  | 	'P': "AEFIJLORSTUY", | ||||||
|  | 	'Q': "AEIOUY", | ||||||
|  | 	'R': "ADEFGHIJKLMNOPSTUVYZ", | ||||||
|  | 	'S': "ACDEIKLOPTUYZ", | ||||||
|  | 	'T': "AEHIJOPRSUWY", | ||||||
|  | 	'U': "BCDFGHJKLMNPRSTVWXZ", | ||||||
|  | 	'V': "AEIOUY", | ||||||
|  | 	'W': "AEIOUY", | ||||||
|  | 	'X': "AEIOUY", | ||||||
|  | 	'Y': "ABCDFGHKLMNPRSTVXZ", | ||||||
|  | 	'Z': "AEILOTUY", | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var ppLog2Map = map[int]float64{ | ||||||
|  | 	1:  0.00000000, | ||||||
|  | 	2:  1.00000000, | ||||||
|  | 	3:  1.58496250, | ||||||
|  | 	4:  2.00000000, | ||||||
|  | 	5:  2.32192809, | ||||||
|  | 	6:  2.58496250, | ||||||
|  | 	7:  2.80735492, | ||||||
|  | 	8:  3.00000000, | ||||||
|  | 	9:  3.16992500, | ||||||
|  | 	10: 3.32192809, | ||||||
|  | 	11: 3.45943162, | ||||||
|  | 	12: 3.58496250, | ||||||
|  | 	13: 3.70043972, | ||||||
|  | 	14: 3.80735492, | ||||||
|  | 	15: 3.90689060, | ||||||
|  | 	16: 4.00000000, | ||||||
|  | 	17: 4.08746284, | ||||||
|  | 	18: 4.16992500, | ||||||
|  | 	19: 4.24792751, | ||||||
|  | 	20: 4.32192809, | ||||||
|  | 	21: 4.39231742, | ||||||
|  | 	22: 4.45943162, | ||||||
|  | 	23: 4.52356196, | ||||||
|  | 	24: 4.58496250, | ||||||
|  | 	25: 4.64385619, | ||||||
|  | 	26: 4.70043972, | ||||||
|  | 	27: 4.75488750, | ||||||
|  | 	28: 4.80735492, | ||||||
|  | 	29: 4.85798100, | ||||||
|  | 	30: 4.90689060, | ||||||
|  | 	31: 4.95419631, | ||||||
|  | 	32: 5.00000000, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	ppVowelMap     = ppMakeSet(ppVowel) | ||||||
|  | 	ppConsonantMap = ppMakeSet(ppConsonant) | ||||||
|  | 	ppEndCharMap   = ppMakeSet(ppEndChar) | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func ppMakeSet(v string) map[uint8]bool { | ||||||
|  | 	mp := make(map[uint8]bool, len(v)) | ||||||
|  | 	for _, chr := range v { | ||||||
|  | 		mp[uint8(chr)] = true | ||||||
|  | 	} | ||||||
|  | 	return mp | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func ppRandInt(rng io.Reader, max int) int { | ||||||
|  | 	v, err := rand.Int(rng, big.NewInt(int64(max))) | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  | 	return int(v.Int64()) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func ppRand(rng io.Reader, chars string, entropy *float64) uint8 { | ||||||
|  | 	chr := chars[ppRandInt(rng, len(chars))] | ||||||
|  |  | ||||||
|  | 	*entropy = *entropy + ppLog2Map[len(chars)] | ||||||
|  |  | ||||||
|  | 	return chr | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func ppCharType(chr uint8) (bool, bool) { | ||||||
|  | 	_, ok1 := ppVowelMap[chr] | ||||||
|  | 	_, ok2 := ppConsonantMap[chr] | ||||||
|  |  | ||||||
|  | 	return ok1, ok2 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func ppCharsetRemove(cs string, set map[uint8]bool, allowEmpty bool) string { | ||||||
|  | 	result := "" | ||||||
|  | 	for _, chr := range cs { | ||||||
|  | 		if _, ok := set[uint8(chr)]; !ok { | ||||||
|  | 			result += string(chr) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if result == "" && !allowEmpty { | ||||||
|  | 		return cs | ||||||
|  | 	} | ||||||
|  | 	return result | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func ppCharsetFilter(cs string, set map[uint8]bool, allowEmpty bool) string { | ||||||
|  | 	result := "" | ||||||
|  | 	for _, chr := range cs { | ||||||
|  | 		if _, ok := set[uint8(chr)]; ok { | ||||||
|  | 			result += string(chr) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if result == "" && !allowEmpty { | ||||||
|  | 		return cs | ||||||
|  | 	} | ||||||
|  | 	return result | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func PronouncablePasswordExt(rng io.Reader, pwlen int) (string, float64) { | ||||||
|  |  | ||||||
|  | 	// kinda pseudo markov-chain - with a few extra rules and no weights... | ||||||
|  |  | ||||||
|  | 	if pwlen <= 0 { | ||||||
|  | 		return "", 0 | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	vowelCount := 0 | ||||||
|  | 	consoCount := 0 | ||||||
|  | 	entropy := float64(0) | ||||||
|  |  | ||||||
|  | 	startChar := ppRand(rng, ppStartChar, &entropy) | ||||||
|  |  | ||||||
|  | 	result := string(startChar) | ||||||
|  | 	currentChar := startChar | ||||||
|  |  | ||||||
|  | 	isVowel, isConsonant := ppCharType(currentChar) | ||||||
|  | 	if isVowel { | ||||||
|  | 		vowelCount = 1 | ||||||
|  | 	} | ||||||
|  | 	if isConsonant { | ||||||
|  | 		consoCount = ppMaxRepeatedConsonant | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	segmentLen := 1 | ||||||
|  |  | ||||||
|  | 	segmentLenTarget := ppSegmentLenMin + ppRandInt(rng, ppSegmentLenMax-ppSegmentLenMin) | ||||||
|  |  | ||||||
|  | 	for len(result) < pwlen { | ||||||
|  |  | ||||||
|  | 		charset := ppContinuation[currentChar] | ||||||
|  | 		if vowelCount >= ppMaxRepeatedVowel { | ||||||
|  | 			charset = ppCharsetRemove(charset, ppVowelMap, false) | ||||||
|  | 		} | ||||||
|  | 		if consoCount >= ppMaxRepeatedConsonant { | ||||||
|  | 			charset = ppCharsetRemove(charset, ppConsonantMap, false) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		lastOfSegment := false | ||||||
|  | 		newSegment := false | ||||||
|  |  | ||||||
|  | 		if len(result)+1 == pwlen { | ||||||
|  | 			// last of result | ||||||
|  | 			charset = ppCharsetFilter(charset, ppEndCharMap, false) | ||||||
|  | 		} else if segmentLen+1 == segmentLenTarget { | ||||||
|  | 			// last of segment | ||||||
|  | 			charsetNew := ppCharsetFilter(charset, ppEndCharMap, true) | ||||||
|  | 			if charsetNew != "" { | ||||||
|  | 				charset = charsetNew | ||||||
|  | 				lastOfSegment = true | ||||||
|  | 			} | ||||||
|  | 		} else if segmentLen >= segmentLenTarget { | ||||||
|  | 			// (perhaps) start of new segment | ||||||
|  | 			if _, ok := ppEndCharMap[currentChar]; ok { | ||||||
|  | 				charset = ppStartChar | ||||||
|  | 				newSegment = true | ||||||
|  | 			} else { | ||||||
|  | 				// continue segment for one more char to (hopefully) find an end-char | ||||||
|  | 				charsetNew := ppCharsetFilter(charset, ppEndCharMap, true) | ||||||
|  | 				if charsetNew != "" { | ||||||
|  | 					charset = charsetNew | ||||||
|  | 					lastOfSegment = true | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			// normal continuation | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		newChar := ppRand(rng, charset, &entropy) | ||||||
|  | 		if lastOfSegment { | ||||||
|  | 			currentChar = newChar | ||||||
|  | 			segmentLen++ | ||||||
|  | 			result += strings.ToLower(string(newChar)) | ||||||
|  | 		} else if newSegment { | ||||||
|  | 			currentChar = newChar | ||||||
|  | 			segmentLen = 1 | ||||||
|  | 			result += strings.ToUpper(string(newChar)) | ||||||
|  | 			segmentLenTarget = ppSegmentLenMin + ppRandInt(rng, ppSegmentLenMax-ppSegmentLenMin) | ||||||
|  | 			vowelCount = 0 | ||||||
|  | 			consoCount = 0 | ||||||
|  | 		} else { | ||||||
|  | 			currentChar = newChar | ||||||
|  | 			segmentLen++ | ||||||
|  | 			result += strings.ToLower(string(newChar)) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		isVowel, isConsonant := ppCharType(currentChar) | ||||||
|  | 		if isVowel { | ||||||
|  | 			vowelCount++ | ||||||
|  | 			consoCount = 0 | ||||||
|  | 		} | ||||||
|  | 		if isConsonant { | ||||||
|  | 			vowelCount = 0 | ||||||
|  | 			if newSegment { | ||||||
|  | 				consoCount = ppMaxRepeatedConsonant | ||||||
|  | 			} else { | ||||||
|  | 				consoCount++ | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return result, entropy | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func PronouncablePassword(len int) string { | ||||||
|  | 	v, _ := PronouncablePasswordExt(rand.Reader, len) | ||||||
|  | 	return v | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func PronouncablePasswordSeeded(seed int64, len int) string { | ||||||
|  |  | ||||||
|  | 	v, _ := PronouncablePasswordExt(mathrand.New(mathrand.NewSource(seed)), len) | ||||||
|  | 	return v | ||||||
|  | } | ||||||
							
								
								
									
										35
									
								
								cryptext/pronouncablePassword_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								cryptext/pronouncablePassword_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | |||||||
|  | package cryptext | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"math/rand" | ||||||
|  | 	"testing" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestPronouncablePasswordExt(t *testing.T) { | ||||||
|  | 	for i := 0; i < 20; i++ { | ||||||
|  | 		pw, entropy := PronouncablePasswordExt(rand.New(rand.NewSource(int64(i))), 16) | ||||||
|  | 		fmt.Printf("[%.2f] => %s\n", entropy, pw) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestPronouncablePasswordSeeded(t *testing.T) { | ||||||
|  | 	for i := 0; i < 20; i++ { | ||||||
|  | 		pw := PronouncablePasswordSeeded(int64(i), 8) | ||||||
|  | 		fmt.Printf("%s\n", pw) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestPronouncablePassword(t *testing.T) { | ||||||
|  | 	for i := 0; i < 20; i++ { | ||||||
|  | 		pw := PronouncablePassword(i + 1) | ||||||
|  | 		fmt.Printf("%s\n", pw) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestPronouncablePasswordWrongLen(t *testing.T) { | ||||||
|  | 	PronouncablePassword(0) | ||||||
|  | 	PronouncablePassword(-1) | ||||||
|  | 	PronouncablePassword(-2) | ||||||
|  | 	PronouncablePassword(-3) | ||||||
|  | } | ||||||
| @@ -9,6 +9,7 @@ import ( | |||||||
| 	"go.mongodb.org/mongo-driver/bson" | 	"go.mongodb.org/mongo-driver/bson" | ||||||
| 	"go.mongodb.org/mongo-driver/bson/primitive" | 	"go.mongodb.org/mongo-driver/bson/primitive" | ||||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||||
|  | 	"math" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
| @@ -667,6 +668,28 @@ func (v MetaValue) rawValueForJson() any { | |||||||
| 		} | 		} | ||||||
| 		return v.Value.(EnumWrap).ValueString | 		return v.Value.(EnumWrap).ValueString | ||||||
| 	} | 	} | ||||||
|  | 	if v.DataType == MDTFloat32 { | ||||||
|  | 		if math.IsNaN(float64(v.Value.(float32))) { | ||||||
|  | 			return "float64::NaN" | ||||||
|  | 		} else if math.IsInf(float64(v.Value.(float32)), +1) { | ||||||
|  | 			return "float64::+inf" | ||||||
|  | 		} else if math.IsInf(float64(v.Value.(float32)), -1) { | ||||||
|  | 			return "float64::-inf" | ||||||
|  | 		} else { | ||||||
|  | 			return v.Value | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if v.DataType == MDTFloat64 { | ||||||
|  | 		if math.IsNaN(v.Value.(float64)) { | ||||||
|  | 			return "float64::NaN" | ||||||
|  | 		} else if math.IsInf(v.Value.(float64), +1) { | ||||||
|  | 			return "float64::+inf" | ||||||
|  | 		} else if math.IsInf(v.Value.(float64), -1) { | ||||||
|  | 			return "float64::-inf" | ||||||
|  | 		} else { | ||||||
|  | 			return v.Value | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| 	return v.Value | 	return v.Value | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -163,16 +163,16 @@ func (pctx PreContext) Start() (*AppContext, *gin.Context, *HTTPResponse) { | |||||||
|  |  | ||||||
| 	ictx, cancel := context.WithTimeout(context.Background(), langext.Coalesce(pctx.timeout, pctx.wrapper.requestTimeout)) | 	ictx, cancel := context.WithTimeout(context.Background(), langext.Coalesce(pctx.timeout, pctx.wrapper.requestTimeout)) | ||||||
|  |  | ||||||
|  | 	actx := CreateAppContext(pctx.ginCtx, ictx, cancel) | ||||||
|  |  | ||||||
| 	if pctx.persistantData.sessionObj != nil { | 	if pctx.persistantData.sessionObj != nil { | ||||||
| 		err := pctx.persistantData.sessionObj.Init(pctx.ginCtx, ictx) | 		err := pctx.persistantData.sessionObj.Init(pctx.ginCtx, actx) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			cancel() | 			actx.Cancel() | ||||||
| 			return nil, nil, langext.Ptr(Error(exerr.Wrap(err, "Failed to init session").Build())) | 			return nil, nil, langext.Ptr(Error(exerr.Wrap(err, "Failed to init session").Build())) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	actx := CreateAppContext(pctx.ginCtx, ictx, cancel) |  | ||||||
|  |  | ||||||
| 	return actx, pctx.ginCtx, nil | 	return actx, pctx.ginCtx, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -53,15 +53,11 @@ func (w *GinRoutesWrapper) Group(relativePath string) *GinRoutesWrapper { | |||||||
| func (w *GinRoutesWrapper) Use(middleware ...gin.HandlerFunc) *GinRoutesWrapper { | func (w *GinRoutesWrapper) Use(middleware ...gin.HandlerFunc) *GinRoutesWrapper { | ||||||
| 	defHandler := langext.ArrCopy(w.defaultHandler) | 	defHandler := langext.ArrCopy(w.defaultHandler) | ||||||
| 	defHandler = append(defHandler, middleware...) | 	defHandler = append(defHandler, middleware...) | ||||||
| 	return &GinRoutesWrapper{wrapper: w.wrapper, routes: w.routes, defaultHandler: defHandler} | 	return &GinRoutesWrapper{wrapper: w.wrapper, routes: w.routes, defaultHandler: defHandler, absPath: w.absPath} | ||||||
| } | } | ||||||
|  |  | ||||||
| func (w *GinRoutesWrapper) WithJSONFilter(filter string) *GinRoutesWrapper { | func (w *GinRoutesWrapper) WithJSONFilter(filter string) *GinRoutesWrapper { | ||||||
| 	defHandler := langext.ArrCopy(w.defaultHandler) | 	return w.Use(func(g *gin.Context) { g.Set("goext.jsonfilter", filter) }) | ||||||
| 	defHandler = append(defHandler, func(g *gin.Context) { |  | ||||||
| 		g.Set("goext.jsonfilter", filter) |  | ||||||
| 	}) |  | ||||||
| 	return &GinRoutesWrapper{wrapper: w.wrapper, routes: w.routes, defaultHandler: defHandler} |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func (w *GinRoutesWrapper) GET(relativePath string) *GinRouteBuilder { | func (w *GinRoutesWrapper) GET(relativePath string) *GinRouteBuilder { | ||||||
| @@ -116,10 +112,7 @@ func (w *GinRouteBuilder) Use(middleware ...gin.HandlerFunc) *GinRouteBuilder { | |||||||
| } | } | ||||||
|  |  | ||||||
| func (w *GinRouteBuilder) WithJSONFilter(filter string) *GinRouteBuilder { | func (w *GinRouteBuilder) WithJSONFilter(filter string) *GinRouteBuilder { | ||||||
| 	w.handlers = append(w.handlers, func(g *gin.Context) { | 	return w.Use(func(g *gin.Context) { g.Set("goext.jsonfilter", filter) }) | ||||||
| 		g.Set("goext.jsonfilter", filter) |  | ||||||
| 	}) |  | ||||||
| 	return w |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func (w *GinRouteBuilder) Handle(handler WHandlerFunc) { | func (w *GinRouteBuilder) Handle(handler WHandlerFunc) { | ||||||
|   | |||||||
| @@ -6,6 +6,6 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| type SessionObject interface { | type SessionObject interface { | ||||||
| 	Init(g *gin.Context, ctx context.Context) error | 	Init(g *gin.Context, ctx *AppContext) error | ||||||
| 	Finish(ctx context.Context, resp HTTPResponse) error | 	Finish(ctx context.Context, resp HTTPResponse) error | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										22
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								go.mod
									
									
									
									
									
								
							| @@ -7,15 +7,15 @@ require ( | |||||||
| 	github.com/glebarez/go-sqlite v1.22.0 // only needed for tests -.- | 	github.com/glebarez/go-sqlite v1.22.0 // only needed for tests -.- | ||||||
| 	github.com/jmoiron/sqlx v1.3.5 | 	github.com/jmoiron/sqlx v1.3.5 | ||||||
| 	github.com/rs/xid v1.5.0 | 	github.com/rs/xid v1.5.0 | ||||||
| 	github.com/rs/zerolog v1.31.0 | 	github.com/rs/zerolog v1.32.0 | ||||||
| 	go.mongodb.org/mongo-driver v1.13.1 | 	go.mongodb.org/mongo-driver v1.14.0 | ||||||
| 	golang.org/x/crypto v0.18.0 | 	golang.org/x/crypto v0.19.0 | ||||||
| 	golang.org/x/sys v0.16.0 | 	golang.org/x/sys v0.17.0 | ||||||
| 	golang.org/x/term v0.16.0 | 	golang.org/x/term v0.17.0 | ||||||
| ) | ) | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
| 	github.com/bytedance/sonic v1.10.2 // indirect | 	github.com/bytedance/sonic v1.11.0 // indirect | ||||||
| 	github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect | 	github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect | ||||||
| 	github.com/chenzhuoyu/iasm v0.9.1 // indirect | 	github.com/chenzhuoyu/iasm v0.9.1 // indirect | ||||||
| 	github.com/dustin/go-humanize v1.0.1 // indirect | 	github.com/dustin/go-humanize v1.0.1 // indirect | ||||||
| @@ -23,14 +23,14 @@ require ( | |||||||
| 	github.com/gin-contrib/sse v0.1.0 // indirect | 	github.com/gin-contrib/sse v0.1.0 // indirect | ||||||
| 	github.com/go-playground/locales v0.14.1 // indirect | 	github.com/go-playground/locales v0.14.1 // indirect | ||||||
| 	github.com/go-playground/universal-translator v0.18.1 // indirect | 	github.com/go-playground/universal-translator v0.18.1 // indirect | ||||||
| 	github.com/go-playground/validator/v10 v10.16.0 // indirect | 	github.com/go-playground/validator/v10 v10.18.0 // indirect | ||||||
| 	github.com/goccy/go-json v0.10.2 // indirect | 	github.com/goccy/go-json v0.10.2 // indirect | ||||||
| 	github.com/golang/snappy v0.0.4 // indirect | 	github.com/golang/snappy v0.0.4 // indirect | ||||||
| 	github.com/google/uuid v1.5.0 // indirect | 	github.com/google/uuid v1.5.0 // indirect | ||||||
| 	github.com/json-iterator/go v1.1.12 // indirect | 	github.com/json-iterator/go v1.1.12 // indirect | ||||||
| 	github.com/klauspost/compress v1.17.4 // indirect | 	github.com/klauspost/compress v1.17.6 // indirect | ||||||
| 	github.com/klauspost/cpuid/v2 v2.2.6 // indirect | 	github.com/klauspost/cpuid/v2 v2.2.7 // indirect | ||||||
| 	github.com/leodido/go-urn v1.2.4 // indirect | 	github.com/leodido/go-urn v1.4.0 // indirect | ||||||
| 	github.com/mattn/go-colorable v0.1.13 // indirect | 	github.com/mattn/go-colorable v0.1.13 // indirect | ||||||
| 	github.com/mattn/go-isatty v0.0.20 // indirect | 	github.com/mattn/go-isatty v0.0.20 // indirect | ||||||
| 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect | 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect | ||||||
| @@ -45,7 +45,7 @@ require ( | |||||||
| 	github.com/xdg-go/stringprep v1.0.4 // indirect | 	github.com/xdg-go/stringprep v1.0.4 // indirect | ||||||
| 	github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect | 	github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect | ||||||
| 	golang.org/x/arch v0.7.0 // indirect | 	golang.org/x/arch v0.7.0 // indirect | ||||||
| 	golang.org/x/net v0.20.0 // indirect | 	golang.org/x/net v0.21.0 // indirect | ||||||
| 	golang.org/x/sync v0.6.0 // indirect | 	golang.org/x/sync v0.6.0 // indirect | ||||||
| 	golang.org/x/text v0.14.0 // indirect | 	golang.org/x/text v0.14.0 // indirect | ||||||
| 	google.golang.org/protobuf v1.32.0 // indirect | 	google.golang.org/protobuf v1.32.0 // indirect | ||||||
|   | |||||||
							
								
								
									
										24
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								go.sum
									
									
									
									
									
								
							| @@ -2,6 +2,8 @@ github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1 | |||||||
| github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= | github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= | ||||||
| github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE= | github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE= | ||||||
| github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= | github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= | ||||||
|  | github.com/bytedance/sonic v1.11.0 h1:FwNNv6Vu4z2Onf1++LNzxB/QhitD8wuTdpZzMTGITWo= | ||||||
|  | github.com/bytedance/sonic v1.11.0/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= | ||||||
| github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= | ||||||
| github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= | ||||||
| github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= | github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= | ||||||
| @@ -31,6 +33,10 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn | |||||||
| github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= | ||||||
| github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE= | github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE= | ||||||
| github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= | github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= | ||||||
|  | github.com/go-playground/validator/v10 v10.17.0 h1:SmVVlfAOtlZncTxRuinDPomC2DkXJ4E5T9gDA0AIH74= | ||||||
|  | github.com/go-playground/validator/v10 v10.17.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= | ||||||
|  | github.com/go-playground/validator/v10 v10.18.0 h1:BvolUXjp4zuvkZ5YN5t7ebzbhlUtPsPm2S9NAZ5nl9U= | ||||||
|  | github.com/go-playground/validator/v10 v10.18.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= | ||||||
| github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= | github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= | ||||||
| github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= | ||||||
| github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= | ||||||
| @@ -55,12 +61,18 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm | |||||||
| github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= | github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= | ||||||
| github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= | github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= | ||||||
| github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= | github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= | ||||||
|  | github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI= | ||||||
|  | github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= | ||||||
| github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= | ||||||
| github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= | github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= | ||||||
| github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= | github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= | ||||||
|  | github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= | ||||||
|  | github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= | ||||||
| github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= | ||||||
| github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= | github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= | ||||||
| github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= | github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= | ||||||
|  | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= | ||||||
|  | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= | ||||||
| github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= | github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= | ||||||
| github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= | ||||||
| github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= | ||||||
| @@ -91,6 +103,8 @@ github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= | |||||||
| github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= | github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= | ||||||
| github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= | github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= | ||||||
| github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= | github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= | ||||||
|  | github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= | ||||||
|  | github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= | ||||||
| github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||||
| github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= | ||||||
| github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= | ||||||
| @@ -118,6 +132,8 @@ github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/ | |||||||
| github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= | ||||||
| go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk= | go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk= | ||||||
| go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo= | go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo= | ||||||
|  | go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= | ||||||
|  | go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= | ||||||
| golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= | ||||||
| golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc= | golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc= | ||||||
| golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= | golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= | ||||||
| @@ -129,6 +145,8 @@ golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= | |||||||
| golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= | golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= | ||||||
| golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= | golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= | ||||||
| golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= | golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= | ||||||
|  | golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= | ||||||
|  | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= | ||||||
| golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= | ||||||
| golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | ||||||
| golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||||
| @@ -139,6 +157,8 @@ golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= | |||||||
| golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= | golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= | ||||||
| golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= | golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= | ||||||
| golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= | golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= | ||||||
|  | golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= | ||||||
|  | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= | ||||||
| golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
| golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
| golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= | golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= | ||||||
| @@ -156,10 +176,14 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | |||||||
| golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= | golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= | ||||||
| golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||||
|  | golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= | ||||||
|  | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||||
| golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | ||||||
| golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= | ||||||
| golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= | golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= | ||||||
| golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= | golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= | ||||||
|  | golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= | ||||||
|  | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= | ||||||
| golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||||
| golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||||
| golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| package goext | package goext | ||||||
|  |  | ||||||
| const GoextVersion = "0.0.373" | const GoextVersion = "0.0.391" | ||||||
|  |  | ||||||
| const GoextVersionTimestamp = "2024-01-14T00:07:01+0100" | const GoextVersionTimestamp = "2024-02-21T16:18:04+0100" | ||||||
|   | |||||||
| @@ -265,6 +265,15 @@ func ArrFirstIndex[T comparable](arr []T, needle T) int { | |||||||
| 	return -1 | 	return -1 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func ArrFirstIndexFunc[T any](arr []T, comp func(v T) bool) int { | ||||||
|  | 	for i, v := range arr { | ||||||
|  | 		if comp(v) { | ||||||
|  | 			return i | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return -1 | ||||||
|  | } | ||||||
|  |  | ||||||
| func ArrLastIndex[T comparable](arr []T, needle T) int { | func ArrLastIndex[T comparable](arr []T, needle T) int { | ||||||
| 	result := -1 | 	result := -1 | ||||||
| 	for i, v := range arr { | 	for i, v := range arr { | ||||||
| @@ -275,6 +284,16 @@ func ArrLastIndex[T comparable](arr []T, needle T) int { | |||||||
| 	return result | 	return result | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func ArrLastIndexFunc[T any](arr []T, comp func(v T) bool) int { | ||||||
|  | 	result := -1 | ||||||
|  | 	for i, v := range arr { | ||||||
|  | 		if comp(v) { | ||||||
|  | 			result = i | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return result | ||||||
|  | } | ||||||
|  |  | ||||||
| func AddToSet[T comparable](set []T, add T) []T { | func AddToSet[T comparable](set []T, add T) []T { | ||||||
| 	for _, v := range set { | 	for _, v := range set { | ||||||
| 		if v == add { | 		if v == add { | ||||||
| @@ -479,3 +498,33 @@ func JoinString(arr []string, delimiter string) string { | |||||||
|  |  | ||||||
| 	return str | 	return str | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // ArrChunk splits the array into buckets of max-size `chunkSize` | ||||||
|  | // order is being kept. | ||||||
|  | // The last chunk may contain less than length elements. | ||||||
|  | // | ||||||
|  | // (chunkSize == -1) means no chunking | ||||||
|  | // | ||||||
|  | // see https://www.php.net/manual/en/function.array-chunk.php | ||||||
|  | func ArrChunk[T any](arr []T, chunkSize int) [][]T { | ||||||
|  | 	if chunkSize == -1 { | ||||||
|  | 		return [][]T{arr} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	res := make([][]T, 0, 1+len(arr)/chunkSize) | ||||||
|  |  | ||||||
|  | 	i := 0 | ||||||
|  | 	for i < len(arr) { | ||||||
|  |  | ||||||
|  | 		right := i + chunkSize | ||||||
|  | 		if right >= len(arr) { | ||||||
|  | 			right = len(arr) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		res = append(res, arr[i:right]) | ||||||
|  |  | ||||||
|  | 		i = right | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return res | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										21
									
								
								langext/must.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								langext/must.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | package langext | ||||||
|  |  | ||||||
|  | // Must returns a value and panics on error | ||||||
|  | // | ||||||
|  | // Usage: Must(methodWithError(...)) | ||||||
|  | func Must[T any](v T, err error) T { | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  | 	return v | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // MustBool returns a value and panics on missing | ||||||
|  | // | ||||||
|  | // Usage: MustBool(methodWithOkayReturn(...)) | ||||||
|  | func MustBool[T any](v T, ok bool) T { | ||||||
|  | 	if !ok { | ||||||
|  | 		panic("not ok") | ||||||
|  | 	} | ||||||
|  | 	return v | ||||||
|  | } | ||||||
							
								
								
									
										29
									
								
								langext/url.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								langext/url.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | |||||||
|  | package langext | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func BuildUrl(url, path string, params *map[string]string) string { | ||||||
|  | 	if path[:1] == "/" && url[len(url)-1:] == "/" { | ||||||
|  | 		url += path[1:] | ||||||
|  | 	} else if path[:1] != "/" && url[len(url)-1:] != "/" { | ||||||
|  | 		url += "/" + path | ||||||
|  | 	} else { | ||||||
|  | 		url += path | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if params == nil { | ||||||
|  | 		return url | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for key, value := range *params { | ||||||
|  | 		if strings.Contains(url, "?") { | ||||||
|  | 			url += fmt.Sprintf("&%s=%s", key, value) | ||||||
|  | 		} else { | ||||||
|  | 			url += fmt.Sprintf("?%s=%s", key, value) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return url | ||||||
|  | } | ||||||
							
								
								
									
										45
									
								
								langext/url_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								langext/url_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | |||||||
|  | package langext | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"gogs.mikescher.com/BlackForestBytes/goext/tst" | ||||||
|  | 	"testing" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestBuildUrl(t *testing.T) { | ||||||
|  | 	tests := []struct { | ||||||
|  | 		Url    string | ||||||
|  | 		Path   string | ||||||
|  | 		Params *map[string]string | ||||||
|  | 		Want   string | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			Url:    "https://test.heydyno.de/", | ||||||
|  | 			Path:   "/testing-01", | ||||||
|  | 			Params: &map[string]string{"param1": "value1"}, | ||||||
|  | 			Want:   "https://test.heydyno.de/testing-01?param1=value1", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Url:    "https://test.heydyno.de", | ||||||
|  | 			Path:   "testing-01", | ||||||
|  | 			Params: &map[string]string{"param1": "value1"}, | ||||||
|  | 			Want:   "https://test.heydyno.de/testing-01?param1=value1", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Url:    "https://test.heydyno.de", | ||||||
|  | 			Path:   "/testing-01", | ||||||
|  | 			Params: nil, | ||||||
|  | 			Want:   "https://test.heydyno.de/testing-01", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Url:    "https://test.heydyno.de/", | ||||||
|  | 			Path:   "testing-01", | ||||||
|  | 			Params: nil, | ||||||
|  | 			Want:   "https://test.heydyno.de/testing-01", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, test := range tests { | ||||||
|  | 		res := BuildUrl(test.Url, test.Path, test.Params) | ||||||
|  | 		tst.AssertEqual(t, res, test.Want) | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										72
									
								
								reflectext/convertToMap.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								reflectext/convertToMap.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | |||||||
|  | package reflectext | ||||||
|  |  | ||||||
|  | import "reflect" | ||||||
|  |  | ||||||
|  | func ConvertStructToMap(v any) any { | ||||||
|  | 	return reflectToMap(reflect.ValueOf(v)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func reflectToMap(fv reflect.Value) any { | ||||||
|  |  | ||||||
|  | 	if fv.Kind() == reflect.Ptr { | ||||||
|  |  | ||||||
|  | 		if fv.IsNil() { | ||||||
|  | 			return nil | ||||||
|  | 		} else { | ||||||
|  | 			return reflectToMap(fv.Elem()) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if fv.Kind() == reflect.Func { | ||||||
|  |  | ||||||
|  | 		// skip | ||||||
|  | 		return nil | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if fv.Kind() == reflect.Array { | ||||||
|  |  | ||||||
|  | 		arrlen := fv.Len() | ||||||
|  | 		arr := make([]any, arrlen) | ||||||
|  | 		for i := 0; i < arrlen; i++ { | ||||||
|  | 			arr[i] = reflectToMap(fv.Index(i)) | ||||||
|  | 		} | ||||||
|  | 		return arr | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if fv.Kind() == reflect.Slice { | ||||||
|  |  | ||||||
|  | 		arrlen := fv.Len() | ||||||
|  | 		arr := make([]any, arrlen) | ||||||
|  | 		for i := 0; i < arrlen; i++ { | ||||||
|  | 			arr[i] = reflectToMap(fv.Index(i)) | ||||||
|  | 		} | ||||||
|  | 		return arr | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if fv.Kind() == reflect.Chan { | ||||||
|  |  | ||||||
|  | 		// skip | ||||||
|  | 		return nil | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if fv.Kind() == reflect.Struct { | ||||||
|  |  | ||||||
|  | 		res := make(map[string]any) | ||||||
|  |  | ||||||
|  | 		for i := 0; i < fv.NumField(); i++ { | ||||||
|  | 			if fv.Type().Field(i).IsExported() { | ||||||
|  | 				res[fv.Type().Field(i).Name] = reflectToMap(fv.Field(i)) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return res | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return fv.Interface() | ||||||
|  | } | ||||||
| @@ -1,13 +1,14 @@ | |||||||
| package sq | package sq | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/exerr" | 	"gogs.mikescher.com/BlackForestBytes/goext/exerr" | ||||||
| 	"reflect" | 	"reflect" | ||||||
| 	"strings" | 	"strings" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func BuildUpdateStatement(q Queryable, tableName string, obj any, idColumn string) (string, PP, error) { | func BuildUpdateStatement[TData any](q Queryable, tableName string, obj TData, idColumn string) (string, PP, error) { | ||||||
| 	rval := reflect.ValueOf(obj) | 	rval := reflect.ValueOf(obj) | ||||||
| 	rtyp := rval.Type() | 	rtyp := rval.Type() | ||||||
|  |  | ||||||
| @@ -70,7 +71,7 @@ func BuildUpdateStatement(q Queryable, tableName string, obj any, idColumn strin | |||||||
| 	return fmt.Sprintf("UPDATE %s SET %s WHERE %s", tableName, strings.Join(setClauses, ", "), matchClause), params, nil | 	return fmt.Sprintf("UPDATE %s SET %s WHERE %s", tableName, strings.Join(setClauses, ", "), matchClause), params, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func BuildInsertStatement(q Queryable, tableName string, obj any) (string, PP, error) { | func BuildInsertStatement[TData any](q Queryable, tableName string, obj TData) (string, PP, error) { | ||||||
| 	rval := reflect.ValueOf(obj) | 	rval := reflect.ValueOf(obj) | ||||||
| 	rtyp := rval.Type() | 	rtyp := rval.Type() | ||||||
|  |  | ||||||
| @@ -96,7 +97,7 @@ func BuildInsertStatement(q Queryable, tableName string, obj any) (string, PP, e | |||||||
| 		if rsfield.Type.Kind() == reflect.Ptr && rvfield.IsNil() { | 		if rsfield.Type.Kind() == reflect.Ptr && rvfield.IsNil() { | ||||||
|  |  | ||||||
| 			fields = append(fields, columnName) | 			fields = append(fields, columnName) | ||||||
| 			values = append(fields, "NULL") | 			values = append(values, "NULL") | ||||||
|  |  | ||||||
| 		} else { | 		} else { | ||||||
|  |  | ||||||
| @@ -106,7 +107,7 @@ func BuildInsertStatement(q Queryable, tableName string, obj any) (string, PP, e | |||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			fields = append(fields, columnName) | 			fields = append(fields, columnName) | ||||||
| 			values = append(fields, ":"+params.Add(val)) | 			values = append(values, ":"+params.Add(val)) | ||||||
|  |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| @@ -116,5 +117,83 @@ func BuildInsertStatement(q Queryable, tableName string, obj any) (string, PP, e | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	//goland:noinspection SqlNoDataSourceInspection | 	//goland:noinspection SqlNoDataSourceInspection | ||||||
| 	return fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)", tableName, strings.Join(fields, ", "), values), params, nil | 	return fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)", tableName, strings.Join(fields, ", "), strings.Join(values, ", ")), params, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func BuildInsertMultipleStatement[TData any](q Queryable, tableName string, vArr []TData) (string, PP, error) { | ||||||
|  |  | ||||||
|  | 	if len(vArr) == 0 { | ||||||
|  | 		return "", nil, errors.New("no data supplied") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	rtyp := reflect.ValueOf(vArr[0]).Type() | ||||||
|  |  | ||||||
|  | 	sqlPrefix := "" | ||||||
|  | 	{ | ||||||
|  | 		columns := make([]string, 0) | ||||||
|  |  | ||||||
|  | 		for i := 0; i < rtyp.NumField(); i++ { | ||||||
|  | 			rsfield := rtyp.Field(i) | ||||||
|  |  | ||||||
|  | 			if !rsfield.IsExported() { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			columnName := rsfield.Tag.Get("db") | ||||||
|  | 			if columnName == "" || columnName == "-" { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			columns = append(columns, "\""+columnName+"\"") | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		sqlPrefix = fmt.Sprintf("INSERT"+" INTO \"%s\" (%s) VALUES", tableName, strings.Join(columns, ", ")) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	pp := PP{} | ||||||
|  |  | ||||||
|  | 	sqlValuesArr := make([]string, 0) | ||||||
|  |  | ||||||
|  | 	for _, v := range vArr { | ||||||
|  |  | ||||||
|  | 		rval := reflect.ValueOf(v) | ||||||
|  |  | ||||||
|  | 		params := make([]string, 0) | ||||||
|  |  | ||||||
|  | 		for i := 0; i < rtyp.NumField(); i++ { | ||||||
|  |  | ||||||
|  | 			rsfield := rtyp.Field(i) | ||||||
|  | 			rvfield := rval.Field(i) | ||||||
|  |  | ||||||
|  | 			if !rsfield.IsExported() { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			columnName := rsfield.Tag.Get("db") | ||||||
|  | 			if columnName == "" || columnName == "-" { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if rsfield.Type.Kind() == reflect.Ptr && rvfield.IsNil() { | ||||||
|  |  | ||||||
|  | 				params = append(params, "NULL") | ||||||
|  |  | ||||||
|  | 			} else { | ||||||
|  |  | ||||||
|  | 				val, err := convertValueToDB(q, rvfield.Interface()) | ||||||
|  | 				if err != nil { | ||||||
|  | 					return "", nil, err | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				params = append(params, ":"+pp.Add(val)) | ||||||
|  |  | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		sqlValuesArr = append(sqlValuesArr, fmt.Sprintf("(%s)", strings.Join(params, ", "))) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	sqlstr := fmt.Sprintf("%s %s", sqlPrefix, strings.Join(sqlValuesArr, ", ")) | ||||||
|  |  | ||||||
|  | 	return sqlstr, pp, nil | ||||||
| } | } | ||||||
|   | |||||||
| @@ -18,9 +18,9 @@ type DBTypeConverter interface { | |||||||
| 	DBToModel(v any) (any, error) | 	DBToModel(v any) (any, error) | ||||||
| } | } | ||||||
|  |  | ||||||
| var ConverterBoolToBit = NewDBTypeConverter[bool, int](func(v bool) (int, error) { | var ConverterBoolToBit = NewDBTypeConverter[bool, int64](func(v bool) (int64, error) { | ||||||
| 	return langext.Conditional(v, 1, 0), nil | 	return langext.Conditional(v, int64(1), int64(0)), nil | ||||||
| }, func(v int) (bool, error) { | }, func(v int64) (bool, error) { | ||||||
| 	if v == 0 { | 	if v == 0 { | ||||||
| 		return false, nil | 		return false, nil | ||||||
| 	} | 	} | ||||||
|   | |||||||
							
								
								
									
										49
									
								
								sq/filter.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								sq/filter.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | |||||||
|  | package sq | ||||||
|  |  | ||||||
|  | import ct "gogs.mikescher.com/BlackForestBytes/goext/cursortoken" | ||||||
|  |  | ||||||
|  | type FilterSort struct { | ||||||
|  | 	Field     string | ||||||
|  | 	Direction ct.SortDirection | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type PaginateFilter interface { | ||||||
|  | 	SQL(params PP) (filterClause string, joinClause string, joinTables []string) | ||||||
|  | 	Sort() []FilterSort | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type genericPaginateFilter struct { | ||||||
|  | 	sql  func(params PP) (filterClause string, joinClause string, joinTables []string) | ||||||
|  | 	sort func() []FilterSort | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (g genericPaginateFilter) SQL(params PP) (filterClause string, joinClause string, joinTables []string) { | ||||||
|  | 	return g.sql(params) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (g genericPaginateFilter) Sort() []FilterSort { | ||||||
|  | 	return g.sort() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func NewPaginateFilter(sql func(params PP) (filterClause string, joinClause string, joinTables []string), sort []FilterSort) PaginateFilter { | ||||||
|  | 	return genericPaginateFilter{ | ||||||
|  | 		sql: func(params PP) (filterClause string, joinClause string, joinTables []string) { | ||||||
|  | 			return sql(params) | ||||||
|  | 		}, | ||||||
|  | 		sort: func() []FilterSort { | ||||||
|  | 			return sort | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func NewSimplePaginateFilter(filterClause string, filterParams PP, sort []FilterSort) PaginateFilter { | ||||||
|  | 	return genericPaginateFilter{ | ||||||
|  | 		sql: func(params PP) (string, string, []string) { | ||||||
|  | 			params.AddAll(filterParams) | ||||||
|  | 			return filterClause, "", nil | ||||||
|  | 		}, | ||||||
|  | 		sort: func() []FilterSort { | ||||||
|  | 			return sort | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										15
									
								
								sq/main_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								sq/main_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | package sq | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"gogs.mikescher.com/BlackForestBytes/goext/exerr" | ||||||
|  | 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||||
|  | 	"os" | ||||||
|  | 	"testing" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestMain(m *testing.M) { | ||||||
|  | 	if !exerr.Initialized() { | ||||||
|  | 		exerr.Init(exerr.ErrorPackageConfigInit{ZeroLogErrTraces: langext.PFalse, ZeroLogAllTraces: langext.PFalse}) | ||||||
|  | 	} | ||||||
|  | 	os.Exit(m.Run()) | ||||||
|  | } | ||||||
| @@ -3,22 +3,11 @@ package sq | |||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	ct "gogs.mikescher.com/BlackForestBytes/goext/cursortoken" |  | ||||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/exerr" | 	"gogs.mikescher.com/BlackForestBytes/goext/exerr" | ||||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||||
| 	pag "gogs.mikescher.com/BlackForestBytes/goext/pagination" | 	pag "gogs.mikescher.com/BlackForestBytes/goext/pagination" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type PaginateFilter interface { |  | ||||||
| 	SQL(params PP) (filterClause string, joinClause string, joinTables []string) |  | ||||||
| 	Sort() []FilterSort |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type FilterSort struct { |  | ||||||
| 	Field     string |  | ||||||
| 	Direction ct.SortDirection |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func Paginate[TData any](ctx context.Context, q Queryable, table string, filter PaginateFilter, scanMode StructScanMode, scanSec StructScanSafety, page int, limit *int) ([]TData, pag.Pagination, error) { | func Paginate[TData any](ctx context.Context, q Queryable, table string, filter PaginateFilter, scanMode StructScanMode, scanSec StructScanSafety, page int, limit *int) ([]TData, pag.Pagination, error) { | ||||||
| 	prepParams := PP{} | 	prepParams := PP{} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -20,6 +20,12 @@ func (pp *PP) Add(v any) string { | |||||||
| 	return id | 	return id | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (pp *PP) AddAll(other PP) { | ||||||
|  | 	for id, v := range other { | ||||||
|  | 		(*pp)[id] = v | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| func PPID() string { | func PPID() string { | ||||||
| 	return "p_" + langext.RandBase62(8) | 	return "p_" + langext.RandBase62(8) | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										179
									
								
								sq/scanner.go
									
									
									
									
									
								
							
							
						
						
									
										179
									
								
								sq/scanner.go
									
									
									
									
									
								
							| @@ -6,8 +6,9 @@ import ( | |||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"github.com/jmoiron/sqlx" | 	"github.com/jmoiron/sqlx" | ||||||
|  | 	"gogs.mikescher.com/BlackForestBytes/goext/exerr" | ||||||
|  | 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||||
| 	"reflect" | 	"reflect" | ||||||
| 	"strings" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type StructScanMode string | type StructScanMode string | ||||||
| @@ -26,43 +27,11 @@ const ( | |||||||
|  |  | ||||||
| func InsertSingle[TData any](ctx context.Context, q Queryable, tableName string, v TData) (sql.Result, error) { | func InsertSingle[TData any](ctx context.Context, q Queryable, tableName string, v TData) (sql.Result, error) { | ||||||
|  |  | ||||||
| 	rval := reflect.ValueOf(v) | 	sqlstr, pp, err := BuildInsertStatement(q, tableName, v) | ||||||
| 	rtyp := rval.Type() | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
| 	columns := make([]string, 0) |  | ||||||
| 	params := make([]string, 0) |  | ||||||
| 	pp := PP{} |  | ||||||
|  |  | ||||||
| 	for i := 0; i < rtyp.NumField(); i++ { |  | ||||||
|  |  | ||||||
| 		rsfield := rtyp.Field(i) |  | ||||||
| 		rvfield := rval.Field(i) |  | ||||||
|  |  | ||||||
| 		if !rsfield.IsExported() { |  | ||||||
| 			continue |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		columnName := rsfield.Tag.Get("db") |  | ||||||
| 		if columnName == "" || columnName == "-" { |  | ||||||
| 			continue |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		paramkey := fmt.Sprintf("_%s", columnName) |  | ||||||
|  |  | ||||||
| 		columns = append(columns, "\""+columnName+"\"") |  | ||||||
| 		params = append(params, ":"+paramkey) |  | ||||||
|  |  | ||||||
| 		val, err := convertValueToDB(q, rvfield.Interface()) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, err |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		pp[paramkey] = val |  | ||||||
|  |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	sqlstr := fmt.Sprintf("INSERT"+" INTO \"%s\" (%s) VALUES (%s)", tableName, strings.Join(columns, ", "), strings.Join(params, ", ")) |  | ||||||
|  |  | ||||||
| 	sqlr, err := q.Exec(ctx, sqlstr, pp) | 	sqlr, err := q.Exec(ctx, sqlstr, pp) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| @@ -71,6 +40,127 @@ func InsertSingle[TData any](ctx context.Context, q Queryable, tableName string, | |||||||
| 	return sqlr, nil | 	return sqlr, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func InsertAndQuerySingle[TData any](ctx context.Context, q Queryable, tableName string, v TData, idColumn string, mode StructScanMode, sec StructScanSafety) (TData, error) { | ||||||
|  |  | ||||||
|  | 	rval := reflect.ValueOf(v) | ||||||
|  |  | ||||||
|  | 	idRVal := fieldByTag(rval, "db", idColumn) | ||||||
|  | 	if !idRVal.IsValid() || idRVal.IsZero() { | ||||||
|  | 		return *new(TData), fmt.Errorf("failed to find idColumn '%s' in %T", idColumn, v) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	idValue, err := convertValueToDB(q, idRVal.Interface()) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return *new(TData), err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	_, err = InsertSingle[TData](ctx, q, tableName, v) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return *new(TData), err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	pp := PP{} | ||||||
|  |  | ||||||
|  | 	//goland:noinspection ALL | ||||||
|  | 	sqlstr := fmt.Sprintf("SELECT * FROM %s WHERE %s = :%s", tableName, idColumn, pp.Add(idValue)) | ||||||
|  |  | ||||||
|  | 	return QuerySingle[TData](ctx, q, sqlstr, pp, mode, sec) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func fieldByTag(rval reflect.Value, tagkey string, tagval string) reflect.Value { | ||||||
|  | 	rtyp := rval.Type() | ||||||
|  | 	for i := 0; i < rtyp.NumField(); i++ { | ||||||
|  | 		rsfield := rtyp.Field(i) | ||||||
|  |  | ||||||
|  | 		if !rsfield.IsExported() { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if rsfield.Tag.Get(tagkey) == tagval { | ||||||
|  | 			return rval.Field(i) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	panic(fmt.Sprintf("tag %s = '%s' not found in %s", tagkey, tagval, rtyp.Name())) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func InsertMultiple[TData any](ctx context.Context, q Queryable, tableName string, vArr []TData, maxBatch int) ([]sql.Result, error) { | ||||||
|  |  | ||||||
|  | 	if len(vArr) == 0 { | ||||||
|  | 		return make([]sql.Result, 0), nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	chunks := langext.ArrChunk(vArr, maxBatch) | ||||||
|  |  | ||||||
|  | 	sqlstrArr := make([]string, 0) | ||||||
|  | 	ppArr := make([]PP, 0) | ||||||
|  |  | ||||||
|  | 	for _, chunk := range chunks { | ||||||
|  |  | ||||||
|  | 		sqlstr, pp, err := BuildInsertMultipleStatement(q, tableName, chunk) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, exerr.Wrap(err, "").Build() | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		sqlstrArr = append(sqlstrArr, sqlstr) | ||||||
|  | 		ppArr = append(ppArr, pp) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	res := make([]sql.Result, 0, len(sqlstrArr)) | ||||||
|  |  | ||||||
|  | 	for i := 0; i < len(sqlstrArr); i++ { | ||||||
|  | 		sqlr, err := q.Exec(ctx, sqlstrArr[i], ppArr[i]) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		res = append(res, sqlr) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return res, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func UpdateSingle[TData any](ctx context.Context, q Queryable, tableName string, v TData, idColumn string) (sql.Result, error) { | ||||||
|  |  | ||||||
|  | 	sqlstr, pp, err := BuildUpdateStatement(q, tableName, v, idColumn) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	sqlr, err := q.Exec(ctx, sqlstr, pp) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return sqlr, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func UpdateAndQuerySingle[TData any](ctx context.Context, q Queryable, tableName string, v TData, idColumn string, mode StructScanMode, sec StructScanSafety) (TData, error) { | ||||||
|  |  | ||||||
|  | 	rval := reflect.ValueOf(v) | ||||||
|  |  | ||||||
|  | 	idRVal := fieldByTag(rval, "db", idColumn) | ||||||
|  | 	if !idRVal.IsValid() || idRVal.IsZero() { | ||||||
|  | 		return *new(TData), fmt.Errorf("failed to find idColumn '%s' in %T", idColumn, v) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	idValue, err := convertValueToDB(q, idRVal.Interface()) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return *new(TData), err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	_, err = UpdateSingle[TData](ctx, q, tableName, v, idColumn) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return *new(TData), err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	pp := PP{} | ||||||
|  |  | ||||||
|  | 	//goland:noinspection ALL | ||||||
|  | 	sqlstr := fmt.Sprintf("SELECT * FROM %s WHERE %s = :%s", tableName, idColumn, pp.Add(idValue)) | ||||||
|  |  | ||||||
|  | 	return QuerySingle[TData](ctx, q, sqlstr, pp, mode, sec) | ||||||
|  | } | ||||||
|  |  | ||||||
| func QuerySingle[TData any](ctx context.Context, q Queryable, sql string, pp PP, mode StructScanMode, sec StructScanSafety) (TData, error) { | func QuerySingle[TData any](ctx context.Context, q Queryable, sql string, pp PP, mode StructScanMode, sec StructScanSafety) (TData, error) { | ||||||
| 	rows, err := q.Query(ctx, sql, pp) | 	rows, err := q.Query(ctx, sql, pp) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -85,6 +175,23 @@ func QuerySingle[TData any](ctx context.Context, q Queryable, sql string, pp PP, | |||||||
| 	return data, nil | 	return data, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func QuerySingleOpt[TData any](ctx context.Context, q Queryable, sqlstr string, pp PP, mode StructScanMode, sec StructScanSafety) (*TData, error) { | ||||||
|  | 	rows, err := q.Query(ctx, sqlstr, pp) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	data, err := ScanSingle[TData](ctx, q, rows, mode, sec, true) | ||||||
|  | 	if errors.Is(err, sql.ErrNoRows) { | ||||||
|  | 		return nil, nil | ||||||
|  | 	} | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return &data, nil | ||||||
|  | } | ||||||
|  |  | ||||||
| func QueryAll[TData any](ctx context.Context, q Queryable, sql string, pp PP, mode StructScanMode, sec StructScanSafety) ([]TData, error) { | func QueryAll[TData any](ctx context.Context, q Queryable, sql string, pp PP, mode StructScanMode, sec StructScanSafety) ([]TData, error) { | ||||||
| 	rows, err := q.Query(ctx, sql, pp) | 	rows, err := q.Query(ctx, sql, pp) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|   | |||||||
							
								
								
									
										237
									
								
								sq/scanner_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										237
									
								
								sq/scanner_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,237 @@ | |||||||
|  | package sq | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"database/sql" | ||||||
|  | 	"fmt" | ||||||
|  | 	"github.com/glebarez/go-sqlite" | ||||||
|  | 	"github.com/jmoiron/sqlx" | ||||||
|  | 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||||
|  | 	"gogs.mikescher.com/BlackForestBytes/goext/tst" | ||||||
|  | 	"path/filepath" | ||||||
|  | 	"testing" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestInsertSingle(t *testing.T) { | ||||||
|  |  | ||||||
|  | 	type request struct { | ||||||
|  | 		ID        string  `json:"id"        db:"id"` | ||||||
|  | 		Timestamp int     `json:"timestamp" db:"timestamp"` | ||||||
|  | 		StrVal    string  `json:"strVal"    db:"str_val"` | ||||||
|  | 		FloatVal  float64 `json:"floatVal"    db:"float_val"` | ||||||
|  | 		Dummy     bool    `json:"dummyBool" db:"dummy_bool"` | ||||||
|  | 		JsonVal   JsonObj `json:"jsonVal" db:"json_val"` | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if !langext.InArray("sqlite3", sql.Drivers()) { | ||||||
|  | 		sqlite.RegisterAsSQLITE3() | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ctx := context.Background() | ||||||
|  |  | ||||||
|  | 	dbdir := t.TempDir() | ||||||
|  | 	dbfile1 := filepath.Join(dbdir, langext.MustHexUUID()+".sqlite3") | ||||||
|  |  | ||||||
|  | 	url := fmt.Sprintf("file:%s?_pragma=journal_mode(%s)&_pragma=timeout(%d)&_pragma=foreign_keys(%s)&_pragma=busy_timeout(%d)", dbfile1, "DELETE", 1000, "true", 1000) | ||||||
|  |  | ||||||
|  | 	xdb := tst.Must(sqlx.Open("sqlite", url))(t) | ||||||
|  |  | ||||||
|  | 	db := NewDB(xdb) | ||||||
|  | 	db.RegisterDefaultConverter() | ||||||
|  |  | ||||||
|  | 	_, err := db.Exec(ctx, ` | ||||||
|  | 		CREATE TABLE requests (  | ||||||
|  | 		    id TEXT NOT NULL,  | ||||||
|  | 		    timestamp INTEGER NOT NULL,  | ||||||
|  | 		    str_val TEXT NOT NULL,  | ||||||
|  | 		    float_val REAL NOT NULL,  | ||||||
|  | 		    dummy_bool INTEGER NOT NULL  CHECK(dummy_bool IN (0, 1)),  | ||||||
|  | 		    json_val TEXT NOT NULL,  | ||||||
|  | 		    PRIMARY KEY (id)  | ||||||
|  | 		) STRICT | ||||||
|  | `, PP{}) | ||||||
|  | 	tst.AssertNoErr(t, err) | ||||||
|  |  | ||||||
|  | 	_, err = InsertSingle(ctx, db, "requests", request{ | ||||||
|  | 		ID:        "9927", | ||||||
|  | 		Timestamp: 12321, | ||||||
|  | 		StrVal:    "hello world", | ||||||
|  | 		Dummy:     true, | ||||||
|  | 		FloatVal:  3.14159, | ||||||
|  | 		JsonVal: JsonObj{ | ||||||
|  | 			"firs":   1, | ||||||
|  | 			"second": true, | ||||||
|  | 		}, | ||||||
|  | 	}) | ||||||
|  | 	tst.AssertNoErr(t, err) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestUpdateSingle(t *testing.T) { | ||||||
|  |  | ||||||
|  | 	type request struct { | ||||||
|  | 		ID        string  `json:"id"        db:"id"` | ||||||
|  | 		Timestamp int     `json:"timestamp" db:"timestamp"` | ||||||
|  | 		StrVal    string  `json:"strVal"    db:"str_val"` | ||||||
|  | 		FloatVal  float64 `json:"floatVal"    db:"float_val"` | ||||||
|  | 		Dummy     bool    `json:"dummyBool" db:"dummy_bool"` | ||||||
|  | 		JsonVal   JsonObj `json:"jsonVal" db:"json_val"` | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if !langext.InArray("sqlite3", sql.Drivers()) { | ||||||
|  | 		sqlite.RegisterAsSQLITE3() | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ctx := context.Background() | ||||||
|  |  | ||||||
|  | 	dbdir := t.TempDir() | ||||||
|  | 	dbfile1 := filepath.Join(dbdir, langext.MustHexUUID()+".sqlite3") | ||||||
|  |  | ||||||
|  | 	url := fmt.Sprintf("file:%s?_pragma=journal_mode(%s)&_pragma=timeout(%d)&_pragma=foreign_keys(%s)&_pragma=busy_timeout(%d)", dbfile1, "DELETE", 1000, "true", 1000) | ||||||
|  |  | ||||||
|  | 	xdb := tst.Must(sqlx.Open("sqlite", url))(t) | ||||||
|  |  | ||||||
|  | 	db := NewDB(xdb) | ||||||
|  | 	db.RegisterDefaultConverter() | ||||||
|  |  | ||||||
|  | 	_, err := db.Exec(ctx, ` | ||||||
|  | 		CREATE TABLE requests (  | ||||||
|  | 		    id TEXT NOT NULL,  | ||||||
|  | 		    timestamp INTEGER NOT NULL,  | ||||||
|  | 		    str_val TEXT NOT NULL,  | ||||||
|  | 		    float_val REAL NOT NULL,  | ||||||
|  | 		    dummy_bool INTEGER NOT NULL  CHECK(dummy_bool IN (0, 1)),  | ||||||
|  | 		    json_val TEXT NOT NULL,  | ||||||
|  | 		    PRIMARY KEY (id)  | ||||||
|  | 		) STRICT | ||||||
|  | `, PP{}) | ||||||
|  | 	tst.AssertNoErr(t, err) | ||||||
|  |  | ||||||
|  | 	_, err = InsertSingle(ctx, db, "requests", request{ | ||||||
|  | 		ID:        "9927", | ||||||
|  | 		Timestamp: 12321, | ||||||
|  | 		StrVal:    "hello world", | ||||||
|  | 		Dummy:     true, | ||||||
|  | 		FloatVal:  3.14159, | ||||||
|  | 		JsonVal: JsonObj{ | ||||||
|  | 			"first":  1, | ||||||
|  | 			"second": true, | ||||||
|  | 		}, | ||||||
|  | 	}) | ||||||
|  | 	tst.AssertNoErr(t, err) | ||||||
|  |  | ||||||
|  | 	v, err := QuerySingle[request](ctx, db, "SELECT * FROM requests WHERE id = '9927' LIMIT 1", PP{}, SModeExtended, Safe) | ||||||
|  | 	tst.AssertNoErr(t, err) | ||||||
|  |  | ||||||
|  | 	tst.AssertEqual(t, v.Timestamp, 12321) | ||||||
|  | 	tst.AssertEqual(t, v.StrVal, "hello world") | ||||||
|  | 	tst.AssertEqual(t, v.Dummy, true) | ||||||
|  | 	tst.AssertEqual(t, v.FloatVal, 3.14159) | ||||||
|  | 	tst.AssertStrRepEqual(t, v.JsonVal["first"], 1) | ||||||
|  | 	tst.AssertStrRepEqual(t, v.JsonVal["second"], true) | ||||||
|  |  | ||||||
|  | 	_, err = UpdateSingle(ctx, db, "requests", request{ | ||||||
|  | 		ID:        "9927", | ||||||
|  | 		Timestamp: 9999, | ||||||
|  | 		StrVal:    "9999 hello world", | ||||||
|  | 		Dummy:     false, | ||||||
|  | 		FloatVal:  123.222, | ||||||
|  | 		JsonVal: JsonObj{ | ||||||
|  | 			"first":  2, | ||||||
|  | 			"second": false, | ||||||
|  | 		}, | ||||||
|  | 	}, "id") | ||||||
|  |  | ||||||
|  | 	v, err = QuerySingle[request](ctx, db, "SELECT * FROM requests WHERE id = '9927' LIMIT 1", PP{}, SModeExtended, Safe) | ||||||
|  | 	tst.AssertNoErr(t, err) | ||||||
|  |  | ||||||
|  | 	tst.AssertEqual(t, v.Timestamp, 9999) | ||||||
|  | 	tst.AssertEqual(t, v.StrVal, "9999 hello world") | ||||||
|  | 	tst.AssertEqual(t, v.Dummy, false) | ||||||
|  | 	tst.AssertEqual(t, v.FloatVal, 123.222) | ||||||
|  | 	tst.AssertStrRepEqual(t, v.JsonVal["first"], 2) | ||||||
|  | 	tst.AssertStrRepEqual(t, v.JsonVal["second"], false) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestInsertMultiple(t *testing.T) { | ||||||
|  |  | ||||||
|  | 	type request struct { | ||||||
|  | 		ID        string  `json:"id"        db:"id"` | ||||||
|  | 		Timestamp int     `json:"timestamp" db:"timestamp"` | ||||||
|  | 		StrVal    string  `json:"strVal"    db:"str_val"` | ||||||
|  | 		FloatVal  float64 `json:"floatVal"    db:"float_val"` | ||||||
|  | 		Dummy     bool    `json:"dummyBool" db:"dummy_bool"` | ||||||
|  | 		JsonVal   JsonObj `json:"jsonVal" db:"json_val"` | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if !langext.InArray("sqlite3", sql.Drivers()) { | ||||||
|  | 		sqlite.RegisterAsSQLITE3() | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ctx := context.Background() | ||||||
|  |  | ||||||
|  | 	dbdir := t.TempDir() | ||||||
|  | 	dbfile1 := filepath.Join(dbdir, langext.MustHexUUID()+".sqlite3") | ||||||
|  |  | ||||||
|  | 	url := fmt.Sprintf("file:%s?_pragma=journal_mode(%s)&_pragma=timeout(%d)&_pragma=foreign_keys(%s)&_pragma=busy_timeout(%d)", dbfile1, "DELETE", 1000, "true", 1000) | ||||||
|  |  | ||||||
|  | 	xdb := tst.Must(sqlx.Open("sqlite", url))(t) | ||||||
|  |  | ||||||
|  | 	db := NewDB(xdb) | ||||||
|  | 	db.RegisterDefaultConverter() | ||||||
|  |  | ||||||
|  | 	_, err := db.Exec(ctx, ` | ||||||
|  | 		CREATE TABLE requests (  | ||||||
|  | 		    id TEXT NOT NULL,  | ||||||
|  | 		    timestamp INTEGER NOT NULL,  | ||||||
|  | 		    str_val TEXT NOT NULL,  | ||||||
|  | 		    float_val REAL NOT NULL,  | ||||||
|  | 		    dummy_bool INTEGER NOT NULL  CHECK(dummy_bool IN (0, 1)),  | ||||||
|  | 		    json_val TEXT NOT NULL,  | ||||||
|  | 		    PRIMARY KEY (id)  | ||||||
|  | 		) STRICT | ||||||
|  | `, PP{}) | ||||||
|  | 	tst.AssertNoErr(t, err) | ||||||
|  |  | ||||||
|  | 	_, err = InsertMultiple(ctx, db, "requests", []request{ | ||||||
|  | 		{ | ||||||
|  | 			ID:        "1", | ||||||
|  | 			Timestamp: 1000, | ||||||
|  | 			StrVal:    "one", | ||||||
|  | 			Dummy:     true, | ||||||
|  | 			FloatVal:  0.1, | ||||||
|  | 			JsonVal: JsonObj{ | ||||||
|  | 				"arr": []int{0}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			ID:        "2", | ||||||
|  | 			Timestamp: 2000, | ||||||
|  | 			StrVal:    "two", | ||||||
|  | 			Dummy:     true, | ||||||
|  | 			FloatVal:  0.2, | ||||||
|  | 			JsonVal: JsonObj{ | ||||||
|  | 				"arr": []int{0, 0}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			ID:        "3", | ||||||
|  | 			Timestamp: 3000, | ||||||
|  | 			StrVal:    "three", | ||||||
|  | 			Dummy:     true, | ||||||
|  | 			FloatVal:  0.3, | ||||||
|  | 			JsonVal: JsonObj{ | ||||||
|  | 				"arr": []int{0, 0, 0}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	}, -1) | ||||||
|  | 	tst.AssertNoErr(t, err) | ||||||
|  |  | ||||||
|  | 	_, err = QuerySingle[request](ctx, db, "SELECT * FROM requests WHERE id = '1' LIMIT 1", PP{}, SModeExtended, Safe) | ||||||
|  | 	tst.AssertNoErr(t, err) | ||||||
|  |  | ||||||
|  | 	_, err = QuerySingle[request](ctx, db, "SELECT * FROM requests WHERE id = '2' LIMIT 1", PP{}, SModeExtended, Safe) | ||||||
|  | 	tst.AssertNoErr(t, err) | ||||||
|  |  | ||||||
|  | 	_, err = QuerySingle[request](ctx, db, "SELECT * FROM requests WHERE id = '3' LIMIT 1", PP{}, SModeExtended, Safe) | ||||||
|  | 	tst.AssertNoErr(t, err) | ||||||
|  | } | ||||||
| @@ -2,6 +2,7 @@ package tst | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"encoding/hex" | 	"encoding/hex" | ||||||
|  | 	"fmt" | ||||||
| 	"reflect" | 	"reflect" | ||||||
| 	"runtime/debug" | 	"runtime/debug" | ||||||
| 	"testing" | 	"testing" | ||||||
| @@ -125,3 +126,17 @@ func AssertNoErr(t *testing.T, anerr error) { | |||||||
| 		t.Error("Function returned an error: " + anerr.Error() + "\n" + string(debug.Stack())) | 		t.Error("Function returned an error: " + anerr.Error() + "\n" + string(debug.Stack())) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func AssertStrRepEqual(t *testing.T, actual any, expected any) { | ||||||
|  | 	t.Helper() | ||||||
|  | 	if fmt.Sprintf("%v", actual) != fmt.Sprintf("%v", expected) { | ||||||
|  | 		t.Errorf("values differ: Actual: '%v', Expected: '%v'", actual, expected) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func AssertStrRepNotEqual(t *testing.T, actual any, expected any) { | ||||||
|  | 	t.Helper() | ||||||
|  | 	if fmt.Sprintf("%v", actual) == fmt.Sprintf("%v", expected) { | ||||||
|  | 		t.Errorf("values do not differ: Actual: '%v', Expected: '%v'", actual, expected) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user