Skip to content

Commit

Permalink
Merge pull request #653 from upper/refactor-memory-and-speed-optimiza…
Browse files Browse the repository at this point in the history
…tions

Memory and speed optimizations
xiam authored Aug 30, 2022
2 parents 82a1771 + ba90d64 commit 8a3fe0c
Showing 54 changed files with 1,177 additions and 2,123 deletions.
2 changes: 1 addition & 1 deletion adapter/cockroachdb/database.go
Original file line number Diff line number Diff line change
@@ -129,7 +129,7 @@ func (*database) CompileStatement(sess sqladapter.Session, stmt *exql.Statement,
}

query, args := sqlbuilder.Preprocess(compiled, args)
query = sqladapter.ReplaceWithDollarSign(query)
query = string(sqladapter.ReplaceWithDollarSign([]byte(query)))
return query, args, nil
}

2 changes: 1 addition & 1 deletion adapter/postgresql/database.go
Original file line number Diff line number Diff line change
@@ -99,7 +99,7 @@ func (*database) CompileStatement(sess sqladapter.Session, stmt *exql.Statement,
}

query, args := sqlbuilder.Preprocess(compiled, args)
query = sqladapter.ReplaceWithDollarSign(query)
query = string(sqladapter.ReplaceWithDollarSign([]byte(query)))
return query, args, nil
}

2 changes: 1 addition & 1 deletion adapter/ql/database.go
Original file line number Diff line number Diff line change
@@ -81,7 +81,7 @@ func (*database) CompileStatement(sess sqladapter.Session, stmt *exql.Statement,
}

query, args := sqlbuilder.Preprocess(compiled, args)
query = sqladapter.ReplaceWithDollarSign(query)
query = string(sqladapter.ReplaceWithDollarSign([]byte(query)))
return query, args, nil
}

2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -11,6 +11,8 @@ require (
github.com/jackc/pgx/v4 v4.15.0
github.com/lib/pq v1.10.4
github.com/mattn/go-sqlite3 v1.14.9
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/segmentio/fasthash v1.0.3
github.com/sirupsen/logrus v1.8.1
github.com/stretchr/testify v1.7.0
golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70 // indirect
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -100,6 +100,8 @@ github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA=
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -112,6 +114,8 @@ github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/segmentio/fasthash v1.0.3 h1:EI9+KE1EwvMLBWwjpRDc+fEM+prwxDYbslddQGtrmhM=
github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
86 changes: 34 additions & 52 deletions internal/cache/cache.go
Original file line number Diff line number Diff line change
@@ -24,25 +24,21 @@ package cache
import (
"container/list"
"errors"
"fmt"
"strconv"
"sync"

"github.com/upper/db/v4/internal/cache/hashstructure"
)

const defaultCapacity = 128

// Cache holds a map of volatile key -> values.
type Cache struct {
cache map[string]*list.Element
li *list.List
capacity int
keys *list.List
items map[uint64]*list.Element
mu sync.RWMutex
capacity int
}

type item struct {
key string
type cacheItem struct {
key uint64
value interface{}
}

@@ -52,11 +48,11 @@ func NewCacheWithCapacity(capacity int) (*Cache, error) {
if capacity < 1 {
return nil, errors.New("Capacity must be greater than zero.")
}
return &Cache{
cache: make(map[string]*list.Element),
li: list.New(),
c := &Cache{
capacity: capacity,
}, nil
}
c.init()
return c, nil
}

// NewCache initializes a new caching space with default settings.
@@ -68,6 +64,11 @@ func NewCache() *Cache {
return c
}

func (c *Cache) init() {
c.items = make(map[uint64]*list.Element)
c.keys = list.New()
}

// Read attempts to retrieve a cached value as a string, if the value does not
// exists returns an empty string and false.
func (c *Cache) Read(h Hashable) (string, bool) {
@@ -84,33 +85,35 @@ func (c *Cache) Read(h Hashable) (string, bool) {
func (c *Cache) ReadRaw(h Hashable) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
data, ok := c.cache[h.Hash()]

item, ok := c.items[h.Hash()]
if ok {
return data.Value.(*item).value, true
return item.Value.(*cacheItem).value, true
}

return nil, false
}

// Write stores a value in memory. If the value already exists its overwritten.
func (c *Cache) Write(h Hashable, value interface{}) {
key := h.Hash()

c.mu.Lock()
defer c.mu.Unlock()

if el, ok := c.cache[key]; ok {
el.Value.(*item).value = value
c.li.MoveToFront(el)
key := h.Hash()

if item, ok := c.items[key]; ok {
item.Value.(*cacheItem).value = value
c.keys.MoveToFront(item)
return
}

c.cache[key] = c.li.PushFront(&item{key, value})
c.items[key] = c.keys.PushFront(&cacheItem{key, value})

for c.li.Len() > c.capacity {
el := c.li.Remove(c.li.Back())
delete(c.cache, el.(*item).key)
if p, ok := el.(*item).value.(HasOnPurge); ok {
p.OnPurge()
for c.keys.Len() > c.capacity {
item := c.keys.Remove(c.keys.Back()).(*cacheItem)
delete(c.items, item.key)
if p, ok := item.value.(HasOnEvict); ok {
p.OnEvict()
}
}
}
@@ -120,33 +123,12 @@ func (c *Cache) Write(h Hashable, value interface{}) {
func (c *Cache) Clear() {
c.mu.Lock()
defer c.mu.Unlock()
for _, el := range c.cache {
if p, ok := el.Value.(*item).value.(HasOnPurge); ok {
p.OnPurge()
}
}
c.cache = make(map[string]*list.Element)
c.li.Init()
}

// Hash returns a hash of the given struct.
func Hash(v interface{}) string {
q, err := hashstructure.Hash(v, nil)
if err != nil {
panic(fmt.Sprintf("Could not hash struct: %v", err.Error()))
for _, item := range c.items {
if p, ok := item.Value.(*cacheItem).value.(HasOnEvict); ok {
p.OnEvict()
}
}
return strconv.FormatUint(q, 10)
}

type hash struct {
name string
}

func (h *hash) Hash() string {
return h.name
}

// String returns a Hashable that produces a hash equal to the given string.
func String(s string) Hashable {
return &hash{s}
c.init()
}
14 changes: 12 additions & 2 deletions internal/cache/cache_test.go
Original file line number Diff line number Diff line change
@@ -23,6 +23,7 @@ package cache

import (
"fmt"
"hash/fnv"
"testing"
)

@@ -32,8 +33,10 @@ type cacheableT struct {
Name string
}

func (ct *cacheableT) Hash() string {
return Hash(ct)
func (ct *cacheableT) Hash() uint64 {
s := fnv.New64()
s.Sum([]byte(ct.Name))
return s.Sum64()
}

var (
@@ -77,6 +80,13 @@ func BenchmarkNewCache(b *testing.B) {
}
}

func BenchmarkNewCacheAndClear(b *testing.B) {
for i := 0; i < b.N; i++ {
c := NewCache()
c.Clear()
}
}

func BenchmarkReadNonExistentValue(b *testing.B) {
z := NewCache()
for i := 0; i < b.N; i++ {
109 changes: 109 additions & 0 deletions internal/cache/hash.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package cache

import (
"fmt"

"github.com/segmentio/fasthash/fnv1a"
)

const (
hashTypeInt uint64 = 1 << iota
hashTypeSignedInt
hashTypeBool
hashTypeString
hashTypeHashable
hashTypeNil
)

type hasher struct {
t uint64
v interface{}
}

func (h *hasher) Hash() uint64 {
return NewHash(h.t, h.v)
}

func NewHashable(t uint64, v interface{}) Hashable {
return &hasher{t: t, v: v}
}

func InitHash(t uint64) uint64 {
return fnv1a.AddUint64(fnv1a.Init64, t)
}

func NewHash(t uint64, in ...interface{}) uint64 {
return AddToHash(InitHash(t), in...)
}

func AddToHash(h uint64, in ...interface{}) uint64 {
for i := range in {
if in[i] == nil {
continue
}
h = addToHash(h, in[i])
}
return h
}

func addToHash(h uint64, in interface{}) uint64 {
switch v := in.(type) {
case uint64:
return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeInt), v)
case uint32:
return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeInt), uint64(v))
case uint16:
return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeInt), uint64(v))
case uint8:
return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeInt), uint64(v))
case uint:
return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeInt), uint64(v))
case int64:
if v < 0 {
return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeSignedInt), uint64(-v))
} else {
return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeInt), uint64(v))
}
case int32:
if v < 0 {
return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeSignedInt), uint64(-v))
} else {
return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeInt), uint64(v))
}
case int16:
if v < 0 {
return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeSignedInt), uint64(-v))
} else {
return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeInt), uint64(v))
}
case int8:
if v < 0 {
return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeSignedInt), uint64(-v))
} else {
return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeInt), uint64(v))
}
case int:
if v < 0 {
return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeSignedInt), uint64(-v))
} else {
return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeInt), uint64(v))
}
case bool:
if v {
return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeBool), 1)
} else {
return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeBool), 2)
}
case string:
return fnv1a.AddString64(fnv1a.AddUint64(h, hashTypeString), v)
case Hashable:
if in == nil {
panic(fmt.Sprintf("could not hash nil element %T", in))
}
return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeHashable), v.Hash())
case nil:
return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeNil), 0)
default:
panic(fmt.Sprintf("unsupported value type %T", in))
}
}
21 changes: 0 additions & 21 deletions internal/cache/hashstructure/LICENSE

