commit
28717a467c
24 changed files with 624 additions and 203 deletions
|
|
@ -34,10 +34,7 @@ jobs:
|
||||||
go get github.com/davecgh/go-spew/spew
|
go get github.com/davecgh/go-spew/spew
|
||||||
go get github.com/jstemmer/go-junit-report
|
go get github.com/jstemmer/go-junit-report
|
||||||
|
|
||||||
- run: mkdir -p $TEST_RESULTS/unit-tests
|
go install github.com/go-jet/jet/cmd/jet
|
||||||
- run: mkdir -p $TEST_RESULTS/integration-tests
|
|
||||||
|
|
||||||
- run: go test -v 2>&1 | go-junit-report > $TEST_RESULTS/unit-tests/results.xml
|
|
||||||
|
|
||||||
- run:
|
- run:
|
||||||
name: Waiting for Postgres to be ready
|
name: Waiting for Postgres to be ready
|
||||||
|
|
@ -51,13 +48,19 @@ jobs:
|
||||||
echo Failed waiting for Postgres && exit 1
|
echo Failed waiting for Postgres && exit 1
|
||||||
|
|
||||||
- run:
|
- run:
|
||||||
name: Run integration tests
|
name: Init Postgres database
|
||||||
command: |
|
command: |
|
||||||
cd tests
|
cd tests
|
||||||
go run ./init/init.go
|
go run ./init/init.go
|
||||||
go test -v 2>&1 | go-junit-report > $TEST_RESULTS/integration-tests/results.xml
|
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
|
- run: mkdir -p $TEST_RESULTS
|
||||||
|
- run: go test -v . ./tests -coverpkg=github.com/go-jet/jet,github.com/go-jet/jet/execution/...,github.com/go-jet/jet/generator/...,github.com/go-jet/jet/internal/... -coverprofile=cover.out 2>&1 | go-junit-report > $TEST_RESULTS/results.xml
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Upload code coverage
|
||||||
|
command: bash <(curl -s https://codecov.io/bash)
|
||||||
|
|
||||||
- store_artifacts: # Upload test summary for display in Artifacts: https://circleci.com/docs/2.0/artifacts/
|
- store_artifacts: # Upload test summary for display in Artifacts: https://circleci.com/docs/2.0/artifacts/
|
||||||
path: /tmp/test-results
|
path: /tmp/test-results
|
||||||
destination: raw-test-output
|
destination: raw-test-output
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
[](https://goreportcard.com/report/github.com/go-jet/jet)
|
[](https://goreportcard.com/report/github.com/go-jet/jet)
|
||||||
[](http://godoc.org/github.com/go-jet/jet)
|
[](http://godoc.org/github.com/go-jet/jet)
|
||||||
|
[](https://codecov.io/gh/go-jet/jet)
|
||||||
[](https://circleci.com/gh/go-jet/jet/tree/develop)
|
[](https://circleci.com/gh/go-jet/jet/tree/develop)
|
||||||
|
|
||||||
Jet is a framework for writing type-safe SQL queries for PostgreSQL in Go, with ability to easily
|
Jet is a framework for writing type-safe SQL queries for PostgreSQL in Go, with ability to easily
|
||||||
|
|
@ -260,6 +261,7 @@ Let's say this is our desired structure:
|
||||||
```go
|
```go
|
||||||
var dest []struct {
|
var dest []struct {
|
||||||
model.Actor
|
model.Actor
|
||||||
|
|
||||||
Films []struct {
|
Films []struct {
|
||||||
model.Film
|
model.Film
|
||||||
Language model.Language
|
Language model.Language
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,16 @@ func TestBoolExpressionNOT_EQ(t *testing.T) {
|
||||||
assertClauseSerialize(t, table1ColBool.NOT_EQ(Bool(true)), "(table1.col_bool != $1)", true)
|
assertClauseSerialize(t, table1ColBool.NOT_EQ(Bool(true)), "(table1.col_bool != $1)", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBoolExpressionIS_DISTINCT_FROM(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColBool.IS_DISTINCT_FROM(table2ColBool), "(table1.col_bool IS DISTINCT FROM table2.col_bool)")
|
||||||
|
assertClauseSerialize(t, table1ColBool.IS_DISTINCT_FROM(Bool(false)), "(table1.col_bool IS DISTINCT FROM $1)", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoolExpressionIS_NOT_DISTINCT_FROM(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColBool.IS_NOT_DISTINCT_FROM(table2ColBool), "(table1.col_bool IS NOT DISTINCT FROM table2.col_bool)")
|
||||||
|
assertClauseSerialize(t, table1ColBool.IS_NOT_DISTINCT_FROM(Bool(false)), "(table1.col_bool IS NOT DISTINCT FROM $1)", false)
|
||||||
|
}
|
||||||
|
|
||||||
func TestBoolExpressionIS_TRUE(t *testing.T) {
|
func TestBoolExpressionIS_TRUE(t *testing.T) {
|
||||||
assertClauseSerialize(t, table1ColBool.IS_TRUE(), "table1.col_bool IS TRUE")
|
assertClauseSerialize(t, table1ColBool.IS_TRUE(), "table1.col_bool IS TRUE")
|
||||||
assertClauseSerialize(t, (Int(2).EQ(table1ColInt)).IS_TRUE(),
|
assertClauseSerialize(t, (Int(2).EQ(table1ColInt)).IS_TRUE(),
|
||||||
|
|
|
||||||
|
|
@ -199,11 +199,6 @@ func (q *sqlBuilder) insertParametrizedArgument(arg interface{}) {
|
||||||
q.writeString(argPlaceholder)
|
q.writeString(argPlaceholder)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *sqlBuilder) reset() {
|
|
||||||
q.buff.Reset()
|
|
||||||
q.args = []interface{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func argToString(value interface{}) string {
|
func argToString(value interface{}) string {
|
||||||
if isNil(value) {
|
if isNil(value) {
|
||||||
return "NULL"
|
return "NULL"
|
||||||
|
|
@ -251,7 +246,7 @@ func argToString(value interface{}) string {
|
||||||
case time.Time:
|
case time.Time:
|
||||||
return stringQuote(string(utils.FormatTimestamp(bindVal)))
|
return stringQuote(string(utils.FormatTimestamp(bindVal)))
|
||||||
default:
|
default:
|
||||||
return "[Unknown type]"
|
return "[Unsupported type]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
33
clause_test.go
Normal file
33
clause_test.go
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
package jet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gotest.tools/assert"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestArgToString(t *testing.T) {
|
||||||
|
assert.Equal(t, argToString(true), "TRUE")
|
||||||
|
assert.Equal(t, argToString(false), "FALSE")
|
||||||
|
|
||||||
|
assert.Equal(t, argToString(int8(-8)), "-8")
|
||||||
|
assert.Equal(t, argToString(int16(-16)), "-16")
|
||||||
|
assert.Equal(t, argToString(int(-32)), "-32")
|
||||||
|
assert.Equal(t, argToString(int32(-32)), "-32")
|
||||||
|
assert.Equal(t, argToString(int64(-64)), "-64")
|
||||||
|
assert.Equal(t, argToString(uint8(8)), "8")
|
||||||
|
assert.Equal(t, argToString(uint16(16)), "16")
|
||||||
|
assert.Equal(t, argToString(uint(32)), "32")
|
||||||
|
assert.Equal(t, argToString(uint32(32)), "32")
|
||||||
|
assert.Equal(t, argToString(uint64(64)), "64")
|
||||||
|
|
||||||
|
assert.Equal(t, argToString("john"), "'john'")
|
||||||
|
assert.Equal(t, argToString([]byte("john")), "'john'")
|
||||||
|
assert.Equal(t, argToString(uuid.MustParse("b68dbff4-a87d-11e9-a7f2-98ded00c39c6")), "'b68dbff4-a87d-11e9-a7f2-98ded00c39c6'")
|
||||||
|
|
||||||
|
time, err := time.Parse("Mon Jan 2 15:04:05 -0700 MST 2006", "Mon Jan 2 15:04:05 -0700 MST 2006")
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Equal(t, argToString(time), "'2006-01-02 15:04:05-07:00'")
|
||||||
|
assert.Equal(t, argToString(map[string]bool{}), "[Unsupported type]")
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"github.com/go-jet/jet/generator/postgres"
|
"github.com/go-jet/jet/generator/postgres"
|
||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -71,7 +70,7 @@ Usage of jet:
|
||||||
|
|
||||||
genData := postgres.DBConnection{
|
genData := postgres.DBConnection{
|
||||||
Host: host,
|
Host: host,
|
||||||
Port: strconv.Itoa(port),
|
Port: port,
|
||||||
User: user,
|
User: user,
|
||||||
Password: password,
|
Password: password,
|
||||||
SslMode: sslmode,
|
SslMode: sslmode,
|
||||||
|
|
|
||||||
45
date_expression_test.go
Normal file
45
date_expression_test.go
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
package jet
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
var dateVar = Date(2000, 12, 30)
|
||||||
|
|
||||||
|
func TestDateExpressionEQ(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColDate.EQ(table2ColDate), "(table1.col_date = table2.col_date)")
|
||||||
|
assertClauseSerialize(t, table1ColDate.EQ(dateVar), "(table1.col_date = $1::date)", "2000-12-30")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDateExpressionNOT_EQ(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColDate.NOT_EQ(table2ColDate), "(table1.col_date != table2.col_date)")
|
||||||
|
assertClauseSerialize(t, table1ColDate.NOT_EQ(dateVar), "(table1.col_date != $1::date)", "2000-12-30")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDateExpressionIS_DISTINCT_FROM(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColDate.IS_DISTINCT_FROM(table2ColDate), "(table1.col_date IS DISTINCT FROM table2.col_date)")
|
||||||
|
assertClauseSerialize(t, table1ColDate.IS_DISTINCT_FROM(dateVar), "(table1.col_date IS DISTINCT FROM $1::date)", "2000-12-30")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDateExpressionIS_NOT_DISTINCT_FROM(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColDate.IS_NOT_DISTINCT_FROM(table2ColDate), "(table1.col_date IS NOT DISTINCT FROM table2.col_date)")
|
||||||
|
assertClauseSerialize(t, table1ColDate.IS_NOT_DISTINCT_FROM(dateVar), "(table1.col_date IS NOT DISTINCT FROM $1::date)", "2000-12-30")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDateExpressionGT(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColDate.GT(table2ColDate), "(table1.col_date > table2.col_date)")
|
||||||
|
assertClauseSerialize(t, table1ColDate.GT(dateVar), "(table1.col_date > $1::date)", "2000-12-30")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDateExpressionGT_EQ(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColDate.GT_EQ(table2ColDate), "(table1.col_date >= table2.col_date)")
|
||||||
|
assertClauseSerialize(t, table1ColDate.GT_EQ(dateVar), "(table1.col_date >= $1::date)", "2000-12-30")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDateExpressionLT(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColDate.LT(table2ColDate), "(table1.col_date < table2.col_date)")
|
||||||
|
assertClauseSerialize(t, table1ColDate.LT(dateVar), "(table1.col_date < $1::date)", "2000-12-30")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDateExpressionLT_EQ(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColDate.LT_EQ(table2ColDate), "(table1.col_date <= table2.col_date)")
|
||||||
|
assertClauseSerialize(t, table1ColDate.LT_EQ(dateVar), "(table1.col_date <= $1::date)", "2000-12-30")
|
||||||
|
}
|
||||||
|
|
@ -743,16 +743,6 @@ func (s *scanContext) typeToColumnIndex(typeName, fieldName string) int {
|
||||||
return index
|
return index
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *scanContext) getCellValue(typeName, fieldName string) interface{} {
|
|
||||||
index := s.typeToColumnIndex(typeName, fieldName)
|
|
||||||
|
|
||||||
if index < 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.rowElem(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *scanContext) rowElem(index int) interface{} {
|
func (s *scanContext) rowElem(index int) interface{} {
|
||||||
|
|
||||||
valuer, ok := s.row[index].(driver.Valuer)
|
valuer, ok := s.row[index].(driver.Valuer)
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,16 @@ func TestFloatExpressionNOT_EQ(t *testing.T) {
|
||||||
assertClauseSerialize(t, table1ColFloat.NOT_EQ(Float(2.11)), "(table1.col_float != $1)", float64(2.11))
|
assertClauseSerialize(t, table1ColFloat.NOT_EQ(Float(2.11)), "(table1.col_float != $1)", float64(2.11))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFloatExpressionIS_DISTINCT_FROM(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColFloat.IS_DISTINCT_FROM(table2ColFloat), "(table1.col_float IS DISTINCT FROM table2.col_float)")
|
||||||
|
assertClauseSerialize(t, table1ColFloat.IS_DISTINCT_FROM(Float(2.11)), "(table1.col_float IS DISTINCT FROM $1)", float64(2.11))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFloatExpressionIS_NOT_DISTINCT_FROM(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColFloat.IS_NOT_DISTINCT_FROM(table2ColFloat), "(table1.col_float IS NOT DISTINCT FROM table2.col_float)")
|
||||||
|
assertClauseSerialize(t, table1ColFloat.IS_NOT_DISTINCT_FROM(Float(2.11)), "(table1.col_float IS NOT DISTINCT FROM $1)", float64(2.11))
|
||||||
|
}
|
||||||
|
|
||||||
func TestFloatExpressionGT(t *testing.T) {
|
func TestFloatExpressionGT(t *testing.T) {
|
||||||
assertClauseSerialize(t, table1ColFloat.GT(table2ColFloat), "(table1.col_float > table2.col_float)")
|
assertClauseSerialize(t, table1ColFloat.GT(table2ColFloat), "(table1.col_float > table2.col_float)")
|
||||||
assertClauseSerialize(t, table1ColFloat.GT(Float(2.11)), "(table1.col_float > $1)", float64(2.11))
|
assertClauseSerialize(t, table1ColFloat.GT(Float(2.11)), "(table1.col_float > $1)", float64(2.11))
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,13 @@ import (
|
||||||
"github.com/go-jet/jet/internal/utils"
|
"github.com/go-jet/jet/internal/utils"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DBConnection contains postgres connection details
|
// DBConnection contains postgres connection details
|
||||||
type DBConnection struct {
|
type DBConnection struct {
|
||||||
Host string
|
Host string
|
||||||
Port string
|
Port int
|
||||||
User string
|
User string
|
||||||
Password string
|
Password string
|
||||||
SslMode string
|
SslMode string
|
||||||
|
|
@ -27,7 +28,7 @@ type DBConnection struct {
|
||||||
func Generate(destDir string, dbConn DBConnection) error {
|
func Generate(destDir string, dbConn DBConnection) error {
|
||||||
|
|
||||||
connectionString := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s %s",
|
connectionString := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s %s",
|
||||||
dbConn.Host, dbConn.Port, dbConn.User, dbConn.Password, dbConn.DBName, dbConn.SslMode, dbConn.Params)
|
dbConn.Host, strconv.Itoa(dbConn.Port), dbConn.User, dbConn.Password, dbConn.DBName, dbConn.SslMode, dbConn.Params)
|
||||||
|
|
||||||
fmt.Println("Connecting to postgres database: " + connectionString)
|
fmt.Println("Connecting to postgres database: " + connectionString)
|
||||||
|
|
||||||
|
|
|
||||||
83
internal/3rdparty/snaker/snaker.go
vendored
83
internal/3rdparty/snaker/snaker.go
vendored
|
|
@ -8,42 +8,9 @@ import (
|
||||||
"unicode"
|
"unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CamelToSnake converts a given string to snake case
|
// SnakeToCamel returns a string converted from snake case to uppercase
|
||||||
func CamelToSnake(s string) string {
|
func SnakeToCamel(s string) string {
|
||||||
var result string
|
return snakeToCamel(s, true)
|
||||||
var words []string
|
|
||||||
var lastPos int
|
|
||||||
rs := []rune(s)
|
|
||||||
|
|
||||||
for i := 0; i < len(rs); i++ {
|
|
||||||
if i > 0 && unicode.IsUpper(rs[i]) {
|
|
||||||
if initialism := startsWithInitialism(s[lastPos:]); initialism != "" {
|
|
||||||
words = append(words, initialism)
|
|
||||||
|
|
||||||
i += len(initialism) - 1
|
|
||||||
lastPos = i
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
words = append(words, s[lastPos:i])
|
|
||||||
lastPos = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// append the last word
|
|
||||||
if s[lastPos:] != "" {
|
|
||||||
words = append(words, s[lastPos:])
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, word := range words {
|
|
||||||
if k > 0 {
|
|
||||||
result += "_"
|
|
||||||
}
|
|
||||||
|
|
||||||
result += strings.ToLower(word)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func snakeToCamel(s string, upperCase bool) string {
|
func snakeToCamel(s string, upperCase bool) string {
|
||||||
|
|
@ -54,28 +21,6 @@ func snakeToCamel(s string, upperCase bool) string {
|
||||||
|
|
||||||
words := strings.Split(s, "_")
|
words := strings.Split(s, "_")
|
||||||
|
|
||||||
//// if there is no underscore, first try commons and then just return
|
|
||||||
//if len(words) == 1 {
|
|
||||||
// if exception := snakeToCamelExceptions[words[0]]; len(exception) > 0 {
|
|
||||||
// return exception
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if upperCase {
|
|
||||||
// if upper := strings.ToUpper(words[0]); commonInitialisms[upper] {
|
|
||||||
// return upper
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// w := []rune(s)
|
|
||||||
// if upperCase {
|
|
||||||
// w[0] = unicode.ToUpper(w[0])
|
|
||||||
// } else {
|
|
||||||
// w[0] = unicode.ToLower(w[0])
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return string(w)
|
|
||||||
//}
|
|
||||||
|
|
||||||
for i, word := range words {
|
for i, word := range words {
|
||||||
if exception := snakeToCamelExceptions[word]; len(exception) > 0 {
|
if exception := snakeToCamelExceptions[word]; len(exception) > 0 {
|
||||||
result += exception
|
result += exception
|
||||||
|
|
@ -117,28 +62,6 @@ func camelizeWord(word string, force bool) string {
|
||||||
return string(runes)
|
return string(runes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SnakeToCamel returns a string converted from snake case to uppercase
|
|
||||||
func SnakeToCamel(s string) string {
|
|
||||||
return snakeToCamel(s, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SnakeToCamelLower returns a string converted from snake case to lowercase
|
|
||||||
func SnakeToCamelLower(s string) string {
|
|
||||||
return snakeToCamel(s, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// startsWithInitialism returns the initialism if the given string begins with it
|
|
||||||
func startsWithInitialism(s string) string {
|
|
||||||
var initialism string
|
|
||||||
// the longest initialism is 5 char, the shortest 2
|
|
||||||
for i := 1; i <= 5; i++ {
|
|
||||||
if len(s) > i-1 && commonInitialisms[s[:i]] {
|
|
||||||
initialism = s[:i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return initialism
|
|
||||||
}
|
|
||||||
|
|
||||||
// commonInitialisms, taken from
|
// commonInitialisms, taken from
|
||||||
// https://github.com/golang/lint/blob/206c0f020eba0f7fbcfbc467a5eb808037df2ed6/lint.go#L731
|
// https://github.com/golang/lint/blob/206c0f020eba0f7fbcfbc467a5eb808037df2ed6/lint.go#L731
|
||||||
var commonInitialisms = map[string]bool{
|
var commonInitialisms = map[string]bool{
|
||||||
|
|
|
||||||
81
internal/3rdparty/snaker/snaker_test.go
vendored
81
internal/3rdparty/snaker/snaker_test.go
vendored
|
|
@ -6,56 +6,6 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ = Describe("Snaker", func() {
|
var _ = Describe("Snaker", func() {
|
||||||
Describe("CamelToSnake test", func() {
|
|
||||||
It("should return an empty string on an empty input", func() {
|
|
||||||
Expect(CamelToSnake("")).To(Equal(""))
|
|
||||||
})
|
|
||||||
|
|
||||||
It("should work with one word", func() {
|
|
||||||
Expect(CamelToSnake("One")).To(Equal("one"))
|
|
||||||
})
|
|
||||||
|
|
||||||
It("should return an uppercase string as separate words", func() {
|
|
||||||
Expect(CamelToSnake("ONE")).To(Equal("o_n_e"))
|
|
||||||
})
|
|
||||||
|
|
||||||
It("should return ID as lowercase", func() {
|
|
||||||
Expect(CamelToSnake("ID")).To(Equal("id"))
|
|
||||||
})
|
|
||||||
|
|
||||||
It("should work with a single lowercase character", func() {
|
|
||||||
Expect(CamelToSnake("i")).To(Equal("i"))
|
|
||||||
})
|
|
||||||
|
|
||||||
It("should work with a single uppcase character", func() {
|
|
||||||
Expect(CamelToSnake("I")).To(Equal("i"))
|
|
||||||
})
|
|
||||||
|
|
||||||
It("should return a long text as expected", func() {
|
|
||||||
Expect(CamelToSnake("ThisHasToBeConvertedCorrectlyID")).To(
|
|
||||||
Equal("this_has_to_be_converted_correctly_id"))
|
|
||||||
})
|
|
||||||
|
|
||||||
It("should return the text as expected if the initialism is in the middle", func() {
|
|
||||||
Expect(CamelToSnake("ThisIDIsFine")).To(Equal("this_id_is_fine"))
|
|
||||||
})
|
|
||||||
|
|
||||||
It("should work with long initialism", func() {
|
|
||||||
Expect(CamelToSnake("ThisHTTPSConnection")).To(Equal("this_https_connection"))
|
|
||||||
})
|
|
||||||
|
|
||||||
It("should work with multi initialisms", func() {
|
|
||||||
Expect(CamelToSnake("HelloHTTPSConnectionID")).To(Equal("hello_https_connection_id"))
|
|
||||||
})
|
|
||||||
|
|
||||||
It("should work with concat initialisms", func() {
|
|
||||||
Expect(CamelToSnake("HTTPSID")).To(Equal("https_id"))
|
|
||||||
})
|
|
||||||
|
|
||||||
It("should work with initialism where only certain characters are uppercase", func() {
|
|
||||||
Expect(CamelToSnake("OAuthClient")).To(Equal("oauth_client"))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
Describe("SnakeToCamel test", func() {
|
Describe("SnakeToCamel test", func() {
|
||||||
It("should return an empty string on an empty input", func() {
|
It("should return an empty string on an empty input", func() {
|
||||||
|
|
@ -87,35 +37,4 @@ var _ = Describe("Snaker", func() {
|
||||||
Expect(SnakeToCamel("oauth_client")).To(Equal("OAuthClient"))
|
Expect(SnakeToCamel("oauth_client")).To(Equal("OAuthClient"))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
Describe("SnakeToCamelLower test", func() {
|
|
||||||
It("should return an empty string on an empty input", func() {
|
|
||||||
Ω(SnakeToCamelLower("")).To(Equal(""))
|
|
||||||
})
|
|
||||||
|
|
||||||
It("should not blow up on trailing _", func() {
|
|
||||||
Ω(SnakeToCamelLower("potato_")).To(Equal("potato"))
|
|
||||||
})
|
|
||||||
|
|
||||||
It("should return a snaked text as camel case", func() {
|
|
||||||
Ω(SnakeToCamelLower("this_has_to_be_uppercased")).To(
|
|
||||||
Equal("thisHasToBeUppercased"))
|
|
||||||
})
|
|
||||||
|
|
||||||
It("should return a snaked text as camel case, except the word ID", func() {
|
|
||||||
Ω(SnakeToCamelLower("this_is_an_id")).To(Equal("thisIsAnID"))
|
|
||||||
})
|
|
||||||
|
|
||||||
It("should return 'id' not as uppercase", func() {
|
|
||||||
Ω(SnakeToCamelLower("this_is_an_identifier")).To(Equal("thisIsAnIdentifier"))
|
|
||||||
})
|
|
||||||
|
|
||||||
It("should simply work with id", func() {
|
|
||||||
Ω(SnakeToCamelLower("id")).To(Equal("id"))
|
|
||||||
})
|
|
||||||
|
|
||||||
It("should simply work with leading id", func() {
|
|
||||||
Ω(SnakeToCamelLower("id_me_please")).To(Equal("idMePlease"))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,6 @@ import (
|
||||||
|
|
||||||
// ToGoIdentifier converts database to Go identifier.
|
// ToGoIdentifier converts database to Go identifier.
|
||||||
func ToGoIdentifier(databaseIdentifier string) string {
|
func ToGoIdentifier(databaseIdentifier string) string {
|
||||||
if len(databaseIdentifier) == 0 {
|
|
||||||
return databaseIdentifier
|
|
||||||
}
|
|
||||||
|
|
||||||
return snaker.SnakeToCamel(replaceInvalidChars(databaseIdentifier))
|
return snaker.SnakeToCamel(replaceInvalidChars(databaseIdentifier))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestToGoIdentifier(t *testing.T) {
|
func TestToGoIdentifier(t *testing.T) {
|
||||||
|
assert.Equal(t, ToGoIdentifier(""), "")
|
||||||
assert.Equal(t, ToGoIdentifier("uuid"), "UUID")
|
assert.Equal(t, ToGoIdentifier("uuid"), "UUID")
|
||||||
assert.Equal(t, ToGoIdentifier("col1"), "Col1")
|
assert.Equal(t, ToGoIdentifier("col1"), "Col1")
|
||||||
assert.Equal(t, ToGoIdentifier("PG-13"), "Pg13")
|
assert.Equal(t, ToGoIdentifier("PG-13"), "Pg13")
|
||||||
|
|
|
||||||
|
|
@ -122,3 +122,91 @@ FROM db.table1
|
||||||
FOR NO KEY UPDATE SKIP LOCKED;
|
FOR NO KEY UPDATE SKIP LOCKED;
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSelectSets(t *testing.T) {
|
||||||
|
select1 := SELECT(table1ColBool).FROM(table1)
|
||||||
|
select2 := SELECT(table2ColBool).FROM(table2)
|
||||||
|
|
||||||
|
assertStatement(t, select1.UNION(select2), `
|
||||||
|
(
|
||||||
|
(
|
||||||
|
SELECT table1.col_bool AS "table1.col_bool"
|
||||||
|
FROM db.table1
|
||||||
|
)
|
||||||
|
UNION
|
||||||
|
(
|
||||||
|
SELECT table2.col_bool AS "table2.col_bool"
|
||||||
|
FROM db.table2
|
||||||
|
)
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
assertStatement(t, select1.UNION_ALL(select2), `
|
||||||
|
(
|
||||||
|
(
|
||||||
|
SELECT table1.col_bool AS "table1.col_bool"
|
||||||
|
FROM db.table1
|
||||||
|
)
|
||||||
|
UNION ALL
|
||||||
|
(
|
||||||
|
SELECT table2.col_bool AS "table2.col_bool"
|
||||||
|
FROM db.table2
|
||||||
|
)
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
|
||||||
|
assertStatement(t, select1.INTERSECT(select2), `
|
||||||
|
(
|
||||||
|
(
|
||||||
|
SELECT table1.col_bool AS "table1.col_bool"
|
||||||
|
FROM db.table1
|
||||||
|
)
|
||||||
|
INTERSECT
|
||||||
|
(
|
||||||
|
SELECT table2.col_bool AS "table2.col_bool"
|
||||||
|
FROM db.table2
|
||||||
|
)
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
|
||||||
|
assertStatement(t, select1.INTERSECT_ALL(select2), `
|
||||||
|
(
|
||||||
|
(
|
||||||
|
SELECT table1.col_bool AS "table1.col_bool"
|
||||||
|
FROM db.table1
|
||||||
|
)
|
||||||
|
INTERSECT ALL
|
||||||
|
(
|
||||||
|
SELECT table2.col_bool AS "table2.col_bool"
|
||||||
|
FROM db.table2
|
||||||
|
)
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
assertStatement(t, select1.EXCEPT(select2), `
|
||||||
|
(
|
||||||
|
(
|
||||||
|
SELECT table1.col_bool AS "table1.col_bool"
|
||||||
|
FROM db.table1
|
||||||
|
)
|
||||||
|
EXCEPT
|
||||||
|
(
|
||||||
|
SELECT table2.col_bool AS "table2.col_bool"
|
||||||
|
FROM db.table2
|
||||||
|
)
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
|
||||||
|
assertStatement(t, select1.EXCEPT_ALL(select2), `
|
||||||
|
(
|
||||||
|
(
|
||||||
|
SELECT table1.col_bool AS "table1.col_bool"
|
||||||
|
FROM db.table1
|
||||||
|
)
|
||||||
|
EXCEPT ALL
|
||||||
|
(
|
||||||
|
SELECT table2.col_bool AS "table2.col_bool"
|
||||||
|
FROM db.table2
|
||||||
|
)
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,16 @@ func TestStringNOT_EQ(t *testing.T) {
|
||||||
assertClauseSerialize(t, table3StrCol.NOT_EQ(String("JOHN")), "(table3.col2 != $1)", "JOHN")
|
assertClauseSerialize(t, table3StrCol.NOT_EQ(String("JOHN")), "(table3.col2 != $1)", "JOHN")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStringExpressionIS_DISTINCT_FROM(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table3StrCol.IS_DISTINCT_FROM(table2ColStr), "(table3.col2 IS DISTINCT FROM table2.col_str)")
|
||||||
|
assertClauseSerialize(t, table3StrCol.IS_DISTINCT_FROM(String("JOHN")), "(table3.col2 IS DISTINCT FROM $1)", "JOHN")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStringExpressionIS_NOT_DISTINCT_FROM(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table3StrCol.IS_NOT_DISTINCT_FROM(table2ColStr), "(table3.col2 IS NOT DISTINCT FROM table2.col_str)")
|
||||||
|
assertClauseSerialize(t, table3StrCol.IS_NOT_DISTINCT_FROM(String("JOHN")), "(table3.col2 IS NOT DISTINCT FROM $1)", "JOHN")
|
||||||
|
}
|
||||||
|
|
||||||
func TestStringGT(t *testing.T) {
|
func TestStringGT(t *testing.T) {
|
||||||
exp := table3StrCol.GT(table2ColStr)
|
exp := table3StrCol.GT(table2ColStr)
|
||||||
assertClauseSerialize(t, exp, "(table3.col2 > table2.col_str)")
|
assertClauseSerialize(t, exp, "(table3.col2 > table2.col_str)")
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ func main() {
|
||||||
|
|
||||||
err = postgres.Generate("./.gentestdata", postgres.DBConnection{
|
err = postgres.Generate("./.gentestdata", postgres.DBConnection{
|
||||||
Host: dbconfig.Host,
|
Host: dbconfig.Host,
|
||||||
Port: "5432",
|
Port: 5432,
|
||||||
User: dbconfig.User,
|
User: dbconfig.User,
|
||||||
Password: dbconfig.Password,
|
Password: dbconfig.Password,
|
||||||
DBName: dbconfig.DBName,
|
DBName: dbconfig.DBName,
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,28 @@ INSERT INTO test_sample.link (url, name) VALUES
|
||||||
assertExec(t, query, 1)
|
assertExec(t, query, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInsertModelObjectEmptyColumnList(t *testing.T) {
|
||||||
|
cleanUpLinkTable(t)
|
||||||
|
var expectedSQL = `
|
||||||
|
INSERT INTO test_sample.link VALUES
|
||||||
|
(1000, 'http://www.duckduckgo.com', 'Duck Duck go', NULL);
|
||||||
|
`
|
||||||
|
|
||||||
|
linkData := model.Link{
|
||||||
|
ID: 1000,
|
||||||
|
URL: "http://www.duckduckgo.com",
|
||||||
|
Name: "Duck Duck go",
|
||||||
|
}
|
||||||
|
|
||||||
|
query := Link.
|
||||||
|
INSERT().
|
||||||
|
MODEL(linkData)
|
||||||
|
|
||||||
|
assertStatementSql(t, query, expectedSQL, int32(1000), "http://www.duckduckgo.com", "Duck Duck go", nil)
|
||||||
|
|
||||||
|
assertExec(t, query, 1)
|
||||||
|
}
|
||||||
|
|
||||||
func TestInsertModelsObject(t *testing.T) {
|
func TestInsertModelsObject(t *testing.T) {
|
||||||
expectedSQL := `
|
expectedSQL := `
|
||||||
INSERT INTO test_sample.link (url, name) VALUES
|
INSERT INTO test_sample.link (url, name) VALUES
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,17 @@
|
||||||
package tests
|
package tests
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"github.com/go-jet/jet/generator/postgres"
|
||||||
"github.com/go-jet/jet/tests/.gentestdata/jetdb/dvds/model"
|
"github.com/go-jet/jet/tests/.gentestdata/jetdb/dvds/model"
|
||||||
"github.com/go-jet/jet/tests/dbconfig"
|
"github.com/go-jet/jet/tests/dbconfig"
|
||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
"github.com/pkg/profile"
|
"github.com/pkg/profile"
|
||||||
"gotest.tools/assert"
|
"gotest.tools/assert"
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
@ -29,7 +33,7 @@ func TestMain(m *testing.M) {
|
||||||
os.Exit(ret)
|
os.Exit(ret)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGenerateModel(t *testing.T) {
|
func TestGeneratedModel(t *testing.T) {
|
||||||
|
|
||||||
actor := model.Actor{}
|
actor := model.Actor{}
|
||||||
|
|
||||||
|
|
@ -58,3 +62,188 @@ func TestGenerateModel(t *testing.T) {
|
||||||
assert.Equal(t, reflect.TypeOf(staff.Email).String(), "*string")
|
assert.Equal(t, reflect.TypeOf(staff.Email).String(), "*string")
|
||||||
assert.Equal(t, reflect.TypeOf(staff.Picture).String(), "*[]uint8")
|
assert.Equal(t, reflect.TypeOf(staff.Picture).String(), "*[]uint8")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const genTestDir2 = "./.gentestdata2"
|
||||||
|
|
||||||
|
func TestCmdGenerator(t *testing.T) {
|
||||||
|
err := os.RemoveAll(genTestDir2)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
cmd := exec.Command("jet", "-dbname=jetdb", "-host=localhost", "-port=5432",
|
||||||
|
"-user=jet", "-password=jet", "-schema=dvds", "-path="+genTestDir2)
|
||||||
|
|
||||||
|
err = cmd.Run()
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
assertGeneratedFiles(t)
|
||||||
|
|
||||||
|
err = os.RemoveAll(genTestDir2)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerator(t *testing.T) {
|
||||||
|
|
||||||
|
err := os.RemoveAll(genTestDir2)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
err = postgres.Generate(genTestDir2, postgres.DBConnection{
|
||||||
|
Host: dbconfig.Host,
|
||||||
|
Port: dbconfig.Port,
|
||||||
|
User: dbconfig.User,
|
||||||
|
Password: dbconfig.Password,
|
||||||
|
SslMode: "disable",
|
||||||
|
Params: "",
|
||||||
|
|
||||||
|
DBName: dbconfig.DBName,
|
||||||
|
SchemaName: "dvds",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
assertGeneratedFiles(t)
|
||||||
|
|
||||||
|
err = os.RemoveAll(genTestDir2)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertGeneratedFiles(t *testing.T) {
|
||||||
|
// Table SQL Builder files
|
||||||
|
tableSQLBuilderFiles, err := ioutil.ReadDir("./.gentestdata2/jetdb/dvds/table")
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
assertFileNameEqual(t, tableSQLBuilderFiles, "actor.go", "address.go", "category.go", "city.go", "country.go",
|
||||||
|
"customer.go", "film.go", "film_actor.go", "film_category.go", "inventory.go", "language.go",
|
||||||
|
"payment.go", "rental.go", "staff.go", "store.go")
|
||||||
|
|
||||||
|
assertFileContent(t, "./.gentestdata2/jetdb/dvds/table/actor.go", "\npackage table", actorSQLBuilderFile)
|
||||||
|
|
||||||
|
// Enums SQL Builder files
|
||||||
|
enumFiles, err := ioutil.ReadDir("./.gentestdata2/jetdb/dvds/enum")
|
||||||
|
|
||||||
|
assertFileNameEqual(t, enumFiles, "mpaa_rating.go")
|
||||||
|
assertFileContent(t, "./.gentestdata2/jetdb/dvds/enum/mpaa_rating.go", "\npackage enum", mpaaRatingEnumFile)
|
||||||
|
|
||||||
|
// Model files
|
||||||
|
modelFiles, err := ioutil.ReadDir("./.gentestdata2/jetdb/dvds/model")
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
assertFileNameEqual(t, modelFiles, "actor.go", "address.go", "category.go", "city.go", "country.go",
|
||||||
|
"customer.go", "film.go", "film_actor.go", "film_category.go", "inventory.go", "language.go",
|
||||||
|
"payment.go", "rental.go", "staff.go", "store.go", "mpaa_rating.go")
|
||||||
|
|
||||||
|
assertFileContent(t, "./.gentestdata2/jetdb/dvds/model/actor.go", "\npackage model", actorModelFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertFileContent(t *testing.T, filePath string, contentBegin string, expectedContent string) {
|
||||||
|
enumFileData, err := ioutil.ReadFile(filePath)
|
||||||
|
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
beginIndex := bytes.Index(enumFileData, []byte(contentBegin))
|
||||||
|
|
||||||
|
//fmt.Println("-"+string(enumFileData[beginIndex:])+"-")
|
||||||
|
|
||||||
|
assert.DeepEqual(t, string(enumFileData[beginIndex:]), expectedContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertFileNameEqual(t *testing.T, fileInfos []os.FileInfo, fileNames ...string) {
|
||||||
|
|
||||||
|
fileNamesMap := map[string]bool{}
|
||||||
|
|
||||||
|
for _, fileInfo := range fileInfos {
|
||||||
|
fileNamesMap[fileInfo.Name()] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, fileName := range fileNames {
|
||||||
|
assert.Assert(t, fileNamesMap[fileName], fileName+" does not exist.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var mpaaRatingEnumFile = `
|
||||||
|
package enum
|
||||||
|
|
||||||
|
import "github.com/go-jet/jet"
|
||||||
|
|
||||||
|
var MpaaRating = &struct {
|
||||||
|
G jet.StringExpression
|
||||||
|
Pg jet.StringExpression
|
||||||
|
Pg13 jet.StringExpression
|
||||||
|
R jet.StringExpression
|
||||||
|
Nc17 jet.StringExpression
|
||||||
|
}{
|
||||||
|
G: jet.NewEnumValue("G"),
|
||||||
|
Pg: jet.NewEnumValue("PG"),
|
||||||
|
Pg13: jet.NewEnumValue("PG-13"),
|
||||||
|
R: jet.NewEnumValue("R"),
|
||||||
|
Nc17: jet.NewEnumValue("NC-17"),
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var actorSQLBuilderFile = `
|
||||||
|
package table
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-jet/jet"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Actor = newActorTable()
|
||||||
|
|
||||||
|
type ActorTable struct {
|
||||||
|
jet.Table
|
||||||
|
|
||||||
|
//Columns
|
||||||
|
ActorID jet.ColumnInteger
|
||||||
|
FirstName jet.ColumnString
|
||||||
|
LastName jet.ColumnString
|
||||||
|
LastUpdate jet.ColumnTimestamp
|
||||||
|
|
||||||
|
AllColumns jet.ColumnList
|
||||||
|
MutableColumns jet.ColumnList
|
||||||
|
}
|
||||||
|
|
||||||
|
// creates new ActorTable with assigned alias
|
||||||
|
func (a *ActorTable) AS(alias string) *ActorTable {
|
||||||
|
aliasTable := newActorTable()
|
||||||
|
|
||||||
|
aliasTable.Table.AS(alias)
|
||||||
|
|
||||||
|
return aliasTable
|
||||||
|
}
|
||||||
|
|
||||||
|
func newActorTable() *ActorTable {
|
||||||
|
var (
|
||||||
|
ActorIDColumn = jet.IntegerColumn("actor_id")
|
||||||
|
FirstNameColumn = jet.StringColumn("first_name")
|
||||||
|
LastNameColumn = jet.StringColumn("last_name")
|
||||||
|
LastUpdateColumn = jet.TimestampColumn("last_update")
|
||||||
|
)
|
||||||
|
|
||||||
|
return &ActorTable{
|
||||||
|
Table: jet.NewTable("dvds", "actor", ActorIDColumn, FirstNameColumn, LastNameColumn, LastUpdateColumn),
|
||||||
|
|
||||||
|
//Columns
|
||||||
|
ActorID: ActorIDColumn,
|
||||||
|
FirstName: FirstNameColumn,
|
||||||
|
LastName: LastNameColumn,
|
||||||
|
LastUpdate: LastUpdateColumn,
|
||||||
|
|
||||||
|
AllColumns: jet.ColumnList{ActorIDColumn, FirstNameColumn, LastNameColumn, LastUpdateColumn},
|
||||||
|
MutableColumns: jet.ColumnList{FirstNameColumn, LastNameColumn, LastUpdateColumn},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var actorModelFile = `
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Actor struct {
|
||||||
|
ActorID int32 ` + "`sql:\"primary_key\"`" + `
|
||||||
|
FirstName string
|
||||||
|
LastName string
|
||||||
|
LastUpdate time.Time
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
|
||||||
|
|
@ -4,34 +4,46 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var timeVar = Time(10, 20, 0, 0)
|
||||||
|
|
||||||
func TestTimeExpressionEQ(t *testing.T) {
|
func TestTimeExpressionEQ(t *testing.T) {
|
||||||
assertClauseSerialize(t, table1ColTime.EQ(table2ColTime), "(table1.col_time = table2.col_time)")
|
assertClauseSerialize(t, table1ColTime.EQ(table2ColTime), "(table1.col_time = table2.col_time)")
|
||||||
assertClauseSerialize(t, table1ColTime.EQ(Time(10, 20, 0, 0)), "(table1.col_time = $1::time without time zone)", "10:20:00.000")
|
assertClauseSerialize(t, table1ColTime.EQ(timeVar), "(table1.col_time = $1::time without time zone)", "10:20:00.000")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTimeExpressionNOT_EQ(t *testing.T) {
|
func TestTimeExpressionNOT_EQ(t *testing.T) {
|
||||||
assertClauseSerialize(t, table1ColTime.NOT_EQ(table2ColTime), "(table1.col_time != table2.col_time)")
|
assertClauseSerialize(t, table1ColTime.NOT_EQ(table2ColTime), "(table1.col_time != table2.col_time)")
|
||||||
assertClauseSerialize(t, table1ColTime.NOT_EQ(Time(10, 20, 0, 0)), "(table1.col_time != $1::time without time zone)", "10:20:00.000")
|
assertClauseSerialize(t, table1ColTime.NOT_EQ(timeVar), "(table1.col_time != $1::time without time zone)", "10:20:00.000")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimeExpressionIS_DISTINCT_FROM(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColTime.IS_DISTINCT_FROM(table2ColTime), "(table1.col_time IS DISTINCT FROM table2.col_time)")
|
||||||
|
assertClauseSerialize(t, table1ColTime.IS_DISTINCT_FROM(timeVar), "(table1.col_time IS DISTINCT FROM $1::time without time zone)", "10:20:00.000")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimeExpressionIS_NOT_DISTINCT_FROM(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColTime.IS_NOT_DISTINCT_FROM(table2ColTime), "(table1.col_time IS NOT DISTINCT FROM table2.col_time)")
|
||||||
|
assertClauseSerialize(t, table1ColTime.IS_NOT_DISTINCT_FROM(timeVar), "(table1.col_time IS NOT DISTINCT FROM $1::time without time zone)", "10:20:00.000")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTimeExpressionLT(t *testing.T) {
|
func TestTimeExpressionLT(t *testing.T) {
|
||||||
assertClauseSerialize(t, table1ColTime.LT(table2ColTime), "(table1.col_time < table2.col_time)")
|
assertClauseSerialize(t, table1ColTime.LT(table2ColTime), "(table1.col_time < table2.col_time)")
|
||||||
assertClauseSerialize(t, table1ColTime.LT(Time(10, 20, 0, 0)), "(table1.col_time < $1::time without time zone)", "10:20:00.000")
|
assertClauseSerialize(t, table1ColTime.LT(timeVar), "(table1.col_time < $1::time without time zone)", "10:20:00.000")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTimeExpressionLT_EQ(t *testing.T) {
|
func TestTimeExpressionLT_EQ(t *testing.T) {
|
||||||
assertClauseSerialize(t, table1ColTime.LT_EQ(table2ColTime), "(table1.col_time <= table2.col_time)")
|
assertClauseSerialize(t, table1ColTime.LT_EQ(table2ColTime), "(table1.col_time <= table2.col_time)")
|
||||||
assertClauseSerialize(t, table1ColTime.LT_EQ(Time(10, 20, 0, 0)), "(table1.col_time <= $1::time without time zone)", "10:20:00.000")
|
assertClauseSerialize(t, table1ColTime.LT_EQ(timeVar), "(table1.col_time <= $1::time without time zone)", "10:20:00.000")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTimeExpressionGT(t *testing.T) {
|
func TestTimeExpressionGT(t *testing.T) {
|
||||||
assertClauseSerialize(t, table1ColTime.GT(table2ColTime), "(table1.col_time > table2.col_time)")
|
assertClauseSerialize(t, table1ColTime.GT(table2ColTime), "(table1.col_time > table2.col_time)")
|
||||||
assertClauseSerialize(t, table1ColTime.GT(Time(10, 20, 0, 0)), "(table1.col_time > $1::time without time zone)", "10:20:00.000")
|
assertClauseSerialize(t, table1ColTime.GT(timeVar), "(table1.col_time > $1::time without time zone)", "10:20:00.000")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTimeExpressionGT_EQ(t *testing.T) {
|
func TestTimeExpressionGT_EQ(t *testing.T) {
|
||||||
assertClauseSerialize(t, table1ColTime.GT_EQ(table2ColTime), "(table1.col_time >= table2.col_time)")
|
assertClauseSerialize(t, table1ColTime.GT_EQ(table2ColTime), "(table1.col_time >= table2.col_time)")
|
||||||
assertClauseSerialize(t, table1ColTime.GT_EQ(Time(10, 20, 0, 0)), "(table1.col_time >= $1::time without time zone)", "10:20:00.000")
|
assertClauseSerialize(t, table1ColTime.GT_EQ(timeVar), "(table1.col_time >= $1::time without time zone)", "10:20:00.000")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTimeExp(t *testing.T) {
|
func TestTimeExp(t *testing.T) {
|
||||||
|
|
|
||||||
52
timestamp_expression_test.go
Normal file
52
timestamp_expression_test.go
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
package jet
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
var timestamp = Timestamp(2000, 1, 31, 10, 20, 0, 0)
|
||||||
|
|
||||||
|
func TestTimestampExpressionEQ(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColTimestamp.EQ(table2ColTimestamp), "(table1.col_timestamp = table2.col_timestamp)")
|
||||||
|
assertClauseSerialize(t, table1ColTimestamp.EQ(timestamp),
|
||||||
|
"(table1.col_timestamp = $1::timestamp without time zone)", "2000-01-31 10:20:00.000")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimestampExpressionNOT_EQ(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColTimestamp.NOT_EQ(table2ColTimestamp), "(table1.col_timestamp != table2.col_timestamp)")
|
||||||
|
assertClauseSerialize(t, table1ColTimestamp.NOT_EQ(timestamp), "(table1.col_timestamp != $1::timestamp without time zone)", "2000-01-31 10:20:00.000")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimestampExpressionIS_DISTINCT_FROM(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColTimestamp.IS_DISTINCT_FROM(table2ColTimestamp), "(table1.col_timestamp IS DISTINCT FROM table2.col_timestamp)")
|
||||||
|
assertClauseSerialize(t, table1ColTimestamp.IS_DISTINCT_FROM(timestamp), "(table1.col_timestamp IS DISTINCT FROM $1::timestamp without time zone)", "2000-01-31 10:20:00.000")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimestampExpressionIS_NOT_DISTINCT_FROM(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColTimestamp.IS_NOT_DISTINCT_FROM(table2ColTimestamp), "(table1.col_timestamp IS NOT DISTINCT FROM table2.col_timestamp)")
|
||||||
|
assertClauseSerialize(t, table1ColTimestamp.IS_NOT_DISTINCT_FROM(timestamp), "(table1.col_timestamp IS NOT DISTINCT FROM $1::timestamp without time zone)", "2000-01-31 10:20:00.000")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimestampExpressionLT(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColTimestamp.LT(table2ColTimestamp), "(table1.col_timestamp < table2.col_timestamp)")
|
||||||
|
assertClauseSerialize(t, table1ColTimestamp.LT(timestamp), "(table1.col_timestamp < $1::timestamp without time zone)", "2000-01-31 10:20:00.000")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimestampExpressionLT_EQ(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColTimestamp.LT_EQ(table2ColTimestamp), "(table1.col_timestamp <= table2.col_timestamp)")
|
||||||
|
assertClauseSerialize(t, table1ColTimestamp.LT_EQ(timestamp), "(table1.col_timestamp <= $1::timestamp without time zone)", "2000-01-31 10:20:00.000")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimestampExpressionGT(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColTimestamp.GT(table2ColTimestamp), "(table1.col_timestamp > table2.col_timestamp)")
|
||||||
|
assertClauseSerialize(t, table1ColTimestamp.GT(timestamp), "(table1.col_timestamp > $1::timestamp without time zone)", "2000-01-31 10:20:00.000")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimestampExpressionGT_EQ(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColTimestamp.GT_EQ(table2ColTimestamp), "(table1.col_timestamp >= table2.col_timestamp)")
|
||||||
|
assertClauseSerialize(t, table1ColTimestamp.GT_EQ(timestamp), "(table1.col_timestamp >= $1::timestamp without time zone)", "2000-01-31 10:20:00.000")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimestampExp(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, TimestampExp(table1ColFloat), "table1.col_float")
|
||||||
|
assertClauseSerialize(t, TimestampExp(table1ColFloat).LT(timestamp),
|
||||||
|
"(table1.col_float < $1::timestamp without time zone)", "2000-01-31 10:20:00.000")
|
||||||
|
}
|
||||||
52
timestampz_expression_test.go
Normal file
52
timestampz_expression_test.go
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
package jet
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
var timestampz = Timestampz(2000, 1, 31, 10, 20, 0, 0, 2)
|
||||||
|
|
||||||
|
func TestTimestampzExpressionEQ(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColTimestampz.EQ(table2ColTimestampz), "(table1.col_timestampz = table2.col_timestampz)")
|
||||||
|
assertClauseSerialize(t, table1ColTimestampz.EQ(timestampz),
|
||||||
|
"(table1.col_timestampz = $1::timestamp with time zone)", "2000-01-31 10:20:00.000 +002")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimestampzExpressionNOT_EQ(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColTimestampz.NOT_EQ(table2ColTimestampz), "(table1.col_timestampz != table2.col_timestampz)")
|
||||||
|
assertClauseSerialize(t, table1ColTimestampz.NOT_EQ(timestampz), "(table1.col_timestampz != $1::timestamp with time zone)", "2000-01-31 10:20:00.000 +002")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimestampzExpressionIS_DISTINCT_FROM(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColTimestampz.IS_DISTINCT_FROM(table2ColTimestampz), "(table1.col_timestampz IS DISTINCT FROM table2.col_timestampz)")
|
||||||
|
assertClauseSerialize(t, table1ColTimestampz.IS_DISTINCT_FROM(timestampz), "(table1.col_timestampz IS DISTINCT FROM $1::timestamp with time zone)", "2000-01-31 10:20:00.000 +002")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimestampzExpressionIS_NOT_DISTINCT_FROM(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColTimestampz.IS_NOT_DISTINCT_FROM(table2ColTimestampz), "(table1.col_timestampz IS NOT DISTINCT FROM table2.col_timestampz)")
|
||||||
|
assertClauseSerialize(t, table1ColTimestampz.IS_NOT_DISTINCT_FROM(timestampz), "(table1.col_timestampz IS NOT DISTINCT FROM $1::timestamp with time zone)", "2000-01-31 10:20:00.000 +002")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimestampzExpressionLT(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColTimestampz.LT(table2ColTimestampz), "(table1.col_timestampz < table2.col_timestampz)")
|
||||||
|
assertClauseSerialize(t, table1ColTimestampz.LT(timestampz), "(table1.col_timestampz < $1::timestamp with time zone)", "2000-01-31 10:20:00.000 +002")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimestampzExpressionLT_EQ(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColTimestampz.LT_EQ(table2ColTimestampz), "(table1.col_timestampz <= table2.col_timestampz)")
|
||||||
|
assertClauseSerialize(t, table1ColTimestampz.LT_EQ(timestampz), "(table1.col_timestampz <= $1::timestamp with time zone)", "2000-01-31 10:20:00.000 +002")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimestampzExpressionGT(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColTimestampz.GT(table2ColTimestampz), "(table1.col_timestampz > table2.col_timestampz)")
|
||||||
|
assertClauseSerialize(t, table1ColTimestampz.GT(timestampz), "(table1.col_timestampz > $1::timestamp with time zone)", "2000-01-31 10:20:00.000 +002")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimestampzExpressionGT_EQ(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColTimestampz.GT_EQ(table2ColTimestampz), "(table1.col_timestampz >= table2.col_timestampz)")
|
||||||
|
assertClauseSerialize(t, table1ColTimestampz.GT_EQ(timestampz), "(table1.col_timestampz >= $1::timestamp with time zone)", "2000-01-31 10:20:00.000 +002")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimestampzExp(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, TimestampzExp(table1ColFloat), "table1.col_float")
|
||||||
|
assertClauseSerialize(t, TimestampzExp(table1ColFloat).LT(timestampz),
|
||||||
|
"(table1.col_float < $1::timestamp with time zone)", "2000-01-31 10:20:00.000 +002")
|
||||||
|
}
|
||||||
51
timez_expression_test.go
Normal file
51
timez_expression_test.go
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
package jet
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
var timezVar = Timez(10, 20, 0, 0, 4)
|
||||||
|
|
||||||
|
func TestTimezExpressionEQ(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColTimez.EQ(table2ColTimez), "(table1.col_timez = table2.col_timez)")
|
||||||
|
assertClauseSerialize(t, table1ColTimez.EQ(timezVar), "(table1.col_timez = $1::time with time zone)", "10:20:00.000 +04")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimezExpressionNOT_EQ(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColTimez.NOT_EQ(table2ColTimez), "(table1.col_timez != table2.col_timez)")
|
||||||
|
assertClauseSerialize(t, table1ColTimez.NOT_EQ(timezVar), "(table1.col_timez != $1::time with time zone)", "10:20:00.000 +04")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimezExpressionIS_DISTINCT_FROM(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColTimez.IS_DISTINCT_FROM(table2ColTimez), "(table1.col_timez IS DISTINCT FROM table2.col_timez)")
|
||||||
|
assertClauseSerialize(t, table1ColTimez.IS_DISTINCT_FROM(timezVar), "(table1.col_timez IS DISTINCT FROM $1::time with time zone)", "10:20:00.000 +04")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimezExpressionIS_NOT_DISTINCT_FROM(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColTimez.IS_NOT_DISTINCT_FROM(table2ColTimez), "(table1.col_timez IS NOT DISTINCT FROM table2.col_timez)")
|
||||||
|
assertClauseSerialize(t, table1ColTimez.IS_NOT_DISTINCT_FROM(timezVar), "(table1.col_timez IS NOT DISTINCT FROM $1::time with time zone)", "10:20:00.000 +04")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimezExpressionLT(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColTimez.LT(table2ColTimez), "(table1.col_timez < table2.col_timez)")
|
||||||
|
assertClauseSerialize(t, table1ColTimez.LT(timezVar), "(table1.col_timez < $1::time with time zone)", "10:20:00.000 +04")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimezExpressionLT_EQ(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColTimez.LT_EQ(table2ColTimez), "(table1.col_timez <= table2.col_timez)")
|
||||||
|
assertClauseSerialize(t, table1ColTimez.LT_EQ(timezVar), "(table1.col_timez <= $1::time with time zone)", "10:20:00.000 +04")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimezExpressionGT(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColTimez.GT(table2ColTimez), "(table1.col_timez > table2.col_timez)")
|
||||||
|
assertClauseSerialize(t, table1ColTimez.GT(timezVar), "(table1.col_timez > $1::time with time zone)", "10:20:00.000 +04")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimezExpressionGT_EQ(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColTimez.GT_EQ(table2ColTimez), "(table1.col_timez >= table2.col_timez)")
|
||||||
|
assertClauseSerialize(t, table1ColTimez.GT_EQ(timezVar), "(table1.col_timez >= $1::time with time zone)", "10:20:00.000 +04")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimezExp(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, TimezExp(table1ColFloat), "table1.col_float")
|
||||||
|
assertClauseSerialize(t, TimezExp(table1ColFloat).LT(Timez(1, 1, 1, 1, 4)),
|
||||||
|
"(table1.col_float < $1::time with time zone)", string("01:01:01.001 +04"))
|
||||||
|
}
|
||||||
|
|
@ -10,7 +10,11 @@ var table1ColInt = IntegerColumn("col_int")
|
||||||
var table1ColFloat = FloatColumn("col_float")
|
var table1ColFloat = FloatColumn("col_float")
|
||||||
var table1Col3 = IntegerColumn("col3")
|
var table1Col3 = IntegerColumn("col3")
|
||||||
var table1ColTime = TimeColumn("col_time")
|
var table1ColTime = TimeColumn("col_time")
|
||||||
|
var table1ColTimez = TimezColumn("col_timez")
|
||||||
|
var table1ColTimestamp = TimestampColumn("col_timestamp")
|
||||||
|
var table1ColTimestampz = TimestampzColumn("col_timestampz")
|
||||||
var table1ColBool = BoolColumn("col_bool")
|
var table1ColBool = BoolColumn("col_bool")
|
||||||
|
var table1ColDate = DateColumn("col_date")
|
||||||
|
|
||||||
var table1 = NewTable(
|
var table1 = NewTable(
|
||||||
"db",
|
"db",
|
||||||
|
|
@ -20,7 +24,12 @@ var table1 = NewTable(
|
||||||
table1ColFloat,
|
table1ColFloat,
|
||||||
table1Col3,
|
table1Col3,
|
||||||
table1ColTime,
|
table1ColTime,
|
||||||
table1ColBool)
|
table1ColTimez,
|
||||||
|
table1ColBool,
|
||||||
|
table1ColDate,
|
||||||
|
table1ColTimestamp,
|
||||||
|
table1ColTimestampz,
|
||||||
|
)
|
||||||
|
|
||||||
var table2Col3 = IntegerColumn("col3")
|
var table2Col3 = IntegerColumn("col3")
|
||||||
var table2Col4 = IntegerColumn("col4")
|
var table2Col4 = IntegerColumn("col4")
|
||||||
|
|
@ -29,6 +38,10 @@ var table2ColFloat = FloatColumn("col_float")
|
||||||
var table2ColStr = StringColumn("col_str")
|
var table2ColStr = StringColumn("col_str")
|
||||||
var table2ColBool = BoolColumn("col_bool")
|
var table2ColBool = BoolColumn("col_bool")
|
||||||
var table2ColTime = TimeColumn("col_time")
|
var table2ColTime = TimeColumn("col_time")
|
||||||
|
var table2ColTimez = TimezColumn("col_timez")
|
||||||
|
var table2ColTimestamp = TimestampColumn("col_timestamp")
|
||||||
|
var table2ColTimestampz = TimestampzColumn("col_timestampz")
|
||||||
|
var table2ColDate = DateColumn("col_date")
|
||||||
|
|
||||||
var table2 = NewTable(
|
var table2 = NewTable(
|
||||||
"db",
|
"db",
|
||||||
|
|
@ -39,7 +52,12 @@ var table2 = NewTable(
|
||||||
table2ColFloat,
|
table2ColFloat,
|
||||||
table2ColStr,
|
table2ColStr,
|
||||||
table2ColBool,
|
table2ColBool,
|
||||||
table2ColTime)
|
table2ColTime,
|
||||||
|
table2ColTimez,
|
||||||
|
table2ColDate,
|
||||||
|
table2ColTimestamp,
|
||||||
|
table2ColTimestampz,
|
||||||
|
)
|
||||||
|
|
||||||
var table3Col1 = IntegerColumn("col1")
|
var table3Col1 = IntegerColumn("col1")
|
||||||
var table3ColInt = IntegerColumn("col_int")
|
var table3ColInt = IntegerColumn("col_int")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue