diff --git a/CHANGELOG.md b/CHANGELOG.md index 15e1527ad8c..15f4fe5fa22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # 2.0.0 (unreleased) **New features** +- [Issue #1623](https://github.com/grafana/grafana/issues/1623). Share Dashboard: Dashboard snapshot sharing (dash and data snapshot), save to local or save to public snapshot dashboard snapshots.raintank.io site - [Issue #1622](https://github.com/grafana/grafana/issues/1622). Share Panel: The share modal now has an embed option, gives you an iframe that you can use to embedd a single graph on another web site - [Issue #718](https://github.com/grafana/grafana/issues/718). Dashboard: When saving a dashboard and another user has made changes inbetween the user is promted with a warning if he really wants to overwrite the other's changes - [Issue #1331](https://github.com/grafana/grafana/issues/1331). Graph & Singlestat: New axis/unit format selector and more units (kbytes, Joule, Watt, eV), and new design for graph axis & grid tab and single stat options tab views @@ -19,6 +20,7 @@ - [Issue #599](https://github.com/grafana/grafana/issues/599). Graph: Added right y axis label setting and graph support - [Issue #1253](https://github.com/grafana/grafana/issues/1253). Graph & Singlestat: Users can now set decimal precision for legend and tooltips (override auto precision) - [Issue #1255](https://github.com/grafana/grafana/issues/1255). Templating: Dashboard will now wait to load until all template variables that have refresh on load set or are initialized via url to be fully loaded and so all variables are in valid state before panels start issuing metric requests. +- [Issue #1344](https://github.com/grafana/grafana/issues/1344). OpenTSDB: Alias patterns (reference tag values), syntax is: $tag_tagname or [[tag_tagname]] **Fixes** - [Issue #1298](https://github.com/grafana/grafana/issues/1298). InfluxDB: Fix handling of empty array in templating variable query diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 79b49db2f0a..857b045ddd8 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -25,12 +25,12 @@ }, { "ImportPath": "github.com/go-xorm/core", - "Rev": "a949e067ced1cb6e6ef5c38b6f28b074fa718f1e" + "Rev": "be6e7ac47dc57bd0ada25322fa526944f66ccaa6" }, { "ImportPath": "github.com/go-xorm/xorm", - "Comment": "v0.4.1-19-g5c23849", - "Rev": "5c23849a66f4593e68909bb6c1fa30651b5b0541" + "Comment": "v0.4.2-58-ge2889e5", + "Rev": "e2889e5517600b82905f1d2ba8b70deb71823ffe" }, { "ImportPath": "github.com/jtolds/gls", @@ -51,7 +51,7 @@ }, { "ImportPath": "github.com/mattn/go-sqlite3", - "Rev": "d10e2c8f62100097910367dee90a9bd89d426a44" + "Rev": "e28cd440fabdd39b9520344bc26829f61db40ece" }, { "ImportPath": "github.com/smartystreets/goconvey/convey", diff --git a/Godeps/_workspace/src/github.com/go-xorm/core/README.md b/Godeps/_workspace/src/github.com/go-xorm/core/README.md new file mode 100644 index 00000000000..0ae94a584ab --- /dev/null +++ b/Godeps/_workspace/src/github.com/go-xorm/core/README.md @@ -0,0 +1,114 @@ +Core is a lightweight wrapper of sql.DB. + +# Open +```Go +db, _ := core.Open(db, connstr) +``` + +# SetMapper +```Go +db.SetMapper(SameMapper()) +``` + +## Scan usage + +### Scan +```Go +rows, _ := db.Query() +for rows.Next() { + rows.Scan() +} +``` + +### ScanMap +```Go +rows, _ := db.Query() +for rows.Next() { + rows.ScanMap() +``` + +### ScanSlice + +You can use `[]string`, `[][]byte`, `[]interface{}`, `[]*string`, `[]sql.NullString` to ScanSclice. Notice, slice's length should be equal or less than select columns. + +```Go +rows, _ := db.Query() +cols, _ := rows.Columns() +for rows.Next() { + var s = make([]string, len(cols)) + rows.ScanSlice(&s) +} +``` + +```Go +rows, _ := db.Query() +cols, _ := rows.Columns() +for rows.Next() { + var s = make([]*string, len(cols)) + rows.ScanSlice(&s) +} +``` + +### ScanStruct +```Go +rows, _ := db.Query() +for rows.Next() { + rows.ScanStructByName() + rows.ScanStructByIndex() +} +``` + +## Query usage +```Go +rows, err := db.Query("select * from table where name = ?", name) + +user = User{ + Name:"lunny", +} +rows, err := db.QueryStruct("select * from table where name = ?Name", + &user) + +var user = map[string]interface{}{ + "name": "lunny", +} +rows, err = db.QueryMap("select * from table where name = ?name", + &user) +``` + +## QueryRow usage +```Go +row := db.QueryRow("select * from table where name = ?", name) + +user = User{ + Name:"lunny", +} +row := db.QueryRowStruct("select * from table where name = ?Name", + &user) + +var user = map[string]interface{}{ + "name": "lunny", +} +row = db.QueryRowMap("select * from table where name = ?name", + &user) +``` + +## Exec usage +```Go +db.Exec("insert into user (`name`, title, age, alias, nick_name,created) values (?,?,?,?,?,?)", name, title, age, alias...) + +user = User{ + Name:"lunny", + Title:"test", + Age: 18, +} +result, err = db.ExecStruct("insert into user (`name`, title, age, alias, nick_name,created) values (?Name,?Title,?Age,?Alias,?NickName,?Created)", + &user) + +var user = map[string]interface{}{ + "Name": "lunny", + "Title": "test", + "Age": 18, +} +result, err = db.ExecMap("insert into user (`name`, title, age, alias, nick_name,created) values (?Name,?Title,?Age,?Alias,?NickName,?Created)", + &user) +``` \ No newline at end of file diff --git a/Godeps/_workspace/src/github.com/go-xorm/core/column.go b/Godeps/_workspace/src/github.com/go-xorm/core/column.go index 18921ca7c4c..52468aa20e6 100644 --- a/Godeps/_workspace/src/github.com/go-xorm/core/column.go +++ b/Godeps/_workspace/src/github.com/go-xorm/core/column.go @@ -121,6 +121,21 @@ func (col *Column) ValueOfV(dataStruct *reflect.Value) (*reflect.Value, error) { col.fieldPath = strings.Split(col.FieldName, ".") } + if dataStruct.Type().Kind() == reflect.Map { + var keyValue reflect.Value + + if len(col.fieldPath) == 1 { + keyValue = reflect.ValueOf(col.FieldName) + } else if len(col.fieldPath) == 2 { + keyValue = reflect.ValueOf(col.fieldPath[1]) + } else { + return nil, fmt.Errorf("Unsupported mutliderive %v", col.FieldName) + } + + fieldValue = dataStruct.MapIndex(keyValue) + return &fieldValue, nil + } + if len(col.fieldPath) == 1 { fieldValue = dataStruct.FieldByName(col.FieldName) } else if len(col.fieldPath) == 2 { diff --git a/Godeps/_workspace/src/github.com/go-xorm/core/dialect.go b/Godeps/_workspace/src/github.com/go-xorm/core/dialect.go index 05375642610..43a22670913 100644 --- a/Godeps/_workspace/src/github.com/go-xorm/core/dialect.go +++ b/Godeps/_workspace/src/github.com/go-xorm/core/dialect.go @@ -47,15 +47,13 @@ type Dialect interface { SupportInsertMany() bool SupportEngine() bool SupportCharset() bool + SupportDropIfExists() bool IndexOnTable() bool ShowCreateNull() bool IndexCheckSql(tableName, idxName string) (string, []interface{}) TableCheckSql(tableName string) (string, []interface{}) - //ColumnCheckSql(tableName, colName string) (string, []interface{}) - //IsTableExist(tableName string) (bool, error) - //IsIndexExist(tableName string, idx *Index) (bool, error) IsColumnExist(tableName string, col *Column) (bool, error) CreateTableSql(table *Table, tableName, storeEngine, charset string) string @@ -65,15 +63,13 @@ type Dialect interface { ModifyColumnSql(tableName string, col *Column) string + //CreateTableIfNotExists(table *Table, tableName, storeEngine, charset string) error + //MustDropTable(tableName string) error + GetColumns(tableName string) ([]string, map[string]*Column, error) GetTables() ([]*Table, error) GetIndexes(tableName string) (map[string]*Index, error) - // Get data from db cell to a struct's field - //GetData(col *Column, fieldValue *reflect.Value, cellData interface{}) error - // Set field data to db - //SetData(col *Column, fieldValue *refelct.Value) (interface{}, error) - Filters() []Filter } @@ -144,6 +140,10 @@ func (db *Base) RollBackStr() string { return "ROLL BACK" } +func (db *Base) SupportDropIfExists() bool { + return true +} + func (db *Base) DropTableSql(tableName string) string { return fmt.Sprintf("DROP TABLE IF EXISTS `%s`", tableName) } @@ -170,35 +170,52 @@ func (db *Base) IsColumnExist(tableName string, col *Column) (bool, error) { return db.HasRecords(query, db.DbName, tableName, col.Name) } +/* +func (db *Base) CreateTableIfNotExists(table *Table, tableName, storeEngine, charset string) error { + sql, args := db.dialect.TableCheckSql(tableName) + rows, err := db.DB().Query(sql, args...) + if db.Logger != nil { + db.Logger.Info("[sql]", sql, args) + } + if err != nil { + return err + } + defer rows.Close() + + if rows.Next() { + return nil + } + + sql = db.dialect.CreateTableSql(table, tableName, storeEngine, charset) + _, err = db.DB().Exec(sql) + if db.Logger != nil { + db.Logger.Info("[sql]", sql) + } + return err +}*/ + func (db *Base) CreateIndexSql(tableName string, index *Index) string { quote := db.dialect.Quote var unique string var idxName string if index.Type == UniqueType { unique = " UNIQUE" - idxName = fmt.Sprintf("UQE_%v_%v", tableName, index.Name) - } else { - idxName = fmt.Sprintf("IDX_%v_%v", tableName, index.Name) } - return fmt.Sprintf("CREATE%s INDEX %v ON %v (%v);", unique, + idxName = index.XName(tableName) + return fmt.Sprintf("CREATE%s INDEX %v ON %v (%v)", unique, quote(idxName), quote(tableName), quote(strings.Join(index.Cols, quote(",")))) } func (db *Base) DropIndexSql(tableName string, index *Index) string { quote := db.dialect.Quote - //var unique string - var idxName string = index.Name - if !strings.HasPrefix(idxName, "UQE_") && - !strings.HasPrefix(idxName, "IDX_") { - if index.Type == UniqueType { - idxName = fmt.Sprintf("UQE_%v_%v", tableName, index.Name) - } else { - idxName = fmt.Sprintf("IDX_%v_%v", tableName, index.Name) - } + var name string + if index.IsRegular { + name = index.XName(tableName) + } else { + name = index.Name } - return fmt.Sprintf("DROP INDEX %v ON %s", - quote(idxName), quote(tableName)) + return fmt.Sprintf("DROP INDEX %v ON %s", quote(name), quote(tableName)) } func (db *Base) ModifyColumnSql(tableName string, col *Column) string { diff --git a/Godeps/_workspace/src/github.com/go-xorm/core/index.go b/Godeps/_workspace/src/github.com/go-xorm/core/index.go index e8f447d7031..73b95175adc 100644 --- a/Godeps/_workspace/src/github.com/go-xorm/core/index.go +++ b/Godeps/_workspace/src/github.com/go-xorm/core/index.go @@ -1,7 +1,9 @@ package core import ( + "fmt" "sort" + "strings" ) const ( @@ -11,9 +13,21 @@ const ( // database index type Index struct { - Name string - Type int - Cols []string + IsRegular bool + Name string + Type int + Cols []string +} + +func (index *Index) XName(tableName string) string { + if !strings.HasPrefix(index.Name, "UQE_") && + !strings.HasPrefix(index.Name, "IDX_") { + if index.Type == UniqueType { + return fmt.Sprintf("UQE_%v_%v", tableName, index.Name) + } + return fmt.Sprintf("IDX_%v_%v", tableName, index.Name) + } + return index.Name } // add columns which will be composite index @@ -24,6 +38,9 @@ func (index *Index) AddColumn(cols ...string) { } func (index *Index) Equal(dst *Index) bool { + if index.Type != dst.Type { + return false + } if len(index.Cols) != len(dst.Cols) { return false } @@ -40,5 +57,5 @@ func (index *Index) Equal(dst *Index) bool { // new an index func NewIndex(name string, indexType int) *Index { - return &Index{name, indexType, make([]string, 0)} + return &Index{true, name, indexType, make([]string, 0)} } diff --git a/Godeps/_workspace/src/github.com/go-xorm/core/mapper.go b/Godeps/_workspace/src/github.com/go-xorm/core/mapper.go index c00dc395211..bb72a156624 100644 --- a/Godeps/_workspace/src/github.com/go-xorm/core/mapper.go +++ b/Godeps/_workspace/src/github.com/go-xorm/core/mapper.go @@ -9,7 +9,6 @@ import ( type IMapper interface { Obj2Table(string) string Table2Obj(string) string - TableName(string) string } type CacheMapper struct { @@ -56,10 +55,6 @@ func (m *CacheMapper) Table2Obj(t string) string { return o } -func (m *CacheMapper) TableName(t string) string { - return t -} - // SameMapper implements IMapper and provides same name between struct and // database table type SameMapper struct { @@ -73,10 +68,6 @@ func (m SameMapper) Table2Obj(t string) string { return t } -func (m SameMapper) TableName(t string) string { - return t -} - // SnakeMapper implements IMapper and provides name transaltion between // struct and database table type SnakeMapper struct { @@ -97,25 +88,6 @@ func snakeCasedName(name string) string { return string(newstr) } -/*func pascal2Sql(s string) (d string) { - d = "" - lastIdx := 0 - for i := 0; i < len(s); i++ { - if s[i] >= 'A' && s[i] <= 'Z' { - if lastIdx < i { - d += s[lastIdx+1 : i] - } - if i != 0 { - d += "_" - } - d += string(s[i] + 32) - lastIdx = i - } - } - d += s[lastIdx+1:] - return -}*/ - func (mapper SnakeMapper) Obj2Table(name string) string { return snakeCasedName(name) } @@ -148,9 +120,103 @@ func (mapper SnakeMapper) Table2Obj(name string) string { return titleCasedName(name) } -func (mapper SnakeMapper) TableName(t string) string { - return t +// GonicMapper implements IMapper. It will consider initialisms when mapping names. +// E.g. id -> ID, user -> User and to table names: UserID -> user_id, MyUID -> my_uid +type GonicMapper map[string]bool + +func isASCIIUpper(r rune) bool { + return 'A' <= r && r <= 'Z' } + +func toASCIIUpper(r rune) rune { + if 'a' <= r && r <= 'z' { + r -= ('a' - 'A') + } + return r +} + +func gonicCasedName(name string) string { + newstr := make([]rune, 0, len(name)+3) + for idx, chr := range name { + if isASCIIUpper(chr) && idx > 0 { + if !isASCIIUpper(newstr[len(newstr)-1]) { + newstr = append(newstr, '_') + } + } + + if !isASCIIUpper(chr) && idx > 1 { + l := len(newstr) + if isASCIIUpper(newstr[l-1]) && isASCIIUpper(newstr[l-2]) { + newstr = append(newstr, newstr[l-1]) + newstr[l-1] = '_' + } + } + + newstr = append(newstr, chr) + } + return strings.ToLower(string(newstr)) +} + +func (mapper GonicMapper) Obj2Table(name string) string { + return gonicCasedName(name) +} + +func (mapper GonicMapper) Table2Obj(name string) string { + newstr := make([]rune, 0) + + name = strings.ToLower(name) + parts := strings.Split(name, "_") + + for _, p := range parts { + _, isInitialism := mapper[strings.ToUpper(p)] + for i, r := range p { + if i == 0 || isInitialism { + r = toASCIIUpper(r) + } + newstr = append(newstr, r) + } + } + + return string(newstr) +} + +// A GonicMapper that contains a list of common initialisms taken from golang/lint +var LintGonicMapper = GonicMapper{ + "API": true, + "ASCII": true, + "CPU": true, + "CSS": true, + "DNS": true, + "EOF": true, + "GUID": true, + "HTML": true, + "HTTP": true, + "HTTPS": true, + "ID": true, + "IP": true, + "JSON": true, + "LHS": true, + "QPS": true, + "RAM": true, + "RHS": true, + "RPC": true, + "SLA": true, + "SMTP": true, + "SSH": true, + "TLS": true, + "TTL": true, + "UI": true, + "UID": true, + "UUID": true, + "URI": true, + "URL": true, + "UTF8": true, + "VM": true, + "XML": true, + "XSRF": true, + "XSS": true, +} + // provide prefix table name support type PrefixMapper struct { Mapper IMapper @@ -165,10 +231,6 @@ func (mapper PrefixMapper) Table2Obj(name string) string { return mapper.Mapper.Table2Obj(name[len(mapper.Prefix):]) } -func (mapper PrefixMapper) TableName(name string) string { - return mapper.Prefix + name -} - func NewPrefixMapper(mapper IMapper, prefix string) PrefixMapper { return PrefixMapper{mapper, prefix} } @@ -187,10 +249,6 @@ func (mapper SuffixMapper) Table2Obj(name string) string { return mapper.Mapper.Table2Obj(name[:len(name)-len(mapper.Suffix)]) } -func (mapper SuffixMapper) TableName(name string) string { - return name + mapper.Suffix -} - func NewSuffixMapper(mapper IMapper, suffix string) SuffixMapper { return SuffixMapper{mapper, suffix} } diff --git a/Godeps/_workspace/src/github.com/go-xorm/core/mapper_test.go b/Godeps/_workspace/src/github.com/go-xorm/core/mapper_test.go new file mode 100644 index 00000000000..043087a2af1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/go-xorm/core/mapper_test.go @@ -0,0 +1,45 @@ +package core + +import ( + "testing" +) + +func TestGonicMapperFromObj(t *testing.T) { + testCases := map[string]string{ + "HTTPLib": "http_lib", + "id": "id", + "ID": "id", + "IDa": "i_da", + "iDa": "i_da", + "IDAa": "id_aa", + "aID": "a_id", + "aaID": "aa_id", + "aaaID": "aaa_id", + "MyREalFunkYLONgNAME": "my_r_eal_funk_ylo_ng_name", + } + + for in, expected := range testCases { + out := gonicCasedName(in) + if out != expected { + t.Errorf("Given %s, expected %s but got %s", in, expected, out) + } + } +} + +func TestGonicMapperToObj(t *testing.T) { + testCases := map[string]string{ + "http_lib": "HTTPLib", + "id": "ID", + "ida": "Ida", + "id_aa": "IDAa", + "aa_id": "AaID", + "my_r_eal_funk_ylo_ng_name": "MyREalFunkYloNgName", + } + + for in, expected := range testCases { + out := LintGonicMapper.Table2Obj(in) + if out != expected { + t.Errorf("Given %s, expected %s but got %s", in, expected, out) + } + } +} diff --git a/Godeps/_workspace/src/github.com/go-xorm/core/pk.go b/Godeps/_workspace/src/github.com/go-xorm/core/pk.go index 61d1371e67c..1810dd944be 100644 --- a/Godeps/_workspace/src/github.com/go-xorm/core/pk.go +++ b/Godeps/_workspace/src/github.com/go-xorm/core/pk.go @@ -1,7 +1,8 @@ package core import ( - "encoding/json" + "bytes" + "encoding/gob" ) type PK []interface{} @@ -12,14 +13,14 @@ func NewPK(pks ...interface{}) *PK { } func (p *PK) ToString() (string, error) { - bs, err := json.Marshal(*p) - if err != nil { - return "", nil - } - - return string(bs), nil + buf := new(bytes.Buffer) + enc := gob.NewEncoder(buf) + err := enc.Encode(*p) + return buf.String(), err } func (p *PK) FromString(content string) error { - return json.Unmarshal([]byte(content), p) + dec := gob.NewDecoder(bytes.NewBufferString(content)) + err := dec.Decode(p) + return err } diff --git a/Godeps/_workspace/src/github.com/go-xorm/core/pk_test.go b/Godeps/_workspace/src/github.com/go-xorm/core/pk_test.go index 5245e574800..05486086e6a 100644 --- a/Godeps/_workspace/src/github.com/go-xorm/core/pk_test.go +++ b/Godeps/_workspace/src/github.com/go-xorm/core/pk_test.go @@ -2,6 +2,7 @@ package core import ( "fmt" + "reflect" "testing" ) @@ -19,4 +20,14 @@ func TestPK(t *testing.T) { t.Error(err) } fmt.Println(s) + + if len(*p) != len(*s) { + t.Fatal("p", *p, "should be equal", *s) + } + + for i, ori := range *p { + if ori != (*s)[i] { + t.Fatal("ori", ori, reflect.ValueOf(ori), "should be equal", (*s)[i], reflect.ValueOf((*s)[i])) + } + } } diff --git a/Godeps/_workspace/src/github.com/go-xorm/core/table.go b/Godeps/_workspace/src/github.com/go-xorm/core/table.go index f7c9d464842..aba1f96e748 100644 --- a/Godeps/_workspace/src/github.com/go-xorm/core/table.go +++ b/Godeps/_workspace/src/github.com/go-xorm/core/table.go @@ -65,13 +65,18 @@ func (table *Table) GetColumnIdx(name string, idx int) *Column { // if has primary key, return column func (table *Table) PKColumns() []*Column { - columns := make([]*Column, 0) - for _, name := range table.PrimaryKeys { - columns = append(columns, table.GetColumn(name)) + columns := make([]*Column, len(table.PrimaryKeys)) + for i, name := range table.PrimaryKeys { + columns[i] = table.GetColumn(name) } return columns } +func (table *Table) ColumnType(name string) reflect.Type { + t, _ := table.Type.FieldByName(name) + return t.Type +} + func (table *Table) AutoIncrColumn() *Column { return table.GetColumn(table.AutoIncrement) } diff --git a/Godeps/_workspace/src/github.com/go-xorm/core/type.go b/Godeps/_workspace/src/github.com/go-xorm/core/type.go index ee7656824c1..73b9921ee63 100644 --- a/Godeps/_workspace/src/github.com/go-xorm/core/type.go +++ b/Godeps/_workspace/src/github.com/go-xorm/core/type.go @@ -70,6 +70,7 @@ var ( NVarchar = "NVARCHAR" TinyText = "TINYTEXT" Text = "TEXT" + Clob = "CLOB" MediumText = "MEDIUMTEXT" LongText = "LONGTEXT" Uuid = "UUID" @@ -120,6 +121,7 @@ var ( MediumText: TEXT_TYPE, LongText: TEXT_TYPE, Uuid: TEXT_TYPE, + Clob: TEXT_TYPE, Date: TIME_TYPE, DateTime: TIME_TYPE, @@ -250,7 +252,7 @@ func Type2SQLType(t reflect.Type) (st SQLType) { case reflect.String: st = SQLType{Varchar, 255, 0} case reflect.Struct: - if t == reflect.TypeOf(c_TIME_DEFAULT) { + if t.ConvertibleTo(reflect.TypeOf(c_TIME_DEFAULT)) { st = SQLType{DateTime, 0, 0} } else { // TODO need to handle association struct @@ -303,7 +305,7 @@ func SQLType2Type(st SQLType) reflect.Type { return reflect.TypeOf(float32(1)) case Double: return reflect.TypeOf(float64(1)) - case Char, Varchar, NVarchar, TinyText, Text, MediumText, LongText, Enum, Set, Uuid: + case Char, Varchar, NVarchar, TinyText, Text, MediumText, LongText, Enum, Set, Uuid, Clob: return reflect.TypeOf("") case TinyBlob, Blob, LongBlob, Bytea, Binary, MediumBlob, VarBinary: return reflect.TypeOf([]byte{}) diff --git a/Godeps/_workspace/src/github.com/go-xorm/xorm/README.md b/Godeps/_workspace/src/github.com/go-xorm/xorm/README.md index 158f5c11dda..fe8aca3c374 100644 --- a/Godeps/_workspace/src/github.com/go-xorm/xorm/README.md +++ b/Godeps/_workspace/src/github.com/go-xorm/xorm/README.md @@ -82,11 +82,13 @@ Or # Cases +* [Wego](http://github.com/go-tango/wego) + * [Docker.cn](https://docker.cn/) * [Gogs](http://try.gogits.org) - [github.com/gogits/gogs](http://github.com/gogits/gogs) -* [Gorevel](http://http://gorevel.cn/) - [github.com/goofcc/gorevel](http://github.com/goofcc/gorevel) +* [Gorevel](http://gorevel.cn/) - [github.com/goofcc/gorevel](http://github.com/goofcc/gorevel) * [Gowalker](http://gowalker.org) - [github.com/Unknwon/gowalker](http://github.com/Unknwon/gowalker) diff --git a/Godeps/_workspace/src/github.com/go-xorm/xorm/README_CN.md b/Godeps/_workspace/src/github.com/go-xorm/xorm/README_CN.md index 5def1c38a57..5a167f9b148 100644 --- a/Godeps/_workspace/src/github.com/go-xorm/xorm/README_CN.md +++ b/Godeps/_workspace/src/github.com/go-xorm/xorm/README_CN.md @@ -44,16 +44,10 @@ xorm是一个简单而强大的Go语言ORM库. 通过它可以使数据库操作 ## 更新日志 -* **v0.4.0 RC1** - 新特性: - * 移动xorm cmd [github.com/go-xorm/cmd](github.com/go-xorm/cmd) - * 在重构一般DB操作核心库 [github.com/go-xorm/core](https://github.com/go-xorm/core) - * 移动测试github.com/复XORM/测试 [github.com/go-xorm/tests](github.com/go-xorm/tests) - - 改进: - * Prepared statement 缓存 - * 添加 Incr API - * 指定时区位置 +* **v0.4.2** + 新特性: + * deleted标记 + * bug fixed [更多更新日志...](https://github.com/go-xorm/manual-zh-CN/tree/master/chapter-16) @@ -78,6 +72,8 @@ xorm是一个简单而强大的Go语言ORM库. 通过它可以使数据库操作 ## 案例 +* [Wego](http://github.com/go-tango/wego) + * [Docker.cn](https://docker.cn/) * [Gogs](http://try.gogits.org) - [github.com/gogits/gogs](http://github.com/gogits/gogs) diff --git a/Godeps/_workspace/src/github.com/go-xorm/xorm/VERSION b/Godeps/_workspace/src/github.com/go-xorm/xorm/VERSION index af81cfc7d96..4e64e0e9e47 100644 --- a/Godeps/_workspace/src/github.com/go-xorm/xorm/VERSION +++ b/Godeps/_workspace/src/github.com/go-xorm/xorm/VERSION @@ -1 +1 @@ -xorm v0.4.1 +xorm v0.4.2.0225 diff --git a/Godeps/_workspace/src/github.com/go-xorm/xorm/doc.go b/Godeps/_workspace/src/github.com/go-xorm/xorm/doc.go index adc1d2d54e1..722088ca775 100644 --- a/Godeps/_workspace/src/github.com/go-xorm/xorm/doc.go +++ b/Godeps/_workspace/src/github.com/go-xorm/xorm/doc.go @@ -63,21 +63,22 @@ There are 7 major ORM methods and many helpful methods to use to operate databas // SELECT * FROM user 4. Query multiple records and record by record handle, there two methods, one is Iterate, -another is Raws +another is Rows err := engine.Iterate(...) // SELECT * FROM user - raws, err := engine.Raws(...) + rows, err := engine.Rows(...) // SELECT * FROM user + defer rows.Close() bean := new(Struct) - for raws.Next() { - err = raws.Scan(bean) + for rows.Next() { + err = rows.Scan(bean) } 5. Update one or more records - affected, err := engine.Update(&user) + affected, err := engine.Id(...).Update(&user) // UPDATE user SET ... 6. Delete one or more records, Delete MUST has conditon @@ -150,6 +151,6 @@ Attention: the above 7 methods should be the last chainable method. engine.Join("LEFT", "userdetail", "user.id=userdetail.id").Find() //SELECT * FROM user LEFT JOIN userdetail ON user.id=userdetail.id -More usage, please visit https://github.com/go-xorm/xorm/blob/master/docs/QuickStartEn.md +More usage, please visit http://xorm.io/docs */ package xorm diff --git a/Godeps/_workspace/src/github.com/go-xorm/xorm/engine.go b/Godeps/_workspace/src/github.com/go-xorm/xorm/engine.go index 700f46a1606..8f0c805dc34 100644 --- a/Godeps/_workspace/src/github.com/go-xorm/xorm/engine.go +++ b/Godeps/_workspace/src/github.com/go-xorm/xorm/engine.go @@ -344,7 +344,7 @@ func (engine *Engine) DBMetas() ([]*core.Table, error) { if col := table.GetColumn(name); col != nil { col.Indexes[index.Name] = true } else { - return nil, fmt.Errorf("Unknown col "+name+" in indexes %v", index) + return nil, fmt.Errorf("Unknown col "+name+" in indexes %v of table", index, table.ColumnsSeq()) } } } @@ -352,6 +352,9 @@ func (engine *Engine) DBMetas() ([]*core.Table, error) { return tables, nil } +/* +dump database all table structs and data to a file +*/ func (engine *Engine) DumpAllToFile(fp string) error { f, err := os.Create(fp) if err != nil { @@ -361,6 +364,9 @@ func (engine *Engine) DumpAllToFile(fp string) error { return engine.DumpAll(f) } +/* +dump database all table structs and data to w +*/ func (engine *Engine) DumpAll(w io.Writer) error { tables, err := engine.DBMetas() if err != nil { @@ -558,6 +564,13 @@ func (engine *Engine) Decr(column string, arg ...interface{}) *Session { return session.Decr(column, arg...) } +// Method SetExpr provides a update string like "column = {expression}" +func (engine *Engine) SetExpr(column string, expression string) *Session { + session := engine.NewSession() + session.IsAutoClose = true + return session.SetExpr(column, expression) +} + // Temporarily change the Get, Find, Update's table func (engine *Engine) Table(tableNameOrBean interface{}) *Session { session := engine.NewSession() @@ -766,7 +779,12 @@ func (engine *Engine) mapType(v reflect.Value) *core.Table { col.IsPrimaryKey = true col.Nullable = false case k == "NULL": - col.Nullable = (strings.ToUpper(tags[j-1]) != "NOT") + if j == 0 { + col.Nullable = true + } else { + col.Nullable = (strings.ToUpper(tags[j-1]) != "NOT") + } + // TODO: for postgres how add autoincr? /*case strings.HasPrefix(k, "AUTOINCR(") && strings.HasSuffix(k, ")"): col.IsAutoIncrement = true @@ -915,7 +933,7 @@ func (engine *Engine) mapType(v reflect.Value) *core.Table { table.AddColumn(col) - if fieldType.Kind() == reflect.Int64 && (col.FieldName == "Id" || strings.HasSuffix(col.FieldName, ".Id")) { + if fieldType.Kind() == reflect.Int64 && (strings.ToUpper(col.FieldName) == "ID" || strings.HasSuffix(strings.ToUpper(col.FieldName), ".ID")) { idFieldColName = col.Name } } // end for @@ -959,40 +977,25 @@ func (engine *Engine) mapping(beans ...interface{}) (e error) { // If a table has any reocrd func (engine *Engine) IsTableEmpty(bean interface{}) (bool, error) { - v := rValue(bean) - t := v.Type() - if t.Kind() != reflect.Struct { - return false, errors.New("bean should be a struct or struct's point") - } - engine.autoMapType(v) session := engine.NewSession() defer session.Close() - rows, err := session.Count(bean) - return rows == 0, err + return session.IsTableEmpty(bean) } // If a table is exist -func (engine *Engine) IsTableExist(bean interface{}) (bool, error) { - v := rValue(bean) - var tableName string - if v.Type().Kind() == reflect.String { - tableName = bean.(string) - } else if v.Type().Kind() == reflect.Struct { - table := engine.autoMapType(v) - tableName = table.Name - } else { - return false, errors.New("bean should be a struct or struct's point") - } - +func (engine *Engine) IsTableExist(beanOrTableName interface{}) (bool, error) { session := engine.NewSession() defer session.Close() - has, err := session.isTableExist(tableName) - return has, err + return session.IsTableExist(beanOrTableName) } func (engine *Engine) IdOf(bean interface{}) core.PK { - table := engine.TableInfo(bean) - v := reflect.Indirect(reflect.ValueOf(bean)) + return engine.IdOfV(reflect.ValueOf(bean)) +} + +func (engine *Engine) IdOfV(rv reflect.Value) core.PK { + v := reflect.Indirect(rv) + table := engine.autoMapType(v) pk := make([]interface{}, len(table.PrimaryKeys)) for i, col := range table.PKColumns() { pkField := v.FieldByName(col.FieldName) @@ -1109,7 +1112,7 @@ func (engine *Engine) Sync(beans ...interface{}) error { session := engine.NewSession() session.Statement.RefTable = table defer session.Close() - isExist, err := session.isColumnExist(table.Name, col) + isExist, err := session.Engine.dialect.IsColumnExist(table.Name, col) if err != nil { return err } @@ -1222,8 +1225,9 @@ func (engine *Engine) CreateTables(beans ...interface{}) error { func (engine *Engine) DropTables(beans ...interface{}) error { session := engine.NewSession() - err := session.Begin() defer session.Close() + + err := session.Begin() if err != nil { return err } @@ -1258,13 +1262,6 @@ func (engine *Engine) Query(sql string, paramStr ...interface{}) (resultsSlice [ return session.Query(sql, paramStr...) } -// Exec a raw sql and return records as []map[string]string -func (engine *Engine) Q(sql string, paramStr ...interface{}) (resultsSlice []map[string]string, err error) { - session := engine.NewSession() - defer session.Close() - return session.Q(sql, paramStr...) -} - // Insert one or more records func (engine *Engine) Insert(beans ...interface{}) (int64, error) { session := engine.NewSession() @@ -1371,18 +1368,11 @@ func (engine *Engine) Import(r io.Reader) ([]sql.Result, error) { scanner.Split(semiColSpliter) - session := engine.NewSession() - defer session.Close() - err := session.newDb() - if err != nil { - return results, err - } - for scanner.Scan() { query := scanner.Text() query = strings.Trim(query, " \t") if len(query) > 0 { - result, err := session.Db.Exec(query) + result, err := engine.DB().Exec(query) results = append(results, result) if err != nil { lastError = err @@ -1409,7 +1399,15 @@ func (engine *Engine) NowTime(sqlTypeName string) interface{} { return engine.FormatTime(sqlTypeName, t) } +func (engine *Engine) NowTime2(sqlTypeName string) (interface{}, time.Time) { + t := time.Now() + return engine.FormatTime(sqlTypeName, t), t +} + func (engine *Engine) FormatTime(sqlTypeName string, t time.Time) (v interface{}) { + if engine.dialect.DBType() == core.ORACLE { + return t + } switch sqlTypeName { case core.Time: s := engine.TZTime(t).Format("2006-01-02 15:04:05") //time.RFC3339 @@ -1419,6 +1417,8 @@ func (engine *Engine) FormatTime(sqlTypeName string, t time.Time) (v interface{} case core.DateTime, core.TimeStamp: if engine.dialect.DBType() == "ql" { v = engine.TZTime(t) + } else if engine.dialect.DBType() == "sqlite3" { + v = engine.TZTime(t).UTC().Format("2006-01-02 15:04:05") } else { v = engine.TZTime(t).Format("2006-01-02 15:04:05") } @@ -1430,6 +1430,8 @@ func (engine *Engine) FormatTime(sqlTypeName string, t time.Time) (v interface{} } else { v = engine.TZTime(t).Format(time.RFC3339Nano) } + case core.BigInt, core.Int: + v = engine.TZTime(t).Unix() default: v = engine.TZTime(t) } diff --git a/Godeps/_workspace/src/github.com/go-xorm/xorm/helpers.go b/Godeps/_workspace/src/github.com/go-xorm/xorm/helpers.go index 4d20141cad3..7eaa1dd1c21 100644 --- a/Godeps/_workspace/src/github.com/go-xorm/xorm/helpers.go +++ b/Godeps/_workspace/src/github.com/go-xorm/xorm/helpers.go @@ -11,6 +11,43 @@ import ( "github.com/go-xorm/core" ) +func isZero(k interface{}) bool { + switch k.(type) { + case int: + return k.(int) == 0 + case int8: + return k.(int8) == 0 + case int16: + return k.(int16) == 0 + case int32: + return k.(int32) == 0 + case int64: + return k.(int64) == 0 + case uint: + return k.(uint) == 0 + case uint8: + return k.(uint8) == 0 + case uint16: + return k.(uint16) == 0 + case uint32: + return k.(uint32) == 0 + case uint64: + return k.(uint64) == 0 + case string: + return k.(string) == "" + } + return false +} + +func isPKZero(pk core.PK) bool { + for _, k := range pk { + if isZero(k) { + return true + } + } + return false +} + func indexNoCase(s, sep string) int { return strings.Index(strings.ToLower(s), strings.ToLower(sep)) } @@ -163,3 +200,182 @@ func rows2maps(rows *core.Rows) (resultsSlice []map[string][]byte, err error) { return resultsSlice, nil } + +func row2map(rows *core.Rows, fields []string) (resultsMap map[string][]byte, err error) { + result := make(map[string][]byte) + scanResultContainers := make([]interface{}, len(fields)) + for i := 0; i < len(fields); i++ { + var scanResultContainer interface{} + scanResultContainers[i] = &scanResultContainer + } + if err := rows.Scan(scanResultContainers...); err != nil { + return nil, err + } + + for ii, key := range fields { + rawValue := reflect.Indirect(reflect.ValueOf(scanResultContainers[ii])) + //if row is null then ignore + if rawValue.Interface() == nil { + //fmt.Println("ignore ...", key, rawValue) + continue + } + + if data, err := value2Bytes(&rawValue); err == nil { + result[key] = data + } else { + return nil, err // !nashtsai! REVIEW, should return err or just error log? + } + } + return result, nil +} + +func row2mapStr(rows *core.Rows, fields []string) (resultsMap map[string]string, err error) { + result := make(map[string]string) + scanResultContainers := make([]interface{}, len(fields)) + for i := 0; i < len(fields); i++ { + var scanResultContainer interface{} + scanResultContainers[i] = &scanResultContainer + } + if err := rows.Scan(scanResultContainers...); err != nil { + return nil, err + } + + for ii, key := range fields { + rawValue := reflect.Indirect(reflect.ValueOf(scanResultContainers[ii])) + //if row is null then ignore + if rawValue.Interface() == nil { + //fmt.Println("ignore ...", key, rawValue) + continue + } + + if data, err := value2String(&rawValue); err == nil { + result[key] = data + } else { + return nil, err // !nashtsai! REVIEW, should return err or just error log? + } + } + return result, nil +} + +func txQuery2(tx *core.Tx, sqlStr string, params ...interface{}) (resultsSlice []map[string]string, err error) { + rows, err := tx.Query(sqlStr, params...) + if err != nil { + return nil, err + } + defer rows.Close() + + return rows2Strings(rows) +} + +func query2(db *core.DB, sqlStr string, params ...interface{}) (resultsSlice []map[string]string, err error) { + s, err := db.Prepare(sqlStr) + if err != nil { + return nil, err + } + defer s.Close() + rows, err := s.Query(params...) + if err != nil { + return nil, err + } + defer rows.Close() + return rows2Strings(rows) +} + +func setColumnTime(bean interface{}, col *core.Column, t time.Time) { + v, err := col.ValueOf(bean) + if err != nil { + return + } + if v.CanSet() { + switch v.Type().Kind() { + case reflect.Struct: + v.Set(reflect.ValueOf(t).Convert(v.Type())) + case reflect.Int, reflect.Int64, reflect.Int32: + v.SetInt(t.Unix()) + case reflect.Uint, reflect.Uint64, reflect.Uint32: + v.SetUint(uint64(t.Unix())) + } + } +} + +func genCols(table *core.Table, session *Session, bean interface{}, useCol bool, includeQuote bool) ([]string, []interface{}, error) { + colNames := make([]string, 0) + args := make([]interface{}, 0) + + for _, col := range table.Columns() { + lColName := strings.ToLower(col.Name) + if useCol && !col.IsVersion && !col.IsCreated && !col.IsUpdated { + if _, ok := session.Statement.columnMap[lColName]; !ok { + continue + } + } + if col.MapType == core.ONLYFROMDB { + continue + } + + fieldValuePtr, err := col.ValueOf(bean) + if err != nil { + session.Engine.LogError(err) + continue + } + fieldValue := *fieldValuePtr + + if col.IsAutoIncrement { + switch fieldValue.Type().Kind() { + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int, reflect.Int64: + if fieldValue.Int() == 0 { + continue + } + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint, reflect.Uint64: + if fieldValue.Uint() == 0 { + continue + } + case reflect.String: + if len(fieldValue.String()) == 0 { + continue + } + } + } + + if col.IsDeleted { + continue + } + + if session.Statement.ColumnStr != "" { + if _, ok := session.Statement.columnMap[lColName]; !ok { + continue + } + } + if session.Statement.OmitStr != "" { + if _, ok := session.Statement.columnMap[lColName]; ok { + continue + } + } + + if (col.IsCreated || col.IsUpdated) && session.Statement.UseAutoTime { + val, t := session.Engine.NowTime2(col.SQLType.Name) + args = append(args, val) + + var colName = col.Name + session.afterClosures = append(session.afterClosures, func(bean interface{}) { + col := table.GetColumn(colName) + setColumnTime(bean, col, t) + }) + } else if col.IsVersion && session.Statement.checkVersion { + args = append(args, 1) + } else { + arg, err := session.value2Interface(col, fieldValue) + if err != nil { + return colNames, args, err + } + args = append(args, arg) + } + + if includeQuote { + colNames = append(colNames, session.Engine.Quote(col.Name)+" = ?") + } else { + colNames = append(colNames, col.Name) + } + } + return colNames, args, nil +} diff --git a/Godeps/_workspace/src/github.com/go-xorm/xorm/mssql_dialect.go b/Godeps/_workspace/src/github.com/go-xorm/xorm/mssql_dialect.go index ceb7c5de917..6fe50fc8c0d 100644 --- a/Godeps/_workspace/src/github.com/go-xorm/xorm/mssql_dialect.go +++ b/Godeps/_workspace/src/github.com/go-xorm/xorm/mssql_dialect.go @@ -270,7 +270,7 @@ func (db *mssql) IsReserved(name string) bool { } func (db *mssql) Quote(name string) string { - return "[" + name + "]" + return "\"" + name + "\"" } func (db *mssql) QuoteStr() string { diff --git a/Godeps/_workspace/src/github.com/go-xorm/xorm/mysql_dialect.go b/Godeps/_workspace/src/github.com/go-xorm/xorm/mysql_dialect.go index 4d32186b6ab..602cd0ecff7 100644 --- a/Godeps/_workspace/src/github.com/go-xorm/xorm/mysql_dialect.go +++ b/Godeps/_workspace/src/github.com/go-xorm/xorm/mysql_dialect.go @@ -218,6 +218,9 @@ func (db *mysql) SqlType(c *core.Column) string { res += ")" case core.NVarchar: res = core.Varchar + case core.Uuid: + res = core.Varchar + c.Length = 40 default: res = t } @@ -317,7 +320,6 @@ func (db *mysql) GetColumns(tableName string) ([]string, map[string]*core.Column if err != nil { return nil, nil, err } - //fmt.Println(columnName, isNullable, colType, colKey, extra, colDefault) col.Name = strings.Trim(columnName, "` ") if "YES" == isNullable { col.Nullable = true @@ -467,15 +469,17 @@ func (db *mysql) GetIndexes(tableName string) (map[string]*core.Index, error) { } colName = strings.Trim(colName, "` ") - + var isRegular bool if strings.HasPrefix(indexName, "IDX_"+tableName) || strings.HasPrefix(indexName, "UQE_"+tableName) { indexName = indexName[5+len(tableName) : len(indexName)] + isRegular = true } var index *core.Index var ok bool if index, ok = indexes[indexName]; !ok { index = new(core.Index) + index.IsRegular = isRegular index.Type = indexType index.Name = indexName indexes[indexName] = index diff --git a/Godeps/_workspace/src/github.com/go-xorm/xorm/oracle_dialect.go b/Godeps/_workspace/src/github.com/go-xorm/xorm/oracle_dialect.go index f599dce93ed..be71c288d0c 100644 --- a/Godeps/_workspace/src/github.com/go-xorm/xorm/oracle_dialect.go +++ b/Godeps/_workspace/src/github.com/go-xorm/xorm/oracle_dialect.go @@ -509,7 +509,7 @@ func (db *oracle) SqlType(c *core.Column) string { var res string switch t := c.SQLType.Name; t { case core.Bit, core.TinyInt, core.SmallInt, core.MediumInt, core.Int, core.Integer, core.BigInt, core.Bool, core.Serial, core.BigSerial: - return "NUMBER" + res = "NUMBER" case core.Binary, core.VarBinary, core.Blob, core.TinyBlob, core.MediumBlob, core.LongBlob, core.Bytea: return core.Blob case core.Time, core.DateTime, core.TimeStamp: @@ -521,7 +521,7 @@ func (db *oracle) SqlType(c *core.Column) string { case core.Text, core.MediumText, core.LongText: res = "CLOB" case core.Char, core.Varchar, core.TinyText: - return "VARCHAR2" + res = "VARCHAR2" default: res = t } @@ -536,6 +536,10 @@ func (db *oracle) SqlType(c *core.Column) string { return res } +func (db *oracle) AutoIncrStr() string { + return "AUTO_INCREMENT" +} + func (db *oracle) SupportInsertMany() bool { return true } @@ -553,10 +557,6 @@ func (db *oracle) QuoteStr() string { return "\"" } -func (db *oracle) AutoIncrStr() string { - return "" -} - func (db *oracle) SupportEngine() bool { return false } @@ -565,19 +565,94 @@ func (db *oracle) SupportCharset() bool { return false } +func (db *oracle) SupportDropIfExists() bool { + return false +} + func (db *oracle) IndexOnTable() bool { return false } +func (db *oracle) DropTableSql(tableName string) string { + return fmt.Sprintf("DROP TABLE `%s`", tableName) +} + +func (b *oracle) CreateTableSql(table *core.Table, tableName, storeEngine, charset string) string { + var sql string + sql = "CREATE TABLE " + if tableName == "" { + tableName = table.Name + } + + sql += b.Quote(tableName) + " (" + + pkList := table.PrimaryKeys + + for _, colName := range table.ColumnsSeq() { + col := table.GetColumn(colName) + /*if col.IsPrimaryKey && len(pkList) == 1 { + sql += col.String(b.dialect) + } else {*/ + sql += col.StringNoPk(b) + //} + sql = strings.TrimSpace(sql) + sql += ", " + } + + if len(pkList) > 0 { + sql += "PRIMARY KEY ( " + sql += b.Quote(strings.Join(pkList, b.Quote(","))) + sql += " ), " + } + + sql = sql[:len(sql)-2] + ")" + if b.SupportEngine() && storeEngine != "" { + sql += " ENGINE=" + storeEngine + } + if b.SupportCharset() { + if len(charset) == 0 { + charset = b.URI().Charset + } + if len(charset) > 0 { + sql += " DEFAULT CHARSET " + charset + } + } + return sql +} + func (db *oracle) IndexCheckSql(tableName, idxName string) (string, []interface{}) { - args := []interface{}{strings.ToUpper(tableName), strings.ToUpper(idxName)} + args := []interface{}{tableName, idxName} return `SELECT INDEX_NAME FROM USER_INDEXES ` + - `WHERE TABLE_NAME = ? AND INDEX_NAME = ?`, args + `WHERE TABLE_NAME = :1 AND INDEX_NAME = :2`, args } func (db *oracle) TableCheckSql(tableName string) (string, []interface{}) { - args := []interface{}{strings.ToUpper(tableName)} - return `SELECT table_name FROM user_tables WHERE table_name = ?`, args + args := []interface{}{tableName} + return `SELECT table_name FROM user_tables WHERE table_name = :1`, args +} + +func (db *oracle) MustDropTable(tableName string) error { + sql, args := db.TableCheckSql(tableName) + if db.Logger != nil { + db.Logger.Info("[sql]", sql, args) + } + + rows, err := db.DB().Query(sql, args...) + if err != nil { + return err + } + defer rows.Close() + + if !rows.Next() { + return nil + } + + sql = "Drop Table \"" + tableName + "\"" + if db.Logger != nil { + db.Logger.Info("[sql]", sql) + } + _, err = db.DB().Exec(sql) + return err } /*func (db *oracle) ColumnCheckSql(tableName, colName string) (string, []interface{}) { @@ -587,9 +662,9 @@ func (db *oracle) TableCheckSql(tableName string) (string, []interface{}) { }*/ func (db *oracle) IsColumnExist(tableName string, col *core.Column) (bool, error) { - args := []interface{}{strings.ToUpper(tableName), strings.ToUpper(col.Name)} - query := "SELECT column_name FROM USER_TAB_COLUMNS WHERE table_name = ?" + - " AND column_name = ?" + args := []interface{}{tableName, col.Name} + query := "SELECT column_name FROM USER_TAB_COLUMNS WHERE table_name = :1" + + " AND column_name = :2" rows, err := db.DB().Query(query, args...) if db.Logger != nil { db.Logger.Info("[sql]", query, args) @@ -606,7 +681,7 @@ func (db *oracle) IsColumnExist(tableName string, col *core.Column) (bool, error } func (db *oracle) GetColumns(tableName string) ([]string, map[string]*core.Column, error) { - args := []interface{}{strings.ToUpper(tableName)} + args := []interface{}{tableName} s := "SELECT column_name,data_default,data_type,data_length,data_precision,data_scale," + "nullable FROM USER_TAB_COLUMNS WHERE table_name = :1" @@ -625,7 +700,7 @@ func (db *oracle) GetColumns(tableName string) ([]string, map[string]*core.Colum col := new(core.Column) col.Indexes = make(map[string]bool) - var colName, colDefault, nullable, dataType, dataPrecision, dataScale string + var colName, colDefault, nullable, dataType, dataPrecision, dataScale *string var dataLen int err = rows.Scan(&colName, &colDefault, &dataType, &dataLen, &dataPrecision, @@ -634,36 +709,66 @@ func (db *oracle) GetColumns(tableName string) ([]string, map[string]*core.Colum return nil, nil, err } - col.Name = strings.Trim(colName, `" `) - col.Default = colDefault + col.Name = strings.Trim(*colName, `" `) + if colDefault != nil { + col.Default = *colDefault + col.DefaultIsEmpty = false + } - if nullable == "Y" { + if *nullable == "Y" { col.Nullable = true } else { col.Nullable = false } - switch dataType { + var ignore bool + + var dt string + var len1, len2 int + dts := strings.Split(*dataType, "(") + dt = dts[0] + if len(dts) > 1 { + lens := strings.Split(dts[1][:len(dts[1])-1], ",") + if len(lens) > 1 { + len1, _ = strconv.Atoi(lens[0]) + len2, _ = strconv.Atoi(lens[1]) + } else { + len1, _ = strconv.Atoi(lens[0]) + } + } + + switch dt { case "VARCHAR2": - col.SQLType = core.SQLType{core.Varchar, 0, 0} + col.SQLType = core.SQLType{core.Varchar, len1, len2} case "TIMESTAMP WITH TIME ZONE": col.SQLType = core.SQLType{core.TimeStampz, 0, 0} + case "NUMBER": + col.SQLType = core.SQLType{core.Double, len1, len2} + case "LONG", "LONG RAW": + col.SQLType = core.SQLType{core.Text, 0, 0} + case "RAW": + col.SQLType = core.SQLType{core.Binary, 0, 0} + case "ROWID": + col.SQLType = core.SQLType{core.Varchar, 18, 0} + case "AQ$_SUBSCRIBERS": + ignore = true default: - col.SQLType = core.SQLType{strings.ToUpper(dataType), 0, 0} + col.SQLType = core.SQLType{strings.ToUpper(dt), len1, len2} } + + if ignore { + continue + } + if _, ok := core.SqlTypes[col.SQLType.Name]; !ok { - return nil, nil, errors.New(fmt.Sprintf("unkonw colType %v", dataType)) + return nil, nil, errors.New(fmt.Sprintf("unkonw colType %v %v", *dataType, col.SQLType)) } col.Length = dataLen if col.SQLType.IsText() || col.SQLType.IsTime() { - if col.Default != "" { + if !col.DefaultIsEmpty { col.Default = "'" + col.Default + "'" - } else { - if col.DefaultIsEmpty { - col.Default = "''" - } } } cols[col.Name] = col diff --git a/Godeps/_workspace/src/github.com/go-xorm/xorm/rows.go b/Godeps/_workspace/src/github.com/go-xorm/xorm/rows.go index c566b125bbc..0def55757c8 100644 --- a/Godeps/_workspace/src/github.com/go-xorm/xorm/rows.go +++ b/Godeps/_workspace/src/github.com/go-xorm/xorm/rows.go @@ -25,11 +25,6 @@ func newRows(session *Session, bean interface{}) (*Rows, error) { rows.session = session rows.beanType = reflect.Indirect(reflect.ValueOf(bean)).Type() - err := rows.session.newDb() - if err != nil { - return nil, err - } - defer rows.session.Statement.Init() var sqlStr string @@ -47,8 +42,8 @@ func newRows(session *Session, bean interface{}) (*Rows, error) { } rows.session.Engine.logSQL(sqlStr, args) - - rows.stmt, err = rows.session.Db.Prepare(sqlStr) + var err error + rows.stmt, err = rows.session.DB().Prepare(sqlStr) if err != nil { rows.lastError = err defer rows.Close() diff --git a/Godeps/_workspace/src/github.com/go-xorm/xorm/session.go b/Godeps/_workspace/src/github.com/go-xorm/xorm/session.go index e5ec7fd0701..0d11d99fd0f 100644 --- a/Godeps/_workspace/src/github.com/go-xorm/xorm/session.go +++ b/Godeps/_workspace/src/github.com/go-xorm/xorm/session.go @@ -17,7 +17,7 @@ import ( // Struct Session keep a pointer to sql.DB and provides all execution of all // kind of database operations. type Session struct { - Db *core.DB + db *core.DB Engine *Engine Tx *core.Tx Statement Statement @@ -66,9 +66,9 @@ func (session *Session) Close() { v.Close() } - if session.Db != nil { + if session.db != nil { //session.Engine.Pool.ReleaseDB(session.Engine, session.Db) - session.Db = nil + session.db = nil session.Tx = nil session.stmtCache = nil session.Init() @@ -158,6 +158,12 @@ func (session *Session) Decr(column string, arg ...interface{}) *Session { return session } +// Method SetExpr provides a query string like "column = {expression}" +func (session *Session) SetExpr(column string, expression string) *Session { + session.Statement.SetExpr(column, expression) + return session +} + // Method Cols provides some columns to special func (session *Session) Cols(columns ...string) *Session { session.Statement.Cols(columns...) @@ -280,26 +286,18 @@ func (session *Session) Having(conditions string) *Session { return session } -func (session *Session) newDb() error { - if session.Db == nil { - /*db, err := session.Engine.Pool.RetrieveDB(session.Engine) - if err != nil { - return err - }*/ - session.Db = session.Engine.db +func (session *Session) DB() *core.DB { + if session.db == nil { + session.db = session.Engine.db session.stmtCache = make(map[uint32]*core.Stmt, 0) } - return nil + return session.db } // Begin a transaction func (session *Session) Begin() error { - err := session.newDb() - if err != nil { - return err - } if session.IsAutoCommit { - tx, err := session.Db.Begin() + tx, err := session.DB().Begin() if err != nil { return err } @@ -450,6 +448,13 @@ func (session *Session) exec(sqlStr string, args ...interface{}) (sql.Result, er return session.Engine.LogSQLExecutionTime(sqlStr, args, func() (sql.Result, error) { if session.IsAutoCommit { + //oci8 can not auto commit (github.com/mattn/go-oci8) + if session.Engine.dialect.DBType() == core.ORACLE { + session.Begin() + r, err := session.Tx.Exec(sqlStr, args...) + session.Commit() + return r, err + } return session.innerExec(sqlStr, args...) } return session.Tx.Exec(sqlStr, args...) @@ -458,10 +463,6 @@ func (session *Session) exec(sqlStr string, args ...interface{}) (sql.Result, er // Exec raw sql func (session *Session) Exec(sqlStr string, args ...interface{}) (sql.Result, error) { - err := session.newDb() - if err != nil { - return nil, err - } defer session.resetStatement() if session.IsAutoClose { defer session.Close() @@ -474,10 +475,6 @@ func (session *Session) Exec(sqlStr string, args ...interface{}) (sql.Result, er func (session *Session) CreateTable(bean interface{}) error { session.Statement.RefTable = session.Engine.TableInfo(bean) - err := session.newDb() - if err != nil { - return err - } defer session.resetStatement() if session.IsAutoClose { defer session.Close() @@ -490,10 +487,6 @@ func (session *Session) CreateTable(bean interface{}) error { func (session *Session) CreateIndexes(bean interface{}) error { session.Statement.RefTable = session.Engine.TableInfo(bean) - err := session.newDb() - if err != nil { - return err - } defer session.resetStatement() if session.IsAutoClose { defer session.Close() @@ -501,7 +494,7 @@ func (session *Session) CreateIndexes(bean interface{}) error { sqls := session.Statement.genIndexSQL() for _, sqlStr := range sqls { - _, err = session.exec(sqlStr) + _, err := session.exec(sqlStr) if err != nil { return err } @@ -513,10 +506,6 @@ func (session *Session) CreateIndexes(bean interface{}) error { func (session *Session) CreateUniques(bean interface{}) error { session.Statement.RefTable = session.Engine.TableInfo(bean) - err := session.newDb() - if err != nil { - return err - } defer session.resetStatement() if session.IsAutoClose { defer session.Close() @@ -524,7 +513,7 @@ func (session *Session) CreateUniques(bean interface{}) error { sqls := session.Statement.genUniqueSQL() for _, sqlStr := range sqls { - _, err = session.exec(sqlStr) + _, err := session.exec(sqlStr) if err != nil { return err } @@ -541,10 +530,6 @@ func (session *Session) createOneTable() error { // to be deleted func (session *Session) createAll() error { - err := session.newDb() - if err != nil { - return err - } defer session.resetStatement() if session.IsAutoClose { defer session.Close() @@ -562,10 +547,6 @@ func (session *Session) createAll() error { // drop indexes func (session *Session) DropIndexes(bean interface{}) error { - err := session.newDb() - if err != nil { - return err - } defer session.resetStatement() if session.IsAutoClose { defer session.Close() @@ -573,7 +554,7 @@ func (session *Session) DropIndexes(bean interface{}) error { sqls := session.Statement.genDelIndexSQL() for _, sqlStr := range sqls { - _, err = session.exec(sqlStr) + _, err := session.exec(sqlStr) if err != nil { return err } @@ -581,31 +562,29 @@ func (session *Session) DropIndexes(bean interface{}) error { return nil } -// DropTable drop a table and all indexes of the table -func (session *Session) DropTable(bean interface{}) error { - err := session.newDb() +// drop table will drop table if exist, if drop failed, it will return error +func (session *Session) DropTable(beanOrTableName interface{}) error { + tableName, err := session.Engine.tableName(beanOrTableName) if err != nil { return err } - defer session.resetStatement() - if session.IsAutoClose { - defer session.Close() + var needDrop = true + if !session.Engine.dialect.SupportDropIfExists() { + sqlStr, args := session.Engine.dialect.TableCheckSql(tableName) + results, err := session.query(sqlStr, args...) + if err != nil { + return err + } + needDrop = len(results) > 0 } - t := reflect.Indirect(reflect.ValueOf(bean)).Type() - defer session.resetStatement() - if t.Kind() == reflect.String { - session.Statement.AltTableName = bean.(string) - } else if t.Kind() == reflect.Struct { - session.Statement.RefTable = session.Engine.TableInfo(bean) - } else { - return errors.New("Unsupported type") + if needDrop { + sqlStr := session.Engine.Dialect().DropTableSql(tableName) + _, err = session.exec(sqlStr) + return err } - - sqlStr := session.Statement.genDropSQL() - _, err = session.exec(sqlStr) - return err + return nil } func (statement *Statement) JoinColumns(cols []*core.Column) string { @@ -642,11 +621,6 @@ func (session *Session) cacheGet(bean interface{}, sqlStr string, args ...interf return false, ErrCacheFailed } - // TODO: remove this after support multi pk cache - /*if len(session.Statement.RefTable.PrimaryKeys) != 1 { - return false, ErrCacheFailed - }*/ - for _, filter := range session.Engine.dialect.Filters() { sqlStr = filter.Do(sqlStr, session.Engine.dialect, session.Statement.RefTable) } @@ -662,7 +636,7 @@ func (session *Session) cacheGet(bean interface{}, sqlStr string, args ...interf table := session.Statement.RefTable if err != nil { var res = make([]string, len(table.PrimaryKeys)) - rows, err := session.Db.Query(newsql, args...) + rows, err := session.DB().Query(newsql, args...) if err != nil { return false, err } @@ -747,11 +721,6 @@ func (session *Session) cacheFind(t reflect.Type, sqlStr string, rowsSlicePtr in return ErrCacheFailed } - // TODO: remove this after multi pk supported - /*if len(session.Statement.RefTable.PrimaryKeys) != 1 { - return ErrCacheFailed - }*/ - for _, filter := range session.Engine.dialect.Filters() { sqlStr = filter.Do(sqlStr, session.Engine.dialect, session.Statement.RefTable) } @@ -765,7 +734,7 @@ func (session *Session) cacheFind(t reflect.Type, sqlStr string, rowsSlicePtr in cacher := session.Engine.getCacher2(table) ids, err := core.GetCacheSql(cacher, session.Statement.TableName(), newsql, args) if err != nil { - rows, err := session.Db.Query(newsql, args...) + rows, err := session.DB().Query(newsql, args...) if err != nil { return err } @@ -909,32 +878,27 @@ func (session *Session) cacheFind(t reflect.Type, sqlStr string, rowsSlicePtr in sliceValue.Set(reflect.Append(sliceValue, reflect.Indirect(reflect.ValueOf(bean)))) } } else if sliceValue.Kind() == reflect.Map { - var key core.PK - if table.PrimaryKeys[0] != "" { - key = ids[j] - } - + var key core.PK = ids[j] + keyType := sliceValue.Type().Key() + var ikey interface{} if len(key) == 1 { - ikey, err := strconv.ParseInt(fmt.Sprintf("%v", key[0]), 10, 64) + ikey, err = Atot(fmt.Sprintf("%v", key[0]), keyType) if err != nil { return err } - if t.Kind() == reflect.Ptr { - sliceValue.SetMapIndex(reflect.ValueOf(ikey), reflect.ValueOf(bean)) - } else { - sliceValue.SetMapIndex(reflect.ValueOf(ikey), reflect.Indirect(reflect.ValueOf(bean))) - } } else { - return errors.New("table have multiple primary keys") + if keyType.Kind() != reflect.Slice { + return errors.New("table have multiple primary keys, key is not core.PK or slice") + } + ikey = key + } + + if t.Kind() == reflect.Ptr { + sliceValue.SetMapIndex(reflect.ValueOf(ikey), reflect.ValueOf(bean)) + } else { + sliceValue.SetMapIndex(reflect.ValueOf(ikey), reflect.Indirect(reflect.ValueOf(bean))) } } - /*} else { - session.Engine.LogDebug("[xorm:cacheFind] cache delete:", tableName, ides[j]) - cacher.DelBean(tableName, ids[j]) - - session.Engine.LogDebug("[xorm:cacheFind] cache clear:", tableName) - cacher.ClearIds(tableName) - }*/ } return nil @@ -981,7 +945,7 @@ func (session *Session) doPrepare(sqlStr string) (stmt *core.Stmt, err error) { var has bool stmt, has = session.stmtCache[crc] if !has { - stmt, err = session.Db.Prepare(sqlStr) + stmt, err = session.DB().Prepare(sqlStr) if err != nil { return nil, err } @@ -993,11 +957,6 @@ func (session *Session) doPrepare(sqlStr string) (stmt *core.Stmt, err error) { // get retrieve one record from database, bean's non-empty fields // will be as conditions func (session *Session) Get(bean interface{}) (bool, error) { - err := session.newDb() - if err != nil { - return false, err - } - defer session.resetStatement() if session.IsAutoClose { defer session.Close() @@ -1030,6 +989,7 @@ func (session *Session) Get(bean interface{}) (bool, error) { } var rawRows *core.Rows + var err error session.queryPreprocess(&sqlStr, args...) if session.IsAutoCommit { stmt, err := session.doPrepare(sqlStr) @@ -1059,11 +1019,6 @@ func (session *Session) Get(bean interface{}) (bool, error) { // Count counts the records. bean's non-empty fields // are conditions. func (session *Session) Count(bean interface{}) (int64, error) { - err := session.newDb() - if err != nil { - return 0, err - } - defer session.resetStatement() if session.IsAutoClose { defer session.Close() @@ -1095,14 +1050,79 @@ func (session *Session) Count(bean interface{}) (int64, error) { return int64(total), err } +func Atot(s string, tp reflect.Type) (interface{}, error) { + var err error + var result interface{} + switch tp.Kind() { + case reflect.Int: + result, err = strconv.Atoi(s) + if err != nil { + return nil, errors.New("convert " + s + " as int: " + err.Error()) + } + case reflect.Int8: + x, err := strconv.Atoi(s) + if err != nil { + return nil, errors.New("convert " + s + " as int16: " + err.Error()) + } + result = int8(x) + case reflect.Int16: + x, err := strconv.Atoi(s) + if err != nil { + return nil, errors.New("convert " + s + " as int16: " + err.Error()) + } + result = int16(x) + case reflect.Int32: + x, err := strconv.Atoi(s) + if err != nil { + return nil, errors.New("convert " + s + " as int32: " + err.Error()) + } + result = int32(x) + case reflect.Int64: + result, err = strconv.ParseInt(s, 10, 64) + if err != nil { + return nil, errors.New("convert " + s + " as int64: " + err.Error()) + } + case reflect.Uint: + x, err := strconv.ParseUint(s, 10, 64) + if err != nil { + return nil, errors.New("convert " + s + " as uint: " + err.Error()) + } + result = uint(x) + case reflect.Uint8: + x, err := strconv.ParseUint(s, 10, 64) + if err != nil { + return nil, errors.New("convert " + s + " as uint8: " + err.Error()) + } + result = uint8(x) + case reflect.Uint16: + x, err := strconv.ParseUint(s, 10, 64) + if err != nil { + return nil, errors.New("convert " + s + " as uint16: " + err.Error()) + } + result = uint16(x) + case reflect.Uint32: + x, err := strconv.ParseUint(s, 10, 64) + if err != nil { + return nil, errors.New("convert " + s + " as uint32: " + err.Error()) + } + result = uint32(x) + case reflect.Uint64: + result, err = strconv.ParseUint(s, 10, 64) + if err != nil { + return nil, errors.New("convert " + s + " as uint64: " + err.Error()) + } + case reflect.String: + result = s + default: + panic("unsupported convert type") + } + return result, nil +} + // Find retrieve records from table, condiBeans's non-empty fields // are conditions. beans could be []Struct, []*Struct, map[int64]Struct // map[int64]*Struct func (session *Session) Find(rowsSlicePtr interface{}, condiBean ...interface{}) error { - err := session.newDb() - if err != nil { - return err - } defer session.resetStatement() if session.IsAutoClose { defer session.Close() @@ -1143,10 +1163,9 @@ func (session *Session) Find(rowsSlicePtr interface{}, condiBean ...interface{}) } else { // !oinume! Add " IS NULL" to WHERE whatever condiBean is given. // See https://github.com/go-xorm/xorm/issues/179 - for _, col := range table.Columns() { - if col.IsDeleted && !session.Statement.unscoped { // tag "deleted" is enabled - session.Statement.ConditionStr = fmt.Sprintf("(%v IS NULL or %v = '0001-01-01 00:00:00') ", session.Engine.Quote(col.Name), session.Engine.Quote(col.Name)) - } + if col := table.DeletedColumn(); col != nil && !session.Statement.unscoped { // tag "deleted" is enabled + session.Statement.ConditionStr = fmt.Sprintf("(%v IS NULL or %v = '0001-01-01 00:00:00') ", + session.Engine.Quote(col.Name), session.Engine.Quote(col.Name)) } } @@ -1156,11 +1175,19 @@ func (session *Session) Find(rowsSlicePtr interface{}, condiBean ...interface{}) var columnStr string = session.Statement.ColumnStr if session.Statement.JoinStr == "" { if columnStr == "" { - columnStr = session.Statement.genColumnStr() + if session.Statement.GroupByStr != "" { + columnStr = session.Statement.Engine.Quote(strings.Replace(session.Statement.GroupByStr, ",", session.Engine.Quote(","), -1)) + } else { + columnStr = session.Statement.genColumnStr() + } } } else { if columnStr == "" { - columnStr = "*" + if session.Statement.GroupByStr != "" { + columnStr = session.Statement.Engine.Quote(strings.Replace(session.Statement.GroupByStr, ",", session.Engine.Quote(","), -1)) + } else { + columnStr = "*" + } } } @@ -1178,6 +1205,7 @@ func (session *Session) Find(rowsSlicePtr interface{}, condiBean ...interface{}) args = session.Statement.RawParams } + var err error if session.Statement.JoinStr == "" { if cacher := session.Engine.getCacher2(table); cacher != nil && session.Statement.UseCache && @@ -1259,7 +1287,9 @@ func (session *Session) Find(rowsSlicePtr interface{}, condiBean ...interface{}) return err } - for i, results := range resultsSlice { + keyType := sliceValue.Type().Key() + + for _, results := range resultsSlice { var newValue reflect.Value if sliceElementType.Kind() == reflect.Ptr { newValue = reflect.New(sliceElementType.Elem()) @@ -1270,18 +1300,29 @@ func (session *Session) Find(rowsSlicePtr interface{}, condiBean ...interface{}) if err != nil { return err } - var key int64 + var key interface{} // if there is only one pk, we can put the id as map key. - // TODO: should know if the column is ints if len(table.PrimaryKeys) == 1 { - x, err := strconv.ParseInt(string(results[table.PrimaryKeys[0]]), 10, 64) + key, err = Atot(string(results[table.PrimaryKeys[0]]), keyType) if err != nil { - return errors.New("pk " + table.PrimaryKeys[0] + " as int64: " + err.Error()) + return err } - key = x } else { - key = int64(i) + if keyType.Kind() != reflect.Slice { + panic("don't support multiple primary key's map has non-slice key type") + } else { + keys := core.PK{} + for _, pk := range table.PrimaryKeys { + skey, err := Atot(string(results[pk]), keyType) + if err != nil { + return err + } + keys = append(keys, skey) + } + key = keys + } } + if sliceElementType.Kind() == reflect.Ptr { sliceValue.SetMapIndex(reflect.ValueOf(key), reflect.ValueOf(newValue.Interface())) } else { @@ -1308,23 +1349,16 @@ func (session *Session) Find(rowsSlicePtr interface{}, condiBean ...interface{}) // Test if database is ok func (session *Session) Ping() error { - err := session.newDb() - if err != nil { - return err - } defer session.resetStatement() if session.IsAutoClose { defer session.Close() } - return session.Db.Ping() + return session.DB().Ping() } +/* func (session *Session) isColumnExist(tableName string, col *core.Column) (bool, error) { - err := session.newDb() - if err != nil { - return false, err - } defer session.resetStatement() if session.IsAutoClose { defer session.Close() @@ -1333,13 +1367,29 @@ func (session *Session) isColumnExist(tableName string, col *core.Column) (bool, //sqlStr, args := session.Engine.dialect.ColumnCheckSql(tableName, colName) //results, err := session.query(sqlStr, args...) //return len(results) > 0, err +}*/ + +func (engine *Engine) tableName(beanOrTableName interface{}) (string, error) { + v := rValue(beanOrTableName) + if v.Type().Kind() == reflect.String { + return beanOrTableName.(string), nil + } else if v.Type().Kind() == reflect.Struct { + table := engine.autoMapType(v) + return table.Name, nil + } + return "", errors.New("bean should be a struct or struct's point") } -func (session *Session) isTableExist(tableName string) (bool, error) { - err := session.newDb() +func (session *Session) IsTableExist(beanOrTableName interface{}) (bool, error) { + tableName, err := session.Engine.tableName(beanOrTableName) if err != nil { return false, err } + + return session.isTableExist(tableName) +} + +func (session *Session) isTableExist(tableName string) (bool, error) { defer session.resetStatement() if session.IsAutoClose { defer session.Close() @@ -1349,11 +1399,38 @@ func (session *Session) isTableExist(tableName string) (bool, error) { return len(results) > 0, err } -func (session *Session) isIndexExist(tableName, idxName string, unique bool) (bool, error) { - err := session.newDb() - if err != nil { - return false, err +func (session *Session) IsTableEmpty(bean interface{}) (bool, error) { + v := rValue(bean) + t := v.Type() + + if t.Kind() == reflect.String { + return session.isTableEmpty(bean.(string)) + } else if t.Kind() == reflect.Struct { + session.Engine.autoMapType(v) + rows, err := session.Count(bean) + return rows == 0, err } + return false, errors.New("bean should be a struct or struct's point") +} + +func (session *Session) isTableEmpty(tableName string) (bool, error) { + defer session.resetStatement() + if session.IsAutoClose { + defer session.Close() + } + + var total int64 + sql := fmt.Sprintf("select count(*) from %s", session.Engine.Quote(tableName)) + err := session.DB().QueryRow(sql).Scan(&total) + session.Engine.logSQL(sql) + if err != nil { + return true, err + } + + return total == 0, nil +} + +func (session *Session) isIndexExist(tableName, idxName string, unique bool) (bool, error) { defer session.resetStatement() if session.IsAutoClose { defer session.Close() @@ -1371,6 +1448,11 @@ func (session *Session) isIndexExist(tableName, idxName string, unique bool) (bo // find if index is exist according cols func (session *Session) isIndexExist2(tableName string, cols []string, unique bool) (bool, error) { + defer session.resetStatement() + if session.IsAutoClose { + defer session.Close() + } + indexes, err := session.Engine.dialect.GetIndexes(tableName) if err != nil { return false, err @@ -1389,10 +1471,6 @@ func (session *Session) isIndexExist2(tableName string, cols []string, unique bo } func (session *Session) addColumn(colName string) error { - err := session.newDb() - if err != nil { - return err - } defer session.resetStatement() if session.IsAutoClose { defer session.Close() @@ -1400,47 +1478,35 @@ func (session *Session) addColumn(colName string) error { col := session.Statement.RefTable.GetColumn(colName) sql, args := session.Statement.genAddColumnStr(col) - _, err = session.exec(sql, args...) + _, err := session.exec(sql, args...) return err } func (session *Session) addIndex(tableName, idxName string) error { - err := session.newDb() - if err != nil { - return err - } defer session.resetStatement() if session.IsAutoClose { defer session.Close() } index := session.Statement.RefTable.Indexes[idxName] sqlStr := session.Engine.dialect.CreateIndexSql(tableName, index) - //genAddIndexStr(indexName(tableName, idxName), cols) - _, err = session.exec(sqlStr) + + _, err := session.exec(sqlStr) return err } func (session *Session) addUnique(tableName, uqeName string) error { - err := session.newDb() - if err != nil { - return err - } defer session.resetStatement() if session.IsAutoClose { defer session.Close() } index := session.Statement.RefTable.Indexes[uqeName] sqlStr := session.Engine.dialect.CreateIndexSql(tableName, index) - _, err = session.exec(sqlStr) + _, err := session.exec(sqlStr) return err } // To be deleted func (session *Session) dropAll() error { - err := session.newDb() - if err != nil { - return err - } defer session.resetStatement() if session.IsAutoClose { defer session.Close() @@ -1449,7 +1515,7 @@ func (session *Session) dropAll() error { for _, table := range session.Engine.Tables { session.Statement.Init() session.Statement.RefTable = table - sqlStr := session.Statement.genDropSQL() + sqlStr := session.Engine.Dialect().DropTableSql(session.Statement.TableName()) _, err := session.exec(sqlStr) if err != nil { return err @@ -1458,62 +1524,6 @@ func (session *Session) dropAll() error { return nil } -func row2mapStr(rows *core.Rows, fields []string) (resultsMap map[string]string, err error) { - result := make(map[string]string) - scanResultContainers := make([]interface{}, len(fields)) - for i := 0; i < len(fields); i++ { - var scanResultContainer interface{} - scanResultContainers[i] = &scanResultContainer - } - if err := rows.Scan(scanResultContainers...); err != nil { - return nil, err - } - - for ii, key := range fields { - rawValue := reflect.Indirect(reflect.ValueOf(scanResultContainers[ii])) - //if row is null then ignore - if rawValue.Interface() == nil { - //fmt.Println("ignore ...", key, rawValue) - continue - } - - if data, err := value2String(&rawValue); err == nil { - result[key] = data - } else { - return nil, err // !nashtsai! REVIEW, should return err or just error log? - } - } - return result, nil -} - -func row2map(rows *core.Rows, fields []string) (resultsMap map[string][]byte, err error) { - result := make(map[string][]byte) - scanResultContainers := make([]interface{}, len(fields)) - for i := 0; i < len(fields); i++ { - var scanResultContainer interface{} - scanResultContainers[i] = &scanResultContainer - } - if err := rows.Scan(scanResultContainers...); err != nil { - return nil, err - } - - for ii, key := range fields { - rawValue := reflect.Indirect(reflect.ValueOf(scanResultContainers[ii])) - //if row is null then ignore - if rawValue.Interface() == nil { - //fmt.Println("ignore ...", key, rawValue) - continue - } - - if data, err := value2Bytes(&rawValue); err == nil { - result[key] = data - } else { - return nil, err // !nashtsai! REVIEW, should return err or just error log? - } - } - return result, nil -} - func (session *Session) getField(dataStruct *reflect.Value, key string, table *core.Table, idx int) *reflect.Value { var col *core.Column if col = table.GetColumnIdx(key, idx); col == nil { @@ -1566,7 +1576,6 @@ func (session *Session) row2Bean(rows *core.Rows, fields []string, fieldsCount i } func (session *Session) _row2Bean(rows *core.Rows, fields []string, fieldsCount int, bean interface{}, dataStruct *reflect.Value, table *core.Table) error { - scanResults := make([]interface{}, fieldsCount) for i := 0; i < len(fields); i++ { var cell interface{} @@ -1686,42 +1695,73 @@ func (session *Session) _row2Bean(rows *core.Rows, fields []string, fieldsCount fieldValue.SetUint(uint64(vv.Int())) } case reflect.Struct: - if fieldType == core.TimeType { + if fieldType.ConvertibleTo(core.TimeType) { if rawValueType == core.TimeType { hasAssigned = true - t := vv.Interface().(time.Time) + t := vv.Convert(core.TimeType).Interface().(time.Time) z, _ := t.Zone() if len(z) == 0 || t.Year() == 0 { // !nashtsai! HACK tmp work around for lib/pq doesn't properly time with location session.Engine.LogDebug("empty zone key[%v] : %v | zone: %v | location: %+v\n", key, t, z, *t.Location()) - tt := time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), + t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), time.Local) - vv = reflect.ValueOf(tt) } // !nashtsai! convert to engine location - t = vv.Interface().(time.Time).In(session.Engine.TZLocation) - vv = reflect.ValueOf(t) - fieldValue.Set(vv) + t = t.In(session.Engine.TZLocation) + fieldValue.Set(reflect.ValueOf(t).Convert(fieldType)) // t = fieldValue.Interface().(time.Time) // z, _ = t.Zone() // session.Engine.LogDebug("fieldValue key[%v]: %v | zone: %v | location: %+v\n", key, t, z, *t.Location()) + } else if rawValueType == core.IntType || rawValueType == core.Int64Type || + rawValueType == core.Int32Type { + hasAssigned = true + t := time.Unix(vv.Int(), 0).In(session.Engine.TZLocation) + vv = reflect.ValueOf(t) + fieldValue.Set(vv) } } else if session.Statement.UseCascade { table := session.Engine.autoMapType(*fieldValue) if table != nil { - var x int64 - if rawValueType.Kind() == reflect.Int64 { - x = vv.Int() + if len(table.PrimaryKeys) > 1 { + panic("unsupported composited primary key cascade") } - if x != 0 { + var pk = make(core.PK, len(table.PrimaryKeys)) + switch rawValueType.Kind() { + case reflect.Int64: + pk[0] = vv.Int() + case reflect.Int: + pk[0] = int(vv.Int()) + case reflect.Int32: + pk[0] = int32(vv.Int()) + case reflect.Int16: + pk[0] = int16(vv.Int()) + case reflect.Int8: + pk[0] = int8(vv.Int()) + case reflect.Uint64: + pk[0] = vv.Uint() + case reflect.Uint: + pk[0] = uint(vv.Uint()) + case reflect.Uint32: + pk[0] = uint32(vv.Uint()) + case reflect.Uint16: + pk[0] = uint16(vv.Uint()) + case reflect.Uint8: + pk[0] = uint8(vv.Uint()) + case reflect.String: + pk[0] = vv.String() + default: + panic("unsupported primary key type cascade") + } + + if !isPKZero(pk) { // !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 structInter := reflect.New(fieldValue.Type()) newsession := session.Engine.NewSession() defer newsession.Close() - has, err := newsession.Id(x).Get(structInter.Interface()) + has, err := newsession.Id(pk).NoCascade().Get(structInter.Interface()) if err != nil { return err } @@ -1882,7 +1922,7 @@ func (session *Session) query(sqlStr string, paramStr ...interface{}) (resultsSl session.queryPreprocess(&sqlStr, paramStr...) if session.IsAutoCommit { - return session.innerQuery(session.Db, sqlStr, paramStr...) + return session.innerQuery(session.DB(), sqlStr, paramStr...) } return session.txQuery(session.Tx, sqlStr, paramStr...) } @@ -1898,7 +1938,6 @@ func (session *Session) txQuery(tx *core.Tx, sqlStr string, params ...interface{ } func (session *Session) innerQuery(db *core.DB, sqlStr string, params ...interface{}) (resultsSlice []map[string][]byte, err error) { - stmt, rows, err := session.Engine.LogSQLQueryTime(sqlStr, params, func() (*core.Stmt, *core.Rows, error) { stmt, err := db.Prepare(sqlStr) if err != nil { @@ -1922,10 +1961,6 @@ func (session *Session) innerQuery(db *core.DB, sqlStr string, params ...interfa // Exec a raw sql and return records as []map[string][]byte func (session *Session) Query(sqlStr string, paramStr ...interface{}) (resultsSlice []map[string][]byte, err error) { - err = session.newDb() - if err != nil { - return nil, err - } defer session.resetStatement() if session.IsAutoClose { defer session.Close() @@ -1941,56 +1976,15 @@ func (session *Session) query2(sqlStr string, paramStr ...interface{}) (resultsS session.queryPreprocess(&sqlStr, paramStr...) if session.IsAutoCommit { - return query2(session.Db, sqlStr, paramStr...) + return query2(session.DB(), sqlStr, paramStr...) } return txQuery2(session.Tx, sqlStr, paramStr...) } -func txQuery2(tx *core.Tx, sqlStr string, params ...interface{}) (resultsSlice []map[string]string, err error) { - rows, err := tx.Query(sqlStr, params...) - if err != nil { - return nil, err - } - defer rows.Close() - - return rows2Strings(rows) -} - -func query2(db *core.DB, sqlStr string, params ...interface{}) (resultsSlice []map[string]string, err error) { - s, err := db.Prepare(sqlStr) - if err != nil { - return nil, err - } - defer s.Close() - rows, err := s.Query(params...) - if err != nil { - return nil, err - } - defer rows.Close() - return rows2Strings(rows) -} - -// Exec a raw sql and return records as []map[string]string -func (session *Session) Q(sqlStr string, paramStr ...interface{}) (resultsSlice []map[string]string, err error) { - err = session.newDb() - if err != nil { - return nil, err - } - defer session.resetStatement() - if session.IsAutoClose { - defer session.Close() - } - return session.query2(sqlStr, paramStr...) -} - // insert one or more beans func (session *Session) Insert(beans ...interface{}) (int64, error) { var affected int64 = 0 - var err error = nil - err = session.newDb() - if err != nil { - return 0, err - } + var err error defer session.resetStatement() if session.IsAutoClose { defer session.Close() @@ -1999,20 +1993,22 @@ func (session *Session) Insert(beans ...interface{}) (int64, error) { for _, bean := range beans { sliceValue := reflect.Indirect(reflect.ValueOf(bean)) if sliceValue.Kind() == reflect.Slice { - if session.Engine.SupportInsertMany() { - cnt, err := session.innerInsertMulti(bean) - if err != nil { - return affected, err - } - affected += cnt - } else { - size := sliceValue.Len() - for i := 0; i < size; i++ { - cnt, err := session.innerInsert(sliceValue.Index(i).Interface()) + size := sliceValue.Len() + if size > 0 { + if session.Engine.SupportInsertMany() { + cnt, err := session.innerInsertMulti(bean) if err != nil { return affected, err } affected += cnt + } else { + for i := 0; i < size; i++ { + cnt, err := session.innerInsert(sliceValue.Index(i).Interface()) + if err != nil { + return affected, err + } + affected += cnt + } } } } else { @@ -2071,13 +2067,28 @@ func (session *Session) innerInsertMulti(rowsSlicePtr interface{}) (int64, error if col.MapType == core.ONLYFROMDB { continue } + if col.IsDeleted { + continue + } if session.Statement.ColumnStr != "" { if _, ok := session.Statement.columnMap[col.Name]; !ok { continue } } + if session.Statement.OmitStr != "" { + if _, ok := session.Statement.columnMap[col.Name]; ok { + continue + } + } if (col.IsCreated || col.IsUpdated) && session.Statement.UseAutoTime { - args = append(args, session.Engine.NowTime(col.SQLType.Name)) + val, t := session.Engine.NowTime2(col.SQLType.Name) + args = append(args, val) + + var colName = col.Name + session.afterClosures = append(session.afterClosures, func(bean interface{}) { + col := table.GetColumn(colName) + setColumnTime(bean, col, t) + }) } else { arg, err := session.value2Interface(col, fieldValue) if err != nil { @@ -2099,13 +2110,28 @@ func (session *Session) innerInsertMulti(rowsSlicePtr interface{}) (int64, error if col.MapType == core.ONLYFROMDB { continue } + if col.IsDeleted { + continue + } if session.Statement.ColumnStr != "" { if _, ok := session.Statement.columnMap[col.Name]; !ok { continue } } + if session.Statement.OmitStr != "" { + if _, ok := session.Statement.columnMap[col.Name]; ok { + continue + } + } if (col.IsCreated || col.IsUpdated) && session.Statement.UseAutoTime { - args = append(args, session.Engine.NowTime(col.SQLType.Name)) + val, t := session.Engine.NowTime2(col.SQLType.Name) + args = append(args, val) + + var colName = col.Name + session.afterClosures = append(session.afterClosures, func(bean interface{}) { + col := table.GetColumn(colName) + setColumnTime(bean, col, t) + }) } else { arg, err := session.value2Interface(col, fieldValue) if err != nil { @@ -2173,16 +2199,20 @@ func (session *Session) innerInsertMulti(rowsSlicePtr interface{}) (int64, error // Insert multiple records func (session *Session) InsertMulti(rowsSlicePtr interface{}) (int64, error) { - err := session.newDb() - if err != nil { - return 0, err + sliceValue := reflect.Indirect(reflect.ValueOf(rowsSlicePtr)) + if sliceValue.Kind() == reflect.Slice { + if sliceValue.Len() > 0 { + defer session.resetStatement() + if session.IsAutoClose { + defer session.Close() + } + return session.innerInsertMulti(rowsSlicePtr) + } else { + return 0, nil + } + } else { + return 0, ErrParamsType } - defer session.resetStatement() - if session.IsAutoClose { - defer session.Close() - } - - return session.innerInsertMulti(rowsSlicePtr) } func (session *Session) byte2Time(col *core.Column, data []byte) (outTime time.Time, outErr error) { @@ -2352,28 +2382,96 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value, fieldValue.SetUint(x) //Currently only support Time type case reflect.Struct: - if fieldType == core.TimeType { + if fieldType.ConvertibleTo(core.TimeType) { x, err := session.byte2Time(col, data) if err != nil { return err } v = x - fieldValue.Set(reflect.ValueOf(v)) + fieldValue.Set(reflect.ValueOf(v).Convert(fieldType)) } else if session.Statement.UseCascade { table := session.Engine.autoMapType(*fieldValue) 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 len(table.PrimaryKeys) > 1 { + panic("unsupported composited primary key cascade") } - if x != 0 { + var pk = make(core.PK, len(table.PrimaryKeys)) + rawValueType := table.ColumnType(table.PKColumns()[0].FieldName) + switch rawValueType.Kind() { + case reflect.Int64: + x, err := strconv.ParseInt(string(data), 10, 64) + if err != nil { + return fmt.Errorf("arg %v as int: %s", key, err.Error()) + } + pk[0] = x + case reflect.Int: + x, err := strconv.ParseInt(string(data), 10, 64) + if err != nil { + return fmt.Errorf("arg %v as int: %s", key, err.Error()) + } + pk[0] = int(x) + case reflect.Int32: + x, err := strconv.ParseInt(string(data), 10, 64) + if err != nil { + return fmt.Errorf("arg %v as int: %s", key, err.Error()) + } + pk[0] = int32(x) + case reflect.Int16: + x, err := strconv.ParseInt(string(data), 10, 64) + if err != nil { + return fmt.Errorf("arg %v as int: %s", key, err.Error()) + } + pk[0] = int16(x) + case reflect.Int8: + x, err := strconv.ParseInt(string(data), 10, 64) + if err != nil { + return fmt.Errorf("arg %v as int: %s", key, err.Error()) + } + pk[0] = int8(x) + case reflect.Uint64: + x, err := strconv.ParseUint(string(data), 10, 64) + if err != nil { + return fmt.Errorf("arg %v as int: %s", key, err.Error()) + } + pk[0] = x + case reflect.Uint: + x, err := strconv.ParseUint(string(data), 10, 64) + if err != nil { + return fmt.Errorf("arg %v as int: %s", key, err.Error()) + } + pk[0] = uint(x) + case reflect.Uint32: + x, err := strconv.ParseUint(string(data), 10, 64) + if err != nil { + return fmt.Errorf("arg %v as int: %s", key, err.Error()) + } + pk[0] = uint32(x) + case reflect.Uint16: + x, err := strconv.ParseUint(string(data), 10, 64) + if err != nil { + return fmt.Errorf("arg %v as int: %s", key, err.Error()) + } + pk[0] = uint16(x) + case reflect.Uint8: + x, err := strconv.ParseUint(string(data), 10, 64) + if err != nil { + return fmt.Errorf("arg %v as int: %s", key, err.Error()) + } + pk[0] = uint8(x) + case reflect.String: + pk[0] = string(data) + default: + panic("unsupported primary key type cascade") + } + + if !isPKZero(pk) { // !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 structInter := reflect.New(fieldValue.Type()) newsession := session.Engine.NewSession() defer newsession.Close() - has, err := newsession.Id(x).Get(structInter.Interface()) + has, err := newsession.Id(pk).NoCascade().Get(structInter.Interface()) if err != nil { return err } @@ -2637,17 +2735,95 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value, structInter := reflect.New(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 len(table.PrimaryKeys) > 1 { + panic("unsupported composited primary key cascade") } - if x != 0 { + var pk = make(core.PK, len(table.PrimaryKeys)) + rawValueType := table.ColumnType(table.PKColumns()[0].FieldName) + switch rawValueType.Kind() { + case reflect.Int64: + x, err := strconv.ParseInt(string(data), 10, 64) + if err != nil { + return fmt.Errorf("arg %v as int: %s", key, err.Error()) + } + + pk[0] = x + case reflect.Int: + x, err := strconv.ParseInt(string(data), 10, 64) + if err != nil { + return fmt.Errorf("arg %v as int: %s", key, err.Error()) + } + + pk[0] = int(x) + case reflect.Int32: + x, err := strconv.ParseInt(string(data), 10, 64) + if err != nil { + return fmt.Errorf("arg %v as int: %s", key, err.Error()) + } + + pk[0] = int32(x) + case reflect.Int16: + x, err := strconv.ParseInt(string(data), 10, 64) + if err != nil { + return fmt.Errorf("arg %v as int: %s", key, err.Error()) + } + + pk[0] = int16(x) + case reflect.Int8: + x, err := strconv.ParseInt(string(data), 10, 64) + if err != nil { + return fmt.Errorf("arg %v as int: %s", key, err.Error()) + } + + pk[0] = x + case reflect.Uint64: + x, err := strconv.ParseUint(string(data), 10, 64) + if err != nil { + return fmt.Errorf("arg %v as int: %s", key, err.Error()) + } + + pk[0] = x + case reflect.Uint: + x, err := strconv.ParseUint(string(data), 10, 64) + if err != nil { + return fmt.Errorf("arg %v as int: %s", key, err.Error()) + } + + pk[0] = uint(x) + case reflect.Uint32: + x, err := strconv.ParseUint(string(data), 10, 64) + if err != nil { + return fmt.Errorf("arg %v as int: %s", key, err.Error()) + } + + pk[0] = uint32(x) + case reflect.Uint16: + x, err := strconv.ParseUint(string(data), 10, 64) + if err != nil { + return fmt.Errorf("arg %v as int: %s", key, err.Error()) + } + + pk[0] = uint16(x) + case reflect.Uint8: + x, err := strconv.ParseUint(string(data), 10, 64) + if err != nil { + return fmt.Errorf("arg %v as int: %s", key, err.Error()) + } + + pk[0] = uint8(x) + case reflect.String: + pk[0] = string(data) + default: + panic("unsupported primary key type cascade") + } + + if !isPKZero(pk) { // !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()) + has, err := newsession.Id(pk).NoCascade().Get(structInter.Interface()) if err != nil { return err } @@ -2804,8 +2980,29 @@ func (session *Session) innerInsert(bean interface{}) (int64, error) { return 0, err } - colPlaces := strings.Repeat("?, ", len(colNames)) - colPlaces = colPlaces[0 : len(colPlaces)-2] + // insert expr columns, override if exists + exprColumns := session.Statement.getExpr() + exprColVals := make([]string, 0, len(exprColumns)) + for _, v := range exprColumns { + // remove the expr columns + for i, colName := range colNames { + if colName == v.colName { + colNames = append(colNames[:i], colNames[i+1:]...) + args = append(args[:i], args[i+1:]...) + } + } + + // append expr column to the end + colNames = append(colNames, v.colName) + exprColVals = append(exprColVals, v.expr) + } + + colPlaces := strings.Repeat("?, ", len(colNames)-len(exprColumns)) + if len(exprColVals) > 0 { + colPlaces = colPlaces + strings.Join(exprColVals, ", ") + } else { + colPlaces = colPlaces[0 : len(colPlaces)-2] + } sqlStr := fmt.Sprintf("INSERT INTO %v%v%v (%v%v%v) VALUES (%v)", session.Engine.QuoteStr(), @@ -2970,10 +3167,6 @@ func (session *Session) innerInsert(bean interface{}) (int64, error) { // The in parameter bean must a struct or a point to struct. The return // parameter is inserted and error func (session *Session) InsertOne(bean interface{}) (int64, error) { - err := session.newDb() - if err != nil { - return 0, err - } defer session.resetStatement() if session.IsAutoClose { defer session.Close() @@ -3068,7 +3261,7 @@ func (session *Session) cacheUpdate(sqlStr string, args ...interface{}) error { session.Engine.LogDebug("[cacheUpdate] get cache sql", newsql, args[nStart:]) ids, err := core.GetCacheSql(cacher, tableName, newsql, args[nStart:]) if err != nil { - rows, err := session.Db.Query(newsql, args[nStart:]...) + rows, err := session.DB().Query(newsql, args[nStart:]...) if err != nil { return err } @@ -3167,10 +3360,6 @@ func (session *Session) cacheUpdate(sqlStr string, args ...interface{}) error { // You should call UseBool if you have bool to use. // 2.float32 & float64 may be not inexact as conditions func (session *Session) Update(bean interface{}, condiBean ...interface{}) (int64, error) { - err := session.newDb() - if err != nil { - return 0, err - } defer session.resetStatement() if session.IsAutoClose { defer session.Close() @@ -3192,6 +3381,7 @@ func (session *Session) Update(bean interface{}, condiBean ...interface{}) (int6 } // -- + var err error if t.Kind() == reflect.Struct { table = session.Engine.TableInfo(bean) session.Statement.RefTable = table @@ -3199,7 +3389,7 @@ func (session *Session) Update(bean interface{}, condiBean ...interface{}) (int6 if session.Statement.ColumnStr == "" { colNames, args = buildUpdates(session.Engine, table, bean, false, false, false, false, session.Statement.allUseBool, session.Statement.useAllCols, - session.Statement.mustColumnMap, true) + session.Statement.mustColumnMap, session.Statement.columnMap, true) } else { colNames, args, err = genCols(table, session, bean, true, true) if err != nil { @@ -3225,7 +3415,15 @@ func (session *Session) Update(bean interface{}, condiBean ...interface{}) (int6 if session.Statement.UseAutoTime && table.Updated != "" { colNames = append(colNames, session.Engine.Quote(table.Updated)+" = ?") - args = append(args, session.Engine.NowTime(table.UpdatedColumn().SQLType.Name)) + col := table.UpdatedColumn() + val, t := session.Engine.NowTime2(col.SQLType.Name) + args = append(args, val) + + var colName = col.Name + session.afterClosures = append(session.afterClosures, func(bean interface{}) { + col := table.GetColumn(colName) + setColumnTime(bean, col, t) + }) } //for update action to like "column = column + ?" @@ -3240,6 +3438,11 @@ func (session *Session) Update(bean interface{}, condiBean ...interface{}) (int6 colNames = append(colNames, session.Engine.Quote(v.colName)+" = "+session.Engine.Quote(v.colName)+" - ?") args = append(args, v.arg) } + //for update action to like "column = expression" + exprColumns := session.Statement.getExpr() + for _, v := range exprColumns { + colNames = append(colNames, session.Engine.Quote(v.colName)+" = "+v.expr) + } var condiColNames []string var condiArgs []interface{} @@ -3289,6 +3492,10 @@ func (session *Session) Update(bean interface{}, condiBean ...interface{}) (int6 } } + if st.LimitN > 0 { + condition = condition + fmt.Sprintf(" LIMIT %d", st.LimitN) + } + sqlStr = fmt.Sprintf("UPDATE %v SET %v, %v %v", session.Engine.Quote(session.Statement.TableName()), strings.Join(colNames, ", "), @@ -3315,6 +3522,10 @@ func (session *Session) Update(bean interface{}, condiBean ...interface{}) (int6 } } + if st.LimitN > 0 { + condition = condition + fmt.Sprintf(" LIMIT %d", st.LimitN) + } + sqlStr = fmt.Sprintf("UPDATE %v SET %v %v", session.Engine.Quote(session.Statement.TableName()), strings.Join(colNames, ", "), @@ -3329,7 +3540,9 @@ func (session *Session) Update(bean interface{}, condiBean ...interface{}) (int6 if err != nil { return 0, err } else if doIncVer { - verValue.SetInt(verValue.Int() + 1) + if verValue != nil && verValue.IsValid() && verValue.CanSet() { + verValue.SetInt(verValue.Int() + 1) + } } if cacher := session.Engine.getCacher2(table); cacher != nil && session.Statement.UseCache { @@ -3426,10 +3639,6 @@ func (session *Session) cacheDelete(sqlStr string, args ...interface{}) error { // Delete records, bean's non-empty fields are conditions func (session *Session) Delete(bean interface{}) (int64, error) { - err := session.newDb() - if err != nil { - return 0, err - } defer session.resetStatement() if session.IsAutoClose { defer session.Close() @@ -3502,7 +3711,15 @@ func (session *Session) Delete(bean interface{}) (int64, error) { session.Statement.Params = append(session.Statement.Params, "") paramsLen := len(session.Statement.Params) copy(session.Statement.Params[1:paramsLen], session.Statement.Params[0:paramsLen-1]) - session.Statement.Params[0] = session.Engine.NowTime(deletedColumn.SQLType.Name) + + val, t := session.Engine.NowTime2(deletedColumn.SQLType.Name) + session.Statement.Params[0] = val + + var colName = deletedColumn.Name + session.afterClosures = append(session.afterClosures, func(bean interface{}) { + col := table.GetColumn(colName) + setColumnTime(bean, col, t) + }) } args = append(session.Statement.Params, args...) @@ -3534,7 +3751,6 @@ func (session *Session) Delete(bean interface{}) (int64, error) { copy(afterClosures, session.afterClosures) session.afterDeleteBeans[bean] = &afterClosures } - } else { if _, ok := interface{}(bean).(AfterInsertProcessor); ok { session.afterDeleteBeans[bean] = nil @@ -3716,79 +3932,3 @@ func (session *Session) Unscoped() *Session { session.Statement.Unscoped() return session } - -func genCols(table *core.Table, session *Session, bean interface{}, useCol bool, includeQuote bool) ([]string, []interface{}, error) { - colNames := make([]string, 0) - args := make([]interface{}, 0) - - for _, col := range table.Columns() { - lColName := strings.ToLower(col.Name) - if useCol && !col.IsVersion && !col.IsCreated && !col.IsUpdated { - if _, ok := session.Statement.columnMap[lColName]; !ok { - continue - } - } - if col.MapType == core.ONLYFROMDB { - continue - } - - fieldValuePtr, err := col.ValueOf(bean) - if err != nil { - session.Engine.LogError(err) - continue - } - fieldValue := *fieldValuePtr - - if col.IsAutoIncrement { - switch fieldValue.Type().Kind() { - case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int, reflect.Int64: - if fieldValue.Int() == 0 { - continue - } - case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint, reflect.Uint64: - if fieldValue.Uint() == 0 { - continue - } - case reflect.String: - if len(fieldValue.String()) == 0 { - continue - } - } - } - - if col.IsDeleted { - continue - } - - if session.Statement.ColumnStr != "" { - if _, ok := session.Statement.columnMap[lColName]; !ok { - continue - } - } - if session.Statement.OmitStr != "" { - if _, ok := session.Statement.columnMap[lColName]; ok { - continue - } - } - - if (col.IsCreated || col.IsUpdated) && session.Statement.UseAutoTime { - args = append(args, session.Engine.NowTime(col.SQLType.Name)) - } else if col.IsVersion && session.Statement.checkVersion { - args = append(args, 1) - //} else if !col.DefaultIsEmpty { - } else { - arg, err := session.value2Interface(col, fieldValue) - if err != nil { - return colNames, args, err - } - args = append(args, arg) - } - - if includeQuote { - colNames = append(colNames, session.Engine.Quote(col.Name)+" = ?") - } else { - colNames = append(colNames, col.Name) - } - } - return colNames, args, nil -} diff --git a/Godeps/_workspace/src/github.com/go-xorm/xorm/sqlite3_dialect.go b/Godeps/_workspace/src/github.com/go-xorm/xorm/sqlite3_dialect.go index 1a0282999c3..cb9e7f54a78 100644 --- a/Godeps/_workspace/src/github.com/go-xorm/xorm/sqlite3_dialect.go +++ b/Godeps/_workspace/src/github.com/go-xorm/xorm/sqlite3_dialect.go @@ -1,6 +1,7 @@ package xorm import ( + "database/sql" "errors" "fmt" "strings" @@ -152,7 +153,7 @@ func (db *sqlite3) Init(d *core.DB, uri *core.Uri, drivername, dataSourceName st func (db *sqlite3) SqlType(c *core.Column) string { switch t := c.SQLType.Name; t { case core.Date, core.DateTime, core.TimeStamp, core.Time: - return core.Numeric + return core.DateTime case core.TimeStampz: return core.Text case core.Char, core.Varchar, core.NVarchar, core.TinyText, core.Text, core.MediumText, core.LongText: @@ -297,6 +298,7 @@ func (db *sqlite3) GetColumns(tableName string) ([]string, map[string]*core.Colu col := new(core.Column) col.Indexes = make(map[string]bool) col.Nullable = true + col.DefaultIsEmpty = true for idx, field := range fields { if idx == 0 { col.Name = strings.Trim(field, "`[] ") @@ -315,8 +317,14 @@ func (db *sqlite3) GetColumns(tableName string) ([]string, map[string]*core.Colu } else { col.Nullable = true } + case "DEFAULT": + col.Default = fields[idx+1] + col.DefaultIsEmpty = false } } + if !col.SQLType.IsNumeric() && !col.DefaultIsEmpty { + col.Default = "'" + col.Default + "'" + } cols[col.Name] = col colSeq = append(colSeq, col.Name) } @@ -366,15 +374,16 @@ func (db *sqlite3) GetIndexes(tableName string) (map[string]*core.Index, error) indexes := make(map[string]*core.Index, 0) for rows.Next() { - var sql string - err = rows.Scan(&sql) + var tmpSql sql.NullString + err = rows.Scan(&tmpSql) if err != nil { return nil, err } - if sql == "" { + if !tmpSql.Valid { continue } + sql := tmpSql.String index := new(core.Index) nNStart := strings.Index(sql, "INDEX") @@ -384,7 +393,6 @@ func (db *sqlite3) GetIndexes(tableName string) (map[string]*core.Index, error) } indexName := strings.Trim(sql[nNStart+6:nNEnd], "` []") - //fmt.Println(indexName) if strings.HasPrefix(indexName, "IDX_"+tableName) || strings.HasPrefix(indexName, "UQE_"+tableName) { index.Name = indexName[5+len(tableName) : len(indexName)] } else { diff --git a/Godeps/_workspace/src/github.com/go-xorm/xorm/statement.go b/Godeps/_workspace/src/github.com/go-xorm/xorm/statement.go index 6c586ae6686..b8f859714ea 100644 --- a/Godeps/_workspace/src/github.com/go-xorm/xorm/statement.go +++ b/Godeps/_workspace/src/github.com/go-xorm/xorm/statement.go @@ -26,6 +26,11 @@ type decrParam struct { arg interface{} } +type exprParam struct { + colName string + expr string +} + // statement save all the sql info for executing SQL type Statement struct { RefTable *core.Table @@ -63,6 +68,7 @@ type Statement struct { inColumns map[string]*inParam incrColumns map[string]incrParam decrColumns map[string]decrParam + exprColumns map[string]exprParam } // init @@ -98,6 +104,7 @@ func (statement *Statement) Init() { statement.inColumns = make(map[string]*inParam) statement.incrColumns = make(map[string]incrParam) statement.decrColumns = make(map[string]decrParam) + statement.exprColumns = make(map[string]exprParam) } // add the raw sql statement @@ -153,9 +160,6 @@ func (statement *Statement) Table(tableNameOrBean interface{}) *Statement { t := v.Type() if t.Kind() == reflect.String { statement.AltTableName = tableNameOrBean.(string) - if statement.AltTableName[0] == '~' { - statement.AltTableName = statement.Engine.TableMapper.TableName(statement.AltTableName[1:]) - } } else if t.Kind() == reflect.Struct { statement.RefTable = statement.Engine.autoMapType(v) } @@ -282,7 +286,7 @@ func (statement *Statement) Table(tableNameOrBean interface{}) *Statement { func buildUpdates(engine *Engine, table *core.Table, bean interface{}, includeVersion bool, includeUpdated bool, includeNil bool, includeAutoIncr bool, allUseBool bool, useAllCols bool, - mustColumnMap map[string]bool, update bool) ([]string, []interface{}) { + mustColumnMap map[string]bool, columnMap map[string]bool, update bool) ([]string, []interface{}) { colNames := make([]string, 0) var args = make([]interface{}, 0) @@ -302,6 +306,9 @@ func buildUpdates(engine *Engine, table *core.Table, bean interface{}, if col.IsDeleted { continue } + if use, ok := columnMap[col.Name]; ok && !use { + continue + } if engine.dialect.DBType() == core.MSSQL && col.SQLType.Name == core.Text { continue @@ -414,13 +421,16 @@ func buildUpdates(engine *Engine, table *core.Table, bean interface{}, if table, ok := engine.Tables[fieldValue.Type()]; ok { if len(table.PrimaryKeys) == 1 { pkField := reflect.Indirect(fieldValue).FieldByName(table.PKColumns()[0].FieldName) - if pkField.Int() != 0 { + // fix non-int pk issues + //if pkField.Int() != 0 { + if pkField.IsValid() && !isZero(pkField.Interface()) { val = pkField.Interface() } else { continue } } else { //TODO: how to handler? + panic("not supported") } } else { val = fieldValue.Interface() @@ -579,24 +589,29 @@ func buildConditions(engine *Engine, table *core.Table, bean interface{}, t := int64(fieldValue.Uint()) val = reflect.ValueOf(&t).Interface() case reflect.Struct: - if fieldType == reflect.TypeOf(time.Now()) { - t := fieldValue.Interface().(time.Time) + if fieldType.ConvertibleTo(core.TimeType) { + t := fieldValue.Convert(core.TimeType).Interface().(time.Time) if !requiredField && (t.IsZero() || !fieldValue.IsValid()) { continue } val = engine.FormatTime(col.SQLType.Name, t) + } else if _, ok := reflect.New(fieldType).Interface().(core.Conversion); ok { + continue } else { engine.autoMapType(fieldValue) if table, ok := engine.Tables[fieldValue.Type()]; ok { if len(table.PrimaryKeys) == 1 { pkField := reflect.Indirect(fieldValue).FieldByName(table.PKColumns()[0].FieldName) - if pkField.Int() != 0 { + // fix non-int pk issues + //if pkField.Int() != 0 { + if pkField.IsValid() && !isZero(pkField.Interface()) { val = pkField.Interface() } else { continue } } else { //TODO: how to handler? + panic("not supported") } } else { val = fieldValue.Interface() @@ -716,6 +731,13 @@ func (statement *Statement) Decr(column string, arg ...interface{}) *Statement { return statement } +// Generate "Update ... Set column = {expression}" statment +func (statement *Statement) SetExpr(column string, expression string) *Statement { + k := strings.ToLower(column) + statement.exprColumns[k] = exprParam{column, expression} + return statement +} + // Generate "Update ... Set column = column + arg" statment func (statement *Statement) getInc() map[string]incrParam { return statement.incrColumns @@ -726,6 +748,11 @@ func (statement *Statement) getDec() map[string]decrParam { return statement.decrColumns } +// Generate "Update ... Set column = {expression}" statment +func (statement *Statement) getExpr() map[string]exprParam { + return statement.exprColumns +} + // Generate "Where column IN (?) " statment func (statement *Statement) In(column string, args ...interface{}) *Statement { k := strings.ToLower(column) @@ -941,15 +968,9 @@ func (statement *Statement) Join(join_operator string, tablename interface{}, co l := len(t) if l > 1 { table := t[0] - if table[0] == '~' { - table = statement.Engine.TableMapper.TableName(table[1:]) - } joinTable = statement.Engine.Quote(table) + " AS " + statement.Engine.Quote(t[1]) } else if l == 1 { table := t[0] - if table[0] == '~' { - table = statement.Engine.TableMapper.TableName(table[1:]) - } joinTable = statement.Engine.Quote(table) } case []interface{}: @@ -962,9 +983,6 @@ func (statement *Statement) Join(join_operator string, tablename interface{}, co t := v.Type() if t.Kind() == reflect.String { table = f.(string) - if table[0] == '~' { - table = statement.Engine.TableMapper.TableName(table[1:]) - } } else if t.Kind() == reflect.Struct { r := statement.Engine.autoMapType(v) table = r.Name @@ -977,9 +995,6 @@ func (statement *Statement) Join(join_operator string, tablename interface{}, co } default: t := fmt.Sprintf("%v", tablename) - if t[0] == '~' { - t = statement.Engine.TableMapper.TableName(t[1:]) - } joinTable = statement.Engine.Quote(t) } if statement.JoinStr != "" { @@ -1105,9 +1120,10 @@ func (s *Statement) genDelIndexSQL() []string { return sqls } +/* func (s *Statement) genDropSQL() string { - return s.Engine.dialect.DropTableSql(s.TableName()) + ";" -} + return s.Engine.dialect.MustDropTa(s.TableName()) + ";" +}*/ func (statement *Statement) genGetSql(bean interface{}) (string, []interface{}) { var table *core.Table @@ -1126,13 +1142,21 @@ func (statement *Statement) genGetSql(bean interface{}) (string, []interface{}) statement.BeanArgs = args var columnStr string = statement.ColumnStr - if statement.JoinStr == "" { - if columnStr == "" { - columnStr = statement.genColumnStr() + if len(statement.JoinStr) == 0 { + if len(columnStr) == 0 { + if statement.GroupByStr != "" { + columnStr = statement.Engine.Quote(strings.Replace(statement.GroupByStr, ",", statement.Engine.Quote(","), -1)) + } else { + columnStr = statement.genColumnStr() + } } } else { - if columnStr == "" { - columnStr = "*" + if len(columnStr) == 0 { + if statement.GroupByStr != "" { + columnStr = statement.Engine.Quote(strings.Replace(statement.GroupByStr, ",", statement.Engine.Quote(","), -1)) + } else { + columnStr = "*" + } } } @@ -1178,14 +1202,16 @@ func (statement *Statement) genCountSql(bean interface{}) (string, []interface{} id = "" } statement.attachInSql() - return statement.genSelectSql(fmt.Sprintf("count(%v) AS %v", id, statement.Engine.Quote("total"))), append(statement.Params, statement.BeanArgs...) + return statement.genSelectSql(fmt.Sprintf("count(%v)", id)), append(statement.Params, statement.BeanArgs...) } func (statement *Statement) genSelectSql(columnStr string) (a string) { - if statement.GroupByStr != "" { - columnStr = statement.Engine.Quote(strings.Replace(statement.GroupByStr, ",", statement.Engine.Quote(","), -1)) - statement.GroupByStr = columnStr - } + /*if statement.GroupByStr != "" { + if columnStr == "" { + columnStr = statement.Engine.Quote(strings.Replace(statement.GroupByStr, ",", statement.Engine.Quote(","), -1)) + } + //statement.GroupByStr = columnStr + }*/ var distinct string if statement.IsDistinct { distinct = "DISTINCT " @@ -1210,7 +1236,11 @@ func (statement *Statement) genSelectSql(columnStr string) (a string) { } var fromStr string = " FROM " + statement.Engine.Quote(statement.TableName()) if statement.TableAlias != "" { - fromStr += " AS " + statement.Engine.Quote(statement.TableAlias) + if statement.Engine.dialect.DBType() == core.ORACLE { + fromStr += " " + statement.Engine.Quote(statement.TableAlias) + } else { + fromStr += " AS " + statement.Engine.Quote(statement.TableAlias) + } } if statement.JoinStr != "" { fromStr = fmt.Sprintf("%v %v", fromStr, statement.JoinStr) @@ -1233,8 +1263,16 @@ func (statement *Statement) genSelectSql(columnStr string) (a string) { column = statement.RefTable.ColumnsSeq()[0] } } - mssqlCondi = fmt.Sprintf("(%s NOT IN (SELECT TOP %d %s%s%s))", - column, statement.Start, column, fromStr, whereStr) + var orderStr string + if len(statement.OrderStr) > 0 { + orderStr = " ORDER BY " + statement.OrderStr + } + var groupStr string + if len(statement.GroupByStr) > 0 { + groupStr = " GROUP BY " + statement.GroupByStr + } + mssqlCondi = fmt.Sprintf("(%s NOT IN (SELECT TOP %d %s%s%s%s%s))", + column, statement.Start, column, fromStr, whereStr, orderStr, groupStr) } } @@ -1258,12 +1296,16 @@ func (statement *Statement) genSelectSql(columnStr string) (a string) { if statement.OrderStr != "" { a = fmt.Sprintf("%v ORDER BY %v", a, statement.OrderStr) } - if statement.Engine.dialect.DBType() != core.MSSQL { + if statement.Engine.dialect.DBType() != core.MSSQL && statement.Engine.dialect.DBType() != core.ORACLE { if statement.Start > 0 { a = fmt.Sprintf("%v LIMIT %v OFFSET %v", a, statement.LimitN, statement.Start) } else if statement.LimitN > 0 { a = fmt.Sprintf("%v LIMIT %v", a, statement.LimitN) } + } else if statement.Engine.dialect.DBType() == core.ORACLE { + if statement.Start != 0 || statement.LimitN != 0 { + a = fmt.Sprintf("SELECT %v FROM (SELECT %v,ROWNUM RN FROM (%v) at WHERE ROWNUM <= %d) aat WHERE RN > %d", columnStr, columnStr, a, statement.Start+statement.LimitN, statement.Start) + } } return diff --git a/Godeps/_workspace/src/github.com/go-xorm/xorm/xorm.go b/Godeps/_workspace/src/github.com/go-xorm/xorm/xorm.go index 76496ddd33a..71644e6c039 100644 --- a/Godeps/_workspace/src/github.com/go-xorm/xorm/xorm.go +++ b/Godeps/_workspace/src/github.com/go-xorm/xorm/xorm.go @@ -13,7 +13,7 @@ import ( ) const ( - Version string = "0.4.1" + Version string = "0.4.2.0225" ) func regDrvsNDialects() bool { @@ -84,17 +84,16 @@ func NewEngine(driverName string, dataSourceName string) (*Engine, error) { TZLocation: time.Local, } + engine.dialect.SetLogger(engine.Logger) + engine.SetMapper(core.NewCacheMapper(new(core.SnakeMapper))) - //engine.Filters = dialect.Filters() - //engine.Cacher = NewLRUCacher() - //err = engine.SetPool(NewSysConnectPool()) - runtime.SetFinalizer(engine, close) - return engine, err + + return engine, nil } // clone an engine func (engine *Engine) Clone() (*Engine, error) { - return NewEngine(engine.dialect.DriverName(), engine.dialect.DataSourceName()) + return NewEngine(engine.DriverName(), engine.DataSourceName()) } diff --git a/Godeps/_workspace/src/github.com/mattn/go-sqlite3/README.md b/Godeps/_workspace/src/github.com/mattn/go-sqlite3/README.md index 9d04745fa78..4383f0cd4ce 100644 --- a/Godeps/_workspace/src/github.com/mattn/go-sqlite3/README.md +++ b/Godeps/_workspace/src/github.com/mattn/go-sqlite3/README.md @@ -41,12 +41,18 @@ FAQ > See: https://github.com/mattn/go-sqlite3/issues/106 > See also: http://www.limitlessfx.com/cross-compile-golang-app-for-windows-from-linux.html +* Want to get time.Time with current locale + + Use `loc=auto` in SQLite3 filename schema like `file:foo.db?loc=auto`. + License ------- MIT: http://mattn.mit-license.org/2012 -sqlite.c, sqlite3.h, sqlite3ext.h +sqlite3-binding.c, sqlite3-binding.h, sqlite3ext.h + +The -binding suffix was added to avoid build failures under gccgo. In this repository, those files are amalgamation code that copied from SQLite3. The license of those codes are depend on the license of SQLite3. diff --git a/Godeps/_workspace/src/github.com/mattn/go-sqlite3/backup.go b/Godeps/_workspace/src/github.com/mattn/go-sqlite3/backup.go index 270446aa724..3807c606b22 100644 --- a/Godeps/_workspace/src/github.com/mattn/go-sqlite3/backup.go +++ b/Godeps/_workspace/src/github.com/mattn/go-sqlite3/backup.go @@ -6,7 +6,7 @@ package sqlite3 /* -#include +#include #include */ import "C" diff --git a/Godeps/_workspace/src/github.com/mattn/go-sqlite3/error_test.go b/Godeps/_workspace/src/github.com/mattn/go-sqlite3/error_test.go index a0061889465..1ccbe5bf858 100644 --- a/Godeps/_workspace/src/github.com/mattn/go-sqlite3/error_test.go +++ b/Godeps/_workspace/src/github.com/mattn/go-sqlite3/error_test.go @@ -231,6 +231,12 @@ func TestExtendedErrorCodes_Unique(t *testing.T) { t.Errorf("Wrong extended error code: %d != %d", sqliteErr.ExtendedCode, ErrConstraintUnique) } + extended := sqliteErr.Code.Extend(3).Error() + expected := "constraint failed" + if extended != expected { + t.Errorf("Wrong basic error code: %q != %q", + extended, expected) + } } } diff --git a/Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3.c b/Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3-binding.c similarity index 100% rename from Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3.c rename to Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3-binding.c diff --git a/Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3.h b/Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3-binding.h similarity index 100% rename from Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3.h rename to Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3-binding.h diff --git a/Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3.go b/Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3.go index d446fb69f42..f4de3fd6f1c 100644 --- a/Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3.go +++ b/Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3.go @@ -6,7 +6,10 @@ package sqlite3 /* -#include +#cgo CFLAGS: -std=gnu99 +#cgo CFLAGS: -DSQLITE_ENABLE_RTREE -DSQLITE_THREADSAFE +#cgo CFLAGS: -DSQLITE_ENABLE_FTS3 -DSQLITE_ENABLE_FTS3_PARENTHESIS +#include #include #include @@ -44,14 +47,23 @@ _sqlite3_bind_blob(sqlite3_stmt *stmt, int n, void *p, int np) { #include #include -static long -_sqlite3_last_insert_rowid(sqlite3* db) { - return (long) sqlite3_last_insert_rowid(db); +static int +_sqlite3_exec(sqlite3* db, const char* pcmd, long* rowid, long* changes) +{ + int rv = sqlite3_exec(db, pcmd, 0, 0, 0); + *rowid = (long) sqlite3_last_insert_rowid(db); + *changes = (long) sqlite3_changes(db); + return rv; } -static long -_sqlite3_changes(sqlite3* db) { - return (long) sqlite3_changes(db); +static int +_sqlite3_step(sqlite3_stmt* stmt, long* rowid, long* changes) +{ + int rv = sqlite3_step(stmt); + sqlite3* db = sqlite3_db_handle(stmt); + *rowid = (long) sqlite3_last_insert_rowid(db); + *changes = (long) sqlite3_changes(db); + return rv; } */ @@ -60,8 +72,11 @@ import ( "database/sql" "database/sql/driver" "errors" + "fmt" "io" + "net/url" "runtime" + "strconv" "strings" "time" "unsafe" @@ -102,7 +117,8 @@ type SQLiteDriver struct { // Conn struct. type SQLiteConn struct { - db *C.sqlite3 + db *C.sqlite3 + loc *time.Location } // Tx struct. @@ -114,6 +130,8 @@ type SQLiteTx struct { type SQLiteStmt struct { c *SQLiteConn s *C.sqlite3_stmt + nv int + nn []string t string closed bool cls bool @@ -174,7 +192,7 @@ func (c *SQLiteConn) Exec(query string, args []driver.Value) (driver.Result, err if s.(*SQLiteStmt).s != nil { na := s.NumInput() if len(args) < na { - return nil, errors.New("args is not enough to execute query") + return nil, fmt.Errorf("Not enough args to execute query. Expected %d, got %d.", na, len(args)) } res, err = s.Exec(args[:na]) if err != nil && err != driver.ErrSkip { @@ -201,6 +219,9 @@ func (c *SQLiteConn) Query(query string, args []driver.Value) (driver.Rows, erro } s.(*SQLiteStmt).cls = true na := s.NumInput() + if len(args) < na { + return nil, fmt.Errorf("Not enough args to execute query. Expected %d, got %d.", na, len(args)) + } rows, err := s.Query(args[:na]) if err != nil && err != driver.ErrSkip { s.Close() @@ -220,14 +241,13 @@ func (c *SQLiteConn) Query(query string, args []driver.Value) (driver.Rows, erro func (c *SQLiteConn) exec(cmd string) (driver.Result, error) { pcmd := C.CString(cmd) defer C.free(unsafe.Pointer(pcmd)) - rv := C.sqlite3_exec(c.db, pcmd, nil, nil, nil) + + var rowid, changes C.long + rv := C._sqlite3_exec(c.db, pcmd, &rowid, &changes) if rv != C.SQLITE_OK { return nil, c.lastError() } - return &SQLiteResult{ - int64(C._sqlite3_last_insert_rowid(c.db)), - int64(C._sqlite3_changes(c.db)), - }, nil + return &SQLiteResult{int64(rowid), int64(changes)}, nil } // Begin transaction. @@ -248,11 +268,51 @@ func errorString(err Error) string { // file:test.db?cache=shared&mode=memory // :memory: // file::memory: +// go-sqlite handle especially query parameters. +// _loc=XXX +// Specify location of time format. It's possible to specify "auto". +// _busy_timeout=XXX +// Specify value for sqlite3_busy_timeout. func (d *SQLiteDriver) Open(dsn string) (driver.Conn, error) { if C.sqlite3_threadsafe() == 0 { return nil, errors.New("sqlite library was not compiled for thread-safe operation") } + var loc *time.Location + busy_timeout := 5000 + pos := strings.IndexRune(dsn, '?') + if pos >= 1 { + params, err := url.ParseQuery(dsn[pos+1:]) + if err != nil { + return nil, err + } + + // _loc + if val := params.Get("_loc"); val != "" { + if val == "auto" { + loc = time.Local + } else { + loc, err = time.LoadLocation(val) + if err != nil { + return nil, fmt.Errorf("Invalid _loc: %v: %v", val, err) + } + } + } + + // _busy_timeout + if val := params.Get("_busy_timeout"); val != "" { + iv, err := strconv.ParseInt(val, 10, 64) + if err != nil { + return nil, fmt.Errorf("Invalid _busy_timeout: %v: %v", val, err) + } + busy_timeout = int(iv) + } + + if !strings.HasPrefix(dsn, "file:") { + dsn = dsn[:pos] + } + } + var db *C.sqlite3 name := C.CString(dsn) defer C.free(unsafe.Pointer(name)) @@ -268,12 +328,12 @@ func (d *SQLiteDriver) Open(dsn string) (driver.Conn, error) { return nil, errors.New("sqlite succeeded without returning a database") } - rv = C.sqlite3_busy_timeout(db, 5000) + rv = C.sqlite3_busy_timeout(db, C.int(busy_timeout)) if rv != C.SQLITE_OK { return nil, Error{Code: ErrNo(rv)} } - conn := &SQLiteConn{db} + conn := &SQLiteConn{db: db, loc: loc} if len(d.Extensions) > 0 { rv = C.sqlite3_enable_load_extension(db, 1) @@ -281,21 +341,15 @@ func (d *SQLiteDriver) Open(dsn string) (driver.Conn, error) { return nil, errors.New(C.GoString(C.sqlite3_errmsg(db))) } - stmt, err := conn.Prepare("SELECT load_extension(?);") - if err != nil { - return nil, err - } - for _, extension := range d.Extensions { - if _, err = stmt.Exec([]driver.Value{extension}); err != nil { - return nil, err + cext := C.CString(extension) + defer C.free(unsafe.Pointer(cext)) + rv = C.sqlite3_load_extension(db, cext, nil, nil) + if rv != C.SQLITE_OK { + return nil, errors.New(C.GoString(C.sqlite3_errmsg(db))) } } - if err = stmt.Close(); err != nil { - return nil, err - } - rv = C.sqlite3_enable_load_extension(db, 0) if rv != C.SQLITE_OK { return nil, errors.New(C.GoString(C.sqlite3_errmsg(db))) @@ -333,10 +387,18 @@ func (c *SQLiteConn) Prepare(query string) (driver.Stmt, error) { return nil, c.lastError() } var t string - if tail != nil && C.strlen(tail) > 0 { + if tail != nil && *tail != '\000' { t = strings.TrimSpace(C.GoString(tail)) } - ss := &SQLiteStmt{c: c, s: s, t: t} + nv := int(C.sqlite3_bind_parameter_count(s)) + var nn []string + for i := 0; i < nv; i++ { + pn := C.GoString(C.sqlite3_bind_parameter_name(s, C.int(i+1))) + if len(pn) > 1 && pn[0] == '$' && 48 <= pn[1] && pn[1] <= 57 { + nn = append(nn, C.GoString(C.sqlite3_bind_parameter_name(s, C.int(i+1)))) + } + } + ss := &SQLiteStmt{c: c, s: s, nv: nv, nn: nn, t: t} runtime.SetFinalizer(ss, (*SQLiteStmt).Close) return ss, nil } @@ -360,7 +422,12 @@ func (s *SQLiteStmt) Close() error { // Return a number of parameters. func (s *SQLiteStmt) NumInput() int { - return int(C.sqlite3_bind_parameter_count(s.s)) + return s.nv +} + +type bindArg struct { + n int + v driver.Value } func (s *SQLiteStmt) bind(args []driver.Value) error { @@ -369,8 +436,24 @@ func (s *SQLiteStmt) bind(args []driver.Value) error { return s.c.lastError() } - for i, v := range args { - n := C.int(i + 1) + var vargs []bindArg + narg := len(args) + vargs = make([]bindArg, narg) + if len(s.nn) > 0 { + for i, v := range s.nn { + if pi, err := strconv.Atoi(v[1:]); err == nil { + vargs[i] = bindArg{pi, args[i]} + } + } + } else { + for i, v := range args { + vargs[i] = bindArg{i + 1, v} + } + } + + for _, varg := range vargs { + n := C.int(varg.n) + v := varg.v switch v := v.(type) { case nil: rv = C.sqlite3_bind_null(s.s, n) @@ -431,19 +514,18 @@ func (r *SQLiteResult) RowsAffected() (int64, error) { func (s *SQLiteStmt) Exec(args []driver.Value) (driver.Result, error) { if err := s.bind(args); err != nil { C.sqlite3_reset(s.s) + C.sqlite3_clear_bindings(s.s) return nil, err } - rv := C.sqlite3_step(s.s) + var rowid, changes C.long + rv := C._sqlite3_step(s.s, &rowid, &changes) if rv != C.SQLITE_ROW && rv != C.SQLITE_OK && rv != C.SQLITE_DONE { + err := s.c.lastError() C.sqlite3_reset(s.s) - return nil, s.c.lastError() + C.sqlite3_clear_bindings(s.s) + return nil, err } - - res := &SQLiteResult{ - int64(C._sqlite3_last_insert_rowid(s.c.db)), - int64(C._sqlite3_changes(s.c.db)), - } - return res, nil + return &SQLiteResult{int64(rowid), int64(changes)}, nil } // Close the rows. @@ -499,7 +581,22 @@ func (rc *SQLiteRows) Next(dest []driver.Value) error { val := int64(C.sqlite3_column_int64(rc.s.s, C.int(i))) switch rc.decltype[i] { case "timestamp", "datetime", "date": - dest[i] = time.Unix(val, 0).Local() + unixTimestamp := strconv.FormatInt(val, 10) + var t time.Time + if len(unixTimestamp) == 13 { + duration, err := time.ParseDuration(unixTimestamp + "ms") + if err != nil { + return fmt.Errorf("error parsing %s value %d, %s", rc.decltype[i], val, err) + } + epoch := time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC) + t = epoch.Add(duration) + } else { + t = time.Unix(val, 0) + } + if rc.s.c.loc != nil { + t = t.In(rc.s.c.loc) + } + dest[i] = t case "boolean": dest[i] = val > 0 default: @@ -531,16 +628,21 @@ func (rc *SQLiteRows) Next(dest []driver.Value) error { switch rc.decltype[i] { case "timestamp", "datetime", "date": + var t time.Time for _, format := range SQLiteTimestampFormats { if timeVal, err = time.ParseInLocation(format, s, time.UTC); err == nil { - dest[i] = timeVal.Local() + t = timeVal break } } if err != nil { // The column is a time value, so return the zero time on parse failure. - dest[i] = time.Time{} + t = time.Time{} } + if rc.s.c.loc != nil { + t = t.In(rc.s.c.loc) + } + dest[i] = t default: dest[i] = []byte(s) } diff --git a/Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3_fts3_test.go b/Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3_fts3_test.go new file mode 100644 index 00000000000..a1cd2172d79 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3_fts3_test.go @@ -0,0 +1,83 @@ +// Copyright (C) 2015 Yasuhiro Matsumoto . +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file. + +package sqlite3 + +import ( + "database/sql" + "os" + "testing" +) + +func TestFTS3(t *testing.T) { + tempFilename := TempFilename() + db, err := sql.Open("sqlite3", tempFilename) + if err != nil { + t.Fatal("Failed to open database:", err) + } + defer os.Remove(tempFilename) + defer db.Close() + + _, err = db.Exec("DROP TABLE foo") + _, err = db.Exec("CREATE VIRTUAL TABLE foo USING fts3(id INTEGER PRIMARY KEY, value TEXT)") + if err != nil { + t.Fatal("Failed to create table:", err) + } + + _, err = db.Exec("INSERT INTO foo(id, value) VALUES(?, ?)", 1, `今日の 晩御飯は 天麩羅よ`) + if err != nil { + t.Fatal("Failed to insert value:", err) + } + + _, err = db.Exec("INSERT INTO foo(id, value) VALUES(?, ?)", 2, `今日は いい 天気だ`) + if err != nil { + t.Fatal("Failed to insert value:", err) + } + + rows, err := db.Query("SELECT id, value FROM foo WHERE value MATCH '今日* 天*'") + if err != nil { + t.Fatal("Unable to query foo table:", err) + } + defer rows.Close() + + for rows.Next() { + var id int + var value string + + if err := rows.Scan(&id, &value); err != nil { + t.Error("Unable to scan results:", err) + continue + } + + if id == 1 && value != `今日の 晩御飯は 天麩羅よ` { + t.Error("Value for id 1 should be `今日の 晩御飯は 天麩羅よ`, but:", value) + } else if id == 2 && value != `今日は いい 天気だ` { + t.Error("Value for id 2 should be `今日は いい 天気だ`, but:", value) + } + } + + rows, err = db.Query("SELECT value FROM foo WHERE value MATCH '今日* 天麩羅*'") + if err != nil { + t.Fatal("Unable to query foo table:", err) + } + defer rows.Close() + + var value string + if !rows.Next() { + t.Fatal("Result should be only one") + } + + if err := rows.Scan(&value); err != nil { + t.Fatal("Unable to scan results:", err) + } + + if value != `今日の 晩御飯は 天麩羅よ` { + t.Fatal("Value should be `今日の 晩御飯は 天麩羅よ`, but:", value) + } + + if rows.Next() { + t.Fatal("Result should be only one") + } +} diff --git a/Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3_other.go b/Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3_other.go index 54b6c7a5e25..8d98b4a3a6c 100644 --- a/Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3_other.go +++ b/Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3_other.go @@ -9,6 +9,6 @@ package sqlite3 /* #cgo CFLAGS: -I. #cgo linux LDFLAGS: -ldl -#cgo CFLAGS: -DSQLITE_ENABLE_RTREE -DSQLITE_THREADSAFE +#cgo LDFLAGS: -lpthread */ import "C" diff --git a/Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3_test.go b/Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3_test.go index 9cc5a0ec5ed..aa8601181b5 100644 --- a/Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3_test.go +++ b/Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3_test.go @@ -9,8 +9,10 @@ import ( "crypto/rand" "database/sql" "encoding/hex" + "net/url" "os" "path/filepath" + "strings" "testing" "time" @@ -309,6 +311,7 @@ func TestTimestamp(t *testing.T) { {"0000-00-00 00:00:00", time.Time{}}, {timestamp1, timestamp1}, {timestamp1.Unix(), timestamp1}, + {timestamp1.UnixNano() / int64(time.Millisecond), timestamp1}, {timestamp1.In(time.FixedZone("TEST", -7*3600)), timestamp1}, {timestamp1.Format("2006-01-02 15:04:05.000"), timestamp1}, {timestamp1.Format("2006-01-02T15:04:05.000"), timestamp1}, @@ -633,6 +636,102 @@ func TestWAL(t *testing.T) { } } +func TestTimezoneConversion(t *testing.T) { + zones := []string{"UTC", "US/Central", "US/Pacific", "Local"} + for _, tz := range zones { + tempFilename := TempFilename() + db, err := sql.Open("sqlite3", tempFilename+"?_loc="+url.QueryEscape(tz)) + if err != nil { + t.Fatal("Failed to open database:", err) + } + defer os.Remove(tempFilename) + defer db.Close() + + _, err = db.Exec("DROP TABLE foo") + _, err = db.Exec("CREATE TABLE foo(id INTEGER, ts TIMESTAMP, dt DATETIME)") + if err != nil { + t.Fatal("Failed to create table:", err) + } + + loc, err := time.LoadLocation(tz) + if err != nil { + t.Fatal("Failed to load location:", err) + } + + timestamp1 := time.Date(2012, time.April, 6, 22, 50, 0, 0, time.UTC) + timestamp2 := time.Date(2006, time.January, 2, 15, 4, 5, 123456789, time.UTC) + timestamp3 := time.Date(2012, time.November, 4, 0, 0, 0, 0, time.UTC) + tests := []struct { + value interface{} + expected time.Time + }{ + {"nonsense", time.Time{}.In(loc)}, + {"0000-00-00 00:00:00", time.Time{}.In(loc)}, + {timestamp1, timestamp1.In(loc)}, + {timestamp1.Unix(), timestamp1.In(loc)}, + {timestamp1.In(time.FixedZone("TEST", -7*3600)), timestamp1.In(loc)}, + {timestamp1.Format("2006-01-02 15:04:05.000"), timestamp1.In(loc)}, + {timestamp1.Format("2006-01-02T15:04:05.000"), timestamp1.In(loc)}, + {timestamp1.Format("2006-01-02 15:04:05"), timestamp1.In(loc)}, + {timestamp1.Format("2006-01-02T15:04:05"), timestamp1.In(loc)}, + {timestamp2, timestamp2.In(loc)}, + {"2006-01-02 15:04:05.123456789", timestamp2.In(loc)}, + {"2006-01-02T15:04:05.123456789", timestamp2.In(loc)}, + {"2012-11-04", timestamp3.In(loc)}, + {"2012-11-04 00:00", timestamp3.In(loc)}, + {"2012-11-04 00:00:00", timestamp3.In(loc)}, + {"2012-11-04 00:00:00.000", timestamp3.In(loc)}, + {"2012-11-04T00:00", timestamp3.In(loc)}, + {"2012-11-04T00:00:00", timestamp3.In(loc)}, + {"2012-11-04T00:00:00.000", timestamp3.In(loc)}, + } + for i := range tests { + _, err = db.Exec("INSERT INTO foo(id, ts, dt) VALUES(?, ?, ?)", i, tests[i].value, tests[i].value) + if err != nil { + t.Fatal("Failed to insert timestamp:", err) + } + } + + rows, err := db.Query("SELECT id, ts, dt FROM foo ORDER BY id ASC") + if err != nil { + t.Fatal("Unable to query foo table:", err) + } + defer rows.Close() + + seen := 0 + for rows.Next() { + var id int + var ts, dt time.Time + + if err := rows.Scan(&id, &ts, &dt); err != nil { + t.Error("Unable to scan results:", err) + continue + } + if id < 0 || id >= len(tests) { + t.Error("Bad row id: ", id) + continue + } + seen++ + if !tests[id].expected.Equal(ts) { + t.Errorf("Timestamp value for id %v (%v) should be %v, not %v", id, tests[id].value, tests[id].expected, ts) + } + if !tests[id].expected.Equal(dt) { + t.Errorf("Datetime value for id %v (%v) should be %v, not %v", id, tests[id].value, tests[id].expected, dt) + } + if tests[id].expected.Location().String() != ts.Location().String() { + t.Errorf("Location for id %v (%v) should be %v, not %v", id, tests[id].value, tests[id].expected.Location().String(), ts.Location().String()) + } + if tests[id].expected.Location().String() != dt.Location().String() { + t.Errorf("Location for id %v (%v) should be %v, not %v", id, tests[id].value, tests[id].expected.Location().String(), dt.Location().String()) + } + } + + if seen != len(tests) { + t.Errorf("Expected to see %d rows", len(tests)) + } + } +} + func TestSuite(t *testing.T) { db, err := sql.Open("sqlite3", ":memory:") if err != nil { @@ -742,3 +841,107 @@ func TestStress(t *testing.T) { db.Close() } } + +func TestDateTimeLocal(t *testing.T) { + zone := "Asia/Tokyo" + tempFilename := TempFilename() + db, err := sql.Open("sqlite3", tempFilename+"?_loc="+zone) + if err != nil { + t.Fatal("Failed to open database:", err) + } + db.Exec("CREATE TABLE foo (dt datetime);") + db.Exec("INSERT INTO foo VALUES('2015-03-05 15:16:17');") + + row := db.QueryRow("select * from foo") + var d time.Time + err = row.Scan(&d) + if err != nil { + t.Fatal("Failed to scan datetime:", err) + } + if d.Hour() == 15 || !strings.Contains(d.String(), "JST") { + t.Fatal("Result should have timezone", d) + } + db.Close() + + db, err = sql.Open("sqlite3", tempFilename) + if err != nil { + t.Fatal("Failed to open database:", err) + } + + row = db.QueryRow("select * from foo") + err = row.Scan(&d) + if err != nil { + t.Fatal("Failed to scan datetime:", err) + } + if d.UTC().Hour() != 15 || !strings.Contains(d.String(), "UTC") { + t.Fatalf("Result should not have timezone %v %v", zone, d.String()) + } + + _, err = db.Exec("DELETE FROM foo") + if err != nil { + t.Fatal("Failed to delete table:", err) + } + dt, err := time.Parse("2006/1/2 15/4/5 -0700 MST", "2015/3/5 15/16/17 +0900 JST") + if err != nil { + t.Fatal("Failed to parse datetime:", err) + } + db.Exec("INSERT INTO foo VALUES(?);", dt) + + db.Close() + db, err = sql.Open("sqlite3", tempFilename+"?_loc="+zone) + if err != nil { + t.Fatal("Failed to open database:", err) + } + + row = db.QueryRow("select * from foo") + err = row.Scan(&d) + if err != nil { + t.Fatal("Failed to scan datetime:", err) + } + if d.Hour() != 15 || !strings.Contains(d.String(), "JST") { + t.Fatalf("Result should have timezone %v %v", zone, d.String()) + } +} + +func TestVersion(t *testing.T) { + s, n, id := Version() + if s == "" || n == 0 || id == "" { + t.Errorf("Version failed %q, %d, %q\n", s, n, id) + } +} + +func TestNumberNamedParams(t *testing.T) { + tempFilename := TempFilename() + db, err := sql.Open("sqlite3", tempFilename) + if err != nil { + t.Fatal("Failed to open database:", err) + } + defer os.Remove(tempFilename) + defer db.Close() + + _, err = db.Exec(` + create table foo (id integer, name text, extra text); + `) + if err != nil { + t.Error("Failed to call db.Query:", err) + } + + _, err = db.Exec(`insert into foo(id, name, extra) values($1, $2, $2)`, 1, "foo") + if err != nil { + t.Error("Failed to call db.Exec:", err) + } + + row := db.QueryRow(`select id, extra from foo where id = $1 and extra = $2`, 1, "foo") + if row == nil { + t.Error("Failed to call db.QueryRow") + } + var id int + var extra string + err = row.Scan(&id, &extra) + if err != nil { + t.Error("Failed to db.Scan:", err) + } + if id != 1 || extra != "foo" { + t.Error("Failed to db.QueryRow: not matched results") + } +} diff --git a/Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3_windows.go b/Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3_windows.go index 84eb457f61b..abc8384e4b8 100644 --- a/Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3_windows.go +++ b/Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3_windows.go @@ -2,6 +2,7 @@ // // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. +// +build windows package sqlite3 @@ -9,6 +10,5 @@ package sqlite3 #cgo CFLAGS: -I. -fno-stack-check -fno-stack-protector -mno-stack-arg-probe #cgo windows,386 CFLAGS: -D_localtime32=localtime #cgo LDFLAGS: -lmingwex -lmingw32 -#cgo CFLAGS: -DSQLITE_ENABLE_RTREE -DSQLITE_THREADSAFE */ import "C" diff --git a/Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3ext.h b/Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3ext.h index ecf93f62f6c..7cc58b6f86b 100644 --- a/Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3ext.h +++ b/Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3ext.h @@ -17,7 +17,7 @@ */ #ifndef _SQLITE3EXT_H_ #define _SQLITE3EXT_H_ -#include "sqlite3.h" +#include "sqlite3-binding.h" typedef struct sqlite3_api_routines sqlite3_api_routines; diff --git a/README.md b/README.md index a55874632dd..fdc7c3b295b 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ [Grafana](http://grafana.org) [![Build Status](https://api.travis-ci.org/grafana/grafana.svg)](https://travis-ci.org/grafana/grafana) [![Coverage Status](https://coveralls.io/repos/grafana/grafana/badge.png)](https://coveralls.io/r/grafana/grafana) [![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/grafana/grafana?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ================ [Website](http://grafana.org) | -[Twitter](http://twitter.com/grafana) | -[IRC](http://webchat.freenode.net/?channels=grafana) | +[Twitter](https://twitter.com/grafana) | +[IRC](https://webchat.freenode.net/?channels=grafana) | [Email](mailto:contact@grafana.org) -Grafana is An open source, feature rich metrics dashboard and graph editor for +Grafana is an open source, feature rich metrics dashboard and graph editor for Graphite, InfluxDB & OpenTSDB. ![](http://grafana.org/assets/img/start_page_bg.png) @@ -84,13 +84,13 @@ grafana admin user that is created on first startup also creates the main accoun - [See it in action](http://grafana.org/docs/features/graphite) ### Graphing -- Fast rendering, even over large timespans. -- Click and drag to zoom. -- Multiple Y-axis. -- Bars, Lines, Points. +- Fast rendering, even over large timespans +- Click and drag to zoom +- Multiple Y-axis +- Bars, Lines, Points - Smart Y-axis formating - Series toggles & color selector -- Legend values, and formating options +- Legend values, and formatting options - Grid thresholds, axis labels - [Annotations](http://grafana.org/docs/features/annotations) @@ -107,7 +107,7 @@ grafana admin user that is created on first startup also creates the main accoun - [Time range controls](http://grafana.org/docs/features/time_range) ### InfluxDB -- Use InfluxDB as a metric data source, annotation source and for dashboard storage +- Use InfluxDB as a metric data source, annotation source, and for dashboard storage - Query editor with series and column typeahead, easy group by and function selection ### OpenTSDB @@ -121,7 +121,7 @@ There are no dependencies, Grafana is a client side application that runs in you Head to [grafana.org](http://grafana.org) and [download](http://grafana.org/download/) the latest release. -Then follow the quick [setup & config guide](http://grafana.org/docs/). If you have any problems please +Then follow the [quick setup & config guide](http://grafana.org/docs/). If you have any problems please read the [troubleshooting guide](http://grafana.org/docs/troubleshooting). ## Documentation & Support @@ -129,12 +129,12 @@ Be sure to read the [getting started guide](http://grafana.org/docs/features/int feature guides. ## Run from master -Grafana uses nodejs and grunt for asset management (css & javascript), unit test runner and javascript syntax verification. +Grafana uses Node.js and Grunt for asset management (css & javascript), unit test runner and javascript syntax verification. - clone repository - install nodejs - npm install (in project root) - npm install -g grunt-cli -- grunt (runt default task that will generate css files) +- grunt (grunt default task that will generate css files) - grunt build (creates optimized & minified release) - grunt release (same as grunt build but will also create tar & zip package) - grunt test (executes jshint and unit tests) diff --git a/build.go b/build.go index f7d77129d92..4ee59928d67 100644 --- a/build.go +++ b/build.go @@ -90,8 +90,8 @@ func main() { func makeLatestDistCopies() { runError("cp", "dist/grafana_"+version+"_amd64.deb", "dist/grafana_latest_amd64.deb") - runError("cp", "dist/grafana-"+strings.Replace(version, "-", "_", 5)+"-1.x86_64.rpm", "dist/grafana-latest-1.x84_64.rpm") - runError("cp", "dist/grafana-"+version+".x86_64.tar.gz", "dist/grafana-latest.x84_64.tar.gz") + runError("cp", "dist/grafana-"+strings.Replace(version, "-", "_", 5)+"-1.x86_64.rpm", "dist/grafana-latest-1.x86_64.rpm") + runError("cp", "dist/grafana-"+version+".x86_64.tar.gz", "dist/grafana-latest.x86_64.tar.gz") } func readVersionFromPackageJson() { diff --git a/conf/defaults.ini b/conf/defaults.ini index 5318e89b2ad..92f2afe002f 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -1,6 +1,12 @@ app_name = Grafana app_mode = production +# Report anonymous usage counters to stats.grafana.org (https). +# No ip addresses are being tracked, only simple counters to track +# running instances, dashboard count and errors. It is very helpful to us. +# Change this option to false to disable reporting. +reporting-enabled = true + [server] ; protocol (http or https) protocol = http @@ -39,7 +45,7 @@ provider = file ; memory: not have any config yet ; file: session file path, e.g. `data/sessions` ; redis: config like redis server addr, poolSize, password, e.g. `127.0.0.1:6379,100,grafana` -; mysql: go-sql-driver/mysql dsn config string, e.g. `root:password@/session_table` +; mysql: go-sql-driver/mysql dsn config string, e.g. `user:password@tcp(127.0.0.1)/database_name` provider_config = data/sessions ; Session cookie name cookie_name = grafana_sess diff --git a/conf/sample.ini b/conf/sample.ini index 4e4c335ae18..f7207668c8f 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -5,6 +5,13 @@ app_mode = production +# Once every 1 hour Grafana will report anonymous data to +# stats.grafana.org (https). No ip addresses are being tracked. +# only simple counters to track running instances, dashboard +# counts and errors. It is very helpful to us. +# Change this option to false to disable reporting. +reporting-enabled = true + [server] ; protocol (http or https) protocol = http diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 1e963b1b540..5d4ddbf8de6 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -34,12 +34,13 @@ pages: - ['installation/migrating_to2.md', 'Installation', 'Migrating from v1.x to v2.x'] - ['guides/gettingstarted.md', 'User Guides', 'Getting started'] -- ['guides/changes_in_v2.md', 'User Guides', 'Changes and New Features in v2.0'] +- ['guides/whats-new-in-v2.md', 'User Guides', "What's New in Grafana v2.0"] - ['guides/screencasts.md', 'User Guides', 'Screencasts'] - ['reference/graph.md', 'Reference', 'Graph Panel'] - ['reference/singlestat.md', 'Reference', 'Singlestat Panel'] - ['reference/dashlist.md', 'Reference', 'Dashlist Panel'] +- ['reference/sharing.md', 'Reference', 'Sharing'] - ['reference/annotations.md', 'Reference', 'Annotations'] - ['reference/timerange.md', 'Reference', 'Time range controls'] - ['reference/search.md', 'Reference', 'Dashboard Search'] diff --git a/docs/sources/guides/gettingstarted.md b/docs/sources/guides/gettingstarted.md index df936138483..9e0048263f8 100644 --- a/docs/sources/guides/gettingstarted.md +++ b/docs/sources/guides/gettingstarted.md @@ -8,7 +8,18 @@ page_keywords: grafana, guide, documentation This guide will help you get started and acquainted with the Grafana user interface. ## Interface overview - + +### Dashboard header + + +1. Side menu toggle +2. Dashboard title & Search dropdown (also includes access to New dashboard, Import & Playlist) +3. Star/unstar current dashboard +4. Share current dashboard (Make sure the dashboard is saved before) +5. Save current dashboard +6. Settings dropdown (dashboard settings, annotations, templating, etc) + + ## New dashboard ![](/img/animated_gifs/new_dashboard.gif) diff --git a/docs/sources/guides/changes_in_v2.md b/docs/sources/guides/whats-new-in-v2.md similarity index 65% rename from docs/sources/guides/changes_in_v2.md rename to docs/sources/guides/whats-new-in-v2.md index c9d08f046bd..bb5c4c15c7d 100644 --- a/docs/sources/guides/changes_in_v2.md +++ b/docs/sources/guides/whats-new-in-v2.md @@ -1,37 +1,41 @@ --- -page_title: Changes and new features in Grafana v2.0 -page_description: Changes and new features in Grafana v2.0 -page_keywords: grafana, changes, features, documentation +page_title: What's New in Grafana v2.0 +page_description: What's new in Grafana v2.0 +page_keywords: grafana, new, changes, features, documentation --- -# Changes and new features in v2.0 - -This is a guide that descriptes some of changes and new features that can be found in Grafana v2.0. +# What's New in Grafana v2.0 +This is a guide that describes some of changes and new features that can be found in Grafana v2.0. ## New dashboard top header 1. Side menu toggle -2. Dashboard search (also includes access to New dashboard, Import & Playlist) -3. Dashboard title -4. Star/unstar current dashboard -5. Share current dashboard (Make sure the dashboard is saved before) -6. Save current dashboard -7. Settings dropdown - - Dashboard settings - - Annotations - - Templating - - Export (exports current dashboard to json file) - - View JSON (view current dashboard json model) - - Save As... (Copy & Save current dashboard under a new name) - - Delete dashboard +2. Dashboard title & Search dropdown (also includes access to New dashboard, Import & Playlist) +3. Star/unstar current dashboard +4. Share current dashboard (Make sure the dashboard is saved before) +5. Save current dashboard +6. Settings dropdown (dashboard settings, annotations, templating, etc) > **Note** In Grafana v2.0 when you change the title of a dashboard and then save it it will no > longer create a new dashboard. It will just change the name for the current dashboard. > To change name and create a new dashboard use the `Save As...` menu option +## Dashboard Snapshot sharing +A dashboard snapshot is an instant way to share an interactive dashboard publicly. When created, we strip sensitive data like queries +(metric, template and annotation) and panel links, leaving only the visible metric data and series names embedded into your dashboard. Dashboard +snapshots can be accessed by anyone who has the link and can reach the URL. + +![](/img/v2/dashboard_snapshot_dialog.png) + +### Publish snapshots +You can publish snapshots to you local instance or to [snapshot.raintank.io](http://snapshot.raintank.io). The later is a free service +that is provided by [Raintank](http://raintank.io) that allows you to publish dashboard snapshots to an external grafana instance. +The same rules still apply, anyone with the link can view it. You can set an expiration time if you want the snapshot to be removed +after a certain time period. + ## Panel time overrides & timeshift In Grafana v2.x you can now override the relative time range for individual panels. You can also add a @@ -59,6 +63,14 @@ upper right of a panel when overriden time range options. The dashboard search view has received a big UI update and polish. You can now see and filter by which dashboard you have personally starred. +## Logarithmic scale + +The Graph panel now supports 3 logarithmic scales, `log base 10`, `log base 32`, `log base 1024`. Logarithmic y-axis +scales are very useful when rendering many series of different order of magnitude on the same scale. For example +latency, network traffic or storage. + +![](/img/v2/graph_logbase10_ms.png) + ## Dashlist panel ![](/img/v2/dashlist_starred.png) @@ -118,3 +130,10 @@ Organizations via a role. That role can be: > per series permissions in Graphite, InfluxDB or OpenTSDB. There are currently no permissions on individual dashboards. + +## Panel IFrame embedding + +You can embed a single panel on another web page using the panel share dialog. Below you should see an iframe +with a graph panel (taken from dashoard snapshot at [snapshot.raintank.io](snapshot.raintank.io). + + diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 048301a7714..a442ee58dc3 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -184,7 +184,7 @@ Valid values are "memory", "file", "mysql", 'postgres'. Default is "memory". This option should be configured differently depending on what type of session provider you have configured. - **file:** session file path, e.g. `data/sessions` -- **mysql:** go-sql-driver/mysql dsn config string, e.g. `root:password@/session_table` +- **mysql:** go-sql-driver/mysql dsn config string, e.g. `user:password@tcp(127.0.0.1)/database_name` if you use mysql or postgres as session store you need to create the session table manually. Mysql Example: diff --git a/docs/sources/installation/performance.md b/docs/sources/installation/performance.md index 535cf72a228..ce41e7a0548 100644 --- a/docs/sources/installation/performance.md +++ b/docs/sources/installation/performance.md @@ -11,6 +11,6 @@ page_keywords: grafana, performance, documentation Graphite 0.9.13 adds a much needed feature to the json rendering API that is very important for Grafana. If you are experiance slow load & rendering times for large time ranges then it is most likely caused by running Graphite 0.9.12 or lower. The latest version of Graphite adds a maxDataPoints parameter to the json render API, without this feature Graphite can return hundreds of thousands of data points per graph, which -can hang your browser. Be sue to upgrade to [0.9.13](http://graphite.readthedocs.org/en/latest/releases/0_9_13.html). +can hang your browser. Be sure to upgrade to [0.9.13](http://graphite.readthedocs.org/en/latest/releases/0_9_13.html). diff --git a/docs/sources/reference/sharing.md b/docs/sources/reference/sharing.md new file mode 100644 index 00000000000..1d613aaddf5 --- /dev/null +++ b/docs/sources/reference/sharing.md @@ -0,0 +1,44 @@ +---- +page_title: Sharing +page_description: Sharing +page_keywords: grafana, sharing, guide, documentation +--- + +# Sharing features +Grafana provides a number of ways to share a dashboard or a specfic panel to other users within your +organization. It also provides ways to publish interactive snapshots that can be accessed by external partners. + +## Share dashboard +Share a dashboard via the share icon in the top nav. This opens the share dialog where you +can get a link to the current dashboard with the current selected time range and template variables. If you have +made changes to the dashboard, make sure those are saved before sending the link. + +### Dashboard snapshot + +A dashboard snapshot is an instant way to share an interactive dashboard publicly. When created, we strip sensitive data like queries +(metric, template and annotation) and panel links, leaving only the visible metric data and series names embedded into your dashboard. Dashboard +snapshots can be accessed by anyone who has the link and can reach the URL. + +![](/img/v2/dashboard_snapshot_dialog.png) + +### Publish snapshots +You can publish snapshots to you local instance or to [snapshot.raintank.io](http://snapshot.raintank.io). The later is a free service +that is provided by [Raintank](http://raintank.io) that allows you to publish dashboard snapshots to an external grafana instance. +The same rules still apply, anyone with the link can view it. You can set an expiration time if you want the snapshot to be removed +after a certain time period. + +## Share Panel +Click a panel title to open the panel menu, then click share in the panel menu to open the Share Panel dialog. Here you +have access to a link that will take you to exactly this panel with the current time range and selected template variables. +You also get a link to service side rendered PNG of the panel. Useful if you want to shara image of the panel. + +### Embed Panel +You can embed a panel using an iframe on another web site. This tab will show you the html that you need to use. + +Example: + +```html + +``` +Below there should be an interactive Grafana graph embedded in an iframe: + diff --git a/latest.json b/latest.json index 90189fabeca..1ca0904fd94 100644 --- a/latest.json +++ b/latest.json @@ -1,4 +1,4 @@ { "version": "1.9.1", - "url": "http://grafanarel.s3.amazonaws.com/grafana-1.9.1.tar.gz" + "url": "https://grafanarel.s3.amazonaws.com/grafana-1.9.1.tar.gz" } diff --git a/main.go b/main.go index f23a544e1e9..3b25b73039d 100644 --- a/main.go +++ b/main.go @@ -39,17 +39,7 @@ func main() { app.Name = "Grafana Backend" app.Usage = "grafana web" app.Version = version - app.Commands = []cli.Command{ - cmd.ListOrgs, - cmd.CreateOrg, - cmd.DeleteOrg, - cmd.ExportDashboard, - cmd.ImportDashboard, - cmd.ListDataSources, - cmd.CreateDataSource, - cmd.DescribeDataSource, - cmd.DeleteDataSource, - cmd.Web} + app.Commands = []cli.Command{cmd.ImportDashboard, cmd.Web} app.Flags = append(app.Flags, []cli.Flag{ cli.StringFlag{ Name: "config", diff --git a/package.json b/package.json index dfaa81c7eb9..926fbf6d5ae 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ }, "license": "Apache License", "dependencies": { - "grunt-jscs": "^0.8.1", + "grunt-jscs": "~1.5.x", "karma-sinon": "^1.0.3", "lodash": "^2.4.1", "sinon": "^1.10.3" diff --git a/pkg/api/admin_users.go b/pkg/api/admin_users.go index d3fa111d333..f7e8fca2b5e 100644 --- a/pkg/api/admin_users.go +++ b/pkg/api/admin_users.go @@ -3,6 +3,7 @@ package api import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/util" @@ -64,6 +65,8 @@ func AdminCreateUser(c *middleware.Context, form dtos.AdminCreateUserForm) { return } + metrics.M_Api_Admin_User_Create.Inc(1) + c.JsonOK("User created") } diff --git a/pkg/api/api.go b/pkg/api/api.go index 8f069c69940..88f1a7a37ee 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -41,6 +41,13 @@ func Register(r *macaron.Macaron) { r.Get("/signup", Index) r.Post("/api/user/signup", bind(m.CreateUserCommand{}), SignUp) + // dashboard snapshots + r.Post("/api/snapshots/", bind(m.CreateDashboardSnapshotCommand{}), CreateDashboardSnapshot) + r.Get("/dashboard/snapshots/*", Index) + + r.Get("/api/snapshots/:key", GetDashboardSnapshot) + r.Get("/api/snapshots-delete/:key", DeleteDashboardSnapshot) + // authed api r.Group("/api", func() { // user diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 8cde5a8bc8a..278264f22d7 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" @@ -27,6 +28,8 @@ func isDasboardStarredByUser(c *middleware.Context, dashId int64) (bool, error) } func GetDashboard(c *middleware.Context) { + metrics.M_Api_Dashboard_Get.Inc(1) + slug := c.Params(":slug") query := m.GetDashboardQuery{Slug: slug, OrgId: c.OrgId} @@ -88,6 +91,8 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) { return } + metrics.M_Api_Dashboard_Post.Inc(1) + c.JSON(200, util.DynMap{"status": "success", "slug": cmd.Result.Slug, "version": cmd.Result.Version}) } diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go new file mode 100644 index 00000000000..8de96ec9f21 --- /dev/null +++ b/pkg/api/dashboard_snapshot.go @@ -0,0 +1,87 @@ +package api + +import ( + "time" + + "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/metrics" + "github.com/grafana/grafana/pkg/middleware" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" +) + +func CreateDashboardSnapshot(c *middleware.Context, cmd m.CreateDashboardSnapshotCommand) { + if cmd.External { + // external snapshot ref requires key and delete key + if cmd.Key == "" || cmd.DeleteKey == "" { + c.JsonApiErr(400, "Missing key and delete key for external snapshot", nil) + return + } + + cmd.OrgId = -1 + cmd.UserId = -1 + metrics.M_Api_Dashboard_Snapshot_External.Inc(1) + } else { + cmd.Key = util.GetRandomString(32) + cmd.DeleteKey = util.GetRandomString(32) + cmd.OrgId = c.OrgId + cmd.UserId = c.UserId + metrics.M_Api_Dashboard_Snapshot_Create.Inc(1) + } + + if err := bus.Dispatch(&cmd); err != nil { + c.JsonApiErr(500, "Failed to create snaphost", err) + return + } + + c.JSON(200, util.DynMap{ + "key": cmd.Key, + "deleteKey": cmd.DeleteKey, + "url": setting.ToAbsUrl("dashboard/snapshot/" + cmd.Key), + "deleteUrl": setting.ToAbsUrl("api/snapshots-delete/" + cmd.DeleteKey), + }) +} + +func GetDashboardSnapshot(c *middleware.Context) { + key := c.Params(":key") + + query := &m.GetDashboardSnapshotQuery{Key: key} + + err := bus.Dispatch(query) + if err != nil { + c.JsonApiErr(500, "Failed to get dashboard snapshot", err) + return + } + + snapshot := query.Result + + // expired snapshots should also be removed from db + if snapshot.Expires.Before(time.Now()) { + c.JsonApiErr(404, "Snapshot not found", err) + return + } + + dto := dtos.Dashboard{ + Model: snapshot.Dashboard, + Meta: dtos.DashboardMeta{IsSnapshot: true}, + } + + metrics.M_Api_Dashboard_Snapshot_Get.Inc(1) + + c.Resp.Header().Set("Cache-Control", "public, max-age=3600") + c.JSON(200, dto) +} + +func DeleteDashboardSnapshot(c *middleware.Context) { + key := c.Params(":key") + cmd := &m.DeleteDashboardSnapshotCommand{DeleteKey: key} + + if err := bus.Dispatch(cmd); err != nil { + c.JsonApiErr(500, "Failed to delete dashboard snapshot", err) + return + } + + c.JSON(200, util.DynMap{"message": "Snapshot deleted. It might take an hour before it's cleared from a CDN cache."}) +} diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index c225c6a5bbb..2cb9da0189f 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -27,9 +27,10 @@ type CurrentUser struct { } type DashboardMeta struct { - IsStarred bool `json:"isStarred"` - IsHome bool `json:"isHome"` - Slug string `json:"slug"` + IsStarred bool `json:"isStarred"` + IsHome bool `json:"isHome"` + IsSnapshot bool `json:"isSnapshot"` + Slug string `json:"slug"` } type Dashboard struct { diff --git a/pkg/api/index.go b/pkg/api/index.go index 3006c54e8ab..4af66f18133 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -47,7 +47,7 @@ func Index(c *middleware.Context) { func NotFound(c *middleware.Context) { if c.IsApiRequest() { - c.JsonApiErr(200, "Not found", nil) + c.JsonApiErr(404, "Not found", nil) return } diff --git a/pkg/api/login.go b/pkg/api/login.go index e7707c53138..56a61697cb9 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" @@ -75,7 +76,6 @@ func LoginView(c *middleware.Context) { } func LoginPost(c *middleware.Context, cmd dtos.LoginCommand) { - userQuery := m.GetUserByLoginQuery{LoginOrEmail: cmd.User} err := bus.Dispatch(&userQuery) @@ -112,6 +112,8 @@ func LoginPost(c *middleware.Context, cmd dtos.LoginCommand) { c.SetCookie("redirect_to", "", -1, setting.AppSubUrl+"/") } + metrics.M_Api_Login_Post.Inc(1) + c.JSON(200, result) } diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go index 9ccb8f0b60d..a234ef02bf3 100644 --- a/pkg/api/login_oauth.go +++ b/pkg/api/login_oauth.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" @@ -81,5 +82,7 @@ func OAuthLogin(ctx *middleware.Context) { // login loginUserWithUser(userQuery.Result, ctx) + metrics.M_Api_Login_OAuth.Inc(1) + ctx.Redirect(setting.AppSubUrl + "/") } diff --git a/pkg/api/org.go b/pkg/api/org.go index 8b41b0e3f5f..ed180b1af77 100644 --- a/pkg/api/org.go +++ b/pkg/api/org.go @@ -2,6 +2,7 @@ package api import ( "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" ) @@ -35,6 +36,8 @@ func CreateOrg(c *middleware.Context, cmd m.CreateOrgCommand) { return } + metrics.M_Api_Org_Create.Inc(1) + c.JsonOK("Organization created") } diff --git a/pkg/api/signup.go b/pkg/api/signup.go index 74f00509b98..63bb34c72ac 100644 --- a/pkg/api/signup.go +++ b/pkg/api/signup.go @@ -2,6 +2,7 @@ package api import ( "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" @@ -26,4 +27,6 @@ func SignUp(c *middleware.Context, cmd m.CreateUserCommand) { loginUserWithUser(&user, c) c.JsonOK("User created and logged in") + + metrics.M_Api_User_SignUp.Inc(1) } diff --git a/pkg/api/static/static.go b/pkg/api/static/static.go new file mode 100644 index 00000000000..43ba6a32b20 --- /dev/null +++ b/pkg/api/static/static.go @@ -0,0 +1,218 @@ +// Copyright 2013 Martini Authors +// Copyright 2014 Unknwon +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package httpstatic + +import ( + "log" + "net/http" + "os" + "path" + "path/filepath" + "strings" + "sync" + + "github.com/Unknwon/macaron" +) + +var Root string + +func init() { + var err error + Root, err = os.Getwd() + if err != nil { + panic("error getting work directory: " + err.Error()) + } +} + +// StaticOptions is a struct for specifying configuration options for the macaron.Static middleware. +type StaticOptions struct { + // Prefix is the optional prefix used to serve the static directory content + Prefix string + // SkipLogging will disable [Static] log messages when a static file is served. + SkipLogging bool + // IndexFile defines which file to serve as index if it exists. + IndexFile string + // Expires defines which user-defined function to use for producing a HTTP Expires Header + // https://developers.google.com/speed/docs/insights/LeverageBrowserCaching + AddHeaders func(ctx *macaron.Context) + // FileSystem is the interface for supporting any implmentation of file system. + FileSystem http.FileSystem +} + +// FIXME: to be deleted. +type staticMap struct { + lock sync.RWMutex + data map[string]*http.Dir +} + +func (sm *staticMap) Set(dir *http.Dir) { + sm.lock.Lock() + defer sm.lock.Unlock() + + sm.data[string(*dir)] = dir +} + +func (sm *staticMap) Get(name string) *http.Dir { + sm.lock.RLock() + defer sm.lock.RUnlock() + + return sm.data[name] +} + +func (sm *staticMap) Delete(name string) { + sm.lock.Lock() + defer sm.lock.Unlock() + + delete(sm.data, name) +} + +var statics = staticMap{sync.RWMutex{}, map[string]*http.Dir{}} + +// staticFileSystem implements http.FileSystem interface. +type staticFileSystem struct { + dir *http.Dir +} + +func newStaticFileSystem(directory string) staticFileSystem { + if !filepath.IsAbs(directory) { + directory = filepath.Join(Root, directory) + } + dir := http.Dir(directory) + statics.Set(&dir) + return staticFileSystem{&dir} +} + +func (fs staticFileSystem) Open(name string) (http.File, error) { + return fs.dir.Open(name) +} + +func prepareStaticOption(dir string, opt StaticOptions) StaticOptions { + // Defaults + if len(opt.IndexFile) == 0 { + opt.IndexFile = "index.html" + } + // Normalize the prefix if provided + if opt.Prefix != "" { + // Ensure we have a leading '/' + if opt.Prefix[0] != '/' { + opt.Prefix = "/" + opt.Prefix + } + // Remove any trailing '/' + opt.Prefix = strings.TrimRight(opt.Prefix, "/") + } + if opt.FileSystem == nil { + opt.FileSystem = newStaticFileSystem(dir) + } + return opt +} + +func prepareStaticOptions(dir string, options []StaticOptions) StaticOptions { + var opt StaticOptions + if len(options) > 0 { + opt = options[0] + } + return prepareStaticOption(dir, opt) +} + +func staticHandler(ctx *macaron.Context, log *log.Logger, opt StaticOptions) bool { + if ctx.Req.Method != "GET" && ctx.Req.Method != "HEAD" { + return false + } + + file := ctx.Req.URL.Path + // if we have a prefix, filter requests by stripping the prefix + if opt.Prefix != "" { + if !strings.HasPrefix(file, opt.Prefix) { + return false + } + file = file[len(opt.Prefix):] + if file != "" && file[0] != '/' { + return false + } + } + + f, err := opt.FileSystem.Open(file) + if err != nil { + return false + } + defer f.Close() + + fi, err := f.Stat() + if err != nil { + return true // File exists but fail to open. + } + + // Try to serve index file + if fi.IsDir() { + // Redirect if missing trailing slash. + if !strings.HasSuffix(ctx.Req.URL.Path, "/") { + http.Redirect(ctx.Resp, ctx.Req.Request, ctx.Req.URL.Path+"/", http.StatusFound) + return true + } + + file = path.Join(file, opt.IndexFile) + f, err = opt.FileSystem.Open(file) + if err != nil { + return false // Discard error. + } + defer f.Close() + + fi, err = f.Stat() + if err != nil || fi.IsDir() { + return true + } + } + + if !opt.SkipLogging { + log.Println("[Static] Serving " + file) + } + + // Add an Expires header to the static content + if opt.AddHeaders != nil { + opt.AddHeaders(ctx) + } + + http.ServeContent(ctx.Resp, ctx.Req.Request, file, fi.ModTime(), f) + return true +} + +// Static returns a middleware handler that serves static files in the given directory. +func Static(directory string, staticOpt ...StaticOptions) macaron.Handler { + opt := prepareStaticOptions(directory, staticOpt) + + return func(ctx *macaron.Context, log *log.Logger) { + staticHandler(ctx, log, opt) + } +} + +// Statics registers multiple static middleware handlers all at once. +func Statics(opt StaticOptions, dirs ...string) macaron.Handler { + if len(dirs) == 0 { + panic("no static directory is given") + } + opts := make([]StaticOptions, len(dirs)) + for i := range dirs { + opts[i] = prepareStaticOption(dirs[i], opt) + } + + return func(ctx *macaron.Context, log *log.Logger) { + for i := range opts { + if staticHandler(ctx, log, opts[i]) { + return + } + } + } +} diff --git a/pkg/cmd/web.go b/pkg/cmd/web.go index 6619e5b1e0e..1fc6e8a999c 100644 --- a/pkg/cmd/web.go +++ b/pkg/cmd/web.go @@ -11,7 +11,6 @@ import ( "path" "path/filepath" "strconv" - "time" "github.com/Unknwon/macaron" "github.com/codegangsta/cli" @@ -20,7 +19,9 @@ import ( _ "github.com/macaron-contrib/session/postgres" "github.com/grafana/grafana/pkg/api" + "github.com/grafana/grafana/pkg/api/static" "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/eventpublisher" @@ -64,14 +65,22 @@ func newMacaron() *macaron.Macaron { } func mapStatic(m *macaron.Macaron, dir string, prefix string) { - m.Use(macaron.Static( + headers := func(c *macaron.Context) { + c.Resp.Header().Set("Cache-Control", "public, max-age=3600") + } + + if setting.Env == setting.DEV { + headers = func(c *macaron.Context) { + c.Resp.Header().Set("Cache-Control", "max-age=0, must-revalidate, no-cache") + } + } + + m.Use(httpstatic.Static( path.Join(setting.StaticRootPath, dir), - macaron.StaticOptions{ + httpstatic.StaticOptions{ SkipLogging: true, Prefix: prefix, - Expires: func() string { - return time.Now().UTC().Format(http.TimeFormat) - }, + AddHeaders: headers, }, )) } @@ -88,6 +97,10 @@ func runWeb(c *cli.Context) { m := newMacaron() api.Register(m) + if setting.ReportingEnabled { + go metrics.StartUsageReportLoop() + } + listenAddr := fmt.Sprintf("%s:%s", setting.HttpAddr, setting.HttpPort) log.Info("Listen: %v://%s%s", setting.Protocol, listenAddr, setting.AppSubUrl) switch setting.Protocol { diff --git a/pkg/metrics/counter.go b/pkg/metrics/counter.go new file mode 100644 index 00000000000..1a4a88be37b --- /dev/null +++ b/pkg/metrics/counter.go @@ -0,0 +1,72 @@ +package metrics + +import "sync/atomic" + +// Counters hold an int64 value that can be incremented and decremented. +type Counter interface { + Clear() + Count() int64 + Dec(int64) + Inc(int64) + Snapshot() Counter +} + +// NewCounter constructs a new StandardCounter. +func NewCounter() Counter { + return &StandardCounter{0} +} + +// CounterSnapshot is a read-only copy of another Counter. +type CounterSnapshot int64 + +// Clear panics. +func (CounterSnapshot) Clear() { + panic("Clear called on a CounterSnapshot") +} + +// Count returns the count at the time the snapshot was taken. +func (c CounterSnapshot) Count() int64 { return int64(c) } + +// Dec panics. +func (CounterSnapshot) Dec(int64) { + panic("Dec called on a CounterSnapshot") +} + +// Inc panics. +func (CounterSnapshot) Inc(int64) { + panic("Inc called on a CounterSnapshot") +} + +// Snapshot returns the snapshot. +func (c CounterSnapshot) Snapshot() Counter { return c } + +// StandardCounter is the standard implementation of a Counter and uses the +// sync/atomic package to manage a single int64 value. +type StandardCounter struct { + count int64 +} + +// Clear sets the counter to zero. +func (c *StandardCounter) Clear() { + atomic.StoreInt64(&c.count, 0) +} + +// Count returns the current count. +func (c *StandardCounter) Count() int64 { + return atomic.LoadInt64(&c.count) +} + +// Dec decrements the counter by the given amount. +func (c *StandardCounter) Dec(i int64) { + atomic.AddInt64(&c.count, -i) +} + +// Inc increments the counter by the given amount. +func (c *StandardCounter) Inc(i int64) { + atomic.AddInt64(&c.count, i) +} + +// Snapshot returns a read-only copy of the counter. +func (c *StandardCounter) Snapshot() Counter { + return CounterSnapshot(c.Count()) +} diff --git a/pkg/metrics/metric_ref.go b/pkg/metrics/metric_ref.go new file mode 100644 index 00000000000..f9e5d693d4c --- /dev/null +++ b/pkg/metrics/metric_ref.go @@ -0,0 +1,39 @@ +package metrics + +type comboCounterRef struct { + usageCounter Counter + metricCounter Counter +} + +func NewComboCounterRef(name string) Counter { + cr := &comboCounterRef{} + cr.usageCounter = UsageStats.GetOrRegister(name, NewCounter).(Counter) + cr.metricCounter = MetricStats.GetOrRegister(name, NewCounter).(Counter) + return cr +} + +func (c comboCounterRef) Clear() { + c.usageCounter.Clear() + c.metricCounter.Clear() +} + +func (c comboCounterRef) Count() int64 { + panic("Count called on a combocounter ref") +} + +// Dec panics. +func (c comboCounterRef) Dec(i int64) { + c.usageCounter.Dec(i) + c.metricCounter.Dec(i) +} + +// Inc panics. +func (c comboCounterRef) Inc(i int64) { + c.usageCounter.Inc(i) + c.metricCounter.Inc(i) +} + +// Snapshot returns the snapshot. +func (c comboCounterRef) Snapshot() Counter { + panic("snapshot called on a combocounter ref") +} diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go new file mode 100644 index 00000000000..f6dab8c8043 --- /dev/null +++ b/pkg/metrics/metrics.go @@ -0,0 +1,29 @@ +package metrics + +var UsageStats = NewRegistry() +var MetricStats = NewRegistry() + +var ( + M_Instance_Start = NewComboCounterRef("instance.start") + + M_Page_Status_200 = NewComboCounterRef("page.status.200") + M_Page_Status_500 = NewComboCounterRef("page.status.500") + M_Page_Status_404 = NewComboCounterRef("page.status.404") + + M_Api_Status_500 = NewComboCounterRef("api.status.500") + M_Api_Status_404 = NewComboCounterRef("api.status.404") + + M_Api_User_SignUp = NewComboCounterRef("api.user.signup") + M_Api_Dashboard_Get = NewComboCounterRef("api.dashboard.get") + M_Api_Dashboard_Post = NewComboCounterRef("api.dashboard.post") + M_Api_Admin_User_Create = NewComboCounterRef("api.admin.user_create") + M_Api_Login_Post = NewComboCounterRef("api.login.post") + M_Api_Login_OAuth = NewComboCounterRef("api.login.oauth") + M_Api_Org_Create = NewComboCounterRef("api.org.create") + + M_Api_Dashboard_Snapshot_Create = NewComboCounterRef("api.dashboard_snapshot.create") + M_Api_Dashboard_Snapshot_External = NewComboCounterRef("api.dashboard_snapshot.external") + M_Api_Dashboard_Snapshot_Get = NewComboCounterRef("api.dashboard_snapshot.get") + + M_Models_Dashboard_Insert = NewComboCounterRef("models.dashboard.insert") +) diff --git a/pkg/metrics/registry.go b/pkg/metrics/registry.go new file mode 100644 index 00000000000..9e1618f3691 --- /dev/null +++ b/pkg/metrics/registry.go @@ -0,0 +1,102 @@ +package metrics + +import ( + "fmt" + "reflect" + "sync" +) + +// DuplicateMetric is the error returned by Registry.Register when a metric +// already exists. If you mean to Register that metric you must first +// Unregister the existing metric. +type DuplicateMetric string + +func (err DuplicateMetric) Error() string { + return fmt.Sprintf("duplicate metric: %s", string(err)) +} + +type Registry interface { + // Call the given function for each registered metric. + Each(func(string, interface{})) + + // Get the metric by the given name or nil if none is registered. + Get(string) interface{} + + // Gets an existing metric or registers the given one. + // The interface can be the metric to register if not found in registry, + // or a function returning the metric for lazy instantiation. + GetOrRegister(string, interface{}) interface{} + + // Register the given metric under the given name. + Register(string, interface{}) error +} + +// The standard implementation of a Registry is a mutex-protected map +// of names to metrics. +type StandardRegistry struct { + metrics map[string]interface{} + mutex sync.Mutex +} + +// Create a new registry. +func NewRegistry() Registry { + return &StandardRegistry{metrics: make(map[string]interface{})} +} + +// Call the given function for each registered metric. +func (r *StandardRegistry) Each(f func(string, interface{})) { + for name, i := range r.registered() { + f(name, i) + } +} + +// Get the metric by the given name or nil if none is registered. +func (r *StandardRegistry) Get(name string) interface{} { + r.mutex.Lock() + defer r.mutex.Unlock() + return r.metrics[name] +} + +// Gets an existing metric or creates and registers a new one. Threadsafe +// alternative to calling Get and Register on failure. +// The interface can be the metric to register if not found in registry, +// or a function returning the metric for lazy instantiation. +func (r *StandardRegistry) GetOrRegister(name string, i interface{}) interface{} { + r.mutex.Lock() + defer r.mutex.Unlock() + if metric, ok := r.metrics[name]; ok { + return metric + } + if v := reflect.ValueOf(i); v.Kind() == reflect.Func { + i = v.Call(nil)[0].Interface() + } + r.register(name, i) + return i +} + +// Register the given metric under the given name. Returns a DuplicateMetric +// if a metric by the given name is already registered. +func (r *StandardRegistry) Register(name string, i interface{}) error { + r.mutex.Lock() + defer r.mutex.Unlock() + return r.register(name, i) +} + +func (r *StandardRegistry) register(name string, i interface{}) error { + if _, ok := r.metrics[name]; ok { + return DuplicateMetric(name) + } + + r.metrics[name] = i + return nil +} + +func (r *StandardRegistry) registered() map[string]interface{} { + metrics := make(map[string]interface{}, len(r.metrics)) + r.mutex.Lock() + defer r.mutex.Unlock() + for name, i := range r.metrics { + metrics[name] = i + } + return metrics +} diff --git a/pkg/metrics/report_usage.go b/pkg/metrics/report_usage.go new file mode 100644 index 00000000000..c8848fb5371 --- /dev/null +++ b/pkg/metrics/report_usage.go @@ -0,0 +1,64 @@ +package metrics + +import ( + "bytes" + "encoding/json" + "net/http" + "strings" + "time" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/log" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" +) + +func StartUsageReportLoop() chan struct{} { + M_Instance_Start.Inc(1) + + ticker := time.NewTicker(time.Hour * 24) + for { + select { + case <-ticker.C: + sendUsageStats() + } + } +} + +func sendUsageStats() { + log.Trace("Sending anonymous usage stats to stats.grafana.org") + + version := strings.Replace(setting.BuildVersion, ".", "_", -1) + + metrics := map[string]interface{}{} + report := map[string]interface{}{ + "version": version, + "metrics": metrics, + } + + statsQuery := m.GetSystemStatsQuery{} + if err := bus.Dispatch(&statsQuery); err != nil { + log.Error(3, "Failed to get system stats", err) + return + } + + UsageStats.Each(func(name string, i interface{}) { + switch metric := i.(type) { + case Counter: + if metric.Count() > 0 { + metrics[name+".count"] = metric.Count() + metric.Clear() + } + } + }) + + metrics["stats.dashboards.count"] = statsQuery.Result.DashboardCount + metrics["stats.users.count"] = statsQuery.Result.UserCount + metrics["stats.orgs.count"] = statsQuery.Result.OrgCount + + out, _ := json.Marshal(report) + data := bytes.NewBuffer(out) + + client := http.Client{Timeout: time.Duration(5 * time.Second)} + go client.Post("https://stats.grafana.org/grafana-usage-report", "application/json", data) +} diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index a15fd075fca..20e1eb196e5 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/apikeygen" "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/metrics" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" ) @@ -99,6 +100,15 @@ func (ctx *Context) Handle(status int, title string, err error) { } } + switch status { + case 200: + metrics.M_Page_Status_200.Inc(1) + case 404: + metrics.M_Page_Status_404.Inc(1) + case 500: + metrics.M_Page_Status_500.Inc(1) + } + ctx.Data["Title"] = title ctx.HTML(status, strconv.Itoa(status)) } @@ -128,7 +138,9 @@ func (ctx *Context) JsonApiErr(status int, message string, err error) { switch status { case 404: resp["message"] = "Not Found" + metrics.M_Api_Status_500.Inc(1) case 500: + metrics.M_Api_Status_404.Inc(1) resp["message"] = "Internal Server Error" } diff --git a/pkg/models/dashboard_snapshot.go b/pkg/models/dashboard_snapshot.go new file mode 100644 index 00000000000..e8f37e2a236 --- /dev/null +++ b/pkg/models/dashboard_snapshot.go @@ -0,0 +1,49 @@ +package models + +import "time" + +// DashboardSnapshot model +type DashboardSnapshot struct { + Id int64 + Name string + Key string + DeleteKey string + OrgId int64 + UserId int64 + External bool + ExternalUrl string + + Expires time.Time + Created time.Time + Updated time.Time + + Dashboard map[string]interface{} +} + +// ----------------- +// COMMANDS + +type CreateDashboardSnapshotCommand struct { + Dashboard map[string]interface{} `json:"dashboard" binding:"Required"` + Expires int64 `json:"expires"` + + // these are passed when storing an external snapshot ref + External bool `json:"external"` + Key string `json:"key"` + DeleteKey string `json:"deleteKey"` + + OrgId int64 `json:"-"` + UserId int64 `json:"-"` + + Result *DashboardSnapshot +} + +type DeleteDashboardSnapshotCommand struct { + DeleteKey string `json:"-"` +} + +type GetDashboardSnapshotQuery struct { + Key string + + Result *DashboardSnapshot +} diff --git a/pkg/models/models.go b/pkg/models/models.go index 189e594576b..c38f0c5a391 100644 --- a/pkg/models/models.go +++ b/pkg/models/models.go @@ -1,5 +1,7 @@ package models +import "errors" + type OAuthType int const ( @@ -7,3 +9,5 @@ const ( GOOGLE TWITTER ) + +var ErrNotFound = errors.New("Not found") diff --git a/pkg/models/stats.go b/pkg/models/stats.go new file mode 100644 index 00000000000..0d83882e666 --- /dev/null +++ b/pkg/models/stats.go @@ -0,0 +1,11 @@ +package models + +type SystemStats struct { + DashboardCount int + UserCount int + OrgCount int +} + +type GetSystemStatsQuery struct { + Result *SystemStats +} diff --git a/pkg/services/sqlstore/apikey_test.go b/pkg/services/sqlstore/apikey_test.go index e86a1c0db33..790c8837def 100644 --- a/pkg/services/sqlstore/apikey_test.go +++ b/pkg/services/sqlstore/apikey_test.go @@ -21,8 +21,8 @@ func TestApiKeyDataAccess(t *testing.T) { Convey("Should be able to get key by name", func() { query := m.GetApiKeyByNameQuery{KeyName: "hello", OrgId: 1} err = GetApiKeyByName(&query) - So(err, ShouldBeNil) + So(err, ShouldBeNil) So(query.Result, ShouldNotBeNil) }) diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index bf748b600f4..0384a5bb6e6 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -6,6 +6,7 @@ import ( "github.com/go-xorm/xorm" "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/metrics" m "github.com/grafana/grafana/pkg/models" ) @@ -48,6 +49,7 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { } if dash.Id == 0 { + metrics.M_Models_Dashboard_Insert.Inc(1) _, err = sess.Insert(dash) } else { dash.Version += 1 diff --git a/pkg/services/sqlstore/dashboard_snapshot.go b/pkg/services/sqlstore/dashboard_snapshot.go new file mode 100644 index 00000000000..0bbb01ed6bd --- /dev/null +++ b/pkg/services/sqlstore/dashboard_snapshot.go @@ -0,0 +1,65 @@ +package sqlstore + +import ( + "time" + + "github.com/go-xorm/xorm" + "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" +) + +func init() { + bus.AddHandler("sql", CreateDashboardSnapshot) + bus.AddHandler("sql", GetDashboardSnapshot) + bus.AddHandler("sql", DeleteDashboardSnapshot) +} + +func CreateDashboardSnapshot(cmd *m.CreateDashboardSnapshotCommand) error { + return inTransaction(func(sess *xorm.Session) error { + + // never + var expires = time.Now().Add(time.Hour * 24 * 365 * 50) + if cmd.Expires > 0 { + expires = time.Now().Add(time.Second * time.Duration(cmd.Expires)) + } + + snapshot := &m.DashboardSnapshot{ + Key: cmd.Key, + DeleteKey: cmd.DeleteKey, + OrgId: cmd.OrgId, + UserId: cmd.UserId, + External: cmd.External, + Dashboard: cmd.Dashboard, + Expires: expires, + Created: time.Now(), + Updated: time.Now(), + } + + _, err := sess.Insert(snapshot) + cmd.Result = snapshot + + return err + }) +} + +func DeleteDashboardSnapshot(cmd *m.DeleteDashboardSnapshotCommand) error { + return inTransaction(func(sess *xorm.Session) error { + var rawSql = "DELETE FROM dashboard_snapshot WHERE delete_key=?" + _, err := sess.Exec(rawSql, cmd.DeleteKey) + return err + }) +} + +func GetDashboardSnapshot(query *m.GetDashboardSnapshotQuery) error { + snapshot := m.DashboardSnapshot{Key: query.Key} + has, err := x.Get(&snapshot) + + if err != nil { + return err + } else if has == false { + return m.ErrNotFound + } + + query.Result = &snapshot + return nil +} diff --git a/pkg/services/sqlstore/dashboard_snapshot_test.go b/pkg/services/sqlstore/dashboard_snapshot_test.go new file mode 100644 index 00000000000..5301f0f1cc9 --- /dev/null +++ b/pkg/services/sqlstore/dashboard_snapshot_test.go @@ -0,0 +1,37 @@ +package sqlstore + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" + + m "github.com/grafana/grafana/pkg/models" +) + +func TestDashboardSnapshotDBAccess(t *testing.T) { + + Convey("Testing DashboardSnapshot data access", t, func() { + InitTestDB(t) + + Convey("Given saved snaphot", func() { + cmd := m.CreateDashboardSnapshotCommand{ + Key: "hej", + Dashboard: map[string]interface{}{ + "hello": "mupp", + }, + } + err := CreateDashboardSnapshot(&cmd) + So(err, ShouldBeNil) + + Convey("Should be able to get snaphot by key", func() { + query := m.GetDashboardSnapshotQuery{Key: "hej"} + err = GetDashboardSnapshot(&query) + So(err, ShouldBeNil) + + So(query.Result, ShouldNotBeNil) + So(query.Result.Dashboard["hello"], ShouldEqual, "mupp") + }) + + }) + }) +} diff --git a/pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go b/pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go new file mode 100644 index 00000000000..4d83dfd5bc6 --- /dev/null +++ b/pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go @@ -0,0 +1,51 @@ +package migrations + +import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + +func addDashboardSnapshotMigrations(mg *Migrator) { + snapshotV4 := Table{ + Name: "dashboard_snapshot", + Columns: []*Column{ + {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + {Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false}, + {Name: "key", Type: DB_NVarchar, Length: 255, Nullable: false}, + {Name: "dashboard", Type: DB_Text, Nullable: false}, + {Name: "expires", Type: DB_DateTime, Nullable: false}, + {Name: "created", Type: DB_DateTime, Nullable: false}, + {Name: "updated", Type: DB_DateTime, Nullable: false}, + }, + Indices: []*Index{ + {Cols: []string{"key"}, Type: UniqueIndex}, + }, + } + + // add v4 + mg.AddMigration("create dashboard_snapshot table v4", NewAddTableMigration(snapshotV4)) + mg.AddMigration("drop table dashboard_snapshot_v4 #1", NewDropTableMigration("dashboard_snapshot")) + + snapshotV5 := Table{ + Name: "dashboard_snapshot", + Columns: []*Column{ + {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + {Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false}, + {Name: "key", Type: DB_NVarchar, Length: 255, Nullable: false}, + {Name: "delete_key", Type: DB_NVarchar, Length: 255, Nullable: false}, + {Name: "org_id", Type: DB_BigInt, Nullable: false}, + {Name: "user_id", Type: DB_BigInt, Nullable: false}, + {Name: "external", Type: DB_Bool, Nullable: false}, + {Name: "external_url", Type: DB_NVarchar, Length: 255, Nullable: false}, + {Name: "dashboard", Type: DB_Text, Nullable: false}, + {Name: "expires", Type: DB_DateTime, Nullable: false}, + {Name: "created", Type: DB_DateTime, Nullable: false}, + {Name: "updated", Type: DB_DateTime, Nullable: false}, + }, + Indices: []*Index{ + {Cols: []string{"key"}, Type: UniqueIndex}, + {Cols: []string{"delete_key"}, Type: UniqueIndex}, + {Cols: []string{"user_id"}}, + }, + } + + mg.AddMigration("create dashboard_snapshot table v5 #2", NewAddTableMigration(snapshotV5)) + addTableIndicesMigrations(mg, "v5", snapshotV5) +} diff --git a/pkg/services/sqlstore/migrations/datasource_mig.go b/pkg/services/sqlstore/migrations/datasource_mig.go index 924e1a16189..4f046b1f8e9 100644 --- a/pkg/services/sqlstore/migrations/datasource_mig.go +++ b/pkg/services/sqlstore/migrations/datasource_mig.go @@ -95,5 +95,5 @@ func addDataSourceMigration(mg *Migrator) { "updated": "updated", })) - mg.AddMigration("Drop old table data_source_v1", NewDropTableMigration("data_source_old")) + mg.AddMigration("Drop old table data_source_v1 #2", NewDropTableMigration("data_source_v1")) } diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go index c48167d97e5..329c6187c9d 100644 --- a/pkg/services/sqlstore/migrations/migrations.go +++ b/pkg/services/sqlstore/migrations/migrations.go @@ -15,6 +15,7 @@ func AddMigrations(mg *Migrator) { addDashboardMigration(mg) addDataSourceMigration(mg) addApiKeyMigrations(mg) + addDashboardSnapshotMigrations(mg) } func addMigrationLogMigrations(mg *Migrator) { diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index 218f24f0142..757953f5b4e 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -22,7 +22,6 @@ import ( var ( x *xorm.Engine dialect migrator.Dialect - tables []interface{} HasEngine bool @@ -80,10 +79,6 @@ func SetEngine(engine *xorm.Engine, enableLog bool) (err error) { return fmt.Errorf("Sqlstore::Migration failed err: %v\n", err) } - if err := x.Sync2(tables...); err != nil { - return fmt.Errorf("sync database struct error: %v\n", err) - } - if enableLog { logPath := path.Join(setting.LogRootPath, "xorm.log") os.MkdirAll(path.Dir(logPath), os.ModePerm) @@ -94,11 +89,13 @@ func SetEngine(engine *xorm.Engine, enableLog bool) (err error) { } x.Logger = xorm.NewSimpleLogger(f) - x.ShowSQL = true - x.ShowInfo = true - x.ShowDebug = true - x.ShowErr = true - x.ShowWarn = true + if setting.Env == setting.DEV { + x.ShowSQL = false + x.ShowInfo = false + x.ShowDebug = false + x.ShowErr = true + x.ShowWarn = true + } } return nil @@ -125,7 +122,7 @@ func getEngine() (*xorm.Engine, error) { DbCfg.User, DbCfg.Pwd, host, port, DbCfg.Name, DbCfg.SslMode) case "sqlite3": os.MkdirAll(path.Dir(DbCfg.Path), os.ModePerm) - cnnstr = "file:" + DbCfg.Path + "?cache=shared&mode=rwc" + cnnstr = "file:" + DbCfg.Path + "?cache=shared&mode=rwc&_loc=Local" default: return nil, fmt.Errorf("Unknown database type: %s", DbCfg.Type) } diff --git a/pkg/services/sqlstore/sqlstore.goconvey b/pkg/services/sqlstore/sqlstore.goconvey new file mode 100644 index 00000000000..92feb5268a5 --- /dev/null +++ b/pkg/services/sqlstore/sqlstore.goconvey @@ -0,0 +1 @@ +-timeout=10s diff --git a/pkg/services/sqlstore/sqlutil/sqlutil.go b/pkg/services/sqlstore/sqlutil/sqlutil.go index f873a1a2f9f..075b56c1c18 100644 --- a/pkg/services/sqlstore/sqlutil/sqlutil.go +++ b/pkg/services/sqlstore/sqlutil/sqlutil.go @@ -11,7 +11,7 @@ type TestDB struct { ConnStr string } -var TestDB_Sqlite3 = TestDB{DriverName: "sqlite3", ConnStr: ":memory:"} +var TestDB_Sqlite3 = TestDB{DriverName: "sqlite3", ConnStr: ":memory:?_loc=Local"} var TestDB_Mysql = TestDB{DriverName: "mysql", ConnStr: "grafana:password@tcp(localhost:3306)/grafana_tests?charset=utf8"} var TestDB_Postgres = TestDB{DriverName: "postgres", ConnStr: "user=grafanatest password=grafanatest host=localhost port=5432 dbname=grafanatest sslmode=disable"} diff --git a/pkg/services/sqlstore/stats.go b/pkg/services/sqlstore/stats.go new file mode 100644 index 00000000000..7995dd43f38 --- /dev/null +++ b/pkg/services/sqlstore/stats.go @@ -0,0 +1,36 @@ +package sqlstore + +import ( + "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" +) + +func init() { + bus.AddHandler("sql", GetSystemStats) +} + +func GetSystemStats(query *m.GetSystemStatsQuery) error { + var rawSql = `SELECT + ( + SELECT COUNT(*) + FROM ` + dialect.Quote("user") + ` + ) AS user_count, + ( + SELECT COUNT(*) + FROM ` + dialect.Quote("org") + ` + ) AS org_count, + ( + SELECT COUNT(*) + FROM ` + dialect.Quote("dashboard") + ` + ) AS dashboard_count + ` + + var stats m.SystemStats + _, err := x.Sql(rawSql).Get(&stats) + if err != nil { + return err + } + + query.Result = &stats + return err +} diff --git a/pkg/services/sqlstore/xorm.log b/pkg/services/sqlstore/xorm.log new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index b8d038dbc29..defa8311e8c 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -96,6 +96,8 @@ var ( PhantomDir string configFiles []string + + ReportingEnabled bool ) func init() { @@ -233,6 +235,8 @@ func NewConfigContext(config string) { ImagesDir = "data/png" PhantomDir = "vendor/phantomjs" + ReportingEnabled = Cfg.Section("").Key("reporting-enabled").MustBool(true) + readSessionConfig() } diff --git a/src/app/directives/graphiteSegment.js b/src/app/directives/graphiteSegment.js index 577bbf5d6b7..c8ad131e6c7 100644 --- a/src/app/directives/graphiteSegment.js +++ b/src/app/directives/graphiteSegment.js @@ -37,12 +37,14 @@ function (angular, app, _, $) { if (selected) { segment.value = selected.value; segment.html = selected.html; + segment.fake = false; segment.expandable = selected.expandable; } else { segment.value = value; segment.html = $sce.trustAsHtml(value); segment.expandable = true; + segment.fake = false; } $scope.segmentValueChanged(segment, $scope.$index); }); @@ -71,7 +73,7 @@ function (angular, app, _, $) { options = _.map($scope.altSegments, function(alt) { return alt.value; }); // add custom values - if (segment.value !== 'select metric' && _.indexOf(options, segment.value) === -1) { + if (!segment.fake && _.indexOf(options, segment.value) === -1) { options.unshift(segment.value); } diff --git a/src/app/directives/templateParamSelector.js b/src/app/directives/templateParamSelector.js index 6cd2ce5a964..3d7a9f964d4 100644 --- a/src/app/directives/templateParamSelector.js +++ b/src/app/directives/templateParamSelector.js @@ -4,87 +4,9 @@ define([ 'lodash', 'jquery', ], -function (angular, app, _, $) { +function (angular, app, _) { 'use strict'; - angular - .module('grafana.directives') - .directive('templateParamSelector', function($compile) { - var inputTemplate = ''; - - var buttonTemplate = '{{variable.current.text}} '; - - return { - link: function($scope, elem) { - var $input = $(inputTemplate); - var $button = $(buttonTemplate); - var variable = $scope.variable; - - $input.appendTo(elem); - $button.appendTo(elem); - - function updateVariableValue(value) { - $scope.$apply(function() { - var selected = _.findWhere(variable.options, { text: value }); - if (!selected) { - selected = { text: value, value: value }; - } - $scope.setVariableValue($scope.variable, selected); - }); - } - - $input.attr('data-provide', 'typeahead'); - $input.typeahead({ - minLength: 0, - items: 1000, - updater: function(value) { - $input.val(value); - $input.trigger('blur'); - return value; - } - }); - - var typeahead = $input.data('typeahead'); - typeahead.lookup = function () { - var options = _.map(variable.options, function(option) { return option.text; }); - this.query = this.$element.val() || ''; - return this.process(options); - }; - - $button.click(function() { - $input.css('width', ($button.width() + 16) + 'px'); - - $button.hide(); - $input.show(); - $input.focus(); - - var typeahead = $input.data('typeahead'); - if (typeahead) { - $input.val(''); - typeahead.lookup(); - } - - }); - - $input.blur(function() { - if ($input.val() !== '') { updateVariableValue($input.val()); } - $input.hide(); - $button.show(); - $button.focus(); - }); - - $scope.$on('$destroy', function() { - $button.unbind(); - typeahead.destroy(); - }); - - $compile(elem.contents())($scope); - } - }; - }); - angular .module('grafana.directives') .directive('variableValueSelect', function($compile, $window, $timeout) { @@ -162,9 +84,9 @@ function (angular, app, _, $) { scope.hide = function() { scope.selectorOpen = false; - if (scope.oldCurrentText !== variable.current.text) { - scope.onUpdated(); - } + // if (scope.oldCurrentText !== variable.current.text) { + // scope.onUpdated(); + // } bodyEl.off('click', scope.bodyOnClick); }; diff --git a/src/app/features/dashboard/all.js b/src/app/features/dashboard/all.js index 811f48cc464..f658011bc3d 100644 --- a/src/app/features/dashboard/all.js +++ b/src/app/features/dashboard/all.js @@ -5,6 +5,7 @@ define([ './playlistCtrl', './rowCtrl', './sharePanelCtrl', + './shareSnapshotCtrl', './submenuCtrl', './dashboardSrv', './keybindings', diff --git a/src/app/features/dashboard/dashboardCtrl.js b/src/app/features/dashboard/dashboardCtrl.js index c194dcdb7ed..76060e64a70 100644 --- a/src/app/features/dashboard/dashboardCtrl.js +++ b/src/app/features/dashboard/dashboardCtrl.js @@ -18,6 +18,7 @@ function (angular, $, config) { dynamicDashboardSrv, dashboardSrv, dashboardViewStateSrv, + contextSrv, $timeout) { $scope.editor = { index: 0 }; @@ -49,7 +50,7 @@ function (angular, $, config) { $scope.dashboard = dashboard; $scope.dashboardViewState = dashboardViewStateSrv.create($scope); - $scope.dashboardMeta = data.meta; + $scope.initDashboardMeta(data.meta, $scope.dashboard); dashboardKeybindings.shortcuts($scope); @@ -60,6 +61,33 @@ function (angular, $, config) { }); }; + $scope.initDashboardMeta = function(meta, dashboard) { + meta.canShare = true; + meta.canSave = true; + meta.canEdit = true; + meta.canStar = true; + + if (contextSrv.hasRole('Viewer')) { + meta.canSave = false; + } + + if (meta.isHome) { + meta.canShare = false; + meta.canStar = false; + meta.canSave = false; + meta.canEdit = false; + } + + if (dashboard.snapshot) { + meta.canEdit = false; + meta.canSave = false; + meta.canStar = false; + meta.canShare = false; + } + + $scope.dashboardMeta = meta; + }; + $scope.updateSubmenuVisibility = function() { $scope.submenuEnabled = $scope.dashboard.hasTemplateVarsOrAnnotations(); }; @@ -135,4 +163,5 @@ function (angular, $, config) { }; }); + }); diff --git a/src/app/features/dashboard/dashboardNavCtrl.js b/src/app/features/dashboard/dashboardNavCtrl.js index 5950c504a3a..17bdfb5efc7 100644 --- a/src/app/features/dashboard/dashboardNavCtrl.js +++ b/src/app/features/dashboard/dashboardNavCtrl.js @@ -11,7 +11,7 @@ function (angular, _, moment) { var module = angular.module('grafana.controllers'); - module.controller('DashboardNavCtrl', function($scope, $rootScope, alertSrv, $location, playlistSrv, backendSrv, timeSrv) { + module.controller('DashboardNavCtrl', function($scope, $rootScope, alertSrv, $location, playlistSrv, backendSrv, timeSrv, $timeout) { $scope.init = function() { $scope.onAppEvent('save-dashboard', $scope.saveDashboard); @@ -157,6 +157,18 @@ function (angular, _, moment) { }); }; + $scope.snapshot = function() { + $scope.dashboard.snapshot = true; + $rootScope.$broadcast('refresh'); + + $timeout(function() { + $scope.exportDashboard(); + $scope.dashboard.snapshot = false; + $scope.appEvent('dashboard-snapshot-cleanup'); + }, 1000); + + }; + $scope.editJson = function() { $scope.appEvent('show-json-editor', { object: $scope.dashboard }); }; diff --git a/src/app/features/dashboard/dashboardSrv.js b/src/app/features/dashboard/dashboardSrv.js index 0eab4f867d9..a4e50b1d70c 100644 --- a/src/app/features/dashboard/dashboardSrv.js +++ b/src/app/features/dashboard/dashboardSrv.js @@ -37,6 +37,7 @@ function (angular, $, kbn, _, moment) { this.templating = this._ensureListExist(data.templating); this.annotations = this._ensureListExist(data.annotations); this.refresh = data.refresh; + this.snapshot = data.snapshot; this.schemaVersion = data.schemaVersion || 0; this.version = data.version || 0; @@ -67,6 +68,16 @@ function (angular, $, kbn, _, moment) { return max + 1; }; + p.forEachPanel = function(callback) { + var i, j, row; + for (i = 0; i < this.rows.length; i++) { + row = this.rows[i]; + for (j = 0; j < row.panels.length; j++) { + callback(row.panels[j], row); + } + } + }; + p.rowSpan = function(row) { return _.reduce(row.panels, function(p,v) { return p + v.span; diff --git a/src/app/features/dashboard/dynamicDashboardSrv.js b/src/app/features/dashboard/dynamicDashboardSrv.js index f7956fd94a7..ba70bd0b018 100644 --- a/src/app/features/dashboard/dynamicDashboardSrv.js +++ b/src/app/features/dashboard/dynamicDashboardSrv.js @@ -103,11 +103,12 @@ function (angular, _) { } for (i = 0; i < copy.panels.length; i++) { - panel = row.panels[i]; + panel = copy.panels[i]; panel.scopedVars = panel.scopedVars || {}; panel.scopedVars[variable.name] = option; } }); + }; this.repeatPanel = function(panel, row, dashboard) { diff --git a/src/app/features/dashboard/partials/shareDashboard.html b/src/app/features/dashboard/partials/shareDashboard.html index e052c5b298f..fefca5a9100 100644 --- a/src/app/features/dashboard/partials/shareDashboard.html +++ b/src/app/features/dashboard/partials/shareDashboard.html @@ -1,12 +1,12 @@ + diff --git a/src/app/features/dashboard/rowCtrl.js b/src/app/features/dashboard/rowCtrl.js index 409fee2fd37..494f10657fc 100644 --- a/src/app/features/dashboard/rowCtrl.js +++ b/src/app/features/dashboard/rowCtrl.js @@ -38,11 +38,6 @@ function (angular, app, _, config) { } }; - // This can be overridden by individual panels - $scope.close_edit = function() { - $scope.$broadcast('render'); - }; - $scope.add_panel = function(panel) { $scope.dashboard.add_panel(panel, $scope.row); }; @@ -81,17 +76,21 @@ function (angular, app, _, config) { $scope.$broadcast('render'); }; - $scope.remove_panel_from_row = function(row, panel) { + $scope.removePanel = function(panel) { $scope.appEvent('confirm-modal', { title: 'Are you sure you want to remove this panel?', icon: 'fa-trash', yesText: 'Delete', onConfirm: function() { - row.panels = _.without(row.panels, panel); + $scope.row.panels = _.without($scope.row.panels, panel); } }); }; + $scope.updatePanelSpan = function(panel, span) { + panel.span = Math.min(Math.max(panel.span + span, 1), 12); + }; + $scope.replacePanel = function(newPanel, oldPanel) { var row = $scope.row; var index = _.indexOf(row.panels, oldPanel); @@ -144,9 +143,11 @@ function (angular, app, _, config) { module.directive('panelWidth', function() { return function(scope, element) { - scope.$watch('panel.span', function() { + function updateWidth() { element[0].style.width = ((scope.panel.span / 1.2) * 10) + '%'; - }); + } + + scope.$watch('panel.span', updateWidth); }; }); diff --git a/src/app/features/dashboard/sharePanelCtrl.js b/src/app/features/dashboard/sharePanelCtrl.js index 40c7ba45a90..eb0a7a7a957 100644 --- a/src/app/features/dashboard/sharePanelCtrl.js +++ b/src/app/features/dashboard/sharePanelCtrl.js @@ -9,7 +9,7 @@ function (angular, _, require, config) { var module = angular.module('grafana.controllers'); - module.controller('SharePanelCtrl', function($scope, $location, $timeout, timeSrv, $element, templateSrv) { + module.controller('SharePanelCtrl', function($scope, $rootScope, $location, $timeout, timeSrv, $element, templateSrv) { $scope.init = function() { $scope.editor = { index: 0 }; @@ -71,12 +71,16 @@ function (angular, _, require, config) { } }); - $scope.shareUrl = baseUrl + "?" + paramsArray.join('&'); + var queryParams = "?" + paramsArray.join('&'); + $scope.shareUrl = baseUrl + queryParams; - $scope.soloUrl = $scope.shareUrl.replace('/dashboard/db/', '/dashboard/solo/'); - $scope.iframeHtml = ''; + var soloUrl = $scope.shareUrl; + soloUrl = soloUrl.replace('/dashboard/db/', '/dashboard/solo/db/'); + soloUrl = soloUrl.replace('/dashboard/snapshot/', '/dashboard/solo/snapshot/'); - $scope.imageUrl = $scope.shareUrl.replace('/dashboard/db/', '/render/dashboard/solo/'); + $scope.iframeHtml = ''; + + $scope.imageUrl = soloUrl.replace('/dashboard/', '/render/dashboard/'); $scope.imageUrl += '&width=1000'; $scope.imageUrl += '&height=500'; }; diff --git a/src/app/features/dashboard/shareSnapshotCtrl.js b/src/app/features/dashboard/shareSnapshotCtrl.js new file mode 100644 index 00000000000..4006abfe4b5 --- /dev/null +++ b/src/app/features/dashboard/shareSnapshotCtrl.js @@ -0,0 +1,111 @@ +define([ + 'angular', + 'lodash', +], +function (angular, _) { + 'use strict'; + + var module = angular.module('grafana.controllers'); + + module.controller('ShareSnapshotCtrl', function($scope, $rootScope, $location, backendSrv, $timeout, timeSrv) { + + $scope.snapshot = { + name: $scope.dashboard.title, + expires: 0, + }; + + $scope.step = 1; + + $scope.expireOptions = [ + {text: '1 Hour', value: 60*60}, + {text: '1 Day', value: 60*60*24}, + {text: '7 Days', value: 60*60*7}, + {text: 'Never', value: 0}, + ]; + + $scope.accessOptions = [ + {text: 'Anyone with the link', value: 1}, + {text: 'Organization users', value: 2}, + {text: 'Public on the web', value: 3}, + ]; + + $scope.externalUrl = 'http://snapshots-origin.raintank.io'; + $scope.apiUrl = '/api/snapshots'; + + $scope.createSnapshot = function(external) { + $scope.dashboard.snapshot = { + timestamp: new Date() + }; + + $scope.loading = true; + $scope.snapshot.external = external; + + $rootScope.$broadcast('refresh'); + + $timeout(function() { + $scope.saveSnapshot(external); + }, 3000); + }; + + $scope.saveSnapshot = function(external) { + var dash = angular.copy($scope.dashboard); + // change title + dash.title = $scope.snapshot.name; + // make relative times absolute + dash.time = timeSrv.timeRange(); + // remove panel queries & links + dash.forEachPanel(function(panel) { + panel.targets = []; + panel.links = []; + }); + // remove annotations + dash.annotations.list = []; + // remove template queries + _.each(dash.templating.list, function(variable) { + variable.query = ""; + variable.refresh = false; + }); + + // cleanup snapshotData + delete $scope.dashboard.snapshot; + $scope.dashboard.forEachPanel(function(panel) { + delete panel.snapshotData; + }); + + var cmdData = { + dashboard: dash, + expires: $scope.snapshot.expires, + }; + + var postUrl = external ? $scope.externalUrl + $scope.apiUrl : $scope.apiUrl; + + backendSrv.post(postUrl, cmdData).then(function(results) { + $scope.loading = false; + + if (external) { + $scope.deleteUrl = results.deleteUrl; + $scope.snapshotUrl = results.url; + $scope.saveExternalSnapshotRef(cmdData, results); + } else { + var baseUrl = $location.absUrl().replace($location.url(), ""); + $scope.snapshotUrl = baseUrl + '/dashboard/snapshot/' + results.key; + $scope.deleteUrl = baseUrl + '/api/snapshots-delete/' + results.deleteKey; + } + + $scope.step = 2; + }, function() { + $scope.loading = false; + }); + }; + + $scope.saveExternalSnapshotRef = function(cmdData, results) { + // save external in local instance as well + cmdData.external = true; + cmdData.key = results.key; + cmdData.deleteKey = results.deleteKey; + backendSrv.post('/api/snapshots/', cmdData); + }; + + }); + +}); diff --git a/src/app/features/panel/panelHelper.js b/src/app/features/panel/panelHelper.js index 3d164798d9b..264c6810688 100644 --- a/src/app/features/panel/panelHelper.js +++ b/src/app/features/panel/panelHelper.js @@ -72,7 +72,13 @@ function (angular, _, kbn, $) { cacheTimeout: scope.panel.cacheTimeout }; - return datasource.query(metricsQuery); + return datasource.query(metricsQuery).then(function(results) { + if (scope.dashboard.snapshot) { + scope.panel.snapshotData = results; + } + + return results; + }); }; }); diff --git a/src/app/features/panel/panelMenu.js b/src/app/features/panel/panelMenu.js index 99b5bcfe3b8..27152ef2b94 100644 --- a/src/app/features/panel/panelMenu.js +++ b/src/app/features/panel/panelMenu.js @@ -22,7 +22,7 @@ function (angular, $, _) { template += '
'; template += ''; template += ''; - template += ''; + template += ''; template += '
'; template += '
'; diff --git a/src/app/features/panel/panelSrv.js b/src/app/features/panel/panelSrv.js index 721167491ce..b9d111d6cc4 100644 --- a/src/app/features/panel/panelSrv.js +++ b/src/app/features/panel/panelSrv.js @@ -41,7 +41,7 @@ function (angular, _, config) { }; $scope.updateColumnSpan = function(span) { - $scope.panel.span = Math.min(Math.max($scope.panel.span + span, 1), 12); + $scope.updatePanelSpan($scope.panel, span); $timeout(function() { $scope.$broadcast('render'); @@ -94,6 +94,13 @@ function (angular, _, config) { $scope.get_data = function() { if ($scope.otherPanelInFullscreenMode()) { return; } + if ($scope.panel.snapshotData) { + if ($scope.loadSnapshot) { + $scope.loadSnapshot($scope.panel.snapshotData); + } + return; + } + delete $scope.panelMeta.error; $scope.panelMeta.loading = true; diff --git a/src/app/features/panel/soloPanelCtrl.js b/src/app/features/panel/soloPanelCtrl.js index c6a01d9ccfe..9068c8f60e8 100644 --- a/src/app/features/panel/soloPanelCtrl.js +++ b/src/app/features/panel/soloPanelCtrl.js @@ -7,16 +7,15 @@ function (angular, $) { var module = angular.module('grafana.routes'); - module.controller('SoloPanelCtrl', - function( - $scope, - backendSrv, - $routeParams, - dashboardSrv, - timeSrv, - $location, - templateValuesSrv, - contextSrv) { + module.controller('SoloPanelCtrl', function( + $scope, + backendSrv, + $routeParams, + dashboardSrv, + timeSrv, + $location, + templateValuesSrv, + contextSrv) { var panelId; @@ -26,12 +25,19 @@ function (angular, $) { var params = $location.search(); panelId = parseInt(params.panelId); - backendSrv.getDashboard($routeParams.slug) - .then(function(dashboard) { - $scope.initPanelScope(dashboard); - }).then(null, function(err) { - $scope.appEvent('alert-error', ['Load panel error', err.message]); - }); + var request; + + if ($routeParams.slug) { + request = backendSrv.getDashboard($routeParams.slug); + } else { + request = backendSrv.get('/api/snapshots/' + $routeParams.key); + } + + request.then(function(dashboard) { + $scope.initPanelScope(dashboard); + }).then(null, function(err) { + $scope.appEvent('alert-error', ['Load panel error', err.message]); + }); }; $scope.initPanelScope = function(dashboard) { diff --git a/src/app/panels/graph/module.js b/src/app/panels/graph/module.js index 3af9f59eb9b..4fd56fb69b4 100644 --- a/src/app/panels/graph/module.js +++ b/src/app/panels/graph/module.js @@ -23,7 +23,7 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) { }; }); - module.controller('GraphCtrl', function($scope, $rootScope, panelSrv, annotationsSrv, panelHelper) { + module.controller('GraphCtrl', function($scope, $rootScope, panelSrv, annotationsSrv, panelHelper, $q) { $scope.panelMeta = new PanelMeta({ panelName: 'Graph', @@ -116,7 +116,7 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) { _.defaults($scope.panel.grid, _d.grid); _.defaults($scope.panel.legend, _d.legend); - $scope.logScales = {'linear': 1, 'log (base 16)': 16, 'log (base 10)': 10, 'log (base 1024)': 1024}; + $scope.logScales = {'linear': 1, 'log (base 10)': 10, 'log (base 32)': 32, 'log (base 1024)': 1024}; $scope.hiddenSeries = {}; $scope.seriesList = []; @@ -140,6 +140,12 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) { }); }; + $scope.loadSnapshot = function(snapshotData) { + panelHelper.updateTimeRange($scope); + $scope.annotationsPromise = $q.when([]); + $scope.dataHandler(snapshotData); + }; + $scope.dataHandler = function(results) { // png renderer returns just a url if (_.isString(results)) { @@ -285,6 +291,7 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) { }; panelSrv.init($scope); + }); }); diff --git a/src/app/panels/graph/styleEditor.html b/src/app/panels/graph/styleEditor.html index c466d2b486e..a5d82bba262 100644 --- a/src/app/panels/graph/styleEditor.html +++ b/src/app/panels/graph/styleEditor.html @@ -32,10 +32,8 @@ - -
Rendering
diff --git a/src/app/panels/singlestat/module.js b/src/app/panels/singlestat/module.js index 81167a83a7d..302fe30d3f5 100644 --- a/src/app/panels/singlestat/module.js +++ b/src/app/panels/singlestat/module.js @@ -81,12 +81,18 @@ function (angular, app, _, TimeSeries, kbn, PanelMeta) { panelHelper.updateTimeRange($scope); return panelHelper.issueMetricQuery($scope, datasource) - .then($scope.dataHandler) - .then(null, function() { + .then($scope.dataHandler, function(err) { + $scope.series = []; $scope.render(); + throw err; }); }; + $scope.loadSnapshot = function(snapshotData) { + panelHelper.updateTimeRange($scope); + $scope.dataHandler(snapshotData); + }; + $scope.dataHandler = function(results) { $scope.series = _.map(results.data, $scope.seriesHandler); $scope.render(); diff --git a/src/app/partials/dashboard.html b/src/app/partials/dashboard.html index 6d7d0634fcd..39e05ebecb6 100644 --- a/src/app/partials/dashboard.html +++ b/src/app/partials/dashboard.html @@ -86,14 +86,12 @@
-
-
-
- Drop here -
-
+
+
+
+ Drop here +
+
diff --git a/src/app/partials/dashboard_topnav.html b/src/app/partials/dashboard_topnav.html index bf3ba635a97..aff1f0bbba7 100644 --- a/src/app/partials/dashboard_topnav.html +++ b/src/app/partials/dashboard_topnav.html @@ -18,19 +18,19 @@
-
    -
  • - ANNOTATIONS: -
  • + {{annotation.name}} diff --git a/src/app/plugins/datasource/elasticsearch/datasource.js b/src/app/plugins/datasource/elasticsearch/datasource.js index 84f34d645c5..976954368ed 100644 --- a/src/app/plugins/datasource/elasticsearch/datasource.js +++ b/src/app/plugins/datasource/elasticsearch/datasource.js @@ -265,7 +265,7 @@ function (angular, _, config, kbn, moment) { query: { query_string: { query: queryString } }, facets: { tags: { terms: { field: "tags", order: "term", size: 50 } } }, size: this.searchMaxResults, - sort: ["_uid"] + sort: ["_uid"], }; return this._post('/dashboard/_search', query) diff --git a/src/app/plugins/datasource/graphite/queryCtrl.js b/src/app/plugins/datasource/graphite/queryCtrl.js index d878386d461..069fa815797 100644 --- a/src/app/plugins/datasource/graphite/queryCtrl.js +++ b/src/app/plugins/datasource/graphite/queryCtrl.js @@ -113,7 +113,7 @@ function (angular, _, config, gfunc, Parser) { function checkOtherSegments(fromIndex) { if (fromIndex === 0) { - $scope.segments.push(new MetricSegment('select metric')); + $scope.segments.push(MetricSegment.newSelectMetric()); return; } @@ -123,13 +123,13 @@ function (angular, _, config, gfunc, Parser) { if (segments.length === 0) { if (path !== '') { $scope.segments = $scope.segments.splice(0, fromIndex); - $scope.segments.push(new MetricSegment('select metric')); + $scope.segments.push(MetricSegment.newSelectMetric()); } return; } if (segments[0].expandable) { if ($scope.segments.length === fromIndex) { - $scope.segments.push(new MetricSegment('select metric')); + $scope.segments.push(MetricSegment.newSelectMetric()); } else { return checkOtherSegments(fromIndex + 1); @@ -238,7 +238,7 @@ function (angular, _, config, gfunc, Parser) { $scope.moveAliasFuncLast(); $scope.smartlyHandleNewAliasByNode(newFunc); - if ($scope.segments.length === 1 && $scope.segments[0].value === 'select metric') { + if ($scope.segments.length === 1 && $scope.segments[0].fake) { $scope.segments = []; } @@ -298,18 +298,17 @@ function (angular, _, config, gfunc, Parser) { return; } - if (_.isString(options)) { - this.value = options; - this.html = $sce.trustAsHtml(this.value); - return; - } - + this.fake = options.fake; this.value = options.value; this.type = options.type; this.expandable = options.expandable; this.html = $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value)); } + MetricSegment.newSelectMetric = function() { + return new MetricSegment({value: 'select metric', fake: true}); + }; + }); module.directive('focusMe', function($timeout, $parse) { diff --git a/src/app/plugins/datasource/influxdb/datasource.js b/src/app/plugins/datasource/influxdb/datasource.js index 26bacfe2c59..3ce22f8743b 100644 --- a/src/app/plugins/datasource/influxdb/datasource.js +++ b/src/app/plugins/datasource/influxdb/datasource.js @@ -36,13 +36,14 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { var timeFilter = getTimeFilter(options); var promises = _.map(options.targets, function(target) { - if (target.hide || !((target.series && target.column) || target.query)) { + if (target.hide) { return []; } // build query var queryBuilder = new InfluxQueryBuilder(target); var query = queryBuilder.build(); + console.log('query builder result:' + query); // replace grafana variables query = query.replace('$timeFilter', timeFilter); @@ -73,40 +74,7 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { }); }; - InfluxDatasource.prototype.listColumns = function(seriesName) { - seriesName = templateSrv.replace(seriesName); - - if(!seriesName.match('^/.*/') && !seriesName.match(/^merge\(.*\)/)) { - seriesName = '"' + seriesName+ '"'; - } - - return this._seriesQuery('select * from ' + seriesName + ' limit 1').then(function(data) { - if (!data) { - return []; - } - return data[0].columns.map(function(item) { - return /^\w+$/.test(item) ? item : ('"' + item + '"'); - }); - }); - }; - - InfluxDatasource.prototype.listSeries = function(query) { - // wrap in regex - if (query && query.length > 0 && query[0] !== '/') { - query = '/' + query + '/'; - } - - return this._seriesQuery('SHOW MEASUREMENTS').then(function(data) { - if (!data || data.length === 0) { - return []; - } - return _.map(data[0].points, function(point) { - return point[1]; - }); - }); - }; - - InfluxDatasource.prototype.metricFindQuery = function (query) { + InfluxDatasource.prototype.metricFindQuery = function (query, queryType) { var interpolated; try { interpolated = templateSrv.replace(query); @@ -115,17 +83,30 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { return $q.reject(err); } - return this._seriesQuery(interpolated) - .then(function (results) { - if (!results || results.length === 0) { return []; } + console.log('metricFindQuery called with: ' + [query, queryType].join(', ')); - return _.map(results[0].points, function (metric) { - return { - text: metric[1], - expandable: false - }; - }); - }); + return this._seriesQuery(interpolated, queryType).then(function (results) { + if (!results || results.results.length === 0) { return []; } + + var influxResults = results.results[0]; + if (!influxResults.series) { + return []; + } + + console.log('metric find query response', results); + var series = influxResults.series[0]; + + switch (queryType) { + case 'MEASUREMENTS': + return _.map(series.values, function(value) { return { text: value[0], expandable: true }; }); + case 'TAG_KEYS': + var tagKeys = _.flatten(series.values); + return _.map(tagKeys, function(tagKey) { return { text: tagKey, expandable: true }; }); + case 'TAG_VALUES': + var tagValues = _.flatten(series.values); + return _.map(tagValues, function(tagValue) { return { text: tagValue, expandable: true }; }); + } + }); }; function retry(deferred, callback, delay) { @@ -143,9 +124,7 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { } InfluxDatasource.prototype._seriesQuery = function(query) { - return this._influxRequest('GET', '/query', { - q: query, - }); + return this._influxRequest('GET', '/query', {q: query}); }; InfluxDatasource.prototype._influxRequest = function(method, url, data) { diff --git a/src/app/plugins/datasource/influxdb/partials/query.editor.html b/src/app/plugins/datasource/influxdb/partials/query.editor.html index d3d7d0ff95d..31e4a22b1dd 100644 --- a/src/app/plugins/datasource/influxdb/partials/query.editor.html +++ b/src/app/plugins/datasource/influxdb/partials/query.editor.html @@ -1,18 +1,47 @@
    -
    -
    -
    + +
    +
    -
    -
    -
      -
    • - -
    • -
    • - group by time -
    • -
    • - -
    • -
    • - -
    • -
    -
    -
    - - -
    -
    - -
    -
    Alias patterns
    -
      -
    • $s = series name
    • -
    • $g = group by
    • -
    • $[0-9] part of series name for series names seperated by dots.
    • -
    -
    - -
    -
    Stacking and fill
    -
      -
    • When stacking is enabled it important that points align
    • -
    • If there are missing points for one series it can cause gaps or missing bars
    • -
    • You must use fill(0), and select a group by time low limit
    • -
    • Use the group by time option below your queries and specify for example >10s if your metrics are written every 10 seconds
    • -
    • This will insert zeros for series that are missing measurements and will make stacking work properly
    • -
    -
    - -
    -
    Group by time
    -
      -
    • Group by time is important, otherwise the query could return many thousands of datapoints that will slow down Grafana
    • -
    • Leave the group by time field empty for each query and it will be calculated based on time range and pixel width of the graph
    • -
    • If you use fill(0) or fill(null) set a low limit for the auto group by time interval
    • -
    • The low limit can only be set in the group by time option below your queries
    • -
    • You set a low limit by adding a greater sign before the interval
    • -
    • Example: >60s if you write metrics to InfluxDB every 60 seconds
    • -
    -
    - -
    - - diff --git a/src/app/plugins/datasource/influxdb/queryBuilder.js b/src/app/plugins/datasource/influxdb/queryBuilder.js index a08d9b5401a..af613ad5e43 100644 --- a/src/app/plugins/datasource/influxdb/queryBuilder.js +++ b/src/app/plugins/datasource/influxdb/queryBuilder.js @@ -1,6 +1,7 @@ define([ + 'lodash' ], -function () { +function (_) { 'use strict'; function InfluxQueryBuilder(target) { @@ -15,36 +16,36 @@ function () { p._buildQuery = function() { var target = this.target; - var query = 'select '; - var seriesName = target.series; - if(!seriesName.match('^/.*/') && !seriesName.match(/^merge\(.*\)/)) { - seriesName = '"' + seriesName+ '"'; + console.log('Build Query: target = ', target); + + if (!target.measurement) { + throw "Metric measurement is missing"; } - if (target.groupby_field) { - query += target.groupby_field + ', '; + var query = 'SELECT '; + var measurement = target.measurement; + var aggregationFunc = target.function || 'mean'; + + if(!measurement.match('^/.*/') && !measurement.match(/^merge\(.*\)/)) { + measurement = '"' + measurement+ '"'; } - query += target.function + '(' + target.column + ')'; - query += ' from ' + seriesName + ' where $timeFilter'; + query += aggregationFunc + '(value)'; + query += ' FROM ' + measurement + ' WHERE '; + query += _.map(target.tags, function(value, key) { + return key + ' = ' + "'" + value + "' AND "; + }); - if (target.condition) { - query += ' and ' + target.condition; - } + query += '$timeFilter'; - query += ' group by time($interval)'; - - if (target.groupby_field) { - query += ', ' + target.groupby_field; - this.groupByField = target.groupby_field; - } + query += ' GROUP BY time($interval)'; if (target.fill) { query += ' fill(' + target.fill + ')'; } - query += " order asc"; + query += " ORDER BY asc"; target.query = query; return query; diff --git a/src/app/plugins/datasource/influxdb/queryCtrl.js b/src/app/plugins/datasource/influxdb/queryCtrl.js index 608b5845d88..d2b8b1e1df7 100644 --- a/src/app/plugins/datasource/influxdb/queryCtrl.js +++ b/src/app/plugins/datasource/influxdb/queryCtrl.js @@ -7,93 +7,23 @@ function (angular, _) { var module = angular.module('grafana.controllers'); - var seriesList = null; - - module.controller('InfluxQueryCtrl', function($scope, $timeout) { + module.controller('InfluxQueryCtrl', function($scope, $timeout, $sce, templateSrv, $q) { $scope.init = function() { - var target = $scope.target; + $scope.segments = $scope.target.segments || []; - target.function = target.function || 'mean'; - target.column = target.column || 'value'; - - // backward compatible correction of schema - if (target.condition_value) { - target.condition = target.condition_key + ' ' + target.condition_op + ' ' + target.condition_value; - delete target.condition_key; - delete target.condition_op; - delete target.condition_value; - } - - if (target.groupby_field_add === false) { - target.groupby_field = ''; - delete target.groupby_field_add; - } - - $scope.rawQuery = true; - - $scope.functions = [ + $scope.functionsSelect = [ 'count', 'mean', 'sum', 'min', 'max', 'mode', 'distinct', 'median', 'derivative', 'stddev', 'first', 'last', 'difference' ]; - $scope.operators = ['=', '=~', '>', '<', '!~', '<>']; - $scope.oldSeries = target.series; - $scope.$on('typeahead-updated', function() { - $timeout($scope.get_data); - }); + checkOtherSegments(0); }; - $scope.showQuery = function () { - $scope.target.rawQuery = true; - }; - - $scope.hideQuery = function () { - $scope.target.rawQuery = false; - }; - - // Cannot use typeahead and ng-change on blur at the same time - $scope.seriesBlur = function() { - if ($scope.oldSeries !== $scope.target.series) { - $scope.oldSeries = $scope.target.series; - $scope.columnList = null; - $scope.get_data(); - } - }; - - $scope.changeFunction = function(func) { - $scope.target.function = func; - $scope.get_data(); - }; - - // called outside of digest - $scope.listColumns = function(query, callback) { - if (!$scope.columnList) { - $scope.$apply(function() { - $scope.datasource.listColumns($scope.target.series).then(function(columns) { - $scope.columnList = columns; - callback(columns); - }); - }); - } - else { - return $scope.columnList; - } - }; - - $scope.listSeries = function(query, callback) { - if (query !== '') { - seriesList = []; - $scope.datasource.listSeries(query).then(function(series) { - seriesList = series; - callback(seriesList); - }); - } - else { - return seriesList; - } + $scope.toggleQueryMode = function () { + $scope.target.rawQuery = !$scope.target.rawQuery; }; $scope.moveMetricQuery = function(fromIndex, toIndex) { @@ -105,6 +35,136 @@ function (angular, _) { $scope.panel.targets.push(clone); }; + $scope.getAltSegments = function (index) { + $scope.altSegments = []; + + var measurement = $scope.segments[0].value; + var queryType, query; + if (index === 0) { + queryType = 'MEASUREMENTS'; + query = 'SHOW MEASUREMENTS'; + } else if (index % 2 === 1) { + queryType = 'TAG_KEYS'; + query = 'SHOW TAG KEYS FROM ' + measurement; + } else { + queryType = 'TAG_VALUES'; + query = "SHOW TAG VALUES FROM " + measurement + " WITH KEY = " + $scope.segments[$scope.segments.length - 2].value; + } + + console.log('getAltSegments: query' , query); + + return $scope.datasource.metricFindQuery(query, queryType).then(function(results) { + console.log('get alt segments: response', results); + $scope.altSegments = _.map(results, function(segment) { + return new MetricSegment({ value: segment.text, expandable: segment.expandable }); + }); + + _.each(templateSrv.variables, function(variable) { + $scope.altSegments.unshift(new MetricSegment({ + type: 'template', + value: '$' + variable.name, + expandable: true, + })); + }); + }, function(err) { + $scope.parserError = err.message || 'Failed to issue metric query'; + }); + }; + + $scope.segmentValueChanged = function (segment, segmentIndex) { + delete $scope.parserError; + + if (segment.expandable) { + return checkOtherSegments(segmentIndex + 1).then(function () { + setSegmentFocus(segmentIndex + 1); + $scope.targetChanged(); + }); + } + else { + $scope.segments = $scope.segments.splice(0, segmentIndex + 1); + } + + setSegmentFocus(segmentIndex + 1); + $scope.targetChanged(); + }; + + $scope.targetChanged = function() { + if ($scope.parserError) { + return; + } + + $scope.target.measurement = ''; + $scope.target.tags = {}; + $scope.target.measurement = $scope.segments[0].value; + + for (var i = 1; i+1 < $scope.segments.length; i += 2) { + var key = $scope.segments[i].value; + $scope.target.tags[key] = $scope.segments[i+1].value; + } + + $scope.$parent.get_data(); + }; + + function checkOtherSegments(fromIndex) { + if (fromIndex === 0) { + $scope.segments.push(MetricSegment.newSelectMetric()); + return; + } + + if ($scope.segments.length === 0) { + throw('should always have a scope segment?'); + } + + if (_.last($scope.segments).fake) { + return $q.when([]); + } else if ($scope.segments.length % 2 === 1) { + $scope.segments.push(MetricSegment.newSelectTag()); + return $q.when([]); + } else { + $scope.segments.push(MetricSegment.newSelectTagValue()); + return $q.when([]); + } + } + + function setSegmentFocus(segmentIndex) { + _.each($scope.segments, function(segment, index) { + segment.focus = segmentIndex === index; + }); + } + + function MetricSegment(options) { + if (options === '*' || options.value === '*') { + this.value = '*'; + this.html = $sce.trustAsHtml(''); + this.expandable = true; + return; + } + + if (_.isString(options)) { + this.value = options; + this.html = $sce.trustAsHtml(this.value); + return; + } + + this.fake = options.fake; + this.value = options.value; + this.type = options.type; + this.expandable = options.expandable; + this.html = $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value)); + } + + MetricSegment.newSelectMetric = function() { + return new MetricSegment({value: 'select metric', fake: true}); + }; + + MetricSegment.newSelectTag = function() { + return new MetricSegment({value: 'select tag', fake: true}); + }; + + MetricSegment.newSelectTagValue = function() { + return new MetricSegment({value: 'select tag value', fake: true}); + }; + }); }); diff --git a/src/app/plugins/datasource/opentsdb/datasource.js b/src/app/plugins/datasource/opentsdb/datasource.js index 3ade07a7aac..cd0c83b7c1d 100644 --- a/src/app/plugins/datasource/opentsdb/datasource.js +++ b/src/app/plugins/datasource/opentsdb/datasource.js @@ -46,13 +46,14 @@ function (angular, _, kbn) { }); }); - return this.performTimeSeriesQuery(queries, start, end) - .then(_.bind(function(response) { - var result = _.map(response.data, _.bind(function(metricData, index) { - return transformMetricData(metricData, groupByTags, this.targets[index]); - }, this)); - return { data: result }; - }, options)); + return this.performTimeSeriesQuery(queries, start, end).then(function(response) { + var metricToTargetMapping = mapMetricsToTargets(response.data, options.targets); + var result = _.map(response.data, function(metricData, index) { + index = metricToTargetMapping[index]; + return transformMetricData(metricData, groupByTags, options.targets[index]); + }); + return { data: result }; + }); }; OpenTSDBDatasource.prototype.performTimeSeriesQuery = function(queries, start, end) { @@ -90,19 +91,8 @@ function (angular, _, kbn) { }; function transformMetricData(md, groupByTags, options) { - var dps = [], - tagData = [], - metricLabel = null; - - if (!_.isEmpty(md.tags)) { - _.each(_.pairs(md.tags), function(tag) { - if (_.has(groupByTags, tag[0])) { - tagData.push(tag[0] + "=" + tag[1]); - } - }); - } - - metricLabel = createMetricLabel(md.metric, tagData, options); + var metricLabel = createMetricLabel(md, options, groupByTags); + var dps = []; // TSDB returns datapoints has a hash of ts => value. // Can't use _.pairs(invert()) because it stringifies keys/values @@ -113,16 +103,31 @@ function (angular, _, kbn) { return { target: metricLabel, datapoints: dps }; } - function createMetricLabel(metric, tagData, options) { + function createMetricLabel(md, options, groupByTags) { if (!_.isUndefined(options) && options.alias) { - return options.alias; + var scopedVars = {}; + _.each(md.tags, function(value, key) { + scopedVars['tag_' + key] = {value: value}; + }); + return templateSrv.replace(options.alias, scopedVars); + } + + var label = md.metric; + var tagData = []; + + if (!_.isEmpty(md.tags)) { + _.each(_.pairs(md.tags), function(tag) { + if (_.has(groupByTags, tag[0])) { + tagData.push(tag[0] + "=" + tag[1]); + } + }); } if (!_.isEmpty(tagData)) { - metric += "{" + tagData.join(", ") + "}"; + label += "{" + tagData.join(", ") + "}"; } - return metric; + return label; } function convertTargetToQuery(target, interval) { @@ -174,6 +179,15 @@ function (angular, _, kbn) { return query; } + function mapMetricsToTargets(metrics, targets) { + return _.map(metrics, function(metricData) { + return _.findIndex(targets, function(target) { + return target.metric === metricData.metric && + _.all(target.tags, function(tagV, tagK) { return metricData.tags[tagK] !== void 0; }); + }); + }); + } + function convertToTSDBTime(date) { if (date === 'now') { return null; diff --git a/src/app/plugins/datasource/opentsdb/partials/query.editor.html b/src/app/plugins/datasource/opentsdb/partials/query.editor.html index 79dd6cd5ffd..a5478ff0cc3 100644 --- a/src/app/plugins/datasource/opentsdb/partials/query.editor.html +++ b/src/app/plugins/datasource/opentsdb/partials/query.editor.html @@ -81,10 +81,11 @@
  • Alias: + Use patterns like $tag_tagname to replace part of the alias for a tag value
  • >>>>>> template_var_multi_select describe('can check if variable exists', function() { beforeEach(function() { _templateSrv.init([{ name: 'test', current: { value: 'oogle' } }]); diff --git a/src/test/test-main.js b/src/test/test-main.js index 1bd25f3732b..d82277725bd 100644 --- a/src/test/test-main.js +++ b/src/test/test-main.js @@ -126,6 +126,7 @@ require([ 'specs/graphiteDatasource-specs', 'specs/influxSeries-specs', 'specs/influxQueryBuilder-specs', + 'specs/influx09-querybuilder-specs', 'specs/influxdb-datasource-specs', 'specs/graph-ctrl-specs', 'specs/graph-specs', diff --git a/src/vendor/bootstrap/less/accordion.less b/src/vendor/bootstrap/less/accordion.less deleted file mode 100644 index d63523bc8c1..00000000000 --- a/src/vendor/bootstrap/less/accordion.less +++ /dev/null @@ -1,34 +0,0 @@ -// -// Accordion -// -------------------------------------------------- - - -// Parent container -.accordion { - margin-bottom: @baseLineHeight; -} - -// Group == heading + body -.accordion-group { - margin-bottom: 2px; - border: 1px solid #e5e5e5; - .border-radius(@baseBorderRadius); -} -.accordion-heading { - border-bottom: 0; -} -.accordion-heading .accordion-toggle { - display: block; - padding: 8px 15px; -} - -// General toggle styles -.accordion-toggle { - cursor: pointer; -} - -// Inner needs the styles because you can't animate properly with any styles on the element -.accordion-inner { - padding: 9px 15px; - border-top: 1px solid #e5e5e5; -} diff --git a/src/vendor/bootstrap/less/bootstrap.less b/src/vendor/bootstrap/less/bootstrap.less index 3eabae1440e..a22c4b24756 100644 --- a/src/vendor/bootstrap/less/bootstrap.less +++ b/src/vendor/bootstrap/less/bootstrap.less @@ -27,23 +27,17 @@ @import "tables.less"; // Components: common -@import "sprites.less"; @import "dropdowns.less"; -@import "wells.less"; @import "component-animations.less"; @import "close.less"; // Components: Buttons & Alerts @import "buttons.less"; -@import "button-groups.less"; @import "alerts.less"; // Note: alerts share common CSS with buttons and thus have styles in buttons.less // Components: Nav @import "navs.less"; @import "navbar.less"; -@import "breadcrumbs.less"; -@import "pagination.less"; -@import "pager.less"; // Components: Popovers @import "modals.less"; @@ -51,13 +45,8 @@ @import "popovers.less"; // Components: Misc -@import "thumbnails.less"; @import "media.less"; @import "labels-badges.less"; -@import "progress-bars.less"; -@import "accordion.less"; -@import "carousel.less"; -@import "hero-unit.less"; // Utility classes @import "utilities.less"; // Has to be last to override when necessary diff --git a/src/vendor/bootstrap/less/breadcrumbs.less b/src/vendor/bootstrap/less/breadcrumbs.less deleted file mode 100644 index f753df6be8c..00000000000 --- a/src/vendor/bootstrap/less/breadcrumbs.less +++ /dev/null @@ -1,24 +0,0 @@ -// -// Breadcrumbs -// -------------------------------------------------- - - -.breadcrumb { - padding: 8px 15px; - margin: 0 0 @baseLineHeight; - list-style: none; - background-color: #f5f5f5; - .border-radius(@baseBorderRadius); - > li { - display: inline-block; - .ie7-inline-block(); - text-shadow: 0 1px 0 @white; - > .divider { - padding: 0 5px; - color: #ccc; - } - } - > .active { - color: @grayLight; - } -} diff --git a/src/vendor/bootstrap/less/carousel.less b/src/vendor/bootstrap/less/carousel.less deleted file mode 100644 index 55bc050144d..00000000000 --- a/src/vendor/bootstrap/less/carousel.less +++ /dev/null @@ -1,158 +0,0 @@ -// -// Carousel -// -------------------------------------------------- - - -.carousel { - position: relative; - margin-bottom: @baseLineHeight; - line-height: 1; -} - -.carousel-inner { - overflow: hidden; - width: 100%; - position: relative; -} - -.carousel-inner { - - > .item { - display: none; - position: relative; - .transition(.6s ease-in-out left); - - // Account for jankitude on images - > img, - > a > img { - display: block; - line-height: 1; - } - } - - > .active, - > .next, - > .prev { display: block; } - - > .active { - left: 0; - } - - > .next, - > .prev { - position: absolute; - top: 0; - width: 100%; - } - - > .next { - left: 100%; - } - > .prev { - left: -100%; - } - > .next.left, - > .prev.right { - left: 0; - } - - > .active.left { - left: -100%; - } - > .active.right { - left: 100%; - } - -} - -// Left/right controls for nav -// --------------------------- - -.carousel-control { - position: absolute; - top: 40%; - left: 15px; - width: 40px; - height: 40px; - margin-top: -20px; - font-size: 60px; - font-weight: 100; - line-height: 30px; - color: @white; - text-align: center; - background: @grayDarker; - border: 3px solid @white; - .border-radius(23px); - .opacity(50); - - // we can't have this transition here - // because webkit cancels the carousel - // animation if you trip this while - // in the middle of another animation - // ;_; - // .transition(opacity .2s linear); - - // Reposition the right one - &.right { - left: auto; - right: 15px; - } - - // Hover/focus state - &:hover, - &:focus { - color: @white; - text-decoration: none; - .opacity(90); - } -} - -// Carousel indicator pips -// ----------------------------- -.carousel-indicators { - position: absolute; - top: 15px; - right: 15px; - z-index: 5; - margin: 0; - list-style: none; - - li { - display: block; - float: left; - width: 10px; - height: 10px; - margin-left: 5px; - text-indent: -999px; - background-color: #ccc; - background-color: rgba(255,255,255,.25); - border-radius: 5px; - } - .active { - background-color: #fff; - } -} - -// Caption for text below images -// ----------------------------- - -.carousel-caption { - position: absolute; - left: 0; - right: 0; - bottom: 0; - padding: 15px; - background: @grayDark; - background: rgba(0,0,0,.75); -} -.carousel-caption h4, -.carousel-caption p { - color: @white; - line-height: @baseLineHeight; -} -.carousel-caption h4 { - margin: 0 0 5px; -} -.carousel-caption p { - margin-bottom: 0; -} diff --git a/src/vendor/bootstrap/less/hero-unit.less b/src/vendor/bootstrap/less/hero-unit.less deleted file mode 100644 index 763d86aeee5..00000000000 --- a/src/vendor/bootstrap/less/hero-unit.less +++ /dev/null @@ -1,25 +0,0 @@ -// -// Hero unit -// -------------------------------------------------- - - -.hero-unit { - padding: 60px; - margin-bottom: 30px; - font-size: 18px; - font-weight: 200; - line-height: @baseLineHeight * 1.5; - color: @heroUnitLeadColor; - background-color: @heroUnitBackground; - .border-radius(6px); - h1 { - margin-bottom: 0; - font-size: 60px; - line-height: 1; - color: @heroUnitHeadingColor; - letter-spacing: -1px; - } - li { - line-height: @baseLineHeight * 1.5; // Reset since we specify in type.less - } -} diff --git a/src/vendor/bootstrap/less/pager.less b/src/vendor/bootstrap/less/pager.less deleted file mode 100644 index 1476188297e..00000000000 --- a/src/vendor/bootstrap/less/pager.less +++ /dev/null @@ -1,43 +0,0 @@ -// -// Pager pagination -// -------------------------------------------------- - - -.pager { - margin: @baseLineHeight 0; - list-style: none; - text-align: center; - .clearfix(); -} -.pager li { - display: inline; -} -.pager li > a, -.pager li > span { - display: inline-block; - padding: 5px 14px; - background-color: #fff; - border: 1px solid #ddd; - .border-radius(15px); -} -.pager li > a:hover, -.pager li > a:focus { - text-decoration: none; - background-color: #f5f5f5; -} -.pager .next > a, -.pager .next > span { - float: right; -} -.pager .previous > a, -.pager .previous > span { - float: left; -} -.pager .disabled > a, -.pager .disabled > a:hover, -.pager .disabled > a:focus, -.pager .disabled > span { - color: @grayLight; - background-color: #fff; - cursor: default; -} \ No newline at end of file diff --git a/src/vendor/bootstrap/less/pagination.less b/src/vendor/bootstrap/less/pagination.less deleted file mode 100644 index a789db2d28b..00000000000 --- a/src/vendor/bootstrap/less/pagination.less +++ /dev/null @@ -1,123 +0,0 @@ -// -// Pagination (multiple pages) -// -------------------------------------------------- - -// Space out pagination from surrounding content -.pagination { - margin: @baseLineHeight 0; -} - -.pagination ul { - // Allow for text-based alignment - display: inline-block; - .ie7-inline-block(); - // Reset default ul styles - margin-left: 0; - margin-bottom: 0; - // Visuals - .border-radius(@baseBorderRadius); - .box-shadow(0 1px 2px rgba(0,0,0,.05)); -} -.pagination ul > li { - display: inline; // Remove list-style and block-level defaults -} -.pagination ul > li > a, -.pagination ul > li > span { - float: left; // Collapse white-space - padding: 4px 12px; - line-height: @baseLineHeight; - text-decoration: none; - background-color: @paginationBackground; - border: 1px solid @paginationBorder; - border-left-width: 0; -} -.pagination ul > li > a:hover, -.pagination ul > li > a:focus, -.pagination ul > .active > a, -.pagination ul > .active > span { - background-color: @paginationActiveBackground; -} -.pagination ul > .active > a, -.pagination ul > .active > span { - color: @grayLight; - cursor: default; -} -.pagination ul > .disabled > span, -.pagination ul > .disabled > a, -.pagination ul > .disabled > a:hover, -.pagination ul > .disabled > a:focus { - color: @grayLight; - background-color: transparent; - cursor: default; -} -.pagination ul > li:first-child > a, -.pagination ul > li:first-child > span { - border-left-width: 1px; - .border-left-radius(@baseBorderRadius); -} -.pagination ul > li:last-child > a, -.pagination ul > li:last-child > span { - .border-right-radius(@baseBorderRadius); -} - - -// Alignment -// -------------------------------------------------- - -.pagination-centered { - text-align: center; -} -.pagination-right { - text-align: right; -} - - -// Sizing -// -------------------------------------------------- - -// Large -.pagination-large { - ul > li > a, - ul > li > span { - padding: @paddingLarge; - font-size: @fontSizeLarge; - } - ul > li:first-child > a, - ul > li:first-child > span { - .border-left-radius(@borderRadiusLarge); - } - ul > li:last-child > a, - ul > li:last-child > span { - .border-right-radius(@borderRadiusLarge); - } -} - -// Small and mini -.pagination-mini, -.pagination-small { - ul > li:first-child > a, - ul > li:first-child > span { - .border-left-radius(@borderRadiusSmall); - } - ul > li:last-child > a, - ul > li:last-child > span { - .border-right-radius(@borderRadiusSmall); - } -} - -// Small -.pagination-small { - ul > li > a, - ul > li > span { - padding: @paddingSmall; - font-size: @fontSizeSmall; - } -} -// Mini -.pagination-mini { - ul > li > a, - ul > li > span { - padding: @paddingMini; - font-size: @fontSizeMini; - } -} diff --git a/src/vendor/bootstrap/less/progress-bars.less b/src/vendor/bootstrap/less/progress-bars.less deleted file mode 100644 index 5e0c3dda018..00000000000 --- a/src/vendor/bootstrap/less/progress-bars.less +++ /dev/null @@ -1,122 +0,0 @@ -// -// Progress bars -// -------------------------------------------------- - - -// ANIMATIONS -// ---------- - -// Webkit -@-webkit-keyframes progress-bar-stripes { - from { background-position: 40px 0; } - to { background-position: 0 0; } -} - -// Firefox -@-moz-keyframes progress-bar-stripes { - from { background-position: 40px 0; } - to { background-position: 0 0; } -} - -// IE9 -@-ms-keyframes progress-bar-stripes { - from { background-position: 40px 0; } - to { background-position: 0 0; } -} - -// Opera -@-o-keyframes progress-bar-stripes { - from { background-position: 0 0; } - to { background-position: 40px 0; } -} - -// Spec -@keyframes progress-bar-stripes { - from { background-position: 40px 0; } - to { background-position: 0 0; } -} - - - -// THE BARS -// -------- - -// Outer container -.progress { - overflow: hidden; - height: @baseLineHeight; - margin-bottom: @baseLineHeight; - #gradient > .vertical(#f5f5f5, #f9f9f9); - .box-shadow(inset 0 1px 2px rgba(0,0,0,.1)); - .border-radius(@baseBorderRadius); -} - -// Bar of progress -.progress .bar { - width: 0%; - height: 100%; - color: @white; - float: left; - font-size: 12px; - text-align: center; - text-shadow: 0 -1px 0 rgba(0,0,0,.25); - #gradient > .vertical(#149bdf, #0480be); - .box-shadow(inset 0 -1px 0 rgba(0,0,0,.15)); - .box-sizing(border-box); - .transition(width .6s ease); -} -.progress .bar + .bar { - .box-shadow(~"inset 1px 0 0 rgba(0,0,0,.15), inset 0 -1px 0 rgba(0,0,0,.15)"); -} - -// Striped bars -.progress-striped .bar { - #gradient > .striped(#149bdf); - .background-size(40px 40px); -} - -// Call animation for the active one -.progress.active .bar { - -webkit-animation: progress-bar-stripes 2s linear infinite; - -moz-animation: progress-bar-stripes 2s linear infinite; - -ms-animation: progress-bar-stripes 2s linear infinite; - -o-animation: progress-bar-stripes 2s linear infinite; - animation: progress-bar-stripes 2s linear infinite; -} - - - -// COLORS -// ------ - -// Danger (red) -.progress-danger .bar, .progress .bar-danger { - #gradient > .vertical(#ee5f5b, #c43c35); -} -.progress-danger.progress-striped .bar, .progress-striped .bar-danger { - #gradient > .striped(#ee5f5b); -} - -// Success (green) -.progress-success .bar, .progress .bar-success { - #gradient > .vertical(#62c462, #57a957); -} -.progress-success.progress-striped .bar, .progress-striped .bar-success { - #gradient > .striped(#62c462); -} - -// Info (teal) -.progress-info .bar, .progress .bar-info { - #gradient > .vertical(#5bc0de, #339bb9); -} -.progress-info.progress-striped .bar, .progress-striped .bar-info { - #gradient > .striped(#5bc0de); -} - -// Warning (orange) -.progress-warning .bar, .progress .bar-warning { - #gradient > .vertical(lighten(@orange, 15%), @orange); -} -.progress-warning.progress-striped .bar, .progress-striped .bar-warning { - #gradient > .striped(lighten(@orange, 15%)); -} diff --git a/src/vendor/bootstrap/less/sprites.less b/src/vendor/bootstrap/less/sprites.less deleted file mode 100644 index 8b137891791..00000000000 --- a/src/vendor/bootstrap/less/sprites.less +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/vendor/bootstrap/less/thumbnails.less b/src/vendor/bootstrap/less/thumbnails.less deleted file mode 100644 index 4fd07d25337..00000000000 --- a/src/vendor/bootstrap/less/thumbnails.less +++ /dev/null @@ -1,53 +0,0 @@ -// -// Thumbnails -// -------------------------------------------------- - - -// Note: `.thumbnails` and `.thumbnails > li` are overriden in responsive files - -// Make wrapper ul behave like the grid -.thumbnails { - margin-left: -@gridGutterWidth; - list-style: none; - .clearfix(); -} -// Fluid rows have no left margin -.row-fluid .thumbnails { - margin-left: 0; -} - -// Float li to make thumbnails appear in a row -.thumbnails > li { - float: left; // Explicity set the float since we don't require .span* classes - margin-bottom: @baseLineHeight; - margin-left: @gridGutterWidth; -} - -// The actual thumbnail (can be `a` or `div`) -.thumbnail { - display: block; - padding: 4px; - line-height: @baseLineHeight; - border: 1px solid #ddd; - .border-radius(@baseBorderRadius); - .box-shadow(0 1px 3px rgba(0,0,0,.055)); - .transition(all .2s ease-in-out); -} -// Add a hover/focus state for linked versions only -a.thumbnail:hover, -a.thumbnail:focus { - border-color: @linkColor; - .box-shadow(0 1px 4px rgba(0,105,214,.25)); -} - -// Images and captions -.thumbnail > img { - display: block; - max-width: 100%; - margin-left: auto; - margin-right: auto; -} -.thumbnail .caption { - padding: 9px; - color: @gray; -} diff --git a/src/vendor/bootstrap/less/wells.less b/src/vendor/bootstrap/less/wells.less deleted file mode 100644 index 84a744b1c5c..00000000000 --- a/src/vendor/bootstrap/less/wells.less +++ /dev/null @@ -1,29 +0,0 @@ -// -// Wells -// -------------------------------------------------- - - -// Base class -.well { - min-height: 20px; - padding: 19px; - margin-bottom: 20px; - background-color: @wellBackground; - border: 1px solid darken(@wellBackground, 7%); - .border-radius(@baseBorderRadius); - .box-shadow(inset 0 1px 1px rgba(0,0,0,.05)); - blockquote { - border-color: #ddd; - border-color: rgba(0,0,0,.15); - } -} - -// Sizes -.well-large { - padding: 24px; - .border-radius(@borderRadiusLarge); -} -.well-small { - padding: 9px; - .border-radius(@borderRadiusSmall); -} diff --git a/src/vendor/css/animate.min.css b/src/vendor/css/animate.min.css deleted file mode 100644 index 3374f9c550d..00000000000 --- a/src/vendor/css/animate.min.css +++ /dev/null @@ -1,3270 +0,0 @@ -@charset "UTF-8"; -/* -Animate.css - http://daneden.me/animate -Licensed under the MIT license - -Copyright (c) 2013 Daniel Eden - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ -body { /* Addresses a small issue in webkit: http://bit.ly/NEdoDq */ - -webkit-backface-visibility: hidden; -} -.animated { - -webkit-animation-duration: 1s; - -moz-animation-duration: 1s; - -o-animation-duration: 1s; - animation-duration: 1s; - -webkit-animation-fill-mode: both; - -moz-animation-fill-mode: both; - -o-animation-fill-mode: both; - animation-fill-mode: both; -} - -.infinite { - -webkit-animation-iteration-count: infinite; - -moz-animation-iteration-count: infinite; - -o-animation-iteration-count: infinite; - animation-iteration-count: infinite; -} - -.animated.hinge { - -webkit-animation-duration: 2s; - -moz-animation-duration: 2s; - -o-animation-duration: 2s; - animation-duration: 2s; -} - -@-webkit-keyframes flash { - 0%, 50%, 100% {opacity: 1;} - 25%, 75% {opacity: 0;} -} - -@-moz-keyframes flash { - 0%, 50%, 100% {opacity: 1;} - 25%, 75% {opacity: 0;} -} - -@-o-keyframes flash { - 0%, 50%, 100% {opacity: 1;} - 25%, 75% {opacity: 0;} -} - -@keyframes flash { - 0%, 50%, 100% {opacity: 1;} - 25%, 75% {opacity: 0;} -} - -.flash { - -webkit-animation-name: flash; - -moz-animation-name: flash; - -o-animation-name: flash; - animation-name: flash; -} -@-webkit-keyframes shake { - 0%, 100% {-webkit-transform: translateX(0);} - 10%, 30%, 50%, 70%, 90% {-webkit-transform: translateX(-10px);} - 20%, 40%, 60%, 80% {-webkit-transform: translateX(10px);} -} - -@-moz-keyframes shake { - 0%, 100% {-moz-transform: translateX(0);} - 10%, 30%, 50%, 70%, 90% {-moz-transform: translateX(-10px);} - 20%, 40%, 60%, 80% {-moz-transform: translateX(10px);} -} - -@-o-keyframes shake { - 0%, 100% {-o-transform: translateX(0);} - 10%, 30%, 50%, 70%, 90% {-o-transform: translateX(-10px);} - 20%, 40%, 60%, 80% {-o-transform: translateX(10px);} -} - -@keyframes shake { - 0%, 100% {transform: translateX(0);} - 10%, 30%, 50%, 70%, 90% {transform: translateX(-10px);} - 20%, 40%, 60%, 80% {transform: translateX(10px);} -} - -.shake { - -webkit-animation-name: shake; - -moz-animation-name: shake; - -o-animation-name: shake; - animation-name: shake; -} -@-webkit-keyframes bounce { - 0%, 20%, 50%, 80%, 100% {-webkit-transform: translateY(0);} - 40% {-webkit-transform: translateY(-30px);} - 60% {-webkit-transform: translateY(-15px);} -} - -@-moz-keyframes bounce { - 0%, 20%, 50%, 80%, 100% {-moz-transform: translateY(0);} - 40% {-moz-transform: translateY(-30px);} - 60% {-moz-transform: translateY(-15px);} -} - -@-o-keyframes bounce { - 0%, 20%, 50%, 80%, 100% {-o-transform: translateY(0);} - 40% {-o-transform: translateY(-30px);} - 60% {-o-transform: translateY(-15px);} -} -@keyframes bounce { - 0%, 20%, 50%, 80%, 100% {transform: translateY(0);} - 40% {transform: translateY(-30px);} - 60% {transform: translateY(-15px);} -} - -.bounce { - -webkit-animation-name: bounce; - -moz-animation-name: bounce; - -o-animation-name: bounce; - animation-name: bounce; -} -@-webkit-keyframes tada { - 0% {-webkit-transform: scale(1);} - 10%, 20% {-webkit-transform: scale(0.9) rotate(-3deg);} - 30%, 50%, 70%, 90% {-webkit-transform: scale(1.1) rotate(3deg);} - 40%, 60%, 80% {-webkit-transform: scale(1.1) rotate(-3deg);} - 100% {-webkit-transform: scale(1) rotate(0);} -} - -@-moz-keyframes tada { - 0% {-moz-transform: scale(1);} - 10%, 20% {-moz-transform: scale(0.9) rotate(-3deg);} - 30%, 50%, 70%, 90% {-moz-transform: scale(1.1) rotate(3deg);} - 40%, 60%, 80% {-moz-transform: scale(1.1) rotate(-3deg);} - 100% {-moz-transform: scale(1) rotate(0);} -} - -@-o-keyframes tada { - 0% {-o-transform: scale(1);} - 10%, 20% {-o-transform: scale(0.9) rotate(-3deg);} - 30%, 50%, 70%, 90% {-o-transform: scale(1.1) rotate(3deg);} - 40%, 60%, 80% {-o-transform: scale(1.1) rotate(-3deg);} - 100% {-o-transform: scale(1) rotate(0);} -} - -@keyframes tada { - 0% {transform: scale(1);} - 10%, 20% {transform: scale(0.9) rotate(-3deg);} - 30%, 50%, 70%, 90% {transform: scale(1.1) rotate(3deg);} - 40%, 60%, 80% {transform: scale(1.1) rotate(-3deg);} - 100% {transform: scale(1) rotate(0);} -} - -.tada { - -webkit-animation-name: tada; - -moz-animation-name: tada; - -o-animation-name: tada; - animation-name: tada; -} -@-webkit-keyframes swing { - 20%, 40%, 60%, 80%, 100% { -webkit-transform-origin: top center; } - 20% { -webkit-transform: rotate(15deg); } - 40% { -webkit-transform: rotate(-10deg); } - 60% { -webkit-transform: rotate(5deg); } - 80% { -webkit-transform: rotate(-5deg); } - 100% { -webkit-transform: rotate(0deg); } -} - -@-moz-keyframes swing { - 20% { -moz-transform: rotate(15deg); } - 40% { -moz-transform: rotate(-10deg); } - 60% { -moz-transform: rotate(5deg); } - 80% { -moz-transform: rotate(-5deg); } - 100% { -moz-transform: rotate(0deg); } -} - -@-o-keyframes swing { - 20% { -o-transform: rotate(15deg); } - 40% { -o-transform: rotate(-10deg); } - 60% { -o-transform: rotate(5deg); } - 80% { -o-transform: rotate(-5deg); } - 100% { -o-transform: rotate(0deg); } -} - -@keyframes swing { - 20% { transform: rotate(15deg); } - 40% { transform: rotate(-10deg); } - 60% { transform: rotate(5deg); } - 80% { transform: rotate(-5deg); } - 100% { transform: rotate(0deg); } -} - -.swing { - -webkit-transform-origin: top center; - -moz-transform-origin: top center; - -o-transform-origin: top center; - transform-origin: top center; - -webkit-animation-name: swing; - -moz-animation-name: swing; - -o-animation-name: swing; - animation-name: swing; -} -/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ - -@-webkit-keyframes wobble { - 0% { -webkit-transform: translateX(0%); } - 15% { -webkit-transform: translateX(-25%) rotate(-5deg); } - 30% { -webkit-transform: translateX(20%) rotate(3deg); } - 45% { -webkit-transform: translateX(-15%) rotate(-3deg); } - 60% { -webkit-transform: translateX(10%) rotate(2deg); } - 75% { -webkit-transform: translateX(-5%) rotate(-1deg); } - 100% { -webkit-transform: translateX(0%); } -} - -@-moz-keyframes wobble { - 0% { -moz-transform: translateX(0%); } - 15% { -moz-transform: translateX(-25%) rotate(-5deg); } - 30% { -moz-transform: translateX(20%) rotate(3deg); } - 45% { -moz-transform: translateX(-15%) rotate(-3deg); } - 60% { -moz-transform: translateX(10%) rotate(2deg); } - 75% { -moz-transform: translateX(-5%) rotate(-1deg); } - 100% { -moz-transform: translateX(0%); } -} - -@-o-keyframes wobble { - 0% { -o-transform: translateX(0%); } - 15% { -o-transform: translateX(-25%) rotate(-5deg); } - 30% { -o-transform: translateX(20%) rotate(3deg); } - 45% { -o-transform: translateX(-15%) rotate(-3deg); } - 60% { -o-transform: translateX(10%) rotate(2deg); } - 75% { -o-transform: translateX(-5%) rotate(-1deg); } - 100% { -o-transform: translateX(0%); } -} - -@keyframes wobble { - 0% { transform: translateX(0%); } - 15% { transform: translateX(-25%) rotate(-5deg); } - 30% { transform: translateX(20%) rotate(3deg); } - 45% { transform: translateX(-15%) rotate(-3deg); } - 60% { transform: translateX(10%) rotate(2deg); } - 75% { transform: translateX(-5%) rotate(-1deg); } - 100% { transform: translateX(0%); } -} - -.wobble { - -webkit-animation-name: wobble; - -moz-animation-name: wobble; - -o-animation-name: wobble; - animation-name: wobble; -} -/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ - -@-webkit-keyframes pulse { - 0% { -webkit-transform: scale(1); } - 50% { -webkit-transform: scale(1.1); } - 100% { -webkit-transform: scale(1); } -} -@-moz-keyframes pulse { - 0% { -moz-transform: scale(1); } - 50% { -moz-transform: scale(1.1); } - 100% { -moz-transform: scale(1); } -} -@-o-keyframes pulse { - 0% { -o-transform: scale(1); } - 50% { -o-transform: scale(1.1); } - 100% { -o-transform: scale(1); } -} -@keyframes pulse { - 0% { transform: scale(1); } - 50% { transform: scale(1.1); } - 100% { transform: scale(1); } -} - -.pulse { - -webkit-animation-name: pulse; - -moz-animation-name: pulse; - -o-animation-name: pulse; - animation-name: pulse; -} -@-webkit-keyframes flip { - 0% { - -webkit-transform: perspective(400px) rotateY(0); - -webkit-animation-timing-function: ease-out; - } - 40% { - -webkit-transform: perspective(400px) translateZ(150px) rotateY(170deg); - -webkit-animation-timing-function: ease-out; - } - 50% { - -webkit-transform: perspective(400px) translateZ(150px) rotateY(190deg) scale(1); - -webkit-animation-timing-function: ease-in; - } - 80% { - -webkit-transform: perspective(400px) rotateY(360deg) scale(.95); - -webkit-animation-timing-function: ease-in; - } - 100% { - -webkit-transform: perspective(400px) scale(1); - -webkit-animation-timing-function: ease-in; - } -} -@-moz-keyframes flip { - 0% { - -moz-transform: perspective(400px) rotateY(0); - -moz-animation-timing-function: ease-out; - } - 40% { - -moz-transform: perspective(400px) translateZ(150px) rotateY(170deg); - -moz-animation-timing-function: ease-out; - } - 50% { - -moz-transform: perspective(400px) translateZ(150px) rotateY(190deg) scale(1); - -moz-animation-timing-function: ease-in; - } - 80% { - -moz-transform: perspective(400px) rotateY(360deg) scale(.95); - -moz-animation-timing-function: ease-in; - } - 100% { - -moz-transform: perspective(400px) scale(1); - -moz-animation-timing-function: ease-in; - } -} -@-o-keyframes flip { - 0% { - -o-transform: perspective(400px) rotateY(0); - -o-animation-timing-function: ease-out; - } - 40% { - -o-transform: perspective(400px) translateZ(150px) rotateY(170deg); - -o-animation-timing-function: ease-out; - } - 50% { - -o-transform: perspective(400px) translateZ(150px) rotateY(190deg) scale(1); - -o-animation-timing-function: ease-in; - } - 80% { - -o-transform: perspective(400px) rotateY(360deg) scale(.95); - -o-animation-timing-function: ease-in; - } - 100% { - -o-transform: perspective(400px) scale(1); - -o-animation-timing-function: ease-in; - } -} -@keyframes flip { - 0% { - transform: perspective(400px) rotateY(0); - animation-timing-function: ease-out; - } - 40% { - transform: perspective(400px) translateZ(150px) rotateY(170deg); - animation-timing-function: ease-out; - } - 50% { - transform: perspective(400px) translateZ(150px) rotateY(190deg) scale(1); - animation-timing-function: ease-in; - } - 80% { - transform: perspective(400px) rotateY(360deg) scale(.95); - animation-timing-function: ease-in; - } - 100% { - transform: perspective(400px) scale(1); - animation-timing-function: ease-in; - } -} - -.flip { - -webkit-backface-visibility: visible !important; - -webkit-animation-name: flip; - -moz-backface-visibility: visible !important; - -moz-animation-name: flip; - -o-backface-visibility: visible !important; - -o-animation-name: flip; - backface-visibility: visible !important; - animation-name: flip; -} -@-webkit-keyframes flipInX { - 0% { - -webkit-transform: perspective(400px) rotateX(90deg); - opacity: 0; - } - - 40% { - -webkit-transform: perspective(400px) rotateX(-10deg); - } - - 70% { - -webkit-transform: perspective(400px) rotateX(10deg); - } - - 100% { - -webkit-transform: perspective(400px) rotateX(0deg); - opacity: 1; - } -} -@-moz-keyframes flipInX { - 0% { - -moz-transform: perspective(400px) rotateX(90deg); - opacity: 0; - } - - 40% { - -moz-transform: perspective(400px) rotateX(-10deg); - } - - 70% { - -moz-transform: perspective(400px) rotateX(10deg); - } - - 100% { - -moz-transform: perspective(400px) rotateX(0deg); - opacity: 1; - } -} -@-o-keyframes flipInX { - 0% { - -o-transform: perspective(400px) rotateX(90deg); - opacity: 0; - } - - 40% { - -o-transform: perspective(400px) rotateX(-10deg); - } - - 70% { - -o-transform: perspective(400px) rotateX(10deg); - } - - 100% { - -o-transform: perspective(400px) rotateX(0deg); - opacity: 1; - } -} -@keyframes flipInX { - 0% { - transform: perspective(400px) rotateX(90deg); - opacity: 0; - } - - 40% { - transform: perspective(400px) rotateX(-10deg); - } - - 70% { - transform: perspective(400px) rotateX(10deg); - } - - 100% { - transform: perspective(400px) rotateX(0deg); - opacity: 1; - } -} - -.flipInX { - -webkit-backface-visibility: visible !important; - -webkit-animation-name: flipInX; - -moz-backface-visibility: visible !important; - -moz-animation-name: flipInX; - -o-backface-visibility: visible !important; - -o-animation-name: flipInX; - backface-visibility: visible !important; - animation-name: flipInX; -} -@-webkit-keyframes flipOutX { - 0% { - -webkit-transform: perspective(400px) rotateX(0deg); - opacity: 1; - } - 100% { - -webkit-transform: perspective(400px) rotateX(90deg); - opacity: 0; - } -} - -@-moz-keyframes flipOutX { - 0% { - -moz-transform: perspective(400px) rotateX(0deg); - opacity: 1; - } - 100% { - -moz-transform: perspective(400px) rotateX(90deg); - opacity: 0; - } -} - -@-o-keyframes flipOutX { - 0% { - -o-transform: perspective(400px) rotateX(0deg); - opacity: 1; - } - 100% { - -o-transform: perspective(400px) rotateX(90deg); - opacity: 0; - } -} - -@keyframes flipOutX { - 0% { - transform: perspective(400px) rotateX(0deg); - opacity: 1; - } - 100% { - transform: perspective(400px) rotateX(90deg); - opacity: 0; - } -} - -.flipOutX { - -webkit-animation-name: flipOutX; - -webkit-backface-visibility: visible !important; - -moz-animation-name: flipOutX; - -moz-backface-visibility: visible !important; - -o-animation-name: flipOutX; - -o-backface-visibility: visible !important; - animation-name: flipOutX; - backface-visibility: visible !important; -} -@-webkit-keyframes flipInY { - 0% { - -webkit-transform: perspective(400px) rotateY(90deg); - opacity: 0; - } - - 40% { - -webkit-transform: perspective(400px) rotateY(-10deg); - } - - 70% { - -webkit-transform: perspective(400px) rotateY(10deg); - } - - 100% { - -webkit-transform: perspective(400px) rotateY(0deg); - opacity: 1; - } -} -@-moz-keyframes flipInY { - 0% { - -moz-transform: perspective(400px) rotateY(90deg); - opacity: 0; - } - - 40% { - -moz-transform: perspective(400px) rotateY(-10deg); - } - - 70% { - -moz-transform: perspective(400px) rotateY(10deg); - } - - 100% { - -moz-transform: perspective(400px) rotateY(0deg); - opacity: 1; - } -} -@-o-keyframes flipInY { - 0% { - -o-transform: perspective(400px) rotateY(90deg); - opacity: 0; - } - - 40% { - -o-transform: perspective(400px) rotateY(-10deg); - } - - 70% { - -o-transform: perspective(400px) rotateY(10deg); - } - - 100% { - -o-transform: perspective(400px) rotateY(0deg); - opacity: 1; - } -} -@keyframes flipInY { - 0% { - transform: perspective(400px) rotateY(90deg); - opacity: 0; - } - - 40% { - transform: perspective(400px) rotateY(-10deg); - } - - 70% { - transform: perspective(400px) rotateY(10deg); - } - - 100% { - transform: perspective(400px) rotateY(0deg); - opacity: 1; - } -} - -.flipInY { - -webkit-backface-visibility: visible !important; - -webkit-animation-name: flipInY; - -moz-backface-visibility: visible !important; - -moz-animation-name: flipInY; - -o-backface-visibility: visible !important; - -o-animation-name: flipInY; - backface-visibility: visible !important; - animation-name: flipInY; -} -@-webkit-keyframes flipOutY { - 0% { - -webkit-transform: perspective(400px) rotateY(0deg); - opacity: 1; - } - 100% { - -webkit-transform: perspective(400px) rotateY(90deg); - opacity: 0; - } -} -@-moz-keyframes flipOutY { - 0% { - -moz-transform: perspective(400px) rotateY(0deg); - opacity: 1; - } - 100% { - -moz-transform: perspective(400px) rotateY(90deg); - opacity: 0; - } -} -@-o-keyframes flipOutY { - 0% { - -o-transform: perspective(400px) rotateY(0deg); - opacity: 1; - } - 100% { - -o-transform: perspective(400px) rotateY(90deg); - opacity: 0; - } -} -@keyframes flipOutY { - 0% { - transform: perspective(400px) rotateY(0deg); - opacity: 1; - } - 100% { - transform: perspective(400px) rotateY(90deg); - opacity: 0; - } -} - -.flipOutY { - -webkit-backface-visibility: visible !important; - -webkit-animation-name: flipOutY; - -moz-backface-visibility: visible !important; - -moz-animation-name: flipOutY; - -o-backface-visibility: visible !important; - -o-animation-name: flipOutY; - backface-visibility: visible !important; - animation-name: flipOutY; -} -@-webkit-keyframes fadeIn { - 0% {opacity: 0;} - 100% {opacity: 1;} -} - -@-moz-keyframes fadeIn { - 0% {opacity: 0;} - 100% {opacity: 1;} -} - -@-o-keyframes fadeIn { - 0% {opacity: 0;} - 100% {opacity: 1;} -} - -@keyframes fadeIn { - 0% {opacity: 0;} - 100% {opacity: 1;} -} - -.fadeIn { - -webkit-animation-name: fadeIn; - -moz-animation-name: fadeIn; - -o-animation-name: fadeIn; - animation-name: fadeIn; -} -@-webkit-keyframes fadeInUp { - 0% { - opacity: 0; - -webkit-transform: translateY(20px); - } - - 100% { - opacity: 1; - -webkit-transform: translateY(0); - } -} - -@-moz-keyframes fadeInUp { - 0% { - opacity: 0; - -moz-transform: translateY(20px); - } - - 100% { - opacity: 1; - -moz-transform: translateY(0); - } -} - -@-o-keyframes fadeInUp { - 0% { - opacity: 0; - -o-transform: translateY(20px); - } - - 100% { - opacity: 1; - -o-transform: translateY(0); - } -} - -@keyframes fadeInUp { - 0% { - opacity: 0; - transform: translateY(20px); - } - - 100% { - opacity: 1; - transform: translateY(0); - } -} - -.fadeInUp { - -webkit-animation-name: fadeInUp; - -moz-animation-name: fadeInUp; - -o-animation-name: fadeInUp; - animation-name: fadeInUp; -} -@-webkit-keyframes fadeInDown { - 0% { - opacity: 0; - -webkit-transform: translateY(-20px); - } - - 100% { - opacity: 1; - -webkit-transform: translateY(0); - } -} - -@-moz-keyframes fadeInDown { - 0% { - opacity: 0; - -moz-transform: translateY(-20px); - } - - 100% { - opacity: 1; - -moz-transform: translateY(0); - } -} - -@-o-keyframes fadeInDown { - 0% { - opacity: 0; - -o-transform: translateY(-20px); - } - - 100% { - opacity: 1; - -o-transform: translateY(0); - } -} - -@keyframes fadeInDown { - 0% { - opacity: 0; - transform: translateY(-20px); - } - - 100% { - opacity: 1; - transform: translateY(0); - } -} - -.fadeInDown { - -webkit-animation-name: fadeInDown; - -moz-animation-name: fadeInDown; - -o-animation-name: fadeInDown; - animation-name: fadeInDown; -} -@-webkit-keyframes fadeInLeft { - 0% { - opacity: 0; - -webkit-transform: translateX(-20px); - } - - 100% { - opacity: 1; - -webkit-transform: translateX(0); - } -} - -@-moz-keyframes fadeInLeft { - 0% { - opacity: 0; - -moz-transform: translateX(-20px); - } - - 100% { - opacity: 1; - -moz-transform: translateX(0); - } -} - -@-o-keyframes fadeInLeft { - 0% { - opacity: 0; - -o-transform: translateX(-20px); - } - - 100% { - opacity: 1; - -o-transform: translateX(0); - } -} - -@keyframes fadeInLeft { - 0% { - opacity: 0; - transform: translateX(-20px); - } - - 100% { - opacity: 1; - transform: translateX(0); - } -} - -.fadeInLeft { - -webkit-animation-name: fadeInLeft; - -moz-animation-name: fadeInLeft; - -o-animation-name: fadeInLeft; - animation-name: fadeInLeft; -} -@-webkit-keyframes fadeInRight { - 0% { - opacity: 0; - -webkit-transform: translateX(20px); - } - - 100% { - opacity: 1; - -webkit-transform: translateX(0); - } -} - -@-moz-keyframes fadeInRight { - 0% { - opacity: 0; - -moz-transform: translateX(20px); - } - - 100% { - opacity: 1; - -moz-transform: translateX(0); - } -} - -@-o-keyframes fadeInRight { - 0% { - opacity: 0; - -o-transform: translateX(20px); - } - - 100% { - opacity: 1; - -o-transform: translateX(0); - } -} - -@keyframes fadeInRight { - 0% { - opacity: 0; - transform: translateX(20px); - } - - 100% { - opacity: 1; - transform: translateX(0); - } -} - -.fadeInRight { - -webkit-animation-name: fadeInRight; - -moz-animation-name: fadeInRight; - -o-animation-name: fadeInRight; - animation-name: fadeInRight; -} -@-webkit-keyframes fadeInUpBig { - 0% { - opacity: 0; - -webkit-transform: translateY(2000px); - } - - 100% { - opacity: 1; - -webkit-transform: translateY(0); - } -} - -@-moz-keyframes fadeInUpBig { - 0% { - opacity: 0; - -moz-transform: translateY(2000px); - } - - 100% { - opacity: 1; - -moz-transform: translateY(0); - } -} - -@-o-keyframes fadeInUpBig { - 0% { - opacity: 0; - -o-transform: translateY(2000px); - } - - 100% { - opacity: 1; - -o-transform: translateY(0); - } -} - -@keyframes fadeInUpBig { - 0% { - opacity: 0; - transform: translateY(2000px); - } - - 100% { - opacity: 1; - transform: translateY(0); - } -} - -.fadeInUpBig { - -webkit-animation-name: fadeInUpBig; - -moz-animation-name: fadeInUpBig; - -o-animation-name: fadeInUpBig; - animation-name: fadeInUpBig; -} -@-webkit-keyframes fadeInDownBig { - 0% { - opacity: 0; - -webkit-transform: translateY(-2000px); - } - - 100% { - opacity: 1; - -webkit-transform: translateY(0); - } -} - -@-moz-keyframes fadeInDownBig { - 0% { - opacity: 0; - -moz-transform: translateY(-2000px); - } - - 100% { - opacity: 1; - -moz-transform: translateY(0); - } -} - -@-o-keyframes fadeInDownBig { - 0% { - opacity: 0; - -o-transform: translateY(-2000px); - } - - 100% { - opacity: 1; - -o-transform: translateY(0); - } -} - -@keyframes fadeInDownBig { - 0% { - opacity: 0; - transform: translateY(-2000px); - } - - 100% { - opacity: 1; - transform: translateY(0); - } -} - -.fadeInDownBig { - -webkit-animation-name: fadeInDownBig; - -moz-animation-name: fadeInDownBig; - -o-animation-name: fadeInDownBig; - animation-name: fadeInDownBig; -} -@-webkit-keyframes fadeInLeftBig { - 0% { - opacity: 0; - -webkit-transform: translateX(-2000px); - } - - 100% { - opacity: 1; - -webkit-transform: translateX(0); - } -} -@-moz-keyframes fadeInLeftBig { - 0% { - opacity: 0; - -moz-transform: translateX(-2000px); - } - - 100% { - opacity: 1; - -moz-transform: translateX(0); - } -} -@-o-keyframes fadeInLeftBig { - 0% { - opacity: 0; - -o-transform: translateX(-2000px); - } - - 100% { - opacity: 1; - -o-transform: translateX(0); - } -} -@keyframes fadeInLeftBig { - 0% { - opacity: 0; - transform: translateX(-2000px); - } - - 100% { - opacity: 1; - transform: translateX(0); - } -} - -.fadeInLeftBig { - -webkit-animation-name: fadeInLeftBig; - -moz-animation-name: fadeInLeftBig; - -o-animation-name: fadeInLeftBig; - animation-name: fadeInLeftBig; -} -@-webkit-keyframes fadeInRightBig { - 0% { - opacity: 0; - -webkit-transform: translateX(2000px); - } - - 100% { - opacity: 1; - -webkit-transform: translateX(0); - } -} - -@-moz-keyframes fadeInRightBig { - 0% { - opacity: 0; - -moz-transform: translateX(2000px); - } - - 100% { - opacity: 1; - -moz-transform: translateX(0); - } -} - -@-o-keyframes fadeInRightBig { - 0% { - opacity: 0; - -o-transform: translateX(2000px); - } - - 100% { - opacity: 1; - -o-transform: translateX(0); - } -} - -@keyframes fadeInRightBig { - 0% { - opacity: 0; - transform: translateX(2000px); - } - - 100% { - opacity: 1; - transform: translateX(0); - } -} - -.fadeInRightBig { - -webkit-animation-name: fadeInRightBig; - -moz-animation-name: fadeInRightBig; - -o-animation-name: fadeInRightBig; - animation-name: fadeInRightBig; -} -@-webkit-keyframes fadeOut { - 0% {opacity: 1;} - 100% {opacity: 0;} -} - -@-moz-keyframes fadeOut { - 0% {opacity: 1;} - 100% {opacity: 0;} -} - -@-o-keyframes fadeOut { - 0% {opacity: 1;} - 100% {opacity: 0;} -} - -@keyframes fadeOut { - 0% {opacity: 1;} - 100% {opacity: 0;} -} - -.fadeOut { - -webkit-animation-name: fadeOut; - -moz-animation-name: fadeOut; - -o-animation-name: fadeOut; - animation-name: fadeOut; -} -@-webkit-keyframes fadeOutUp { - 0% { - opacity: 1; - -webkit-transform: translateY(0); - } - - 100% { - opacity: 0; - -webkit-transform: translateY(-20px); - } -} -@-moz-keyframes fadeOutUp { - 0% { - opacity: 1; - -moz-transform: translateY(0); - } - - 100% { - opacity: 0; - -moz-transform: translateY(-20px); - } -} -@-o-keyframes fadeOutUp { - 0% { - opacity: 1; - -o-transform: translateY(0); - } - - 100% { - opacity: 0; - -o-transform: translateY(-20px); - } -} -@keyframes fadeOutUp { - 0% { - opacity: 1; - transform: translateY(0); - } - - 100% { - opacity: 0; - transform: translateY(-20px); - } -} - -.fadeOutUp { - -webkit-animation-name: fadeOutUp; - -moz-animation-name: fadeOutUp; - -o-animation-name: fadeOutUp; - animation-name: fadeOutUp; -} -@-webkit-keyframes fadeOutDown { - 0% { - opacity: 1; - -webkit-transform: translateY(0); - } - - 100% { - opacity: 0; - -webkit-transform: translateY(20px); - } -} - -@-moz-keyframes fadeOutDown { - 0% { - opacity: 1; - -moz-transform: translateY(0); - } - - 100% { - opacity: 0; - -moz-transform: translateY(20px); - } -} - -@-o-keyframes fadeOutDown { - 0% { - opacity: 1; - -o-transform: translateY(0); - } - - 100% { - opacity: 0; - -o-transform: translateY(20px); - } -} - -@keyframes fadeOutDown { - 0% { - opacity: 1; - transform: translateY(0); - } - - 100% { - opacity: 0; - transform: translateY(20px); - } -} - -.fadeOutDown { - -webkit-animation-name: fadeOutDown; - -moz-animation-name: fadeOutDown; - -o-animation-name: fadeOutDown; - animation-name: fadeOutDown; -} -@-webkit-keyframes fadeOutLeft { - 0% { - opacity: 1; - -webkit-transform: translateX(0); - } - - 100% { - opacity: 0; - -webkit-transform: translateX(-20px); - } -} - -@-moz-keyframes fadeOutLeft { - 0% { - opacity: 1; - -moz-transform: translateX(0); - } - - 100% { - opacity: 0; - -moz-transform: translateX(-20px); - } -} - -@-o-keyframes fadeOutLeft { - 0% { - opacity: 1; - -o-transform: translateX(0); - } - - 100% { - opacity: 0; - -o-transform: translateX(-20px); - } -} - -@keyframes fadeOutLeft { - 0% { - opacity: 1; - transform: translateX(0); - } - - 100% { - opacity: 0; - transform: translateX(-20px); - } -} - -.fadeOutLeft { - -webkit-animation-name: fadeOutLeft; - -moz-animation-name: fadeOutLeft; - -o-animation-name: fadeOutLeft; - animation-name: fadeOutLeft; -} -@-webkit-keyframes fadeOutRight { - 0% { - opacity: 1; - -webkit-transform: translateX(0); - } - - 100% { - opacity: 0; - -webkit-transform: translateX(20px); - } -} - -@-moz-keyframes fadeOutRight { - 0% { - opacity: 1; - -moz-transform: translateX(0); - } - - 100% { - opacity: 0; - -moz-transform: translateX(20px); - } -} - -@-o-keyframes fadeOutRight { - 0% { - opacity: 1; - -o-transform: translateX(0); - } - - 100% { - opacity: 0; - -o-transform: translateX(20px); - } -} - -@keyframes fadeOutRight { - 0% { - opacity: 1; - transform: translateX(0); - } - - 100% { - opacity: 0; - transform: translateX(20px); - } -} - -.fadeOutRight { - -webkit-animation-name: fadeOutRight; - -moz-animation-name: fadeOutRight; - -o-animation-name: fadeOutRight; - animation-name: fadeOutRight; -} -@-webkit-keyframes fadeOutUpBig { - 0% { - opacity: 1; - -webkit-transform: translateY(0); - } - - 100% { - opacity: 0; - -webkit-transform: translateY(-2000px); - } -} - -@-moz-keyframes fadeOutUpBig { - 0% { - opacity: 1; - -moz-transform: translateY(0); - } - - 100% { - opacity: 0; - -moz-transform: translateY(-2000px); - } -} - -@-o-keyframes fadeOutUpBig { - 0% { - opacity: 1; - -o-transform: translateY(0); - } - - 100% { - opacity: 0; - -o-transform: translateY(-2000px); - } -} - -@keyframes fadeOutUpBig { - 0% { - opacity: 1; - transform: translateY(0); - } - - 100% { - opacity: 0; - transform: translateY(-2000px); - } -} - -.fadeOutUpBig { - -webkit-animation-name: fadeOutUpBig; - -moz-animation-name: fadeOutUpBig; - -o-animation-name: fadeOutUpBig; - animation-name: fadeOutUpBig; -} -@-webkit-keyframes fadeOutDownBig { - 0% { - opacity: 1; - -webkit-transform: translateY(0); - } - - 100% { - opacity: 0; - -webkit-transform: translateY(2000px); - } -} - -@-moz-keyframes fadeOutDownBig { - 0% { - opacity: 1; - -moz-transform: translateY(0); - } - - 100% { - opacity: 0; - -moz-transform: translateY(2000px); - } -} - -@-o-keyframes fadeOutDownBig { - 0% { - opacity: 1; - -o-transform: translateY(0); - } - - 100% { - opacity: 0; - -o-transform: translateY(2000px); - } -} - -@keyframes fadeOutDownBig { - 0% { - opacity: 1; - transform: translateY(0); - } - - 100% { - opacity: 0; - transform: translateY(2000px); - } -} - -.fadeOutDownBig { - -webkit-animation-name: fadeOutDownBig; - -moz-animation-name: fadeOutDownBig; - -o-animation-name: fadeOutDownBig; - animation-name: fadeOutDownBig; -} -@-webkit-keyframes fadeOutLeftBig { - 0% { - opacity: 1; - -webkit-transform: translateX(0); - } - - 100% { - opacity: 0; - -webkit-transform: translateX(-2000px); - } -} - -@-moz-keyframes fadeOutLeftBig { - 0% { - opacity: 1; - -moz-transform: translateX(0); - } - - 100% { - opacity: 0; - -moz-transform: translateX(-2000px); - } -} - -@-o-keyframes fadeOutLeftBig { - 0% { - opacity: 1; - -o-transform: translateX(0); - } - - 100% { - opacity: 0; - -o-transform: translateX(-2000px); - } -} - -@keyframes fadeOutLeftBig { - 0% { - opacity: 1; - transform: translateX(0); - } - - 100% { - opacity: 0; - transform: translateX(-2000px); - } -} - -.fadeOutLeftBig { - -webkit-animation-name: fadeOutLeftBig; - -moz-animation-name: fadeOutLeftBig; - -o-animation-name: fadeOutLeftBig; - animation-name: fadeOutLeftBig; -} -@-webkit-keyframes fadeOutRightBig { - 0% { - opacity: 1; - -webkit-transform: translateX(0); - } - - 100% { - opacity: 0; - -webkit-transform: translateX(2000px); - } -} -@-moz-keyframes fadeOutRightBig { - 0% { - opacity: 1; - -moz-transform: translateX(0); - } - - 100% { - opacity: 0; - -moz-transform: translateX(2000px); - } -} -@-o-keyframes fadeOutRightBig { - 0% { - opacity: 1; - -o-transform: translateX(0); - } - - 100% { - opacity: 0; - -o-transform: translateX(2000px); - } -} -@keyframes fadeOutRightBig { - 0% { - opacity: 1; - transform: translateX(0); - } - - 100% { - opacity: 0; - transform: translateX(2000px); - } -} - -.fadeOutRightBig { - -webkit-animation-name: fadeOutRightBig; - -moz-animation-name: fadeOutRightBig; - -o-animation-name: fadeOutRightBig; - animation-name: fadeOutRightBig; -} -@-webkit-keyframes bounceIn { - 0% { - opacity: 0; - -webkit-transform: scale(.3); - } - - 50% { - opacity: 1; - -webkit-transform: scale(1.05); - } - - 70% { - -webkit-transform: scale(.9); - } - - 100% { - -webkit-transform: scale(1); - } -} - -@-moz-keyframes bounceIn { - 0% { - opacity: 0; - -moz-transform: scale(.3); - } - - 50% { - opacity: 1; - -moz-transform: scale(1.05); - } - - 70% { - -moz-transform: scale(.9); - } - - 100% { - -moz-transform: scale(1); - } -} - -@-o-keyframes bounceIn { - 0% { - opacity: 0; - -o-transform: scale(.3); - } - - 50% { - opacity: 1; - -o-transform: scale(1.05); - } - - 70% { - -o-transform: scale(.9); - } - - 100% { - -o-transform: scale(1); - } -} - -@keyframes bounceIn { - 0% { - opacity: 0; - transform: scale(.3); - } - - 50% { - opacity: 1; - transform: scale(1.05); - } - - 70% { - transform: scale(.9); - } - - 100% { - transform: scale(1); - } -} - -.bounceIn { - -webkit-animation-name: bounceIn; - -moz-animation-name: bounceIn; - -o-animation-name: bounceIn; - animation-name: bounceIn; -} -@-webkit-keyframes bounceInUp { - 0% { - opacity: 0; - -webkit-transform: translateY(2000px); - } - - 60% { - opacity: 1; - -webkit-transform: translateY(-30px); - } - - 80% { - -webkit-transform: translateY(10px); - } - - 100% { - -webkit-transform: translateY(0); - } -} -@-moz-keyframes bounceInUp { - 0% { - opacity: 0; - -moz-transform: translateY(2000px); - } - - 60% { - opacity: 1; - -moz-transform: translateY(-30px); - } - - 80% { - -moz-transform: translateY(10px); - } - - 100% { - -moz-transform: translateY(0); - } -} - -@-o-keyframes bounceInUp { - 0% { - opacity: 0; - -o-transform: translateY(2000px); - } - - 60% { - opacity: 1; - -o-transform: translateY(-30px); - } - - 80% { - -o-transform: translateY(10px); - } - - 100% { - -o-transform: translateY(0); - } -} - -@keyframes bounceInUp { - 0% { - opacity: 0; - transform: translateY(2000px); - } - - 60% { - opacity: 1; - transform: translateY(-30px); - } - - 80% { - transform: translateY(10px); - } - - 100% { - transform: translateY(0); - } -} - -.bounceInUp { - -webkit-animation-name: bounceInUp; - -moz-animation-name: bounceInUp; - -o-animation-name: bounceInUp; - animation-name: bounceInUp; -} -@-webkit-keyframes bounceInDown { - 0% { - opacity: 0; - -webkit-transform: translateY(-2000px); - } - - 60% { - opacity: 1; - -webkit-transform: translateY(30px); - } - - 80% { - -webkit-transform: translateY(-10px); - } - - 100% { - -webkit-transform: translateY(0); - } -} - -@-moz-keyframes bounceInDown { - 0% { - opacity: 0; - -moz-transform: translateY(-2000px); - } - - 60% { - opacity: 1; - -moz-transform: translateY(30px); - } - - 80% { - -moz-transform: translateY(-10px); - } - - 100% { - -moz-transform: translateY(0); - } -} - -@-o-keyframes bounceInDown { - 0% { - opacity: 0; - -o-transform: translateY(-2000px); - } - - 60% { - opacity: 1; - -o-transform: translateY(30px); - } - - 80% { - -o-transform: translateY(-10px); - } - - 100% { - -o-transform: translateY(0); - } -} - -@keyframes bounceInDown { - 0% { - opacity: 0; - transform: translateY(-2000px); - } - - 60% { - opacity: 1; - transform: translateY(30px); - } - - 80% { - transform: translateY(-10px); - } - - 100% { - transform: translateY(0); - } -} - -.bounceInDown { - -webkit-animation-name: bounceInDown; - -moz-animation-name: bounceInDown; - -o-animation-name: bounceInDown; - animation-name: bounceInDown; -} -@-webkit-keyframes bounceInLeft { - 0% { - opacity: 0; - -webkit-transform: translateX(-2000px); - } - - 60% { - opacity: 1; - -webkit-transform: translateX(30px); - } - - 80% { - -webkit-transform: translateX(-10px); - } - - 100% { - -webkit-transform: translateX(0); - } -} - -@-moz-keyframes bounceInLeft { - 0% { - opacity: 0; - -moz-transform: translateX(-2000px); - } - - 60% { - opacity: 1; - -moz-transform: translateX(30px); - } - - 80% { - -moz-transform: translateX(-10px); - } - - 100% { - -moz-transform: translateX(0); - } -} - -@-o-keyframes bounceInLeft { - 0% { - opacity: 0; - -o-transform: translateX(-2000px); - } - - 60% { - opacity: 1; - -o-transform: translateX(30px); - } - - 80% { - -o-transform: translateX(-10px); - } - - 100% { - -o-transform: translateX(0); - } -} - -@keyframes bounceInLeft { - 0% { - opacity: 0; - transform: translateX(-2000px); - } - - 60% { - opacity: 1; - transform: translateX(30px); - } - - 80% { - transform: translateX(-10px); - } - - 100% { - transform: translateX(0); - } -} - -.bounceInLeft { - -webkit-animation-name: bounceInLeft; - -moz-animation-name: bounceInLeft; - -o-animation-name: bounceInLeft; - animation-name: bounceInLeft; -} -@-webkit-keyframes bounceInRight { - 0% { - opacity: 0; - -webkit-transform: translateX(2000px); - } - - 60% { - opacity: 1; - -webkit-transform: translateX(-30px); - } - - 80% { - -webkit-transform: translateX(10px); - } - - 100% { - -webkit-transform: translateX(0); - } -} - -@-moz-keyframes bounceInRight { - 0% { - opacity: 0; - -moz-transform: translateX(2000px); - } - - 60% { - opacity: 1; - -moz-transform: translateX(-30px); - } - - 80% { - -moz-transform: translateX(10px); - } - - 100% { - -moz-transform: translateX(0); - } -} - -@-o-keyframes bounceInRight { - 0% { - opacity: 0; - -o-transform: translateX(2000px); - } - - 60% { - opacity: 1; - -o-transform: translateX(-30px); - } - - 80% { - -o-transform: translateX(10px); - } - - 100% { - -o-transform: translateX(0); - } -} - -@keyframes bounceInRight { - 0% { - opacity: 0; - transform: translateX(2000px); - } - - 60% { - opacity: 1; - transform: translateX(-30px); - } - - 80% { - transform: translateX(10px); - } - - 100% { - transform: translateX(0); - } -} - -.bounceInRight { - -webkit-animation-name: bounceInRight; - -moz-animation-name: bounceInRight; - -o-animation-name: bounceInRight; - animation-name: bounceInRight; -} -@-webkit-keyframes bounceOut { - 0% { - -webkit-transform: scale(1); - } - - 25% { - -webkit-transform: scale(.95); - } - - 50% { - opacity: 1; - -webkit-transform: scale(1.1); - } - - 100% { - opacity: 0; - -webkit-transform: scale(.3); - } -} - -@-moz-keyframes bounceOut { - 0% { - -moz-transform: scale(1); - } - - 25% { - -moz-transform: scale(.95); - } - - 50% { - opacity: 1; - -moz-transform: scale(1.1); - } - - 100% { - opacity: 0; - -moz-transform: scale(.3); - } -} - -@-o-keyframes bounceOut { - 0% { - -o-transform: scale(1); - } - - 25% { - -o-transform: scale(.95); - } - - 50% { - opacity: 1; - -o-transform: scale(1.1); - } - - 100% { - opacity: 0; - -o-transform: scale(.3); - } -} - -@keyframes bounceOut { - 0% { - transform: scale(1); - } - - 25% { - transform: scale(.95); - } - - 50% { - opacity: 1; - transform: scale(1.1); - } - - 100% { - opacity: 0; - transform: scale(.3); - } -} - -.bounceOut { - -webkit-animation-name: bounceOut; - -moz-animation-name: bounceOut; - -o-animation-name: bounceOut; - animation-name: bounceOut; -} -@-webkit-keyframes bounceOutUp { - 0% { - -webkit-transform: translateY(0); - } - - 20% { - opacity: 1; - -webkit-transform: translateY(20px); - } - - 100% { - opacity: 0; - -webkit-transform: translateY(-2000px); - } -} - -@-moz-keyframes bounceOutUp { - 0% { - -moz-transform: translateY(0); - } - - 20% { - opacity: 1; - -moz-transform: translateY(20px); - } - - 100% { - opacity: 0; - -moz-transform: translateY(-2000px); - } -} - -@-o-keyframes bounceOutUp { - 0% { - -o-transform: translateY(0); - } - - 20% { - opacity: 1; - -o-transform: translateY(20px); - } - - 100% { - opacity: 0; - -o-transform: translateY(-2000px); - } -} - -@keyframes bounceOutUp { - 0% { - transform: translateY(0); - } - - 20% { - opacity: 1; - transform: translateY(20px); - } - - 100% { - opacity: 0; - transform: translateY(-2000px); - } -} - -.bounceOutUp { - -webkit-animation-name: bounceOutUp; - -moz-animation-name: bounceOutUp; - -o-animation-name: bounceOutUp; - animation-name: bounceOutUp; -} -@-webkit-keyframes bounceOutDown { - 0% { - -webkit-transform: translateY(0); - } - - 20% { - opacity: 1; - -webkit-transform: translateY(-20px); - } - - 100% { - opacity: 0; - -webkit-transform: translateY(2000px); - } -} - -@-moz-keyframes bounceOutDown { - 0% { - -moz-transform: translateY(0); - } - - 20% { - opacity: 1; - -moz-transform: translateY(-20px); - } - - 100% { - opacity: 0; - -moz-transform: translateY(2000px); - } -} - -@-o-keyframes bounceOutDown { - 0% { - -o-transform: translateY(0); - } - - 20% { - opacity: 1; - -o-transform: translateY(-20px); - } - - 100% { - opacity: 0; - -o-transform: translateY(2000px); - } -} - -@keyframes bounceOutDown { - 0% { - transform: translateY(0); - } - - 20% { - opacity: 1; - transform: translateY(-20px); - } - - 100% { - opacity: 0; - transform: translateY(2000px); - } -} - -.bounceOutDown { - -webkit-animation-name: bounceOutDown; - -moz-animation-name: bounceOutDown; - -o-animation-name: bounceOutDown; - animation-name: bounceOutDown; -} -@-webkit-keyframes bounceOutLeft { - 0% { - -webkit-transform: translateX(0); - } - - 20% { - opacity: 1; - -webkit-transform: translateX(20px); - } - - 100% { - opacity: 0; - -webkit-transform: translateX(-2000px); - } -} - -@-moz-keyframes bounceOutLeft { - 0% { - -moz-transform: translateX(0); - } - - 20% { - opacity: 1; - -moz-transform: translateX(20px); - } - - 100% { - opacity: 0; - -moz-transform: translateX(-2000px); - } -} - -@-o-keyframes bounceOutLeft { - 0% { - -o-transform: translateX(0); - } - - 20% { - opacity: 1; - -o-transform: translateX(20px); - } - - 100% { - opacity: 0; - -o-transform: translateX(-2000px); - } -} - -@keyframes bounceOutLeft { - 0% { - transform: translateX(0); - } - - 20% { - opacity: 1; - transform: translateX(20px); - } - - 100% { - opacity: 0; - transform: translateX(-2000px); - } -} - -.bounceOutLeft { - -webkit-animation-name: bounceOutLeft; - -moz-animation-name: bounceOutLeft; - -o-animation-name: bounceOutLeft; - animation-name: bounceOutLeft; -} -@-webkit-keyframes bounceOutRight { - 0% { - -webkit-transform: translateX(0); - } - - 20% { - opacity: 1; - -webkit-transform: translateX(-20px); - } - - 100% { - opacity: 0; - -webkit-transform: translateX(2000px); - } -} - -@-moz-keyframes bounceOutRight { - 0% { - -moz-transform: translateX(0); - } - - 20% { - opacity: 1; - -moz-transform: translateX(-20px); - } - - 100% { - opacity: 0; - -moz-transform: translateX(2000px); - } -} - -@-o-keyframes bounceOutRight { - 0% { - -o-transform: translateX(0); - } - - 20% { - opacity: 1; - -o-transform: translateX(-20px); - } - - 100% { - opacity: 0; - -o-transform: translateX(2000px); - } -} - -@keyframes bounceOutRight { - 0% { - transform: translateX(0); - } - - 20% { - opacity: 1; - transform: translateX(-20px); - } - - 100% { - opacity: 0; - transform: translateX(2000px); - } -} - -.bounceOutRight { - -webkit-animation-name: bounceOutRight; - -moz-animation-name: bounceOutRight; - -o-animation-name: bounceOutRight; - animation-name: bounceOutRight; -} -@-webkit-keyframes rotateIn { - 0% { - -webkit-transform-origin: center center; - -webkit-transform: rotate(-200deg); - opacity: 0; - } - - 100% { - -webkit-transform-origin: center center; - -webkit-transform: rotate(0); - opacity: 1; - } -} -@-moz-keyframes rotateIn { - 0% { - -moz-transform-origin: center center; - -moz-transform: rotate(-200deg); - opacity: 0; - } - - 100% { - -moz-transform-origin: center center; - -moz-transform: rotate(0); - opacity: 1; - } -} -@-o-keyframes rotateIn { - 0% { - -o-transform-origin: center center; - -o-transform: rotate(-200deg); - opacity: 0; - } - - 100% { - -o-transform-origin: center center; - -o-transform: rotate(0); - opacity: 1; - } -} -@keyframes rotateIn { - 0% { - transform-origin: center center; - transform: rotate(-200deg); - opacity: 0; - } - - 100% { - transform-origin: center center; - transform: rotate(0); - opacity: 1; - } -} - -.rotateIn { - -webkit-animation-name: rotateIn; - -moz-animation-name: rotateIn; - -o-animation-name: rotateIn; - animation-name: rotateIn; -} -@-webkit-keyframes rotateInUpLeft { - 0% { - -webkit-transform-origin: left bottom; - -webkit-transform: rotate(90deg); - opacity: 0; - } - - 100% { - -webkit-transform-origin: left bottom; - -webkit-transform: rotate(0); - opacity: 1; - } -} - -@-moz-keyframes rotateInUpLeft { - 0% { - -moz-transform-origin: left bottom; - -moz-transform: rotate(90deg); - opacity: 0; - } - - 100% { - -moz-transform-origin: left bottom; - -moz-transform: rotate(0); - opacity: 1; - } -} - -@-o-keyframes rotateInUpLeft { - 0% { - -o-transform-origin: left bottom; - -o-transform: rotate(90deg); - opacity: 0; - } - - 100% { - -o-transform-origin: left bottom; - -o-transform: rotate(0); - opacity: 1; - } -} - -@keyframes rotateInUpLeft { - 0% { - transform-origin: left bottom; - transform: rotate(90deg); - opacity: 0; - } - - 100% { - transform-origin: left bottom; - transform: rotate(0); - opacity: 1; - } -} - -.rotateInUpLeft { - -webkit-animation-name: rotateInUpLeft; - -moz-animation-name: rotateInUpLeft; - -o-animation-name: rotateInUpLeft; - animation-name: rotateInUpLeft; -} -@-webkit-keyframes rotateInDownLeft { - 0% { - -webkit-transform-origin: left bottom; - -webkit-transform: rotate(-90deg); - opacity: 0; - } - - 100% { - -webkit-transform-origin: left bottom; - -webkit-transform: rotate(0); - opacity: 1; - } -} - -@-moz-keyframes rotateInDownLeft { - 0% { - -moz-transform-origin: left bottom; - -moz-transform: rotate(-90deg); - opacity: 0; - } - - 100% { - -moz-transform-origin: left bottom; - -moz-transform: rotate(0); - opacity: 1; - } -} - -@-o-keyframes rotateInDownLeft { - 0% { - -o-transform-origin: left bottom; - -o-transform: rotate(-90deg); - opacity: 0; - } - - 100% { - -o-transform-origin: left bottom; - -o-transform: rotate(0); - opacity: 1; - } -} - -@keyframes rotateInDownLeft { - 0% { - transform-origin: left bottom; - transform: rotate(-90deg); - opacity: 0; - } - - 100% { - transform-origin: left bottom; - transform: rotate(0); - opacity: 1; - } -} - -.rotateInDownLeft { - -webkit-animation-name: rotateInDownLeft; - -moz-animation-name: rotateInDownLeft; - -o-animation-name: rotateInDownLeft; - animation-name: rotateInDownLeft; -} -@-webkit-keyframes rotateInUpRight { - 0% { - -webkit-transform-origin: right bottom; - -webkit-transform: rotate(-90deg); - opacity: 0; - } - - 100% { - -webkit-transform-origin: right bottom; - -webkit-transform: rotate(0); - opacity: 1; - } -} - -@-moz-keyframes rotateInUpRight { - 0% { - -moz-transform-origin: right bottom; - -moz-transform: rotate(-90deg); - opacity: 0; - } - - 100% { - -moz-transform-origin: right bottom; - -moz-transform: rotate(0); - opacity: 1; - } -} - -@-o-keyframes rotateInUpRight { - 0% { - -o-transform-origin: right bottom; - -o-transform: rotate(-90deg); - opacity: 0; - } - - 100% { - -o-transform-origin: right bottom; - -o-transform: rotate(0); - opacity: 1; - } -} - -@keyframes rotateInUpRight { - 0% { - transform-origin: right bottom; - transform: rotate(-90deg); - opacity: 0; - } - - 100% { - transform-origin: right bottom; - transform: rotate(0); - opacity: 1; - } -} - -.rotateInUpRight { - -webkit-animation-name: rotateInUpRight; - -moz-animation-name: rotateInUpRight; - -o-animation-name: rotateInUpRight; - animation-name: rotateInUpRight; -} -@-webkit-keyframes rotateInDownRight { - 0% { - -webkit-transform-origin: right bottom; - -webkit-transform: rotate(90deg); - opacity: 0; - } - - 100% { - -webkit-transform-origin: right bottom; - -webkit-transform: rotate(0); - opacity: 1; - } -} - -@-moz-keyframes rotateInDownRight { - 0% { - -moz-transform-origin: right bottom; - -moz-transform: rotate(90deg); - opacity: 0; - } - - 100% { - -moz-transform-origin: right bottom; - -moz-transform: rotate(0); - opacity: 1; - } -} - -@-o-keyframes rotateInDownRight { - 0% { - -o-transform-origin: right bottom; - -o-transform: rotate(90deg); - opacity: 0; - } - - 100% { - -o-transform-origin: right bottom; - -o-transform: rotate(0); - opacity: 1; - } -} - -@keyframes rotateInDownRight { - 0% { - transform-origin: right bottom; - transform: rotate(90deg); - opacity: 0; - } - - 100% { - transform-origin: right bottom; - transform: rotate(0); - opacity: 1; - } -} - -.rotateInDownRight { - -webkit-animation-name: rotateInDownRight; - -moz-animation-name: rotateInDownRight; - -o-animation-name: rotateInDownRight; - animation-name: rotateInDownRight; -} -@-webkit-keyframes rotateOut { - 0% { - -webkit-transform-origin: center center; - -webkit-transform: rotate(0); - opacity: 1; - } - - 100% { - -webkit-transform-origin: center center; - -webkit-transform: rotate(200deg); - opacity: 0; - } -} - -@-moz-keyframes rotateOut { - 0% { - -moz-transform-origin: center center; - -moz-transform: rotate(0); - opacity: 1; - } - - 100% { - -moz-transform-origin: center center; - -moz-transform: rotate(200deg); - opacity: 0; - } -} - -@-o-keyframes rotateOut { - 0% { - -o-transform-origin: center center; - -o-transform: rotate(0); - opacity: 1; - } - - 100% { - -o-transform-origin: center center; - -o-transform: rotate(200deg); - opacity: 0; - } -} - -@keyframes rotateOut { - 0% { - transform-origin: center center; - transform: rotate(0); - opacity: 1; - } - - 100% { - transform-origin: center center; - transform: rotate(200deg); - opacity: 0; - } -} - -.rotateOut { - -webkit-animation-name: rotateOut; - -moz-animation-name: rotateOut; - -o-animation-name: rotateOut; - animation-name: rotateOut; -} -@-webkit-keyframes rotateOutUpLeft { - 0% { - -webkit-transform-origin: left bottom; - -webkit-transform: rotate(0); - opacity: 1; - } - - 100% { - -webkit-transform-origin: left bottom; - -webkit-transform: rotate(-90deg); - opacity: 0; - } -} - -@-moz-keyframes rotateOutUpLeft { - 0% { - -moz-transform-origin: left bottom; - -moz-transform: rotate(0); - opacity: 1; - } - - 100% { - -moz-transform-origin: left bottom; - -moz-transform: rotate(-90deg); - opacity: 0; - } -} - -@-o-keyframes rotateOutUpLeft { - 0% { - -o-transform-origin: left bottom; - -o-transform: rotate(0); - opacity: 1; - } - - 100% { - -o-transform-origin: left bottom; - -o-transform: rotate(-90deg); - opacity: 0; - } -} - -@keyframes rotateOutUpLeft { - 0% { - transform-origin: left bottom; - transform: rotate(0); - opacity: 1; - } - - 100% { - transform-origin: left bottom; - transform: rotate(-90deg); - opacity: 0; - } -} - -.rotateOutUpLeft { - -webkit-animation-name: rotateOutUpLeft; - -moz-animation-name: rotateOutUpLeft; - -o-animation-name: rotateOutUpLeft; - animation-name: rotateOutUpLeft; -} -@-webkit-keyframes rotateOutDownLeft { - 0% { - -webkit-transform-origin: left bottom; - -webkit-transform: rotate(0); - opacity: 1; - } - - 100% { - -webkit-transform-origin: left bottom; - -webkit-transform: rotate(90deg); - opacity: 0; - } -} - -@-moz-keyframes rotateOutDownLeft { - 0% { - -moz-transform-origin: left bottom; - -moz-transform: rotate(0); - opacity: 1; - } - - 100% { - -moz-transform-origin: left bottom; - -moz-transform: rotate(90deg); - opacity: 0; - } -} - -@-o-keyframes rotateOutDownLeft { - 0% { - -o-transform-origin: left bottom; - -o-transform: rotate(0); - opacity: 1; - } - - 100% { - -o-transform-origin: left bottom; - -o-transform: rotate(90deg); - opacity: 0; - } -} - -@keyframes rotateOutDownLeft { - 0% { - transform-origin: left bottom; - transform: rotate(0); - opacity: 1; - } - - 100% { - transform-origin: left bottom; - transform: rotate(90deg); - opacity: 0; - } -} - -.rotateOutDownLeft { - -webkit-animation-name: rotateOutDownLeft; - -moz-animation-name: rotateOutDownLeft; - -o-animation-name: rotateOutDownLeft; - animation-name: rotateOutDownLeft; -} -@-webkit-keyframes rotateOutUpRight { - 0% { - -webkit-transform-origin: right bottom; - -webkit-transform: rotate(0); - opacity: 1; - } - - 100% { - -webkit-transform-origin: right bottom; - -webkit-transform: rotate(90deg); - opacity: 0; - } -} - -@-moz-keyframes rotateOutUpRight { - 0% { - -moz-transform-origin: right bottom; - -moz-transform: rotate(0); - opacity: 1; - } - - 100% { - -moz-transform-origin: right bottom; - -moz-transform: rotate(90deg); - opacity: 0; - } -} - -@-o-keyframes rotateOutUpRight { - 0% { - -o-transform-origin: right bottom; - -o-transform: rotate(0); - opacity: 1; - } - - 100% { - -o-transform-origin: right bottom; - -o-transform: rotate(90deg); - opacity: 0; - } -} - -@keyframes rotateOutUpRight { - 0% { - transform-origin: right bottom; - transform: rotate(0); - opacity: 1; - } - - 100% { - transform-origin: right bottom; - transform: rotate(90deg); - opacity: 0; - } -} - -.rotateOutUpRight { - -webkit-animation-name: rotateOutUpRight; - -moz-animation-name: rotateOutUpRight; - -o-animation-name: rotateOutUpRight; - animation-name: rotateOutUpRight; -} -@-webkit-keyframes rotateOutDownRight { - 0% { - -webkit-transform-origin: right bottom; - -webkit-transform: rotate(0); - opacity: 1; - } - - 100% { - -webkit-transform-origin: right bottom; - -webkit-transform: rotate(-90deg); - opacity: 0; - } -} - -@-moz-keyframes rotateOutDownRight { - 0% { - -moz-transform-origin: right bottom; - -moz-transform: rotate(0); - opacity: 1; - } - - 100% { - -moz-transform-origin: right bottom; - -moz-transform: rotate(-90deg); - opacity: 0; - } -} - -@-o-keyframes rotateOutDownRight { - 0% { - -o-transform-origin: right bottom; - -o-transform: rotate(0); - opacity: 1; - } - - 100% { - -o-transform-origin: right bottom; - -o-transform: rotate(-90deg); - opacity: 0; - } -} - -@keyframes rotateOutDownRight { - 0% { - transform-origin: right bottom; - transform: rotate(0); - opacity: 1; - } - - 100% { - transform-origin: right bottom; - transform: rotate(-90deg); - opacity: 0; - } -} - -.rotateOutDownRight { - -webkit-animation-name: rotateOutDownRight; - -moz-animation-name: rotateOutDownRight; - -o-animation-name: rotateOutDownRight; - animation-name: rotateOutDownRight; -} -@-webkit-keyframes hinge { - 0% { -webkit-transform: rotate(0); -webkit-transform-origin: top left; -webkit-animation-timing-function: ease-in-out; } - 20%, 60% { -webkit-transform: rotate(80deg); -webkit-transform-origin: top left; -webkit-animation-timing-function: ease-in-out; } - 40% { -webkit-transform: rotate(60deg); -webkit-transform-origin: top left; -webkit-animation-timing-function: ease-in-out; } - 80% { -webkit-transform: rotate(60deg) translateY(0); opacity: 1; -webkit-transform-origin: top left; -webkit-animation-timing-function: ease-in-out; } - 100% { -webkit-transform: translateY(700px); opacity: 0; } -} - -@-moz-keyframes hinge { - 0% { -moz-transform: rotate(0); -moz-transform-origin: top left; -moz-animation-timing-function: ease-in-out; } - 20%, 60% { -moz-transform: rotate(80deg); -moz-transform-origin: top left; -moz-animation-timing-function: ease-in-out; } - 40% { -moz-transform: rotate(60deg); -moz-transform-origin: top left; -moz-animation-timing-function: ease-in-out; } - 80% { -moz-transform: rotate(60deg) translateY(0); opacity: 1; -moz-transform-origin: top left; -moz-animation-timing-function: ease-in-out; } - 100% { -moz-transform: translateY(700px); opacity: 0; } -} - -@-o-keyframes hinge { - 0% { -o-transform: rotate(0); -o-transform-origin: top left; -o-animation-timing-function: ease-in-out; } - 20%, 60% { -o-transform: rotate(80deg); -o-transform-origin: top left; -o-animation-timing-function: ease-in-out; } - 40% { -o-transform: rotate(60deg); -o-transform-origin: top left; -o-animation-timing-function: ease-in-out; } - 80% { -o-transform: rotate(60deg) translateY(0); opacity: 1; -o-transform-origin: top left; -o-animation-timing-function: ease-in-out; } - 100% { -o-transform: translateY(700px); opacity: 0; } -} - -@keyframes hinge { - 0% { transform: rotate(0); transform-origin: top left; animation-timing-function: ease-in-out; } - 20%, 60% { transform: rotate(80deg); transform-origin: top left; animation-timing-function: ease-in-out; } - 40% { transform: rotate(60deg); transform-origin: top left; animation-timing-function: ease-in-out; } - 80% { transform: rotate(60deg) translateY(0); opacity: 1; transform-origin: top left; animation-timing-function: ease-in-out; } - 100% { transform: translateY(700px); opacity: 0; } -} - -.hinge { - -webkit-animation-name: hinge; - -moz-animation-name: hinge; - -o-animation-name: hinge; - animation-name: hinge; -} -/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ - -@-webkit-keyframes rollIn { - 0% { opacity: 0; -webkit-transform: translateX(-100%) rotate(-120deg); } - 100% { opacity: 1; -webkit-transform: translateX(0px) rotate(0deg); } -} - -@-moz-keyframes rollIn { - 0% { opacity: 0; -moz-transform: translateX(-100%) rotate(-120deg); } - 100% { opacity: 1; -moz-transform: translateX(0px) rotate(0deg); } -} - -@-o-keyframes rollIn { - 0% { opacity: 0; -o-transform: translateX(-100%) rotate(-120deg); } - 100% { opacity: 1; -o-transform: translateX(0px) rotate(0deg); } -} - -@keyframes rollIn { - 0% { opacity: 0; transform: translateX(-100%) rotate(-120deg); } - 100% { opacity: 1; transform: translateX(0px) rotate(0deg); } -} - -.rollIn { - -webkit-animation-name: rollIn; - -moz-animation-name: rollIn; - -o-animation-name: rollIn; - animation-name: rollIn; -} -/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ - -@-webkit-keyframes rollOut { - 0% { - opacity: 1; - -webkit-transform: translateX(0px) rotate(0deg); - } - - 100% { - opacity: 0; - -webkit-transform: translateX(100%) rotate(120deg); - } -} - -@-moz-keyframes rollOut { - 0% { - opacity: 1; - -moz-transform: translateX(0px) rotate(0deg); - } - - 100% { - opacity: 0; - -moz-transform: translateX(100%) rotate(120deg); - } -} - -@-o-keyframes rollOut { - 0% { - opacity: 1; - -o-transform: translateX(0px) rotate(0deg); - } - - 100% { - opacity: 0; - -o-transform: translateX(100%) rotate(120deg); - } -} - -@keyframes rollOut { - 0% { - opacity: 1; - transform: translateX(0px) rotate(0deg); - } - - 100% { - opacity: 0; - transform: translateX(100%) rotate(120deg); - } -} - -.rollOut { - -webkit-animation-name: rollOut; - -moz-animation-name: rollOut; - -o-animation-name: rollOut; - animation-name: rollOut; -} - -/* originally authored by Angelo Rohit - https://github.com/angelorohit */ - -@-webkit-keyframes lightSpeedIn { - 0% { -webkit-transform: translateX(100%) skewX(-30deg); opacity: 0; } - 60% { -webkit-transform: translateX(-20%) skewX(30deg); opacity: 1; } - 80% { -webkit-transform: translateX(0%) skewX(-15deg); opacity: 1; } - 100% { -webkit-transform: translateX(0%) skewX(0deg); opacity: 1; } -} - -@-moz-keyframes lightSpeedIn { - 0% { -moz-transform: translateX(100%) skewX(-30deg); opacity: 0; } - 60% { -moz-transform: translateX(-20%) skewX(30deg); opacity: 1; } - 80% { -moz-transform: translateX(0%) skewX(-15deg); opacity: 1; } - 100% { -moz-transform: translateX(0%) skewX(0deg); opacity: 1; } -} - -@-o-keyframes lightSpeedIn { - 0% { -o-transform: translateX(100%) skewX(-30deg); opacity: 0; } - 60% { -o-transform: translateX(-20%) skewX(30deg); opacity: 1; } - 80% { -o-transform: translateX(0%) skewX(-15deg); opacity: 1; } - 100% { -o-transform: translateX(0%) skewX(0deg); opacity: 1; } -} - -@keyframes lightSpeedIn { - 0% { transform: translateX(100%) skewX(-30deg); opacity: 0; } - 60% { transform: translateX(-20%) skewX(30deg); opacity: 1; } - 80% { transform: translateX(0%) skewX(-15deg); opacity: 1; } - 100% { transform: translateX(0%) skewX(0deg); opacity: 1; } -} - -.lightSpeedIn { - -webkit-animation-name: lightSpeedIn; - -moz-animation-name: lightSpeedIn; - -o-animation-name: lightSpeedIn; - animation-name: lightSpeedIn; - - -webkit-animation-timing-function: ease-out; - -moz-animation-timing-function: ease-out; - -o-animation-timing-function: ease-out; - animation-timing-function: ease-out; -} - -.animated.lightSpeedIn { - -webkit-animation-duration: 0.5s; - -moz-animation-duration: 0.5s; - -o-animation-duration: 0.5s; - animation-duration: 0.5s; -} - -/* originally authored by Angelo Rohit - https://github.com/angelorohit */ - -@-webkit-keyframes lightSpeedOut { - 0% { -webkit-transform: translateX(0%) skewX(0deg); opacity: 1; } - 100% { -webkit-transform: translateX(100%) skewX(-30deg); opacity: 0; } -} - -@-moz-keyframes lightSpeedOut { - 0% { -moz-transform: translateX(0%) skewX(0deg); opacity: 1; } - 100% { -moz-transform: translateX(100%) skewX(-30deg); opacity: 0; } -} - -@-o-keyframes lightSpeedOut { - 0% { -o-transform: translateX(0%) skewX(0deg); opacity: 1; } - 100% { -o-transform: translateX(100%) skewX(-30deg); opacity: 0; } -} - -@keyframes lightSpeedOut { - 0% { transform: translateX(0%) skewX(0deg); opacity: 1; } - 100% { transform: translateX(100%) skewX(-30deg); opacity: 0; } -} - -.lightSpeedOut { - -webkit-animation-name: lightSpeedOut; - -moz-animation-name: lightSpeedOut; - -o-animation-name: lightSpeedOut; - animation-name: lightSpeedOut; - - -webkit-animation-timing-function: ease-in; - -moz-animation-timing-function: ease-in; - -o-animation-timing-function: ease-in; - animation-timing-function: ease-in; -} - -.animated.lightSpeedOut { - -webkit-animation-duration: 0.25s; - -moz-animation-duration: 0.25s; - -o-animation-duration: 0.25s; - animation-duration: 0.25s; -} - -/* originally authored by Angelo Rohit - https://github.com/angelorohit */ - -@-webkit-keyframes wiggle { - 0% { -webkit-transform: skewX(9deg); } - 10% { -webkit-transform: skewX(-8deg); } - 20% { -webkit-transform: skewX(7deg); } - 30% { -webkit-transform: skewX(-6deg); } - 40% { -webkit-transform: skewX(5deg); } - 50% { -webkit-transform: skewX(-4deg); } - 60% { -webkit-transform: skewX(3deg); } - 70% { -webkit-transform: skewX(-2deg); } - 80% { -webkit-transform: skewX(1deg); } - 90% { -webkit-transform: skewX(0deg); } - 100% { -webkit-transform: skewX(0deg); } -} - -@-moz-keyframes wiggle { - 0% { -moz-transform: skewX(9deg); } - 10% { -moz-transform: skewX(-8deg); } - 20% { -moz-transform: skewX(7deg); } - 30% { -moz-transform: skewX(-6deg); } - 40% { -moz-transform: skewX(5deg); } - 50% { -moz-transform: skewX(-4deg); } - 60% { -moz-transform: skewX(3deg); } - 70% { -moz-transform: skewX(-2deg); } - 80% { -moz-transform: skewX(1deg); } - 90% { -moz-transform: skewX(0deg); } - 100% { -moz-transform: skewX(0deg); } -} - -@-o-keyframes wiggle { - 0% { -o-transform: skewX(9deg); } - 10% { -o-transform: skewX(-8deg); } - 20% { -o-transform: skewX(7deg); } - 30% { -o-transform: skewX(-6deg); } - 40% { -o-transform: skewX(5deg); } - 50% { -o-transform: skewX(-4deg); } - 60% { -o-transform: skewX(3deg); } - 70% { -o-transform: skewX(-2deg); } - 80% { -o-transform: skewX(1deg); } - 90% { -o-transform: skewX(0deg); } - 100% { -o-transform: skewX(0deg); } -} - -@keyframes wiggle { - 0% { transform: skewX(9deg); } - 10% { transform: skewX(-8deg); } - 20% { transform: skewX(7deg); } - 30% { transform: skewX(-6deg); } - 40% { transform: skewX(5deg); } - 50% { transform: skewX(-4deg); } - 60% { transform: skewX(3deg); } - 70% { transform: skewX(-2deg); } - 80% { transform: skewX(1deg); } - 90% { transform: skewX(0deg); } - 100% { transform: skewX(0deg); } -} - -.wiggle { - -webkit-animation-name: wiggle; - -moz-animation-name: wiggle; - -o-animation-name: wiggle; - animation-name: wiggle; - - -webkit-animation-timing-function: ease-in; - -moz-animation-timing-function: ease-in; - -o-animation-timing-function: ease-in; - animation-timing-function: ease-in; -} - -.animated.wiggle { - -webkit-animation-duration: 0.75s; - -moz-animation-duration: 0.75s; - -o-animation-duration: 0.75s; - animation-duration: 0.75s; -} diff --git a/tasks/options/concat.js b/tasks/options/concat.js index 2a88d23bb77..36a69965229 100644 --- a/tasks/options/concat.js +++ b/tasks/options/concat.js @@ -7,7 +7,6 @@ module.exports = function(config) { '<%= srcDir %>/vendor/css/normalize.min.css', '<%= srcDir %>/vendor/css/timepicker.css', '<%= srcDir %>/vendor/css/spectrum.css', - '<%= srcDir %>/vendor/css/animate.min.css', '<%= srcDir %>/css/bootstrap.dark.min.css', '<%= srcDir %>/css/bootstrap-responsive.min.css', '<%= srcDir %>/vendor/css/font-awesome.min.css' @@ -19,7 +18,6 @@ module.exports = function(config) { '<%= srcDir %>/vendor/css/normalize.min.css', '<%= srcDir %>/vendor/css/timepicker.css', '<%= srcDir %>/vendor/css/spectrum.css', - '<%= srcDir %>/vendor/css/animate.min.css', '<%= srcDir %>/css/bootstrap.light.min.css', '<%= srcDir %>/css/bootstrap-responsive.min.css', '<%= srcDir %>/vendor/css/font-awesome.min.css' diff --git a/tasks/options/requirejs.js b/tasks/options/requirejs.js index 947553f1266..d9edf76ada4 100644 --- a/tasks/options/requirejs.js +++ b/tasks/options/requirejs.js @@ -61,6 +61,7 @@ module.exports = function(config,grunt) { 'controllers/all', 'routes/all', 'components/partials', + 'plugins/datasource/grafana/datasource', ] } ];