This file was deleted.

61 changes: 0 additions & 61 deletions internal/cache/hashstructure/README.md

This file was deleted.

325 changes: 0 additions & 325 deletions internal/cache/hashstructure/hashstructure.go

This file was deleted.

357 changes: 0 additions & 357 deletions internal/cache/hashstructure/hashstructure_test.go

This file was deleted.

15 changes: 0 additions & 15 deletions internal/cache/hashstructure/include.go

This file was deleted.

8 changes: 4 additions & 4 deletions internal/cache/interface.go
Original file line number Diff line number Diff line change
@@ -24,11 +24,11 @@ package cache
// Hashable types must implement a method that returns a key. This key will be
// associated with a cached value.
type Hashable interface {
Hash() string
Hash() uint64
}

// HasOnPurge type is (optionally) implemented by cache objects to clean after
// HasOnEvict type is (optionally) implemented by cache objects to clean after
// themselves.
type HasOnPurge interface {
OnPurge()
type HasOnEvict interface {
OnEvict()
}
38 changes: 20 additions & 18 deletions internal/sqladapter/exql/column.go
Original file line number Diff line number Diff line change
@@ -3,18 +3,18 @@ package exql
import (
"fmt"
"strings"

"github.com/upper/db/v4/internal/cache"
)

type columnT struct {
type columnWithAlias struct {
Name string
Alias string
}

// Column represents a SQL column.
type Column struct {
Name interface{}
Alias string
hash hash
Name interface{}
}

var _ = Fragment(&Column{})
@@ -25,8 +25,11 @@ func ColumnWithName(name string) *Column {
}

// Hash returns a unique identifier for the struct.
func (c *Column) Hash() string {
return c.hash.Hash(c)
func (c *Column) Hash() uint64 {
if c == nil {
return cache.NewHash(FragmentType_Column, nil)
}
return cache.NewHash(FragmentType_Column, c.Name)
}

// Compile transforms the ColumnValue into an equivalent SQL representation.
@@ -35,20 +38,17 @@ func (c *Column) Compile(layout *Template) (compiled string, err error) {
return z, nil
}

alias := c.Alias

var alias string
switch value := c.Name.(type) {
case string:
input := trimString(value)

chunks := separateByAS(input)
value = trimString(value)

chunks := separateByAS(value)
if len(chunks) == 1 {
chunks = separateBySpace(input)
chunks = separateBySpace(value)
}

name := chunks[0]

nameChunks := strings.SplitN(name, layout.ColumnSeparator, 2)

for i := range nameChunks {
@@ -65,17 +65,19 @@ func (c *Column) Compile(layout *Template) (compiled string, err error) {
alias = trimString(chunks[1])
alias = layout.MustCompile(layout.IdentifierQuote, Raw{Value: alias})
}
case Raw:
compiled = value.String()
case compilable:
compiled, err = value.Compile(layout)
if err != nil {
return "", err
}
default:
compiled = fmt.Sprintf("%v", c.Name)
return "", fmt.Errorf(errExpectingHashableFmt, c.Name)
}

if alias != "" {
compiled = layout.MustCompile(layout.ColumnAliasLayout, columnT{compiled, alias})
compiled = layout.MustCompile(layout.ColumnAliasLayout, columnWithAlias{compiled, alias})
}

layout.Write(c, compiled)

return
}
67 changes: 16 additions & 51 deletions internal/sqladapter/exql/column_test.go
Original file line number Diff line number Diff line change
@@ -2,76 +2,36 @@ package exql

import (
"testing"
)

func TestColumnHash(t *testing.T) {
var s, e string

column := Column{Name: "role.name"}

s = column.Hash()
e = "*exql.Column:5663680925324531495"

if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}
}
"github.com/stretchr/testify/assert"
)

func TestColumnString(t *testing.T) {

column := Column{Name: "role.name"}

s, err := column.Compile(defaultTemplate)
if err != nil {
t.Fatal()
}

e := `"role"."name"`
if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}
assert.NoError(t, err)
assert.Equal(t, `"role"."name"`, s)
}

func TestColumnAs(t *testing.T) {
column := Column{Name: "role.name as foo"}

s, err := column.Compile(defaultTemplate)
if err != nil {
t.Fatal()
}

e := `"role"."name" AS "foo"`
if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}
assert.NoError(t, err)
assert.Equal(t, `"role"."name" AS "foo"`, s)
}

func TestColumnImplicitAs(t *testing.T) {
column := Column{Name: "role.name foo"}

s, err := column.Compile(defaultTemplate)
if err != nil {
t.Fatal()
}

e := `"role"."name" AS "foo"`
if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}
assert.NoError(t, err)
assert.Equal(t, `"role"."name" AS "foo"`, s)
}

func TestColumnRaw(t *testing.T) {
column := Column{Name: Raw{Value: "role.name As foo"}}

column := Column{Name: &Raw{Value: "role.name As foo"}}
s, err := column.Compile(defaultTemplate)
if err != nil {
t.Fatal()
}

e := `role.name As foo`
if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}
assert.NoError(t, err)
assert.Equal(t, `role.name As foo`, s)
}

