Merge remote-tracking branch 'origin/master' into feature/cacher-tag

This commit is contained in:
Nash Tsai 2014-07-21 11:30:05 +08:00
commit f20e1f7c0e
11 changed files with 218 additions and 76 deletions

View File

@ -2,7 +2,7 @@
Xorm is a simple and powerful ORM for Go. Xorm is a simple and powerful ORM for Go.
[![Build Status](https://drone.io/github.com/go-xorm/xorm/status.png)](https://drone.io/github.com/go-xorm/xorm/latest) [![Go Walker](http://gowalker.org/api/v1/badge)](http://gowalker.org/github.com/go-xorm/xorm) [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/lunny/xorm/trend.png)](https://bitdeli.com/free "Bitdeli Badge") [![Build Status](https://drone.io/github.com/go-xorm/tests/status.png)](https://drone.io/github.com/go-xorm/xorm/latest) [![Go Walker](http://gowalker.org/api/v1/badge)](http://gowalker.org/github.com/go-xorm/xorm) [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/lunny/xorm/trend.png)](https://bitdeli.com/free "Bitdeli Badge")
# Features # Features
@ -37,8 +37,12 @@ Drivers for Go's sql package which currently support database/sql includes:
* Postgres: [github.com/lib/pq](https://github.com/lib/pq) * Postgres: [github.com/lib/pq](https://github.com/lib/pq)
* MsSql: [github.com/denisenkom/go-mssqldb](https://github.com/denisenkom/go-mssqldb)
* MsSql: [github.com/lunny/godbc](https://github.com/lunny/godbc) * MsSql: [github.com/lunny/godbc](https://github.com/lunny/godbc)
# Changelog # Changelog
* **v0.4.0 RC1** * **v0.4.0 RC1**

View File

@ -4,7 +4,7 @@
xorm是一个简单而强大的Go语言ORM库. 通过它可以使数据库操作非常简便。 xorm是一个简单而强大的Go语言ORM库. 通过它可以使数据库操作非常简便。
[![Build Status](https://drone.io/github.com/go-xorm/xorm/status.png)](https://drone.io/github.com/go-xorm/xorm/latest) [![Go Walker](http://gowalker.org/api/v1/badge)](http://gowalker.org/github.com/go-xorm/xorm) [![Build Status](https://drone.io/github.com/go-xorm/tests/status.png)](https://drone.io/github.com/go-xorm/xorm/latest) [![Go Walker](http://gowalker.org/api/v1/badge)](http://gowalker.org/github.com/go-xorm/xorm)
## 特性 ## 特性
@ -38,6 +38,8 @@ xorm是一个简单而强大的Go语言ORM库. 通过它可以使数据库操作
* Postgres: [github.com/lib/pq](https://github.com/lib/pq) * Postgres: [github.com/lib/pq](https://github.com/lib/pq)
* MsSql: [github.com/denisenkom/go-mssqldb](https://github.com/denisenkom/go-mssqldb)
* MsSql: [github.com/lunny/godbc](https://github.com/lunny/godbc) * MsSql: [github.com/lunny/godbc](https://github.com/lunny/godbc)
## 更新日志 ## 更新日志

View File

@ -91,7 +91,7 @@ f, err := os.Create("sql.log")
println(err.Error()) println(err.Error())
return return
} }
engine.Logger = f engine.Logger = xorm.NewSimpleLogger(f)
``` ```
3.Engine provide DB connection pool settings. 3.Engine provide DB connection pool settings.

View File

@ -95,7 +95,7 @@ f, err := os.Create("sql.log")
println(err.Error()) println(err.Error())
return return
} }
engine.Logger = f engine.Logger = xorm.NewSimpleLogger(f)
``` ```
3.engine内部支持连接池接口。 3.engine内部支持连接池接口。

View File

@ -469,6 +469,13 @@ func (engine *Engine) Incr(column string, arg ...interface{}) *Session {
return session.Incr(column, arg...) return session.Incr(column, arg...)
} }
// Method Decr provides a update string like "column = column - ?"
func (engine *Engine) Decr(column string, arg ...interface{}) *Session {
session := engine.NewSession()
session.IsAutoClose = true
return session.Decr(column, arg...)
}
// Temporarily change the Get, Find, Update's table // Temporarily change the Get, Find, Update's table
func (engine *Engine) Table(tableNameOrBean interface{}) *Session { func (engine *Engine) Table(tableNameOrBean interface{}) *Session {
session := engine.NewSession() session := engine.NewSession()
@ -1110,21 +1117,21 @@ func (engine *Engine) Sync2(beans ...interface{}) error {
if engine.dialect.DBType() == core.MYSQL { if engine.dialect.DBType() == core.MYSQL {
_, err = engine.Exec(engine.dialect.ModifyColumnSql(table.Name, col)) _, err = engine.Exec(engine.dialect.ModifyColumnSql(table.Name, col))
} else { } else {
engine.LogWarn("Table %s Column %s Old data type is %s, new data type is %s", engine.LogWarn(fmt.Sprintf("Table %s Column %s db type is %s, struct type is %s\n",
table.Name, col.Name, oriCol.SQLType.Name, col.SQLType.Name) table.Name, col.Name, oriCol.SQLType.Name, col.SQLType.Name))
} }
} else { } else {
engine.LogWarn("Table %s Column %s Old data type is %s, new data type is %s", engine.LogWarn(fmt.Sprintf("Table %s Column %s db type is %s, struct type is %s",
table.Name, col.Name, oriCol.SQLType.Name, col.SQLType.Name) table.Name, col.Name, oriCol.SQLType.Name, col.SQLType.Name))
} }
} }
if col.Default != oriCol.Default { if col.Default != oriCol.Default {
engine.LogWarn("Table %s Column %s Old default is %s, new default is %s", engine.LogWarn(fmt.Sprintf("Table %s Column %s db default is %s, struct default is %s",
table.Name, col.Name, oriCol.Default, col.Default) table.Name, col.Name, oriCol.Default, col.Default))
} }
if col.Nullable != oriCol.Nullable { if col.Nullable != oriCol.Nullable {
engine.LogWarn("Table %s Column %s Old nullable is %v, new nullable is %v", engine.LogWarn(fmt.Sprintf("Table %s Column %s db nullable is %v, struct nullable is %v",
table.Name, col.Name, oriCol.Nullable, col.Nullable) table.Name, col.Name, oriCol.Nullable, col.Nullable))
} }
} else { } else {
session := engine.NewSession() session := engine.NewSession()
@ -1430,6 +1437,8 @@ func (engine *Engine) FormatTime(sqlTypeName string, t time.Time) (v interface{}
case core.TimeStampz: case core.TimeStampz:
if engine.dialect.DBType() == core.MSSQL { if engine.dialect.DBType() == core.MSSQL {
v = engine.TZTime(t).Format("2006-01-02T15:04:05.9999999Z07:00") v = engine.TZTime(t).Format("2006-01-02T15:04:05.9999999Z07:00")
} else if engine.DriverName() == "mssql" {
v = engine.TZTime(t)
} else { } else {
v = engine.TZTime(t).Format(time.RFC3339Nano) v = engine.TZTime(t).Format(time.RFC3339Nano)
} }

View File

@ -46,6 +46,8 @@ func (db *postgres) SqlType(c *core.Column) string {
res = core.Real res = core.Real
case core.TinyText, core.MediumText, core.LongText: case core.TinyText, core.MediumText, core.LongText:
res = core.Text res = core.Text
case core.Uuid:
res = core.Uuid
case core.Blob, core.TinyBlob, core.MediumBlob, core.LongBlob: case core.Blob, core.TinyBlob, core.MediumBlob, core.LongBlob:
return core.Bytea return core.Bytea
case core.Double: case core.Double:
@ -143,8 +145,17 @@ func (db *postgres) IsColumnExist(tableName string, col *core.Column) (bool, err
func (db *postgres) GetColumns(tableName string) ([]string, map[string]*core.Column, error) { func (db *postgres) GetColumns(tableName string) ([]string, map[string]*core.Column, error) {
args := []interface{}{tableName} args := []interface{}{tableName}
s := "SELECT column_name, column_default, is_nullable, data_type, character_maximum_length" + s := `SELECT column_name, column_default, is_nullable, data_type, character_maximum_length, numeric_precision, numeric_precision_radix ,
", numeric_precision, numeric_precision_radix FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = $1" CASE WHEN p.contype = 'p' THEN true ELSE false END AS primarykey,
CASE WHEN p.contype = 'u' THEN true ELSE false END AS uniquekey
FROM pg_attribute f
JOIN pg_class c ON c.oid = f.attrelid JOIN pg_type t ON t.oid = f.atttypid
LEFT JOIN pg_attrdef d ON d.adrelid = c.oid AND d.adnum = f.attnum
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
LEFT JOIN pg_constraint p ON p.conrelid = c.oid AND f.attnum = ANY (p.conkey)
LEFT JOIN pg_class AS g ON p.confrelid = g.oid
LEFT JOIN INFORMATION_SCHEMA.COLUMNS s ON s.column_name=f.attname AND c.relname=s.table_name
WHERE c.relkind = 'r'::char AND c.relname = $1 AND f.attnum > 0 ORDER BY f.attnum;`
rows, err := db.DB().Query(s, args...) rows, err := db.DB().Query(s, args...)
if err != nil { if err != nil {
@ -161,11 +172,12 @@ func (db *postgres) GetColumns(tableName string) ([]string, map[string]*core.Col
var colName, isNullable, dataType string var colName, isNullable, dataType string
var maxLenStr, colDefault, numPrecision, numRadix *string var maxLenStr, colDefault, numPrecision, numRadix *string
err = rows.Scan(&colName, &colDefault, &isNullable, &dataType, &maxLenStr, &numPrecision, &numRadix) var isPK, isUnique bool
err = rows.Scan(&colName, &colDefault, &isNullable, &dataType, &maxLenStr, &numPrecision, &numRadix, &isPK, &isUnique)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
//fmt.Println(args,colName, isNullable, dataType,maxLenStr, colDefault, numPrecision, numRadix,isPK ,isUnique)
var maxLen int var maxLen int
if maxLenStr != nil { if maxLenStr != nil {
maxLen, err = strconv.Atoi(*maxLenStr) maxLen, err = strconv.Atoi(*maxLenStr)
@ -176,8 +188,8 @@ func (db *postgres) GetColumns(tableName string) ([]string, map[string]*core.Col
col.Name = strings.Trim(colName, `" `) col.Name = strings.Trim(colName, `" `)
if colDefault != nil { if colDefault != nil || isPK {
if strings.HasPrefix(*colDefault, "nextval") { if isPK {
col.IsPrimaryKey = true col.IsPrimaryKey = true
} else { } else {
col.Default = *colDefault col.Default = *colDefault

View File

@ -3,6 +3,8 @@ package xorm
import ( import (
"errors" "errors"
"fmt" "fmt"
"net/url"
"sort"
"strings" "strings"
"github.com/go-xorm/core" "github.com/go-xorm/core"
@ -29,6 +31,53 @@ func errorf(s string, args ...interface{}) {
panic(fmt.Errorf("pq: %s", fmt.Sprintf(s, args...))) panic(fmt.Errorf("pq: %s", fmt.Sprintf(s, args...)))
} }
func parseURL(connstr string) (string, error) {
u, err := url.Parse(connstr)
if err != nil {
return "", err
}
if u.Scheme != "postgres" {
return "", fmt.Errorf("invalid connection protocol: %s", u.Scheme)
}
var kvs []string
escaper := strings.NewReplacer(` `, `\ `, `'`, `\'`, `\`, `\\`)
accrue := func(k, v string) {
if v != "" {
kvs = append(kvs, k+"="+escaper.Replace(v))
}
}
if u.User != nil {
v := u.User.Username()
accrue("user", v)
v, _ = u.User.Password()
accrue("password", v)
}
i := strings.Index(u.Host, ":")
if i < 0 {
accrue("host", u.Host)
} else {
accrue("host", u.Host[:i])
accrue("port", u.Host[i+1:])
}
if u.Path != "" {
accrue("dbname", u.Path[1:])
}
q := u.Query()
for k := range q {
accrue(k, q.Get(k))
}
sort.Strings(kvs) // Makes testing easier (not a performance concern)
return strings.Join(kvs, " "), nil
}
func parseOpts(name string, o values) { func parseOpts(name string, o values) {
if len(name) == 0 { if len(name) == 0 {
return return
@ -49,6 +98,13 @@ func parseOpts(name string, o values) {
func (p *pqDriver) Parse(driverName, dataSourceName string) (*core.Uri, error) { func (p *pqDriver) Parse(driverName, dataSourceName string) (*core.Uri, error) {
db := &core.Uri{DbType: core.POSTGRES} db := &core.Uri{DbType: core.POSTGRES}
o := make(values) o := make(values)
var err error
if strings.HasPrefix(dataSourceName, "postgres://") {
dataSourceName, err = parseURL(dataSourceName)
if err != nil {
return nil, err
}
}
parseOpts(dataSourceName, o) parseOpts(dataSourceName, o)
db.DbName = o.Get("dbname") db.DbName = o.Get("dbname")

View File

@ -40,6 +40,7 @@ type Session struct {
afterClosures []func(interface{}) afterClosures []func(interface{})
stmtCache map[uint32]*core.Stmt //key: hash.Hash32 of (queryStr, len(queryStr)) stmtCache map[uint32]*core.Stmt //key: hash.Hash32 of (queryStr, len(queryStr))
cascadeDeep int
} }
// Method Init reset the session as the init status. // Method Init reset the session as the init status.
@ -145,6 +146,12 @@ func (session *Session) Incr(column string, arg ...interface{}) *Session {
return session return session
} }
// Method Decr provides a query string like "count = count - 1"
func (session *Session) Decr(column string, arg ...interface{}) *Session {
session.Statement.Decr(column, arg...)
return session
}
// Method Cols provides some columns to special // Method Cols provides some columns to special
func (session *Session) Cols(columns ...string) *Session { func (session *Session) Cols(columns ...string) *Session {
session.Statement.Cols(columns...) session.Statement.Cols(columns...)
@ -389,7 +396,8 @@ func (session *Session) scanMapIntoStruct(obj interface{}, objMap map[string][]b
for key, data := range objMap { for key, data := range objMap {
if col = table.GetColumn(key); col == nil { if col = table.GetColumn(key); col == nil {
session.Engine.LogWarn(fmt.Sprintf("table %v's has not column %v. %v", table.Name, key, table.Columns())) session.Engine.LogWarn(fmt.Sprintf("struct %v's has not field %v. %v",
table.Type.Name(), key, table.ColumnsSeq()))
continue continue
} }
@ -895,7 +903,7 @@ func (session *Session) Iterate(bean interface{}, fun IterFunc) error {
rows, err := session.Rows(bean) rows, err := session.Rows(bean)
if err != nil { if err != nil {
return err return err
} else { }
defer rows.Close() defer rows.Close()
//b := reflect.New(iterator.beanType).Interface() //b := reflect.New(iterator.beanType).Interface()
i := 0 i := 0
@ -913,8 +921,6 @@ func (session *Session) Iterate(bean interface{}, fun IterFunc) error {
} }
return err return err
} }
return nil
}
func (session *Session) doPrepare(sqlStr string) (stmt *core.Stmt, err error) { func (session *Session) doPrepare(sqlStr string) (stmt *core.Stmt, err error) {
crc := crc32.ChecksumIEEE([]byte(sqlStr)) crc := crc32.ChecksumIEEE([]byte(sqlStr))
@ -2451,6 +2457,38 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value,
} }
fieldValue.Set(reflect.ValueOf(&x)) fieldValue.Set(reflect.ValueOf(&x))
default: default:
if fieldType.Elem().Kind() == reflect.Struct {
if session.Statement.UseCascade {
structInter := reflect.New(fieldType.Elem())
fmt.Println(structInter, fieldType.Elem())
table := session.Engine.autoMapType(structInter.Elem())
if table != nil {
x, err := strconv.ParseInt(string(data), 10, 64)
if err != nil {
return fmt.Errorf("arg %v as int: %s", key, err.Error())
}
if x != 0 {
// !nashtsai! TODO for hasOne relationship, it's preferred to use join query for eager fetch
// however, also need to consider adding a 'lazy' attribute to xorm tag which allow hasOne
// property to be fetched lazily
newsession := session.Engine.NewSession()
defer newsession.Close()
has, err := newsession.Id(x).Get(structInter.Interface())
if err != nil {
return err
}
if has {
v = structInter.Interface()
fieldValue.Set(reflect.ValueOf(v))
} else {
return errors.New("cascade obj is not exist!")
}
}
}
} else {
return fmt.Errorf("unsupported struct type in Scan: %s", fieldValue.Type().String())
}
}
return fmt.Errorf("unsupported type in Scan: %s", reflect.TypeOf(v).String()) return fmt.Errorf("unsupported type in Scan: %s", reflect.TypeOf(v).String())
} }
default: default:
@ -2565,6 +2603,8 @@ func (session *Session) value2Interface(col *core.Column, fieldValue reflect.Val
} else { } else {
return nil, ErrUnSupportedType return nil, ErrUnSupportedType
} }
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
return int64(fieldValue.Uint()), nil
default: default:
return fieldValue.Interface(), nil return fieldValue.Interface(), nil
} }
@ -3009,6 +3049,13 @@ func (session *Session) Update(bean interface{}, condiBean ...interface{}) (int6
colNames = append(colNames, session.Engine.Quote(v.colName)+" = "+session.Engine.Quote(v.colName)+" + ?") colNames = append(colNames, session.Engine.Quote(v.colName)+" = "+session.Engine.Quote(v.colName)+" + ?")
args = append(args, v.arg) args = append(args, v.arg)
} }
//for update action to like "column = column - ?"
decColumns := session.Statement.getDec()
for _, v := range decColumns {
colNames = append(colNames, session.Engine.Quote(v.colName)+" = "+session.Engine.Quote(v.colName)+" - ?")
args = append(args, v.arg)
}
var condiColNames []string var condiColNames []string
var condiArgs []interface{} var condiArgs []interface{}

View File

@ -129,7 +129,7 @@ func (db *sqlite3) GetColumns(tableName string) ([]string, map[string]*core.Colu
} }
nStart := strings.Index(name, "(") nStart := strings.Index(name, "(")
nEnd := strings.Index(name, ")") nEnd := strings.LastIndex(name, ")")
colCreates := strings.Split(name[nStart+1:nEnd], ",") colCreates := strings.Split(name[nStart+1:nEnd], ",")
cols := make(map[string]*core.Column) cols := make(map[string]*core.Column)
colSeq := make([]string, 0) colSeq := make([]string, 0)

View File

@ -20,6 +20,11 @@ type incrParam struct {
arg interface{} arg interface{}
} }
type decrParam struct {
colName string
arg interface{}
}
// statement save all the sql info for executing SQL // statement save all the sql info for executing SQL
type Statement struct { type Statement struct {
RefTable *core.Table RefTable *core.Table
@ -54,6 +59,7 @@ type Statement struct {
mustColumnMap map[string]bool mustColumnMap map[string]bool
inColumns map[string]*inParam inColumns map[string]*inParam
incrColumns map[string]incrParam incrColumns map[string]incrParam
decrColumns map[string]decrParam
} }
// init // init
@ -85,6 +91,7 @@ func (statement *Statement) Init() {
statement.checkVersion = true statement.checkVersion = true
statement.inColumns = make(map[string]*inParam) statement.inColumns = make(map[string]*inParam)
statement.incrColumns = make(map[string]incrParam) statement.incrColumns = make(map[string]incrParam)
statement.decrColumns = make(map[string]decrParam)
} }
// add the raw sql statement // add the raw sql statement
@ -375,7 +382,8 @@ func buildUpdates(engine *Engine, table *core.Table, bean interface{},
if !requiredField && fieldValue.Uint() == 0 { if !requiredField && fieldValue.Uint() == 0 {
continue continue
} }
val = fieldValue.Interface() t := int64(fieldValue.Uint())
val = reflect.ValueOf(&t).Interface()
case reflect.Struct: case reflect.Struct:
if fieldType == reflect.TypeOf(time.Now()) { if fieldType == reflect.TypeOf(time.Now()) {
t := fieldValue.Interface().(time.Time) t := fieldValue.Interface().(time.Time)
@ -546,7 +554,8 @@ func buildConditions(engine *Engine, table *core.Table, bean interface{},
if !requiredField && fieldValue.Uint() == 0 { if !requiredField && fieldValue.Uint() == 0 {
continue continue
} }
val = fieldValue.Interface() t := int64(fieldValue.Uint())
val = reflect.ValueOf(&t).Interface()
case reflect.Struct: case reflect.Struct:
if fieldType == reflect.TypeOf(time.Now()) { if fieldType == reflect.TypeOf(time.Now()) {
t := fieldValue.Interface().(time.Time) t := fieldValue.Interface().(time.Time)
@ -674,11 +683,27 @@ func (statement *Statement) Incr(column string, arg ...interface{}) *Statement {
return statement return statement
} }
// Generate "Update ... Set column = column - arg" statment
func (statement *Statement) Decr(column string, arg ...interface{}) *Statement {
k := strings.ToLower(column)
if len(arg) > 0 {
statement.decrColumns[k] = decrParam{column, arg[0]}
} else {
statement.decrColumns[k] = decrParam{column, 1}
}
return statement
}
// Generate "Update ... Set column = column + arg" statment // Generate "Update ... Set column = column + arg" statment
func (statement *Statement) getInc() map[string]incrParam { func (statement *Statement) getInc() map[string]incrParam {
return statement.incrColumns return statement.incrColumns
} }
// Generate "Update ... Set column = column - arg" statment
func (statement *Statement) getDec() map[string]decrParam {
return statement.decrColumns
}
// Generate "Where column IN (?) " statment // Generate "Where column IN (?) " statment
func (statement *Statement) In(column string, args ...interface{}) *Statement { func (statement *Statement) In(column string, args ...interface{}) *Statement {
k := strings.ToLower(column) k := strings.ToLower(column)

17
xorm.go
View File

@ -1,7 +1,6 @@
package xorm package xorm
import ( import (
"database/sql"
"errors" "errors"
"fmt" "fmt"
"os" "os"
@ -17,18 +16,13 @@ const (
Version string = "0.4" Version string = "0.4"
) )
// !nashtsai! implicit register drivers and dialects is no good, as init() can be called before sql driver got registered
// func init() {
// regDrvsNDialects()
// }
func regDrvsNDialects() bool { func regDrvsNDialects() bool {
if core.RegisteredDriverSize() == 0 {
providedDrvsNDialects := map[string]struct { providedDrvsNDialects := map[string]struct {
dbType core.DbType dbType core.DbType
getDriver func() core.Driver getDriver func() core.Driver
getDialect func() core.Dialect getDialect func() core.Dialect
}{ }{
"mssql": {"mssql", func() core.Driver { return &odbcDriver{} }, func() core.Dialect { return &mssql{} }},
"odbc": {"mssql", func() core.Driver { return &odbcDriver{} }, func() core.Dialect { return &mssql{} }}, // !nashtsai! TODO change this when supporting MS Access "odbc": {"mssql", func() core.Driver { return &odbcDriver{} }, func() core.Dialect { return &mssql{} }}, // !nashtsai! TODO change this when supporting MS Access
"mysql": {"mysql", func() core.Driver { return &mysqlDriver{} }, func() core.Dialect { return &mysql{} }}, "mysql": {"mysql", func() core.Driver { return &mysqlDriver{} }, func() core.Dialect { return &mysql{} }},
"mymysql": {"mysql", func() core.Driver { return &mymysqlDriver{} }, func() core.Dialect { return &mysql{} }}, "mymysql": {"mysql", func() core.Driver { return &mymysqlDriver{} }, func() core.Dialect { return &mysql{} }},
@ -39,19 +33,12 @@ func regDrvsNDialects() bool {
} }
for driverName, v := range providedDrvsNDialects { for driverName, v := range providedDrvsNDialects {
_, err := sql.Open(driverName, "") if driver := core.QueryDriver(driverName); driver == nil {
if err == nil {
// fmt.Printf("driver succeed: %v\n", driverName)
core.RegisterDriver(driverName, v.getDriver()) core.RegisterDriver(driverName, v.getDriver())
core.RegisterDialect(v.dbType, v.getDialect()) core.RegisterDialect(v.dbType, v.getDialect())
} else {
// fmt.Printf("driver failed: %v | err: %v\n", driverName, err)
} }
} }
return true return true
} else {
return false
}
} }
func close(engine *Engine) { func close(engine *Engine) {