diff --git a/util/sqlgen/benchmark_test.go b/util/sqlgen/benchmark_test.go index f04428ae3945f06677e044d7934a67d938906b84..3cc2e25f2052bf6705902427aaf5cc419cbd3bf6 100644 --- a/util/sqlgen/benchmark_test.go +++ b/util/sqlgen/benchmark_test.go @@ -1,6 +1,8 @@ package sqlgen import ( + "fmt" + "math/rand" "testing" ) @@ -47,8 +49,29 @@ func BenchmarkTable(b *testing.B) { } func BenchmarkCompileTable(b *testing.B) { + var t string for i := 0; i < b.N; i++ { - _ = Table{"foo"}.Compile(defaultTemplate) + t = Table{"foo"}.Compile(defaultTemplate) + if t != `"foo"` { + b.Fatal("Caching failed.") + } + } +} + +func BenchmarkCompileRandomTable(b *testing.B) { + var t string + var m, n int + var s, e string + + for i := 0; i < b.N; i++ { + m, n = rand.Int(), rand.Int() + s = fmt.Sprintf(`%s as %s`, m, n) + e = fmt.Sprintf(`"%s" AS "%s"`, m, n) + + t = Table{s}.Compile(defaultTemplate) + if t != e { + b.Fatal() + } } } diff --git a/util/sqlgen/default.go b/util/sqlgen/default.go index 0c855a6acb4ca5e742c1ba11d50f33c7874611dc..9038db0fa9cd0eb9097c815ebc7f26edee4391f8 100644 --- a/util/sqlgen/default.go +++ b/util/sqlgen/default.go @@ -142,4 +142,5 @@ var defaultTemplate = &Template{ defaultSelectCountLayout, defaultGroupByLayout, nil, + nil, } diff --git a/util/sqlgen/table.go b/util/sqlgen/table.go index 50331770a9b0601e16c489f39fb3463e4a8dcf01..141c4dc2052c2b34d0b369b9077c41f419edba02 100644 --- a/util/sqlgen/table.go +++ b/util/sqlgen/table.go @@ -21,12 +21,14 @@ type Table struct { } func quotedTableName(layout *Template, input string) string { - input = strings.TrimSpace(input) + input = trimString(input) - chunks := reAliasSeparator.Split(input, 2) + // chunks := reAliasSeparator.Split(input, 2) + chunks := separateByAS(input) if len(chunks) == 1 { - chunks = reSpaceSeparator.Split(input, 2) + // chunks = reSpaceSeparator.Split(input, 2) + chunks = separateBySpace(input) } name := chunks[0] @@ -34,7 +36,8 @@ func quotedTableName(layout *Template, input string) string { nameChunks := strings.SplitN(name, layout.ColumnSeparator, 2) for i := range nameChunks { - nameChunks[i] = strings.TrimSpace(nameChunks[i]) + // nameChunks[i] = strings.TrimSpace(nameChunks[i]) + nameChunks[i] = trimString(nameChunks[i]) nameChunks[i] = mustParse(layout.IdentifierQuote, Raw{nameChunks[i]}) } @@ -43,7 +46,8 @@ func quotedTableName(layout *Template, input string) string { var alias string if len(chunks) > 1 { - alias = strings.TrimSpace(chunks[1]) + // alias = strings.TrimSpace(chunks[1]) + alias = trimString(chunks[1]) alias = mustParse(layout.IdentifierQuote, Raw{alias}) } @@ -56,16 +60,29 @@ func (self Table) Hash() string { func (self Table) Compile(layout *Template) (compiled string) { - // Splitting tables by a comma - parts := reTableSeparator.Split(self.Name, -1) + if self.Name == "" { + return + } - l := len(parts) + if layout.isCached(self) { - for i := 0; i < l; i++ { - parts[i] = quotedTableName(layout, parts[i]) - } + compiled = layout.getCache(self) + + } else { + + // Splitting tables by a comma + parts := separateByComma(self.Name) - compiled = strings.Join(parts, layout.IdentifierSeparator) + l := len(parts) + + for i := 0; i < l; i++ { + parts[i] = quotedTableName(layout, parts[i]) + } + + compiled = strings.Join(parts, layout.IdentifierSeparator) + + layout.setCache(self, compiled) + } - return compiled + return } diff --git a/util/sqlgen/table_test.go b/util/sqlgen/table_test.go index d6a88c736d740570ab763702ad8427e43f04d99e..dbae8be34841b8255a383c519fbd32d8b0444f43 100644 --- a/util/sqlgen/table_test.go +++ b/util/sqlgen/table_test.go @@ -87,3 +87,31 @@ func TestTableMultipleAlias(t *testing.T) { t.Fatalf("Got: %s, Expecting: %s", s, e) } } + +func TestTableMinimal(t *testing.T) { + var s, e string + var table Table + + table = Table{"a"} + + s = trim(table.Compile(defaultTemplate)) + e = `"a"` + + if s != e { + t.Fatalf("Got: %s, Expecting: %s", s, e) + } +} + +func TestTableEmpty(t *testing.T) { + var s, e string + var table Table + + table = Table{""} + + s = trim(table.Compile(defaultTemplate)) + e = `` + + if s != e { + t.Fatalf("Got: %s, Expecting: %s", s, e) + } +} diff --git a/util/sqlgen/template.go b/util/sqlgen/template.go index ac399094ff8860fb0562651ea3d7566f3c783c7d..7502e27f73b0b80bf92916d98d4241ceac3d31c4 100644 --- a/util/sqlgen/template.go +++ b/util/sqlgen/template.go @@ -30,6 +30,11 @@ type Template struct { SelectCountLayout string GroupByLayout string cache map[interface{}]string + cachedTemplates map[string]string +} + +type cacheable interface { + Hash() string } func (self *Template) SetCache(key interface{}, value string) { @@ -47,3 +52,27 @@ func (self *Template) Cache(key interface{}) (string, bool) { } return "", false } + +func (self *Template) getCache(i cacheable) string { + if self.cachedTemplates == nil { + return "" + } + return self.cachedTemplates[i.Hash()] +} + +func (self *Template) setCache(i cacheable, s string) { + if self.cachedTemplates == nil { + self.cachedTemplates = map[string]string{} + } + self.cachedTemplates[i.Hash()] = s +} + +func (self *Template) isCached(i cacheable) bool { + if self.cachedTemplates == nil { + return false + } + if _, ok := self.cachedTemplates[i.Hash()]; ok { + return true + } + return false +} diff --git a/util/sqlgen/utilities.go b/util/sqlgen/utilities.go new file mode 100644 index 0000000000000000000000000000000000000000..40bfe1e04ab17d458df54f2b5eac3f68f69e1f29 --- /dev/null +++ b/util/sqlgen/utilities.go @@ -0,0 +1,149 @@ +package sqlgen + +import ( + "strings" +) + +const ( + stageExpect = iota + stageCapture + stageClose +) + +func isSpace(in byte) bool { + return in == ' ' || in == '\t' || in == '\r' || in == '\n' +} + +func trimString(in string) string { + + start, end := 0, len(in)-1 + + // Where do we start cutting? + for ; start <= end; start++ { + if isSpace(in[start]) == false { + break + } + } + + // Where do we end cutting? + for ; end >= start; end-- { + if isSpace(in[end]) == false { + break + } + } + + return in[start : end+1] +} + +func trimByte(in []byte) []byte { + + start, end := 0, len(in)-1 + + // Where do we start cutting? + for ; start <= end; start++ { + if isSpace(in[start]) == false { + break + } + } + + // Where do we end cutting? + for ; end >= start; end-- { + if isSpace(in[end]) == false { + break + } + } + + return in[start : end+1] +} + +/* +// Separates by a comma, ignoring spaces too. +// This was slower than strings.Split. +func separateByComma(in string) (out []string) { + + out = []string{} + + start, lim := 0, len(in)-1 + + for start < lim { + var end int + + for end = start; end <= lim; end++ { + // Is a comma? + if in[end] == ',' { + break + } + } + + out = append(out, trimString(in[start:end])) + + start = end + 1 + } + + return +} +*/ + +// Separates by a comma, ignoring spaces too. +func separateByComma(in string) (out []string) { + out = strings.Split(in, ",") + for i := range out { + out[i] = trimString(out[i]) + } + return +} + +// Separates by spaces, ignoring spaces too. +func separateBySpace(in string) (out []string) { + l := len(in) + + if l == 0 { + return []string{""} + } + + out = make([]string, 0, l) + + pre := strings.Split(in, " ") + + for i := range pre { + pre[i] = trimString(pre[i]) + if pre[i] != "" { + out = append(out, pre[i]) + } + } + + return +} + +func separateByAS(in string) (out []string) { + out = []string{} + + if len(in) < 6 { + // Min expression: "a AS b" + return []string{in} + } + + start, lim := 0, len(in)-1 + + for start <= lim { + var end int + + for end = start; end <= lim; end++ { + if end > 3 && isSpace(in[end]) && isSpace(in[end-3]) { + if (in[end-1] == 's' || in[end-1] == 'S') && (in[end-2] == 'a' || in[end-2] == 'A') { + break + } + } + } + + if end < lim { + out = append(out, trimString(in[start:end-3])) + } else { + out = append(out, trimString(in[start:end])) + } + + start = end + 1 + } + + return +} diff --git a/util/sqlgen/utilities_test.go b/util/sqlgen/utilities_test.go new file mode 100644 index 0000000000000000000000000000000000000000..87cff171c458d7b80a817a5c58d1d9398dfba091 --- /dev/null +++ b/util/sqlgen/utilities_test.go @@ -0,0 +1,271 @@ +package sqlgen + +import ( + "bytes" + "regexp" + "strings" + "testing" +) + +func TestUtilIsSpace(t *testing.T) { + if isSpace(' ') == false { + t.Fail() + } + if isSpace('\n') == false { + t.Fail() + } + if isSpace('\t') == false { + t.Fail() + } + if isSpace('\r') == false { + t.Fail() + } + if isSpace('x') == true { + t.Fail() + } +} + +func TestUtilTrimByte(t *testing.T) { + var trimmed []byte + + trimmed = trimByte([]byte(" \t\nHello World! \n")) + if string(trimmed) != "Hello World!" { + t.Fatalf("Got: %s\n", string(trimmed)) + } + + trimmed = trimByte([]byte("Nope")) + if string(trimmed) != "Nope" { + t.Fatalf("Got: %s\n", string(trimmed)) + } + + trimmed = trimByte([]byte("")) + if string(trimmed) != "" { + t.Fatalf("Got: %s\n", string(trimmed)) + } + + trimmed = trimByte(nil) + if string(trimmed) != "" { + t.Fatalf("Got: %s\n", string(trimmed)) + } +} + +func TestUtilSeparateByComma(t *testing.T) { + chunks := separateByComma("Hello,,World!,Enjoy") + + if len(chunks) != 4 { + t.Fatal() + } + + if chunks[0] != "Hello" { + t.Fatal() + } + if chunks[1] != "" { + t.Fatal() + } + if chunks[2] != "World!" { + t.Fatal() + } + if chunks[3] != "Enjoy" { + t.Fatal() + } +} + +func TestUtilSeparateBySpace(t *testing.T) { + chunks := separateBySpace(" Hello World! Enjoy") + + if len(chunks) != 3 { + t.Fatal() + } + + if chunks[0] != "Hello" { + t.Fatal() + } + if chunks[1] != "World!" { + t.Fatal() + } + if chunks[2] != "Enjoy" { + t.Fatal() + } +} + +func TestUtilSeparateByAS(t *testing.T) { + var chunks []string + + var tests = []string{ + `table.Name AS myTableAlias`, + `table.Name AS myTableAlias`, + "table.Name\tAS\r\nmyTableAlias", + } + + for _, test := range tests { + chunks = separateByAS(test) + + if len(chunks) != 2 { + t.Fatalf(`Expecting 2 results.`) + } + + if chunks[0] != "table.Name" { + t.Fatal(`Expecting first result to be "table.Name".`) + } + if chunks[1] != "myTableAlias" { + t.Fatal(`Expecting second result to be myTableAlias.`) + } + } + + // Single character. + chunks = separateByAS("a") + + if len(chunks) != 1 { + t.Fatalf(`Expecting 1 results.`) + } + + if chunks[0] != "a" { + t.Fatal(`Expecting first result to be "a".`) + } + + // Empty name + chunks = separateByAS("") + + if len(chunks) != 1 { + t.Fatalf(`Expecting 1 results.`) + } + + if chunks[0] != "" { + t.Fatal(`Expecting first result to be "".`) + } + + // Single name + chunks = separateByAS(" A Single Table ") + + if len(chunks) != 1 { + t.Fatalf(`Expecting 1 results.`) + } + + if chunks[0] != "A Single Table" { + t.Fatal(`Expecting first result to be "ASingleTable".`) + } + + // Minimal expression. + chunks = separateByAS("a AS b") + + if len(chunks) != 2 { + t.Fatalf(`Expecting 2 results.`) + } + + if chunks[0] != "a" { + t.Fatal(`Expecting first result to be "a".`) + } + + if chunks[1] != "b" { + t.Fatal(`Expecting first result to be "b".`) + } + + // Minimal expression with spaces. + chunks = separateByAS(" a AS b ") + + if len(chunks) != 2 { + t.Fatalf(`Expecting 2 results.`) + } + + if chunks[0] != "a" { + t.Fatal(`Expecting first result to be "a".`) + } + + if chunks[1] != "b" { + t.Fatal(`Expecting first result to be "b".`) + } + + // Minimal expression + 1 with spaces. + chunks = separateByAS(" a AS bb ") + + if len(chunks) != 2 { + t.Fatalf(`Expecting 2 results.`) + } + + if chunks[0] != "a" { + t.Fatal(`Expecting first result to be "a".`) + } + + if chunks[1] != "bb" { + t.Fatal(`Expecting first result to be "bb".`) + } +} + +func BenchmarkUtilIsSpace(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = isSpace(' ') + } +} + +func BenchmarkUtilTrimByte(b *testing.B) { + s := []byte(" Hello world! ") + for i := 0; i < b.N; i++ { + _ = trimByte(s) + } +} + +func BenchmarkUtilTrimString(b *testing.B) { + s := " Hello world! " + for i := 0; i < b.N; i++ { + _ = trimString(s) + } +} + +func BenchmarkUtilStdBytesTrimSpace(b *testing.B) { + s := []byte(" Hello world! ") + for i := 0; i < b.N; i++ { + _ = bytes.TrimSpace(s) + } +} + +func BenchmarkUtilStdStringsTrimSpace(b *testing.B) { + s := " Hello world! " + for i := 0; i < b.N; i++ { + _ = strings.TrimSpace(s) + } +} + +func BenchmarkUtilSeparateByComma(b *testing.B) { + s := "Hello,,World!,Enjoy" + for i := 0; i < b.N; i++ { + _ = separateByComma(s) + } +} + +func BenchmarkUtilSeparateBySpace(b *testing.B) { + s := " Hello World! Enjoy" + for i := 0; i < b.N; i++ { + _ = separateBySpace(s) + } +} + +func BenchmarkUtilSeparateByAS(b *testing.B) { + s := "table.Name AS myTableAlias" + for i := 0; i < b.N; i++ { + _ = separateByAS(s) + } +} + +func BenchmarkUtilSeparateByCommaRegExp(b *testing.B) { + sep := regexp.MustCompile(`\s*?,\s*?`) + s := "Hello,,World!,Enjoy" + for i := 0; i < b.N; i++ { + _ = sep.Split(s, -1) + } +} + +func BenchmarkUtilSeparateBySpaceRegExp(b *testing.B) { + sep := regexp.MustCompile(`\s+`) + s := " Hello World! Enjoy" + for i := 0; i < b.N; i++ { + _ = sep.Split(s, -1) + } +} + +func BenchmarkUtilSeparateByASRegExp(b *testing.B) { + sep := regexp.MustCompile(`(?i:\s+AS\s+)`) + s := "table.Name AS myTableAlias" + for i := 0; i < b.N; i++ { + _ = sep.Split(s, -1) + } +}