func BenchmarkColumnWithName(b *testing.B) {
@@ -82,13 +42,15 @@ func BenchmarkColumnWithName(b *testing.B) {

func BenchmarkColumnHash(b *testing.B) {
c := Column{Name: "name"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
c.Hash()
}
}

func BenchmarkColumnCompile(b *testing.B) {
c := Column{Name: "name"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = c.Compile(defaultTemplate)
}
@@ -103,20 +65,23 @@ func BenchmarkColumnCompileNoCache(b *testing.B) {

func BenchmarkColumnWithDotCompile(b *testing.B) {
c := Column{Name: "role.name"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = c.Compile(defaultTemplate)
}
}

func BenchmarkColumnWithImplicitAsKeywordCompile(b *testing.B) {
c := Column{Name: "role.name foo"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = c.Compile(defaultTemplate)
}
}

func BenchmarkColumnWithAsKeywordCompile(b *testing.B) {
c := Column{Name: "role.name AS foo"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = c.Compile(defaultTemplate)
}
19 changes: 12 additions & 7 deletions internal/sqladapter/exql/column_value.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package exql

import (
"github.com/upper/db/v4/internal/cache"
"strings"
)

@@ -9,7 +10,6 @@ type ColumnValue struct {
Column Fragment
Operator string
Value Fragment
hash hash
}

var _ = Fragment(&ColumnValue{})
@@ -21,8 +21,11 @@ type columnValueT struct {
}

// Hash returns a unique identifier for the struct.
func (c *ColumnValue) Hash() string {
return c.hash.Hash(c)
func (c *ColumnValue) Hash() uint64 {
if c == nil {
return cache.NewHash(FragmentType_ColumnValue, nil)
}
return cache.NewHash(FragmentType_ColumnValue, c.Column, c.Operator, c.Value)
}

// Compile transforms the ColumnValue into an equivalent SQL representation.
@@ -58,7 +61,6 @@ func (c *ColumnValue) Compile(layout *Template) (compiled string, err error) {
// ColumnValues represents an array of ColumnValue
type ColumnValues struct {
ColumnValues []Fragment
hash hash
}

var _ = Fragment(&ColumnValues{})
@@ -71,13 +73,16 @@ func JoinColumnValues(values ...Fragment) *ColumnValues {
// Insert adds a column to the columns array.
func (c *ColumnValues) Insert(values ...Fragment) *ColumnValues {
c.ColumnValues = append(c.ColumnValues, values...)
c.hash.Reset()
return c
}

// Hash returns a unique identifier for the struct.
func (c *ColumnValues) Hash() string {
return c.hash.Hash(c)
func (c *ColumnValues) Hash() uint64 {
h := cache.InitHash(FragmentType_ColumnValues)
for i := range c.ColumnValues {
h = cache.AddToHash(h, c.ColumnValues[i])
}
return h
}

// Compile transforms the ColumnValues into its SQL representation.
87 changes: 22 additions & 65 deletions internal/sqladapter/exql/column_value_test.go
Original file line number Diff line number Diff line change
@@ -2,61 +2,20 @@ package exql

import (
"testing"
)

func TestColumnValueHash(t *testing.T) {
var s, e string

c := &ColumnValue{Column: ColumnWithName("id"), Operator: "=", Value: NewValue(1)}

s = c.Hash()
e = `*exql.ColumnValue:4950005282640920683`

if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}
}

func TestColumnValuesHash(t *testing.T) {
var s, e string

c := JoinColumnValues(
&ColumnValue{Column: ColumnWithName("id"), Operator: "=", Value: NewValue(1)},
&ColumnValue{Column: ColumnWithName("id"), Operator: "=", Value: NewValue(2)},
)

s = c.Hash()
e = `*exql.ColumnValues:8728513848368010747`

if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}
}
"github.com/stretchr/testify/assert"
)

func TestColumnValue(t *testing.T) {
cv := &ColumnValue{Column: ColumnWithName("id"), Operator: "=", Value: NewValue(1)}

s, err := cv.Compile(defaultTemplate)
if err != nil {
t.Fatal()
}

e := `"id" = '1'`
if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}

cv = &ColumnValue{Column: ColumnWithName("date"), Operator: "=", Value: NewValue(RawValue("NOW()"))}
assert.NoError(t, err)
assert.Equal(t, `"id" = '1'`, s)

cv = &ColumnValue{Column: ColumnWithName("date"), Operator: "=", Value: &Raw{Value: "NOW()"}}
s, err = cv.Compile(defaultTemplate)
if err != nil {
t.Fatal()
}

e = `"date" = NOW()`
if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}
assert.NoError(t, err)
assert.Equal(t, `"date" = NOW()`, s)
}

func TestColumnValues(t *testing.T) {
@@ -69,14 +28,8 @@ func TestColumnValues(t *testing.T) {
)

s, err := cvs.Compile(defaultTemplate)
if err != nil {
t.Fatal()
}

e := `"id" > '8', "other"."id" < 100, "name" = 'Haruki Murakami', "created" >= NOW(), "modified" <= NOW()`
if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}
assert.NoError(t, err)
assert.Equal(t, `"id" > '8', "other"."id" < 100, "name" = 'Haruki Murakami', "created" >= NOW(), "modified" <= NOW()`, s)
}

func BenchmarkNewColumnValue(b *testing.B) {
@@ -87,13 +40,15 @@ func BenchmarkNewColumnValue(b *testing.B) {

func BenchmarkColumnValueHash(b *testing.B) {
cv := &ColumnValue{Column: ColumnWithName("id"), Operator: "=", Value: NewValue(1)}
b.ResetTimer()
for i := 0; i < b.N; i++ {
cv.Hash()
}
}

func BenchmarkColumnValueCompile(b *testing.B) {
cv := &ColumnValue{Column: ColumnWithName("id"), Operator: "=", Value: NewValue(1)}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = cv.Compile(defaultTemplate)
}
@@ -121,11 +76,12 @@ func BenchmarkJoinColumnValues(b *testing.B) {
func BenchmarkColumnValuesHash(b *testing.B) {
cvs := JoinColumnValues(
&ColumnValue{Column: ColumnWithName("id"), Operator: ">", Value: NewValue(8)},
&ColumnValue{Column: ColumnWithName("other.id"), Operator: "<", Value: NewValue(Raw{Value: "100"})},
&ColumnValue{Column: ColumnWithName("other.id"), Operator: "<", Value: NewValue(&Raw{Value: "100"})},
&ColumnValue{Column: ColumnWithName("name"), Operator: "=", Value: NewValue("Haruki Murakami")},
&ColumnValue{Column: ColumnWithName("created"), Operator: ">=", Value: NewValue(Raw{Value: "NOW()"})},
&ColumnValue{Column: ColumnWithName("modified"), Operator: "<=", Value: NewValue(Raw{Value: "NOW()"})},
&ColumnValue{Column: ColumnWithName("created"), Operator: ">=", Value: NewValue(&Raw{Value: "NOW()"})},
&ColumnValue{Column: ColumnWithName("modified"), Operator: "<=", Value: NewValue(&Raw{Value: "NOW()"})},
)
b.ResetTimer()
for i := 0; i < b.N; i++ {
cvs.Hash()
}
@@ -134,11 +90,12 @@ func BenchmarkColumnValuesHash(b *testing.B) {
func BenchmarkColumnValuesCompile(b *testing.B) {
cvs := JoinColumnValues(
&ColumnValue{Column: ColumnWithName("id"), Operator: ">", Value: NewValue(8)},
&ColumnValue{Column: ColumnWithName("other.id"), Operator: "<", Value: NewValue(Raw{Value: "100"})},
&ColumnValue{Column: ColumnWithName("other.id"), Operator: "<", Value: NewValue(&Raw{Value: "100"})},
&ColumnValue{Column: ColumnWithName("name"), Operator: "=", Value: NewValue("Haruki Murakami")},
&ColumnValue{Column: ColumnWithName("created"), Operator: ">=", Value: NewValue(Raw{Value: "NOW()"})},
&ColumnValue{Column: ColumnWithName("modified"), Operator: "<=", Value: NewValue(Raw{Value: "NOW()"})},
&ColumnValue{Column: ColumnWithName("created"), Operator: ">=", Value: NewValue(&Raw{Value: "NOW()"})},
&ColumnValue{Column: ColumnWithName("modified"), Operator: "<=", Value: NewValue(&Raw{Value: "NOW()"})},
)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = cvs.Compile(defaultTemplate)
}
@@ -148,10 +105,10 @@ func BenchmarkColumnValuesCompileNoCache(b *testing.B) {
for i := 0; i < b.N; i++ {
cvs := JoinColumnValues(
&ColumnValue{Column: ColumnWithName("id"), Operator: ">", Value: NewValue(8)},
&ColumnValue{Column: ColumnWithName("other.id"), Operator: "<", Value: NewValue(Raw{Value: "100"})},
&ColumnValue{Column: ColumnWithName("other.id"), Operator: "<", Value: NewValue(&Raw{Value: "100"})},
&ColumnValue{Column: ColumnWithName("name"), Operator: "=", Value: NewValue("Haruki Murakami")},
&ColumnValue{Column: ColumnWithName("created"), Operator: ">=", Value: NewValue(Raw{Value: "NOW()"})},
&ColumnValue{Column: ColumnWithName("modified"), Operator: "<=", Value: NewValue(Raw{Value: "NOW()"})},
&ColumnValue{Column: ColumnWithName("created"), Operator: ">=", Value: NewValue(&Raw{Value: "NOW()"})},
&ColumnValue{Column: ColumnWithName("modified"), Operator: "<=", Value: NewValue(&Raw{Value: "NOW()"})},
)
_, _ = cvs.Compile(defaultTemplate)
}
15 changes: 11 additions & 4 deletions internal/sqladapter/exql/columns.go
Original file line number Diff line number Diff line change
@@ -2,19 +2,27 @@ package exql

import (
"strings"

"github.com/upper/db/v4/internal/cache"
)

// Columns represents an array of Column.
type Columns struct {
Columns []Fragment
hash hash
}

var _ = Fragment(&Columns{})

// Hash returns a unique identifier.
func (c *Columns) Hash() string {
return c.hash.Hash(c)
func (c *Columns) Hash() uint64 {
if c == nil {
return cache.NewHash(FragmentType_Columns, nil)
}
h := cache.InitHash(FragmentType_Columns)
for i := range c.Columns {
h = cache.AddToHash(h, c.Columns[i])
}
return h
}

// JoinColumns creates and returns an array of Column.
@@ -48,7 +56,6 @@ func (c *Columns) IsEmpty() bool {

// Compile transforms the Columns into an equivalent SQL representation.
func (c *Columns) Compile(layout *Template) (compiled string, err error) {

if z, ok := layout.Read(c); ok {
return z, nil
}
14 changes: 6 additions & 8 deletions internal/sqladapter/exql/columns_test.go
Original file line number Diff line number Diff line change
@@ -2,6 +2,8 @@ package exql

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestColumns(t *testing.T) {
@@ -14,14 +16,8 @@ func TestColumns(t *testing.T) {
)

s, err := columns.Compile(defaultTemplate)
if err != nil {
t.Fatal()
}

e := `"id", "customer", "service_id", "role"."name", "role"."id"`
if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}
assert.NoError(t, err)
assert.Equal(t, `"id", "customer", "service_id", "role"."name", "role"."id"`, s)
}

func BenchmarkJoinColumns(b *testing.B) {
@@ -42,6 +38,7 @@ func BenchmarkColumnsHash(b *testing.B) {
&Column{Name: "role.name"},
&Column{Name: "role.id"},
)
b.ResetTimer()
for i := 0; i < b.N; i++ {
c.Hash()
}
@@ -55,6 +52,7 @@ func BenchmarkColumnsCompile(b *testing.B) {
&Column{Name: "role.name"},
&Column{Name: "role.id"},
)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = c.Compile(defaultTemplate)
}
12 changes: 9 additions & 3 deletions internal/sqladapter/exql/database.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package exql

import (
"github.com/upper/db/v4/internal/cache"
)

// Database represents a SQL database.
type Database struct {
Name string
hash hash
}

var _ = Fragment(&Database{})
@@ -14,8 +17,11 @@ func DatabaseWithName(name string) *Database {
}

// Hash returns a unique identifier for the struct.
func (d *Database) Hash() string {
return d.hash.Hash(d)
func (d *Database) Hash() uint64 {
if d == nil {
return cache.NewHash(FragmentType_Database, nil)
}
return cache.NewHash(FragmentType_Database, d.Name)
}

// Compile transforms the Database into an equivalent SQL representation.
32 changes: 8 additions & 24 deletions internal/sqladapter/exql/database_test.go
Original file line number Diff line number Diff line change
@@ -1,46 +1,30 @@
package exql

import (
"fmt"
"strconv"
"testing"
)

func TestDatabaseHash(t *testing.T) {
var s, e string

column := Database{Name: "users"}

s = column.Hash()
e = `*exql.Database:16777957551305673389`

if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}
}
"github.com/stretchr/testify/assert"
)

func TestDatabaseCompile(t *testing.T) {
column := Database{Name: "name"}

s, err := column.Compile(defaultTemplate)
if err != nil {
t.Fatal()
}

e := `"name"`
if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}
assert.NoError(t, err)
assert.Equal(t, `"name"`, s)
}

func BenchmarkDatabaseHash(b *testing.B) {
c := Database{Name: "name"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
c.Hash()
}
}

func BenchmarkDatabaseCompile(b *testing.B) {
c := Database{Name: "name"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = c.Compile(defaultTemplate)
}
@@ -55,7 +39,7 @@ func BenchmarkDatabaseCompileNoCache(b *testing.B) {

func BenchmarkDatabaseCompileNoCache2(b *testing.B) {
for i := 0; i < b.N; i++ {
c := Database{Name: fmt.Sprintf("name: %v", i)}
c := Database{Name: strconv.Itoa(i)}
_, _ = c.Compile(defaultTemplate)
}
}
5 changes: 5 additions & 0 deletions internal/sqladapter/exql/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package exql

const (
errExpectingHashableFmt = "expecting hashable value, got %T"
)
12 changes: 9 additions & 3 deletions internal/sqladapter/exql/group_by.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package exql

import (
"github.com/upper/db/v4/internal/cache"
)

// GroupBy represents a SQL's "group by" statement.
type GroupBy struct {
Columns Fragment
hash hash
}

var _ = Fragment(&GroupBy{})
@@ -13,8 +16,11 @@ type groupByT struct {
}

// Hash returns a unique identifier.
func (g *GroupBy) Hash() string {
return g.hash.Hash(g)
func (g *GroupBy) Hash() uint64 {
if g == nil {
return cache.NewHash(FragmentType_GroupBy, nil)
}
return cache.NewHash(FragmentType_GroupBy, g.Columns)
}

// GroupByColumns creates and returns a GroupBy with the given column.
9 changes: 5 additions & 4 deletions internal/sqladapter/exql/group_by_test.go
Original file line number Diff line number Diff line change
@@ -2,6 +2,8 @@ package exql

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestGroupBy(t *testing.T) {
@@ -14,10 +16,7 @@ func TestGroupBy(t *testing.T) {
)

s := mustTrim(columns.Compile(defaultTemplate))
e := `GROUP BY "id", "customer", "service_id", "role"."name", "role"."id"`
if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}
assert.Equal(t, `GROUP BY "id", "customer", "service_id", "role"."name", "role"."id"`, s)
}

func BenchmarkGroupByColumns(b *testing.B) {
@@ -38,6 +37,7 @@ func BenchmarkGroupByHash(b *testing.B) {
&Column{Name: "role.name"},
&Column{Name: "role.id"},
)
b.ResetTimer()
for i := 0; i < b.N; i++ {
c.Hash()
}
@@ -51,6 +51,7 @@ func BenchmarkGroupByCompile(b *testing.B) {
&Column{Name: "role.name"},
&Column{Name: "role.id"},
)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = c.Compile(defaultTemplate)
}
26 changes: 0 additions & 26 deletions internal/sqladapter/exql/hash.go

This file was deleted.

38 changes: 26 additions & 12 deletions internal/sqladapter/exql/join.go
Original file line number Diff line number Diff line change
@@ -2,6 +2,8 @@ package exql

import (
"strings"

"github.com/upper/db/v4/internal/cache"
)

type innerJoinT struct {
@@ -14,14 +16,20 @@ type innerJoinT struct {
// Joins represents the union of different join conditions.
type Joins struct {
Conditions []Fragment
hash hash
}

var _ = Fragment(&Joins{})

// Hash returns a unique identifier for the struct.
func (j *Joins) Hash() string {
return j.hash.Hash(j)
func (j *Joins) Hash() uint64 {
if j == nil {
return cache.NewHash(FragmentType_Joins, nil)
}
h := cache.InitHash(FragmentType_Joins)
for i := range j.Conditions {
h = cache.AddToHash(h, j.Conditions[i])
}
return h
}

// Compile transforms the Where into an equivalent SQL representation.
@@ -66,14 +74,16 @@ type Join struct {
Table Fragment
On Fragment
Using Fragment
hash hash
}

var _ = Fragment(&Join{})

// Hash returns a unique identifier for the struct.
func (j *Join) Hash() string {
return j.hash.Hash(j)
func (j *Join) Hash() uint64 {
if j == nil {
return cache.NewHash(FragmentType_Join, nil)
}
return cache.NewHash(FragmentType_Join, j.Type, j.Table, j.On, j.Using)
}

// Compile transforms the Join into its equivalent SQL representation.
@@ -118,9 +128,11 @@ type On Where

var _ = Fragment(&On{})

// Hash returns a unique identifier.
func (o *On) Hash() string {
return o.hash.Hash(o)
func (o *On) Hash() uint64 {
if o == nil {
return cache.NewHash(FragmentType_On, nil)
}
return cache.NewHash(FragmentType_On, (*Where)(o))
}

// Compile transforms the On into an equivalent SQL representation.
@@ -151,9 +163,11 @@ type usingT struct {
Columns string
}

// Hash returns a unique identifier.
func (u *Using) Hash() string {
return u.hash.Hash(u)
func (u *Using) Hash() uint64 {
if u == nil {
return cache.NewHash(FragmentType_Using, nil)
}
return cache.NewHash(FragmentType_Using, (*Columns)(u))
}

// Compile transforms the Using into an equivalent SQL representation.
93 changes: 21 additions & 72 deletions internal/sqladapter/exql/join_test.go
Original file line number Diff line number Diff line change
@@ -3,11 +3,11 @@ package exql
import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
)

func TestOnAndRawOrAnd(t *testing.T) {
var s, e string

on := OnConditions(
JoinWithAnd(
&ColumnValue{Column: &Column{Name: "id"}, Operator: ">", Value: NewValue(&Raw{Value: "8"})},
@@ -25,33 +25,21 @@ func TestOnAndRawOrAnd(t *testing.T) {
),
)

s = mustTrim(on.Compile(defaultTemplate))
e = `ON (("id" > 8 AND "id" < 99) AND "name" = 'John' AND city_id = 728 AND ("last_name" = 'Smith' OR "last_name" = 'Reyes') AND ("age" > 18 AND "age" < 41))`

if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}
s := mustTrim(on.Compile(defaultTemplate))
assert.Equal(t, `ON (("id" > 8 AND "id" < 99) AND "name" = 'John' AND city_id = 728 AND ("last_name" = 'Smith' OR "last_name" = 'Reyes') AND ("age" > 18 AND "age" < 41))`, s)
}

func TestUsing(t *testing.T) {
var s, e string

using := UsingColumns(
&Column{Name: "country"},
&Column{Name: "state"},
)

s = mustTrim(using.Compile(defaultTemplate))
e = `USING ("country", "state")`

if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}
s := mustTrim(using.Compile(defaultTemplate))
assert.Equal(t, `USING ("country", "state")`, s)
}

func TestJoinOn(t *testing.T) {
var s, e string

join := JoinConditions(
&Join{
Table: TableWithName("countries c"),
@@ -70,17 +58,11 @@ func TestJoinOn(t *testing.T) {
},
)

s = mustTrim(join.Compile(defaultTemplate))
e = `JOIN "countries" AS "c" ON ("p"."country_id" = "a"."id" AND "p"."country_code" = "a"."code")`

if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}
s := mustTrim(join.Compile(defaultTemplate))
assert.Equal(t, `JOIN "countries" AS "c" ON ("p"."country_id" = "a"."id" AND "p"."country_code" = "a"."code")`, s)
}

func TestInnerJoinOn(t *testing.T) {
var s, e string

join := JoinConditions(&Join{
Type: "INNER",
Table: TableWithName("countries c"),
@@ -98,94 +80,60 @@ func TestInnerJoinOn(t *testing.T) {
),
})

s = mustTrim(join.Compile(defaultTemplate))
e = `INNER JOIN "countries" AS "c" ON ("p"."country_id" = "a"."id" AND "p"."country_code" = "a"."code")`

if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}
s := mustTrim(join.Compile(defaultTemplate))
assert.Equal(t, `INNER JOIN "countries" AS "c" ON ("p"."country_id" = "a"."id" AND "p"."country_code" = "a"."code")`, s)
}

func TestLeftJoinUsing(t *testing.T) {
var s, e string

join := JoinConditions(&Join{
Type: "LEFT",
Table: TableWithName("countries"),
Using: UsingColumns(ColumnWithName("name")),
})

s = mustTrim(join.Compile(defaultTemplate))
e = `LEFT JOIN "countries" USING ("name")`

if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}
s := mustTrim(join.Compile(defaultTemplate))
assert.Equal(t, `LEFT JOIN "countries" USING ("name")`, s)
}

func TestNaturalJoinOn(t *testing.T) {
var s, e string

join := JoinConditions(&Join{
Table: TableWithName("countries"),
})

s = mustTrim(join.Compile(defaultTemplate))
e = `NATURAL JOIN "countries"`

if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}
s := mustTrim(join.Compile(defaultTemplate))
assert.Equal(t, `NATURAL JOIN "countries"`, s)
}

func TestNaturalInnerJoinOn(t *testing.T) {
var s, e string

join := JoinConditions(&Join{
Type: "INNER",
Table: TableWithName("countries"),
})

s = mustTrim(join.Compile(defaultTemplate))
e = `NATURAL INNER JOIN "countries"`

if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}
s := mustTrim(join.Compile(defaultTemplate))
assert.Equal(t, `NATURAL INNER JOIN "countries"`, s)
}

func TestCrossJoin(t *testing.T) {
var s, e string

join := JoinConditions(&Join{
Type: "CROSS",
Table: TableWithName("countries"),
})

s = mustTrim(join.Compile(defaultTemplate))
e = `CROSS JOIN "countries"`

if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}
s := mustTrim(join.Compile(defaultTemplate))
assert.Equal(t, `CROSS JOIN "countries"`, s)
}

func TestMultipleJoins(t *testing.T) {
var s, e string

join := JoinConditions(&Join{
Type: "LEFT",
Table: TableWithName("countries"),
}, &Join{
Table: TableWithName("cities"),
})

s = mustTrim(join.Compile(defaultTemplate))
e = `NATURAL LEFT JOIN "countries" NATURAL JOIN "cities"`

if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}
s := mustTrim(join.Compile(defaultTemplate))
assert.Equal(t, `NATURAL LEFT JOIN "countries" NATURAL JOIN "cities"`, s)
}

func BenchmarkJoin(b *testing.B) {
@@ -224,6 +172,7 @@ func BenchmarkCompileJoin(b *testing.B) {
},
),
})
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = j.Compile(defaultTemplate)
}
51 changes: 31 additions & 20 deletions internal/sqladapter/exql/order_by.go
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
package exql

import (
"fmt"
"strings"

"github.com/upper/db/v4/internal/cache"
)

// Order represents the order in which SQL results are sorted.
type Order uint8

// Possible values for Order
const (
DefaultOrder = Order(iota)
Ascendent
Descendent
Order_Default Order = iota

Order_Ascendent
Order_Descendent
)

func (o Order) Hash() uint64 {
return cache.NewHash(FragmentType_Order, uint8(o))
}

// SortColumn represents the column-order relation in an ORDER BY clause.
type SortColumn struct {
Column Fragment
Order
hash hash
}

var _ = Fragment(&SortColumn{})
@@ -34,15 +39,13 @@ var _ = Fragment(&SortColumn{})
// SortColumns represents the columns in an ORDER BY clause.
type SortColumns struct {
Columns []Fragment
hash hash
}

var _ = Fragment(&SortColumns{})

// OrderBy represents an ORDER BY clause.
type OrderBy struct {
SortColumns Fragment
hash hash
}

var _ = Fragment(&OrderBy{})
@@ -62,8 +65,11 @@ func JoinWithOrderBy(sc *SortColumns) *OrderBy {
}

// Hash returns a unique identifier for the struct.
func (s *SortColumn) Hash() string {
return s.hash.Hash(s)
func (s *SortColumn) Hash() uint64 {
if s == nil {
return cache.NewHash(FragmentType_SortColumn, nil)
}
return cache.NewHash(FragmentType_SortColumn, s.Column, s.Order)
}

// Compile transforms the SortColumn into an equivalent SQL representation.
@@ -93,8 +99,15 @@ func (s *SortColumn) Compile(layout *Template) (compiled string, err error) {
}

// Hash returns a unique identifier for the struct.
func (s *SortColumns) Hash() string {
return s.hash.Hash(s)
func (s *SortColumns) Hash() uint64 {
if s == nil {
return cache.NewHash(FragmentType_SortColumns, nil)
}
h := cache.InitHash(FragmentType_SortColumns)
for i := range s.Columns {
h = cache.AddToHash(h, s.Columns[i])
}
return h
}

// Compile transforms the SortColumns into an equivalent SQL representation.
@@ -120,8 +133,11 @@ func (s *SortColumns) Compile(layout *Template) (compiled string, err error) {
}

// Hash returns a unique identifier for the struct.
func (s *OrderBy) Hash() string {
return s.hash.Hash(s)
func (s *OrderBy) Hash() uint64 {
if s == nil {
return cache.NewHash(FragmentType_OrderBy, nil)
}
return cache.NewHash(FragmentType_OrderBy, s.SortColumns)
}

// Compile transforms the SortColumn into an equivalent SQL representation.
@@ -147,17 +163,12 @@ func (s *OrderBy) Compile(layout *Template) (compiled string, err error) {
return
}

// Hash returns a unique identifier.
func (s *Order) Hash() string {
return fmt.Sprintf("%T.%d", s, uint8(*s))
}

// Compile transforms the SortColumn into an equivalent SQL representation.
func (s Order) Compile(layout *Template) (string, error) {
switch s {
case Ascendent:
case Order_Ascendent:
return layout.AscKeyword, nil
case Descendent:
case Order_Descendent:
return layout.DescKeyword, nil
}
return "", nil
31 changes: 15 additions & 16 deletions internal/sqladapter/exql/order_by_test.go
Original file line number Diff line number Diff line change
@@ -2,6 +2,8 @@ package exql

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestOrderBy(t *testing.T) {
@@ -12,38 +14,29 @@ func TestOrderBy(t *testing.T) {
)

s := mustTrim(o.Compile(defaultTemplate))
e := `ORDER BY "foo"`
if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}
assert.Equal(t, `ORDER BY "foo"`, s)
}

func TestOrderByRaw(t *testing.T) {
o := JoinWithOrderBy(
JoinSortColumns(
&SortColumn{Column: RawValue("CASE WHEN id IN ? THEN 0 ELSE 1 END")},
&SortColumn{Column: &Raw{Value: "CASE WHEN id IN ? THEN 0 ELSE 1 END"}},
),
)

s := mustTrim(o.Compile(defaultTemplate))
e := `ORDER BY CASE WHEN id IN ? THEN 0 ELSE 1 END`
if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}
assert.Equal(t, `ORDER BY CASE WHEN id IN ? THEN 0 ELSE 1 END`, s)
}

func TestOrderByDesc(t *testing.T) {
o := JoinWithOrderBy(
JoinSortColumns(
&SortColumn{Column: &Column{Name: "foo"}, Order: Descendent},
&SortColumn{Column: &Column{Name: "foo"}, Order: Order_Descendent},
),
)

s := mustTrim(o.Compile(defaultTemplate))
e := `ORDER BY "foo" DESC`
if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}
assert.Equal(t, `ORDER BY "foo" DESC`, s)
}

func BenchmarkOrderBy(b *testing.B) {
@@ -62,6 +55,7 @@ func BenchmarkOrderByHash(b *testing.B) {
&SortColumn{Column: &Column{Name: "foo"}},
),
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
o.Hash()
}
@@ -73,6 +67,7 @@ func BenchmarkCompileOrderByCompile(b *testing.B) {
&SortColumn{Column: &Column{Name: "foo"}},
),
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = o.Compile(defaultTemplate)
}
@@ -90,28 +85,30 @@ func BenchmarkCompileOrderByCompileNoCache(b *testing.B) {
}

func BenchmarkCompileOrderCompile(b *testing.B) {
o := Descendent
o := Order_Descendent
for i := 0; i < b.N; i++ {
_, _ = o.Compile(defaultTemplate)
}
}

func BenchmarkCompileOrderCompileNoCache(b *testing.B) {
for i := 0; i < b.N; i++ {
o := Descendent
o := Order_Descendent
_, _ = o.Compile(defaultTemplate)
}
}

func BenchmarkSortColumnHash(b *testing.B) {
s := &SortColumn{Column: &Column{Name: "foo"}}
b.ResetTimer()
for i := 0; i < b.N; i++ {
s.Hash()
}
}

func BenchmarkSortColumnCompile(b *testing.B) {
s := &SortColumn{Column: &Column{Name: "foo"}}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = s.Compile(defaultTemplate)
}
@@ -129,6 +126,7 @@ func BenchmarkSortColumnsHash(b *testing.B) {
&SortColumn{Column: &Column{Name: "foo"}},
&SortColumn{Column: &Column{Name: "bar"}},
)
b.ResetTimer()
for i := 0; i < b.N; i++ {
s.Hash()
}
@@ -139,6 +137,7 @@ func BenchmarkSortColumnsCompile(b *testing.B) {
&SortColumn{Column: &Column{Name: "foo"}},
&SortColumn{Column: &Column{Name: "bar"}},
)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = s.Compile(defaultTemplate)
}
26 changes: 18 additions & 8 deletions internal/sqladapter/exql/raw.go
Original file line number Diff line number Diff line change
@@ -2,7 +2,8 @@ package exql

import (
"fmt"
"strings"

"github.com/upper/db/v4/internal/cache"
)

var (
@@ -11,18 +12,27 @@ var (

// Raw represents a value that is meant to be used in a query without escaping.
type Raw struct {
Value string // Value should not be modified after assigned.
hash hash
Value string
}

// RawValue creates and returns a new raw value.
func RawValue(v string) *Raw {
return &Raw{Value: strings.TrimSpace(v)}
func NewRawValue(v interface{}) (*Raw, error) {
switch t := v.(type) {
case string:
return &Raw{Value: t}, nil
case int, uint, int64, uint64, int32, uint32, int16, uint16:
return &Raw{Value: fmt.Sprintf("%d", t)}, nil
case fmt.Stringer:
return &Raw{Value: t.String()}, nil
}
return nil, fmt.Errorf("unexpected type: %T", v)
}

// Hash returns a unique identifier for the struct.
func (r *Raw) Hash() string {
return r.hash.Hash(r)
func (r *Raw) Hash() uint64 {
if r == nil {
return cache.NewHash(FragmentType_Raw, nil)
}
return cache.NewHash(FragmentType_Raw, r.Value)
}

// Compile returns the raw value.
40 changes: 9 additions & 31 deletions internal/sqladapter/exql/raw_test.go
Original file line number Diff line number Diff line change
@@ -2,47 +2,22 @@ package exql

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestRawString(t *testing.T) {
raw := &Raw{Value: "foo"}

s, err := raw.Compile(defaultTemplate)
if err != nil {
t.Fatal()
}

e := `foo`
if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}
assert.NoError(t, err)
assert.Equal(t, `foo`, s)
}

func TestRawCompile(t *testing.T) {
raw := &Raw{Value: "foo"}

s, err := raw.Compile(defaultTemplate)
if err != nil {
t.Fatal()
}

e := `foo`
if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}
}

func TestRawHash(t *testing.T) {
var s, e string

raw := &Raw{Value: "foo"}

s = raw.Hash()
e = `*exql.Raw:5772950988983410957`

if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}
assert.NoError(t, err)
assert.Equal(t, `foo`, s)
}

func BenchmarkRawCreate(b *testing.B) {
@@ -53,20 +28,23 @@ func BenchmarkRawCreate(b *testing.B) {

func BenchmarkRawString(b *testing.B) {
raw := &Raw{Value: "foo"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = raw.String()
}
}

func BenchmarkRawCompile(b *testing.B) {
raw := &Raw{Value: "foo"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = raw.Compile(defaultTemplate)
}
}

func BenchmarkRawHash(b *testing.B) {
raw := &Raw{Value: "foo"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
raw.Hash()
}
12 changes: 9 additions & 3 deletions internal/sqladapter/exql/returning.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
package exql

import (
"github.com/upper/db/v4/internal/cache"
)

// Returning represents a RETURNING clause.
type Returning struct {
*Columns
hash hash
}

// Hash returns a unique identifier for the struct.
func (r *Returning) Hash() string {
return r.hash.Hash(r)
func (r *Returning) Hash() uint64 {
if r == nil {
return cache.NewHash(FragmentType_Returning, nil)
}
return cache.NewHash(FragmentType_Returning, r.Columns)
}

var _ = Fragment(&Returning{})
27 changes: 24 additions & 3 deletions internal/sqladapter/exql/statement.go
Original file line number Diff line number Diff line change
@@ -4,6 +4,8 @@ import (
"errors"
"reflect"
"strings"

"github.com/upper/db/v4/internal/cache"
)

var errUnknownTemplateType = errors.New("Unknown template type")
@@ -28,7 +30,6 @@ type Statement struct {

SQL string

hash hash
amendFn func(string) string
}

@@ -40,8 +41,28 @@ func (layout *Template) doCompile(c Fragment) (string, error) {
}

// Hash returns a unique identifier for the struct.
func (s *Statement) Hash() string {
return s.hash.Hash(s)
func (s *Statement) Hash() uint64 {
if s == nil {
return cache.NewHash(FragmentType_Statement, nil)
}
return cache.NewHash(
FragmentType_Statement,
s.Type,
s.Table,
s.Database,
s.Columns,
s.Values,
s.Distinct,
s.ColumnValues,
s.OrderBy,
s.GroupBy,
s.Joins,
s.Where,
s.Returning,
s.Limit,
s.Offset,
s.SQL,
)
}

func (s *Statement) SetAmendment(amendFn func(string) string) {
784 changes: 293 additions & 491 deletions internal/sqladapter/exql/statement_test.go

Large diffs are not rendered by default.

10 changes: 7 additions & 3 deletions internal/sqladapter/exql/table.go
Original file line number Diff line number Diff line change
@@ -2,6 +2,8 @@ package exql

import (
"strings"

"github.com/upper/db/v4/internal/cache"
)

type tableT struct {
@@ -12,7 +14,6 @@ type tableT struct {
// Table struct represents a SQL table.
type Table struct {
Name interface{}
hash hash
}

var _ = Fragment(&Table{})
@@ -57,8 +58,11 @@ func TableWithName(name string) *Table {
}

// Hash returns a string hash of the table value.
func (t *Table) Hash() string {
return t.hash.Hash(t)
func (t *Table) Hash() uint64 {
if t == nil {
return cache.NewHash(FragmentType_Table, nil)
}
return cache.NewHash(FragmentType_Table, t.Name)
}

// Compile transforms a table struct into a SQL chunk.
78 changes: 12 additions & 66 deletions internal/sqladapter/exql/table_test.go
Original file line number Diff line number Diff line change
@@ -1,111 +1,55 @@
package exql

import (
"github.com/stretchr/testify/assert"

"testing"
)

func TestTableSimple(t *testing.T) {
var s, e string

table := TableWithName("artist")

s = mustTrim(table.Compile(defaultTemplate))
e = `"artist"`

if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}
assert.Equal(t, `"artist"`, mustTrim(table.Compile(defaultTemplate)))
}

func TestTableCompound(t *testing.T) {
var s, e string

table := TableWithName("artist.foo")

s = mustTrim(table.Compile(defaultTemplate))
e = `"artist"."foo"`

if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}
assert.Equal(t, `"artist"."foo"`, mustTrim(table.Compile(defaultTemplate)))
}

func TestTableCompoundAlias(t *testing.T) {
var s, e string

table := TableWithName("artist.foo AS baz")

s = mustTrim(table.Compile(defaultTemplate))
e = `"artist"."foo" AS "baz"`

if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}
assert.Equal(t, `"artist"."foo" AS "baz"`, mustTrim(table.Compile(defaultTemplate)))
}

func TestTableImplicitAlias(t *testing.T) {
var s, e string

table := TableWithName("artist.foo baz")

s = mustTrim(table.Compile(defaultTemplate))
e = `"artist"."foo" AS "baz"`

if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}
assert.Equal(t, `"artist"."foo" AS "baz"`, mustTrim(table.Compile(defaultTemplate)))
}

func TestTableMultiple(t *testing.T) {
var s, e string

table := TableWithName("artist.foo, artist.bar, artist.baz")

s = mustTrim(table.Compile(defaultTemplate))
e = `"artist"."foo", "artist"."bar", "artist"."baz"`

if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}
assert.Equal(t, `"artist"."foo", "artist"."bar", "artist"."baz"`, mustTrim(table.Compile(defaultTemplate)))
}

func TestTableMultipleAlias(t *testing.T) {
var s, e string

table := TableWithName("artist.foo AS foo, artist.bar as bar, artist.baz As baz")

s = mustTrim(table.Compile(defaultTemplate))
e = `"artist"."foo" AS "foo", "artist"."bar" AS "bar", "artist"."baz" AS "baz"`

if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}
assert.Equal(t, `"artist"."foo" AS "foo", "artist"."bar" AS "bar", "artist"."baz" AS "baz"`, mustTrim(table.Compile(defaultTemplate)))
}

func TestTableMinimal(t *testing.T) {
var s, e string

table := TableWithName("a")

s = mustTrim(table.Compile(defaultTemplate))
e = `"a"`

if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}
assert.Equal(t, `"a"`, mustTrim(table.Compile(defaultTemplate)))
}

func TestTableEmpty(t *testing.T) {
var s, e string

table := TableWithName("")

s = mustTrim(table.Compile(defaultTemplate))
e = ``

if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}
assert.Equal(t, "", mustTrim(table.Compile(defaultTemplate)))
}

func BenchmarkTableWithName(b *testing.B) {
@@ -116,13 +60,15 @@ func BenchmarkTableWithName(b *testing.B) {

func BenchmarkTableHash(b *testing.B) {
t := TableWithName("name")
b.ResetTimer()
for i := 0; i < b.N; i++ {
t.Hash()
}
}

func BenchmarkTableCompile(b *testing.B) {
t := TableWithName("name")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = t.Compile(defaultTemplate)
}
20 changes: 16 additions & 4 deletions internal/sqladapter/exql/template.go
Original file line number Diff line number Diff line change
@@ -11,11 +11,11 @@ import (
)

// Type is the type of SQL query the statement represents.
type Type uint
type Type uint8

// Values for Type.
const (
NoOp = Type(iota)
NoOp Type = iota

Truncate
DropTable
@@ -29,13 +29,25 @@ const (
SQL
)

func (t Type) Hash() uint64 {
return cache.NewHash(FragmentType_StatementType, uint8(t))
}

type (
// Limit represents the SQL limit in a query.
Limit int
Limit int64
// Offset represents the SQL offset in a query.
Offset int
Offset int64
)

func (t Limit) Hash() uint64 {
return cache.NewHash(FragmentType_Limit, uint64(t))
}

func (t Offset) Hash() uint64 {
return cache.NewHash(FragmentType_Offset, uint64(t))
}

// Template is an SQL template.
type Template struct {
AndKeyword string
35 changes: 35 additions & 0 deletions internal/sqladapter/exql/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package exql

const (
FragmentType_None uint64 = iota + 713910251627

FragmentType_And
FragmentType_Column
FragmentType_ColumnValue
FragmentType_ColumnValues
FragmentType_Columns
FragmentType_Database
FragmentType_GroupBy
FragmentType_Join
FragmentType_Joins
FragmentType_Nil
FragmentType_Or
FragmentType_Limit
FragmentType_Offset
FragmentType_OrderBy
FragmentType_Order
FragmentType_Raw
FragmentType_Returning
FragmentType_SortBy
FragmentType_SortColumn
FragmentType_SortColumns
FragmentType_Statement
FragmentType_StatementType
FragmentType_Table
FragmentType_Value
FragmentType_On
FragmentType_Using
FragmentType_ValueGroups
FragmentType_Values
FragmentType_Where
)
185 changes: 56 additions & 129 deletions internal/sqladapter/exql/utilities_test.go
Original file line number Diff line number Diff line change
@@ -6,6 +6,8 @@ import (
"strings"
"testing"
"unicode"

"github.com/stretchr/testify/assert"
)

const (
@@ -20,90 +22,64 @@ var (
stringWithLeadingBlanks = string(bytesWithLeadingBlanks)
)

func TestUtilIsBlankSymbol(t *testing.T) {
if isBlankSymbol(' ') == false {
t.Fail()
}
if isBlankSymbol('\n') == false {
t.Fail()
}
if isBlankSymbol('\t') == false {
t.Fail()
}
if isBlankSymbol('\r') == false {
t.Fail()
}
if isBlankSymbol('x') == true {
t.Fail()
var (
reInvisible = regexp.MustCompile(`[\t\n\r]`)
reSpace = regexp.MustCompile(`\s+`)
)

func mustTrim(a string, err error) string {
if err != nil {
panic(err.Error())
}
a = reInvisible.ReplaceAllString(strings.TrimSpace(a), " ")
a = reSpace.ReplaceAllString(strings.TrimSpace(a), " ")
return a
}

func TestUtilIsBlankSymbol(t *testing.T) {
assert.True(t, isBlankSymbol(' '))
assert.True(t, isBlankSymbol('\n'))
assert.True(t, isBlankSymbol('\t'))
assert.True(t, isBlankSymbol('\r'))
assert.False(t, isBlankSymbol('x'))
}

func TestUtilTrimBytes(t *testing.T) {
var trimmed []byte

trimmed = trimBytes([]byte(" \t\nHello World! \n"))
if string(trimmed) != "Hello World!" {
t.Fatalf("Got: %s\n", string(trimmed))
}
assert.Equal(t, "Hello World!", string(trimmed))

trimmed = trimBytes([]byte("Nope"))
if string(trimmed) != "Nope" {
t.Fatalf("Got: %s\n", string(trimmed))
}
assert.Equal(t, "Nope", string(trimmed))

trimmed = trimBytes([]byte(""))
if string(trimmed) != "" {
t.Fatalf("Got: %s\n", string(trimmed))
}
assert.Equal(t, "", string(trimmed))

trimmed = trimBytes([]byte(" "))
if string(trimmed) != "" {
t.Fatalf("Got: %s\n", string(trimmed))
}
assert.Equal(t, "", string(trimmed))

trimmed = trimBytes(nil)
if string(trimmed) != "" {
t.Fatalf("Got: %s\n", string(trimmed))
}
assert.Equal(t, "", string(trimmed))
}

func TestUtilSeparateByComma(t *testing.T) {
chunks := separateByComma("Hello,,World!,Enjoy")
assert.Equal(t, 4, len(chunks))

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()
}
assert.Equal(t, "Hello", chunks[0])
assert.Equal(t, "", chunks[1])
assert.Equal(t, "World!", chunks[2])
assert.Equal(t, "Enjoy", chunks[3])
}

func TestUtilSeparateBySpace(t *testing.T) {
chunks := separateBySpace(" Hello World! Enjoy")
assert.Equal(t, 3, len(chunks))

if len(chunks) != 3 {
t.Fatal()
}

if chunks[0] != "Hello" {
t.Fatal()
}
if chunks[1] != "World!" {
t.Fatal()
}
if chunks[2] != "Enjoy" {
t.Fatal()
}
assert.Equal(t, "Hello", chunks[0])
assert.Equal(t, "World!", chunks[1])
assert.Equal(t, "Enjoy", chunks[2])
}

func TestUtilSeparateByAS(t *testing.T) {
@@ -117,96 +93,44 @@ func TestUtilSeparateByAS(t *testing.T) {

for _, test := range tests {
chunks = separateByAS(test)
assert.Len(t, chunks, 2)

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.`)
}
assert.Equal(t, "table.Name", chunks[0])
assert.Equal(t, "myTableAlias", chunks[1])
}

// 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".`)
}
assert.Len(t, chunks, 1)
assert.Equal(t, "a", chunks[0])

// Empty name
chunks = separateByAS("")

if len(chunks) != 1 {
t.Fatalf(`Expecting 1 results.`)
}

if chunks[0] != "" {
t.Fatal(`Expecting first result to be "".`)
}
assert.Len(t, chunks, 1)
assert.Equal(t, "", chunks[0])

// 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".`)
}
assert.Len(t, chunks, 1)
assert.Equal(t, "A Single Table", chunks[0])

// 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".`)
}
assert.Len(t, chunks, 2)
assert.Equal(t, "a", chunks[0])
assert.Equal(t, "b", chunks[1])

// 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".`)
}
assert.Len(t, chunks, 2)
assert.Equal(t, "a", chunks[0])
assert.Equal(t, "b", chunks[1])

// 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".`)
}
assert.Len(t, chunks, 2)
assert.Equal(t, "a", chunks[0])
assert.Equal(t, "bb", chunks[1])
}

func BenchmarkUtilIsBlankSymbol(b *testing.B) {
@@ -252,6 +176,7 @@ func BenchmarkUtilSeparateByComma(b *testing.B) {

func BenchmarkUtilRegExpSeparateByComma(b *testing.B) {
sep := regexp.MustCompile(`\s*?,\s*?`)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = sep.Split(stringWithCommas, -1)
}
@@ -265,6 +190,7 @@ func BenchmarkUtilSeparateBySpace(b *testing.B) {

func BenchmarkUtilRegExpSeparateBySpace(b *testing.B) {
sep := regexp.MustCompile(`\s+`)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = sep.Split(stringWithSpaces, -1)
}
@@ -278,6 +204,7 @@ func BenchmarkUtilSeparateByAS(b *testing.B) {

func BenchmarkUtilRegExpSeparateByAS(b *testing.B) {
sep := regexp.MustCompile(`(?i:\s+AS\s+)`)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = sep.Split(stringWithASKeyword, -1)
}
69 changes: 40 additions & 29 deletions internal/sqladapter/exql/value.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package exql

import (
"fmt"
"strings"

"github.com/upper/db/v4/internal/cache"
)

// ValueGroups represents an array of value groups.
type ValueGroups struct {
Values []*Values
hash hash
}

func (vg *ValueGroups) IsEmpty() bool {
@@ -28,7 +28,6 @@ var _ = Fragment(&ValueGroups{})
// Values represents an array of Value.
type Values struct {
Values []Fragment
hash hash
}

func (vs *Values) IsEmpty() bool {
@@ -38,12 +37,16 @@ func (vs *Values) IsEmpty() bool {
return false
}

// NewValueGroup creates and returns an array of values.
func NewValueGroup(v ...Fragment) *Values {
return &Values{Values: v}
}

var _ = Fragment(&Values{})

// Value represents an escaped SQL value.
type Value struct {
V interface{}
hash hash
V interface{}
}

var _ = Fragment(&Value{})
@@ -53,50 +56,51 @@ func NewValue(v interface{}) *Value {
return &Value{V: v}
}

// NewValueGroup creates and returns an array of values.
func NewValueGroup(v ...Fragment) *Values {
return &Values{Values: v}
}

// Hash returns a unique identifier for the struct.
func (v *Value) Hash() string {
return v.hash.Hash(v)
}

func (v *Value) IsEmpty() bool {
return false
func (v *Value) Hash() uint64 {
if v == nil {
return cache.NewHash(FragmentType_Value, nil)
}
return cache.NewHash(FragmentType_Value, v.V)
}

// Compile transforms the Value into an equivalent SQL representation.
func (v *Value) Compile(layout *Template) (compiled string, err error) {

if z, ok := layout.Read(v); ok {
return z, nil
}

switch t := v.V.(type) {
case Raw:
compiled, err = t.Compile(layout)
switch value := v.V.(type) {
case compilable:
compiled, err = value.Compile(layout)
if err != nil {
return "", err
}
case Fragment:
compiled, err = t.Compile(layout)
default:
value, err := NewRawValue(v.V)
if err != nil {
return "", err
}
default:
compiled = layout.MustCompile(layout.ValueQuote, RawValue(fmt.Sprintf(`%v`, v.V)))
compiled = layout.MustCompile(
layout.ValueQuote,
value,
)
}

layout.Write(v, compiled)

return
}

// Hash returns a unique identifier for the struct.
func (vs *Values) Hash() string {
return vs.hash.Hash(vs)
func (vs *Values) Hash() uint64 {
if vs == nil {
return cache.NewHash(FragmentType_Values, nil)
}
h := cache.InitHash(FragmentType_Values)
for i := range vs.Values {
h = cache.AddToHash(h, vs.Values[i])
}
return h
}

// Compile transforms the Values into an equivalent SQL representation.
@@ -122,8 +126,15 @@ func (vs *Values) Compile(layout *Template) (compiled string, err error) {
}

// Hash returns a unique identifier for the struct.
func (vg *ValueGroups) Hash() string {
return vg.hash.Hash(vg)
func (vg *ValueGroups) Hash() uint64 {
if vg == nil {
return cache.NewHash(FragmentType_ValueGroups, nil)
}
h := cache.InitHash(FragmentType_ValueGroups)
for i := range vg.Values {
h = cache.AddToHash(h, vg.Values[i])
}
return h
}

// Compile transforms the ValueGroups into an equivalent SQL representation.
65 changes: 46 additions & 19 deletions internal/sqladapter/exql/value_test.go
Original file line number Diff line number Diff line change
@@ -2,31 +2,59 @@ package exql

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestValue(t *testing.T) {
val := NewValue(1)

s, err := val.Compile(defaultTemplate)
if err != nil {
t.Fatal()
assert.NoError(t, err)
assert.Equal(t, `'1'`, s)

val = NewValue(&Raw{Value: "NOW()"})

s, err = val.Compile(defaultTemplate)
assert.NoError(t, err)
assert.Equal(t, `NOW()`, s)
}

func TestSameRawValue(t *testing.T) {
{
val := NewValue(&Raw{Value: `"1"`})

s, err := val.Compile(defaultTemplate)
assert.NoError(t, err)
assert.Equal(t, `"1"`, s)
}
{
val := NewValue(&Raw{Value: `'1'`})

e := `'1'`
if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
s, err := val.Compile(defaultTemplate)
assert.NoError(t, err)
assert.Equal(t, `'1'`, s)
}
{
val := NewValue(&Raw{Value: `1`})

val = NewValue(&Raw{Value: "NOW()"})
s, err := val.Compile(defaultTemplate)
assert.NoError(t, err)
assert.Equal(t, `1`, s)
}
{
val := NewValue("1")

s, err = val.Compile(defaultTemplate)
if err != nil {
t.Fatal()
s, err := val.Compile(defaultTemplate)
assert.NoError(t, err)
assert.Equal(t, `'1'`, s)
}
{
val := NewValue(1)

e = `NOW()`
if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
s, err := val.Compile(defaultTemplate)
assert.NoError(t, err)
assert.Equal(t, `'1'`, s)
}
}

@@ -38,14 +66,9 @@ func TestValues(t *testing.T) {
)

s, err := val.Compile(defaultTemplate)
if err != nil {
t.Fatal()
}
assert.NoError(t, err)

e := `(1, 2, '3')`
if s != e {
t.Fatalf("Got: %s, Expecting: %s", s, e)
}
assert.Equal(t, `(1, 2, '3')`, s)
}

func BenchmarkValue(b *testing.B) {
@@ -56,13 +79,15 @@ func BenchmarkValue(b *testing.B) {

func BenchmarkValueHash(b *testing.B) {
v := NewValue("a")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = v.Hash()
}
}

func BenchmarkValueCompile(b *testing.B) {
v := NewValue("a")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = v.Compile(defaultTemplate)
}
@@ -83,13 +108,15 @@ func BenchmarkValues(b *testing.B) {

func BenchmarkValuesHash(b *testing.B) {
vs := NewValueGroup(NewValue("a"), NewValue("b"))
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = vs.Hash()
}
}

func BenchmarkValuesCompile(b *testing.B) {
vs := NewValueGroup(NewValue("a"), NewValue("b"))
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = vs.Compile(defaultTemplate)
}
Loading

0 comments on commit 8a3fe0c

Please sign in to comment.