Преглед изворни кода

Merge branch 'develop' into panel_repeat

Conflicts:
	src/app/partials/roweditor.html
	src/app/partials/submenu.html
	src/css/less/submenu.less
	src/test/specs/templateSrv-specs.js
Torkel Ödegaard пре 10 година
родитељ
комит
2933b89a82
100 измењених фајлова са 3017 додато и 760 уклоњено
  1. 2 0
      CHANGELOG.md
  2. 4 4
      Godeps/Godeps.json
  3. 114 0
      Godeps/_workspace/src/github.com/go-xorm/core/README.md
  4. 15 0
      Godeps/_workspace/src/github.com/go-xorm/core/column.go
  5. 40 23
      Godeps/_workspace/src/github.com/go-xorm/core/dialect.go
  6. 21 4
      Godeps/_workspace/src/github.com/go-xorm/core/index.go
  7. 96 38
      Godeps/_workspace/src/github.com/go-xorm/core/mapper.go
  8. 45 0
      Godeps/_workspace/src/github.com/go-xorm/core/mapper_test.go
  9. 9 8
      Godeps/_workspace/src/github.com/go-xorm/core/pk.go
  10. 11 0
      Godeps/_workspace/src/github.com/go-xorm/core/pk_test.go
  11. 8 3
      Godeps/_workspace/src/github.com/go-xorm/core/table.go
  12. 4 2
      Godeps/_workspace/src/github.com/go-xorm/core/type.go
  13. 3 1
      Godeps/_workspace/src/github.com/go-xorm/xorm/README.md
  14. 6 10
      Godeps/_workspace/src/github.com/go-xorm/xorm/README_CN.md
  15. 1 1
      Godeps/_workspace/src/github.com/go-xorm/xorm/VERSION
  16. 7 6
      Godeps/_workspace/src/github.com/go-xorm/xorm/doc.go
  17. 46 44
      Godeps/_workspace/src/github.com/go-xorm/xorm/engine.go
  18. 216 0
      Godeps/_workspace/src/github.com/go-xorm/xorm/helpers.go
  19. 1 1
      Godeps/_workspace/src/github.com/go-xorm/xorm/mssql_dialect.go
  20. 6 2
      Godeps/_workspace/src/github.com/go-xorm/xorm/mysql_dialect.go
  21. 132 27
      Godeps/_workspace/src/github.com/go-xorm/xorm/oracle_dialect.go
  22. 2 7
      Godeps/_workspace/src/github.com/go-xorm/xorm/rows.go
  23. 294 283
      Godeps/_workspace/src/github.com/go-xorm/xorm/session.go
  24. 13 5
      Godeps/_workspace/src/github.com/go-xorm/xorm/sqlite3_dialect.go
  25. 78 36
      Godeps/_workspace/src/github.com/go-xorm/xorm/statement.go
  26. 6 7
      Godeps/_workspace/src/github.com/go-xorm/xorm/xorm.go
  27. 7 1
      Godeps/_workspace/src/github.com/mattn/go-sqlite3/README.md
  28. 1 1
      Godeps/_workspace/src/github.com/mattn/go-sqlite3/backup.go
  29. 6 0
      Godeps/_workspace/src/github.com/mattn/go-sqlite3/error_test.go
  30. 0 0
      Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3-binding.c
  31. 0 0
      Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3-binding.h
  32. 145 43
      Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3.go
  33. 83 0
      Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3_fts3_test.go
  34. 1 1
      Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3_other.go
  35. 203 0
      Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3_test.go
  36. 1 1
      Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3_windows.go
  37. 1 1
      Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3ext.h
  38. 12 12
      README.md
  39. 2 2
      build.go
  40. 7 1
      conf/defaults.ini
  41. 7 0
      conf/sample.ini
  42. 2 1
      docs/mkdocs.yml
  43. 12 1
      docs/sources/guides/gettingstarted.md
  44. 38 19
      docs/sources/guides/whats-new-in-v2.md
  45. 1 1
      docs/sources/installation/configuration.md
  46. 1 1
      docs/sources/installation/performance.md
  47. 44 0
      docs/sources/reference/sharing.md
  48. 1 1
      latest.json
  49. 1 11
      main.go
  50. 1 1
      package.json
  51. 3 0
      pkg/api/admin_users.go
  52. 7 0
      pkg/api/api.go
  53. 5 0
      pkg/api/dashboard.go
  54. 87 0
      pkg/api/dashboard_snapshot.go
  55. 4 3
      pkg/api/dtos/models.go
  56. 1 1
      pkg/api/index.go
  57. 3 1
      pkg/api/login.go
  58. 3 0
      pkg/api/login_oauth.go
  59. 3 0
      pkg/api/org.go
  60. 3 0
      pkg/api/signup.go
  61. 218 0
      pkg/api/static/static.go
  62. 19 6
      pkg/cmd/web.go
  63. 72 0
      pkg/metrics/counter.go
  64. 39 0
      pkg/metrics/metric_ref.go
  65. 29 0
      pkg/metrics/metrics.go
  66. 102 0
      pkg/metrics/registry.go
  67. 64 0
      pkg/metrics/report_usage.go
  68. 12 0
      pkg/middleware/middleware.go
  69. 49 0
      pkg/models/dashboard_snapshot.go
  70. 4 0
      pkg/models/models.go
  71. 11 0
      pkg/models/stats.go
  72. 1 1
      pkg/services/sqlstore/apikey_test.go
  73. 2 0
      pkg/services/sqlstore/dashboard.go
  74. 65 0
      pkg/services/sqlstore/dashboard_snapshot.go
  75. 37 0
      pkg/services/sqlstore/dashboard_snapshot_test.go
  76. 51 0
      pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go
  77. 1 1
      pkg/services/sqlstore/migrations/datasource_mig.go
  78. 1 0
      pkg/services/sqlstore/migrations/migrations.go
  79. 8 11
      pkg/services/sqlstore/sqlstore.go
  80. 1 0
      pkg/services/sqlstore/sqlstore.goconvey
  81. 1 1
      pkg/services/sqlstore/sqlutil/sqlutil.go
  82. 36 0
      pkg/services/sqlstore/stats.go
  83. 0 0
      pkg/services/sqlstore/xorm.log
  84. 4 0
      pkg/setting/setting.go
  85. 3 1
      src/app/directives/graphiteSegment.js
  86. 4 82
      src/app/directives/templateParamSelector.js
  87. 1 0
      src/app/features/dashboard/all.js
  88. 30 1
      src/app/features/dashboard/dashboardCtrl.js
  89. 13 1
      src/app/features/dashboard/dashboardNavCtrl.js
  90. 11 0
      src/app/features/dashboard/dashboardSrv.js
  91. 2 1
      src/app/features/dashboard/dynamicDashboardSrv.js
  92. 73 3
      src/app/features/dashboard/partials/shareDashboard.html
  93. 10 9
      src/app/features/dashboard/rowCtrl.js
  94. 9 5
      src/app/features/dashboard/sharePanelCtrl.js
  95. 111 0
      src/app/features/dashboard/shareSnapshotCtrl.js
  96. 7 1
      src/app/features/panel/panelHelper.js
  97. 1 1
      src/app/features/panel/panelMenu.js
  98. 8 1
      src/app/features/panel/panelSrv.js
  99. 22 16
      src/app/features/panel/soloPanelCtrl.js
  100. 9 2
      src/app/panels/graph/module.js

+ 2 - 0
CHANGELOG.md

@@ -1,6 +1,7 @@
 # 2.0.0 (unreleased)
 # 2.0.0 (unreleased)
 
 
 **New features**
 **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 #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 #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
 - [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 #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 #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 #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**
 **Fixes**
 - [Issue #1298](https://github.com/grafana/grafana/issues/1298). InfluxDB: Fix handling of empty array in templating variable query
 - [Issue #1298](https://github.com/grafana/grafana/issues/1298). InfluxDB: Fix handling of empty array in templating variable query

+ 4 - 4
Godeps/Godeps.json

@@ -25,12 +25,12 @@
 		},
 		},
 		{
 		{
 			"ImportPath": "github.com/go-xorm/core",
 			"ImportPath": "github.com/go-xorm/core",
-			"Rev": "a949e067ced1cb6e6ef5c38b6f28b074fa718f1e"
+			"Rev": "be6e7ac47dc57bd0ada25322fa526944f66ccaa6"
 		},
 		},
 		{
 		{
 			"ImportPath": "github.com/go-xorm/xorm",
 			"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",
 			"ImportPath": "github.com/jtolds/gls",
@@ -51,7 +51,7 @@
 		},
 		},
 		{
 		{
 			"ImportPath": "github.com/mattn/go-sqlite3",
 			"ImportPath": "github.com/mattn/go-sqlite3",
-			"Rev": "d10e2c8f62100097910367dee90a9bd89d426a44"
+			"Rev": "e28cd440fabdd39b9520344bc26829f61db40ece"
 		},
 		},
 		{
 		{
 			"ImportPath": "github.com/smartystreets/goconvey/convey",
 			"ImportPath": "github.com/smartystreets/goconvey/convey",

+ 114 - 0
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)
+```

+ 15 - 0
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, ".")
 		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 {
 	if len(col.fieldPath) == 1 {
 		fieldValue = dataStruct.FieldByName(col.FieldName)
 		fieldValue = dataStruct.FieldByName(col.FieldName)
 	} else if len(col.fieldPath) == 2 {
 	} else if len(col.fieldPath) == 2 {

+ 40 - 23
Godeps/_workspace/src/github.com/go-xorm/core/dialect.go

@@ -47,15 +47,13 @@ type Dialect interface {
 	SupportInsertMany() bool
 	SupportInsertMany() bool
 	SupportEngine() bool
 	SupportEngine() bool
 	SupportCharset() bool
 	SupportCharset() bool
+	SupportDropIfExists() bool
 	IndexOnTable() bool
 	IndexOnTable() bool
 	ShowCreateNull() bool
 	ShowCreateNull() bool
 
 
 	IndexCheckSql(tableName, idxName string) (string, []interface{})
 	IndexCheckSql(tableName, idxName string) (string, []interface{})
 	TableCheckSql(tableName 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)
 	IsColumnExist(tableName string, col *Column) (bool, error)
 
 
 	CreateTableSql(table *Table, tableName, storeEngine, charset string) string
 	CreateTableSql(table *Table, tableName, storeEngine, charset string) string
@@ -65,15 +63,13 @@ type Dialect interface {
 
 
 	ModifyColumnSql(tableName string, col *Column) string
 	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)
 	GetColumns(tableName string) ([]string, map[string]*Column, error)
 	GetTables() ([]*Table, error)
 	GetTables() ([]*Table, error)
 	GetIndexes(tableName string) (map[string]*Index, 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
 	Filters() []Filter
 }
 }
 
 
@@ -144,6 +140,10 @@ func (db *Base) RollBackStr() string {
 	return "ROLL BACK"
 	return "ROLL BACK"
 }
 }
 
 
+func (db *Base) SupportDropIfExists() bool {
+	return true
+}
+
 func (db *Base) DropTableSql(tableName string) string {
 func (db *Base) DropTableSql(tableName string) string {
 	return fmt.Sprintf("DROP TABLE IF EXISTS `%s`", tableName)
 	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)
 	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 {
 func (db *Base) CreateIndexSql(tableName string, index *Index) string {
 	quote := db.dialect.Quote
 	quote := db.dialect.Quote
 	var unique string
 	var unique string
 	var idxName string
 	var idxName string
 	if index.Type == UniqueType {
 	if index.Type == UniqueType {
 		unique = " UNIQUE"
 		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(idxName), quote(tableName),
 		quote(strings.Join(index.Cols, quote(","))))
 		quote(strings.Join(index.Cols, quote(","))))
 }
 }
 
 
 func (db *Base) DropIndexSql(tableName string, index *Index) string {
 func (db *Base) DropIndexSql(tableName string, index *Index) string {
 	quote := db.dialect.Quote
 	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 {
 func (db *Base) ModifyColumnSql(tableName string, col *Column) string {

+ 21 - 4
Godeps/_workspace/src/github.com/go-xorm/core/index.go

@@ -1,7 +1,9 @@
 package core
 package core
 
 
 import (
 import (
+	"fmt"
 	"sort"
 	"sort"
+	"strings"
 )
 )
 
 
 const (
 const (
@@ -11,9 +13,21 @@ const (
 
 
 // database index
 // database index
 type Index struct {
 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
 // add columns which will be composite index
@@ -24,6 +38,9 @@ func (index *Index) AddColumn(cols ...string) {
 }
 }
 
 
 func (index *Index) Equal(dst *Index) bool {
 func (index *Index) Equal(dst *Index) bool {
+	if index.Type != dst.Type {
+		return false
+	}
 	if len(index.Cols) != len(dst.Cols) {
 	if len(index.Cols) != len(dst.Cols) {
 		return false
 		return false
 	}
 	}
@@ -40,5 +57,5 @@ func (index *Index) Equal(dst *Index) bool {
 
 
 // new an index
 // new an index
 func NewIndex(name string, indexType int) *Index {
 func NewIndex(name string, indexType int) *Index {
-	return &Index{name, indexType, make([]string, 0)}
+	return &Index{true, name, indexType, make([]string, 0)}
 }
 }

+ 96 - 38
Godeps/_workspace/src/github.com/go-xorm/core/mapper.go

@@ -9,7 +9,6 @@ import (
 type IMapper interface {
 type IMapper interface {
 	Obj2Table(string) string
 	Obj2Table(string) string
 	Table2Obj(string) string
 	Table2Obj(string) string
-	TableName(string) string
 }
 }
 
 
 type CacheMapper struct {
 type CacheMapper struct {
@@ -56,10 +55,6 @@ func (m *CacheMapper) Table2Obj(t string) string {
 	return o
 	return o
 }
 }
 
 
-func (m *CacheMapper) TableName(t string) string {
-	return t
-}
-
 // SameMapper implements IMapper and provides same name between struct and
 // SameMapper implements IMapper and provides same name between struct and
 // database table
 // database table
 type SameMapper struct {
 type SameMapper struct {
@@ -73,10 +68,6 @@ func (m SameMapper) Table2Obj(t string) string {
 	return t
 	return t
 }
 }
 
 
-func (m SameMapper) TableName(t string) string {
-	return t
-}
-
 // SnakeMapper implements IMapper and provides name transaltion between
 // SnakeMapper implements IMapper and provides name transaltion between
 // struct and database table
 // struct and database table
 type SnakeMapper struct {
 type SnakeMapper struct {
@@ -97,25 +88,6 @@ func snakeCasedName(name string) string {
 	return string(newstr)
 	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 {
 func (mapper SnakeMapper) Obj2Table(name string) string {
 	return snakeCasedName(name)
 	return snakeCasedName(name)
 }
 }
@@ -148,9 +120,103 @@ func (mapper SnakeMapper) Table2Obj(name string) string {
 	return titleCasedName(name)
 	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
 // provide prefix table name support
 type PrefixMapper struct {
 type PrefixMapper struct {
 	Mapper IMapper
 	Mapper IMapper
@@ -165,10 +231,6 @@ func (mapper PrefixMapper) Table2Obj(name string) string {
 	return mapper.Mapper.Table2Obj(name[len(mapper.Prefix):])
 	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 {
 func NewPrefixMapper(mapper IMapper, prefix string) PrefixMapper {
 	return PrefixMapper{mapper, prefix}
 	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)])
 	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 {
 func NewSuffixMapper(mapper IMapper, suffix string) SuffixMapper {
 	return SuffixMapper{mapper, suffix}
 	return SuffixMapper{mapper, suffix}
 }
 }

+ 45 - 0
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)
+		}
+	}
+}

+ 9 - 8
Godeps/_workspace/src/github.com/go-xorm/core/pk.go

@@ -1,7 +1,8 @@
 package core
 package core
 
 
 import (
 import (
-	"encoding/json"
+	"bytes"
+	"encoding/gob"
 )
 )
 
 
 type PK []interface{}
 type PK []interface{}
@@ -12,14 +13,14 @@ func NewPK(pks ...interface{}) *PK {
 }
 }
 
 
 func (p *PK) ToString() (string, error) {
 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 {
 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
 }
 }

+ 11 - 0
Godeps/_workspace/src/github.com/go-xorm/core/pk_test.go

@@ -2,6 +2,7 @@ package core
 
 
 import (
 import (
 	"fmt"
 	"fmt"
+	"reflect"
 	"testing"
 	"testing"
 )
 )
 
 
@@ -19,4 +20,14 @@ func TestPK(t *testing.T) {
 		t.Error(err)
 		t.Error(err)
 	}
 	}
 	fmt.Println(s)
 	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]))
+		}
+	}
 }
 }

+ 8 - 3
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
 // if has primary key, return column
 func (table *Table) PKColumns() []*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
 	return columns
 }
 }
 
 
+func (table *Table) ColumnType(name string) reflect.Type {
+	t, _ := table.Type.FieldByName(name)
+	return t.Type
+}
+
 func (table *Table) AutoIncrColumn() *Column {
 func (table *Table) AutoIncrColumn() *Column {
 	return table.GetColumn(table.AutoIncrement)
 	return table.GetColumn(table.AutoIncrement)
 }
 }

+ 4 - 2
Godeps/_workspace/src/github.com/go-xorm/core/type.go

@@ -70,6 +70,7 @@ var (
 	NVarchar   = "NVARCHAR"
 	NVarchar   = "NVARCHAR"
 	TinyText   = "TINYTEXT"
 	TinyText   = "TINYTEXT"
 	Text       = "TEXT"
 	Text       = "TEXT"
+	Clob       = "CLOB"
 	MediumText = "MEDIUMTEXT"
 	MediumText = "MEDIUMTEXT"
 	LongText   = "LONGTEXT"
 	LongText   = "LONGTEXT"
 	Uuid       = "UUID"
 	Uuid       = "UUID"
@@ -120,6 +121,7 @@ var (
 		MediumText: TEXT_TYPE,
 		MediumText: TEXT_TYPE,
 		LongText:   TEXT_TYPE,
 		LongText:   TEXT_TYPE,
 		Uuid:       TEXT_TYPE,
 		Uuid:       TEXT_TYPE,
+		Clob:       TEXT_TYPE,
 
 
 		Date:       TIME_TYPE,
 		Date:       TIME_TYPE,
 		DateTime:   TIME_TYPE,
 		DateTime:   TIME_TYPE,
@@ -250,7 +252,7 @@ func Type2SQLType(t reflect.Type) (st SQLType) {
 	case reflect.String:
 	case reflect.String:
 		st = SQLType{Varchar, 255, 0}
 		st = SQLType{Varchar, 255, 0}
 	case reflect.Struct:
 	case reflect.Struct:
-		if t == reflect.TypeOf(c_TIME_DEFAULT) {
+		if t.ConvertibleTo(reflect.TypeOf(c_TIME_DEFAULT)) {
 			st = SQLType{DateTime, 0, 0}
 			st = SQLType{DateTime, 0, 0}
 		} else {
 		} else {
 			// TODO need to handle association struct
 			// TODO need to handle association struct
@@ -303,7 +305,7 @@ func SQLType2Type(st SQLType) reflect.Type {
 		return reflect.TypeOf(float32(1))
 		return reflect.TypeOf(float32(1))
 	case Double:
 	case Double:
 		return reflect.TypeOf(float64(1))
 		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("")
 		return reflect.TypeOf("")
 	case TinyBlob, Blob, LongBlob, Bytea, Binary, MediumBlob, VarBinary:
 	case TinyBlob, Blob, LongBlob, Bytea, Binary, MediumBlob, VarBinary:
 		return reflect.TypeOf([]byte{})
 		return reflect.TypeOf([]byte{})

+ 3 - 1
Godeps/_workspace/src/github.com/go-xorm/xorm/README.md

@@ -82,11 +82,13 @@ Or
 
 
 # Cases
 # Cases
 
 
+* [Wego](http://github.com/go-tango/wego)
+
 * [Docker.cn](https://docker.cn/)
 * [Docker.cn](https://docker.cn/)
 
 
 * [Gogs](http://try.gogits.org) - [github.com/gogits/gogs](http://github.com/gogits/gogs)
 * [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)
 * [Gowalker](http://gowalker.org) - [github.com/Unknwon/gowalker](http://github.com/Unknwon/gowalker)
 
 

+ 6 - 10
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)
 [更多更新日志...](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/)
 * [Docker.cn](https://docker.cn/)
 
 
 * [Gogs](http://try.gogits.org) - [github.com/gogits/gogs](http://github.com/gogits/gogs)
 * [Gogs](http://try.gogits.org) - [github.com/gogits/gogs](http://github.com/gogits/gogs)

+ 1 - 1
Godeps/_workspace/src/github.com/go-xorm/xorm/VERSION

@@ -1 +1 @@
-xorm v0.4.1
+xorm v0.4.2.0225

+ 7 - 6
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
     // SELECT * FROM user
 
 
 4. Query multiple records and record by record handle, there two methods, one is Iterate,
 4. Query multiple records and record by record handle, there two methods, one is Iterate,
-another is Raws
+another is Rows
 
 
     err := engine.Iterate(...)
     err := engine.Iterate(...)
     // SELECT * FROM user
     // SELECT * FROM user
 
 
-    raws, err := engine.Raws(...)
+    rows, err := engine.Rows(...)
     // SELECT * FROM user
     // SELECT * FROM user
+    defer rows.Close()
     bean := new(Struct)
     bean := new(Struct)
-    for raws.Next() {
-        err = raws.Scan(bean)
+    for rows.Next() {
+        err = rows.Scan(bean)
     }
     }
 
 
 5. Update one or more records
 5. Update one or more records
 
 
-    affected, err := engine.Update(&user)
+    affected, err := engine.Id(...).Update(&user)
     // UPDATE user SET ...
     // UPDATE user SET ...
 
 
 6. Delete one or more records, Delete MUST has conditon
 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()
     engine.Join("LEFT", "userdetail", "user.id=userdetail.id").Find()
     //SELECT * FROM user LEFT JOIN userdetail ON user.id=userdetail.id
     //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
 package xorm

+ 46 - 44
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 {
 				if col := table.GetColumn(name); col != nil {
 					col.Indexes[index.Name] = true
 					col.Indexes[index.Name] = true
 				} else {
 				} 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
 	return tables, nil
 }
 }
 
 
+/*
+dump database all table structs and data to a file
+*/
 func (engine *Engine) DumpAllToFile(fp string) error {
 func (engine *Engine) DumpAllToFile(fp string) error {
 	f, err := os.Create(fp)
 	f, err := os.Create(fp)
 	if err != nil {
 	if err != nil {
@@ -361,6 +364,9 @@ func (engine *Engine) DumpAllToFile(fp string) error {
 	return engine.DumpAll(f)
 	return engine.DumpAll(f)
 }
 }
 
 
+/*
+dump database all table structs and data to w
+*/
 func (engine *Engine) DumpAll(w io.Writer) error {
 func (engine *Engine) DumpAll(w io.Writer) error {
 	tables, err := engine.DBMetas()
 	tables, err := engine.DBMetas()
 	if err != nil {
 	if err != nil {
@@ -558,6 +564,13 @@ func (engine *Engine) Decr(column string, arg ...interface{}) *Session {
 	return session.Decr(column, arg...)
 	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
 // Temporarily change the Get, Find, Update's table
 func (engine *Engine) Table(tableNameOrBean interface{}) *Session {
 func (engine *Engine) Table(tableNameOrBean interface{}) *Session {
 	session := engine.NewSession()
 	session := engine.NewSession()
@@ -766,7 +779,12 @@ func (engine *Engine) mapType(v reflect.Value) *core.Table {
 						col.IsPrimaryKey = true
 						col.IsPrimaryKey = true
 						col.Nullable = false
 						col.Nullable = false
 					case k == "NULL":
 					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, ")"):
 					/*case strings.HasPrefix(k, "AUTOINCR(") && strings.HasSuffix(k, ")"):
 					col.IsAutoIncrement = true
 					col.IsAutoIncrement = true
 
 
@@ -915,7 +933,7 @@ func (engine *Engine) mapType(v reflect.Value) *core.Table {
 
 
 		table.AddColumn(col)
 		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
 			idFieldColName = col.Name
 		}
 		}
 	} // end for
 	} // end for
@@ -959,40 +977,25 @@ func (engine *Engine) mapping(beans ...interface{}) (e error) {
 
 
 // If a table has any reocrd
 // If a table has any reocrd
 func (engine *Engine) IsTableEmpty(bean interface{}) (bool, error) {
 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()
 	session := engine.NewSession()
 	defer session.Close()
 	defer session.Close()
-	rows, err := session.Count(bean)
-	return rows == 0, err
+	return session.IsTableEmpty(bean)
 }
 }
 
 
 // If a table is exist
 // 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()
 	session := engine.NewSession()
 	defer session.Close()
 	defer session.Close()
-	has, err := session.isTableExist(tableName)
-	return has, err
+	return session.IsTableExist(beanOrTableName)
 }
 }
 
 
 func (engine *Engine) IdOf(bean interface{}) core.PK {
 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))
 	pk := make([]interface{}, len(table.PrimaryKeys))
 	for i, col := range table.PKColumns() {
 	for i, col := range table.PKColumns() {
 		pkField := v.FieldByName(col.FieldName)
 		pkField := v.FieldByName(col.FieldName)
@@ -1109,7 +1112,7 @@ func (engine *Engine) Sync(beans ...interface{}) error {
 				session := engine.NewSession()
 				session := engine.NewSession()
 				session.Statement.RefTable = table
 				session.Statement.RefTable = table
 				defer session.Close()
 				defer session.Close()
-				isExist, err := session.isColumnExist(table.Name, col)
+				isExist, err := session.Engine.dialect.IsColumnExist(table.Name, col)
 				if err != nil {
 				if err != nil {
 					return err
 					return err
 				}
 				}
@@ -1222,8 +1225,9 @@ func (engine *Engine) CreateTables(beans ...interface{}) error {
 
 
 func (engine *Engine) DropTables(beans ...interface{}) error {
 func (engine *Engine) DropTables(beans ...interface{}) error {
 	session := engine.NewSession()
 	session := engine.NewSession()
-	err := session.Begin()
 	defer session.Close()
 	defer session.Close()
+
+	err := session.Begin()
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -1258,13 +1262,6 @@ func (engine *Engine) Query(sql string, paramStr ...interface{}) (resultsSlice [
 	return session.Query(sql, paramStr...)
 	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
 // Insert one or more records
 func (engine *Engine) Insert(beans ...interface{}) (int64, error) {
 func (engine *Engine) Insert(beans ...interface{}) (int64, error) {
 	session := engine.NewSession()
 	session := engine.NewSession()
@@ -1371,18 +1368,11 @@ func (engine *Engine) Import(r io.Reader) ([]sql.Result, error) {
 
 
 	scanner.Split(semiColSpliter)
 	scanner.Split(semiColSpliter)
 
 
-	session := engine.NewSession()
-	defer session.Close()
-	err := session.newDb()
-	if err != nil {
-		return results, err
-	}
-
 	for scanner.Scan() {
 	for scanner.Scan() {
 		query := scanner.Text()
 		query := scanner.Text()
 		query = strings.Trim(query, " \t")
 		query = strings.Trim(query, " \t")
 		if len(query) > 0 {
 		if len(query) > 0 {
-			result, err := session.Db.Exec(query)
+			result, err := engine.DB().Exec(query)
 			results = append(results, result)
 			results = append(results, result)
 			if err != nil {
 			if err != nil {
 				lastError = err
 				lastError = err
@@ -1409,7 +1399,15 @@ func (engine *Engine) NowTime(sqlTypeName string) interface{} {
 	return engine.FormatTime(sqlTypeName, t)
 	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{}) {
 func (engine *Engine) FormatTime(sqlTypeName string, t time.Time) (v interface{}) {
+	if engine.dialect.DBType() == core.ORACLE {
+		return t
+	}
 	switch sqlTypeName {
 	switch sqlTypeName {
 	case core.Time:
 	case core.Time:
 		s := engine.TZTime(t).Format("2006-01-02 15:04:05") //time.RFC3339
 		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:
 	case core.DateTime, core.TimeStamp:
 		if engine.dialect.DBType() == "ql" {
 		if engine.dialect.DBType() == "ql" {
 			v = engine.TZTime(t)
 			v = engine.TZTime(t)
+		} else if engine.dialect.DBType() == "sqlite3" {
+			v = engine.TZTime(t).UTC().Format("2006-01-02 15:04:05")
 		} else {
 		} else {
 			v = engine.TZTime(t).Format("2006-01-02 15:04:05")
 			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 {
 		} else {
 			v = engine.TZTime(t).Format(time.RFC3339Nano)
 			v = engine.TZTime(t).Format(time.RFC3339Nano)
 		}
 		}
+	case core.BigInt, core.Int:
+		v = engine.TZTime(t).Unix()
 	default:
 	default:
 		v = engine.TZTime(t)
 		v = engine.TZTime(t)
 	}
 	}

+ 216 - 0
Godeps/_workspace/src/github.com/go-xorm/xorm/helpers.go

@@ -11,6 +11,43 @@ import (
 	"github.com/go-xorm/core"
 	"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 {
 func indexNoCase(s, sep string) int {
 	return strings.Index(strings.ToLower(s), strings.ToLower(sep))
 	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
 	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
+}

+ 1 - 1
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 {
 func (db *mssql) Quote(name string) string {
-	return "[" + name + "]"
+	return "\"" + name + "\""
 }
 }
 
 
 func (db *mssql) QuoteStr() string {
 func (db *mssql) QuoteStr() string {

+ 6 - 2
Godeps/_workspace/src/github.com/go-xorm/xorm/mysql_dialect.go

@@ -218,6 +218,9 @@ func (db *mysql) SqlType(c *core.Column) string {
 		res += ")"
 		res += ")"
 	case core.NVarchar:
 	case core.NVarchar:
 		res = core.Varchar
 		res = core.Varchar
+	case core.Uuid:
+		res = core.Varchar
+		c.Length = 40
 	default:
 	default:
 		res = t
 		res = t
 	}
 	}
@@ -317,7 +320,6 @@ func (db *mysql) GetColumns(tableName string) ([]string, map[string]*core.Column
 		if err != nil {
 		if err != nil {
 			return nil, nil, err
 			return nil, nil, err
 		}
 		}
-		//fmt.Println(columnName, isNullable, colType, colKey, extra, colDefault)
 		col.Name = strings.Trim(columnName, "` ")
 		col.Name = strings.Trim(columnName, "` ")
 		if "YES" == isNullable {
 		if "YES" == isNullable {
 			col.Nullable = true
 			col.Nullable = true
@@ -467,15 +469,17 @@ func (db *mysql) GetIndexes(tableName string) (map[string]*core.Index, error) {
 		}
 		}
 
 
 		colName = strings.Trim(colName, "` ")
 		colName = strings.Trim(colName, "` ")
-
+		var isRegular bool
 		if strings.HasPrefix(indexName, "IDX_"+tableName) || strings.HasPrefix(indexName, "UQE_"+tableName) {
 		if strings.HasPrefix(indexName, "IDX_"+tableName) || strings.HasPrefix(indexName, "UQE_"+tableName) {
 			indexName = indexName[5+len(tableName) : len(indexName)]
 			indexName = indexName[5+len(tableName) : len(indexName)]
+			isRegular = true
 		}
 		}
 
 
 		var index *core.Index
 		var index *core.Index
 		var ok bool
 		var ok bool
 		if index, ok = indexes[indexName]; !ok {
 		if index, ok = indexes[indexName]; !ok {
 			index = new(core.Index)
 			index = new(core.Index)
+			index.IsRegular = isRegular
 			index.Type = indexType
 			index.Type = indexType
 			index.Name = indexName
 			index.Name = indexName
 			indexes[indexName] = index
 			indexes[indexName] = index

+ 132 - 27
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
 	var res string
 	switch t := c.SQLType.Name; t {
 	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:
 	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:
 	case core.Binary, core.VarBinary, core.Blob, core.TinyBlob, core.MediumBlob, core.LongBlob, core.Bytea:
 		return core.Blob
 		return core.Blob
 	case core.Time, core.DateTime, core.TimeStamp:
 	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:
 	case core.Text, core.MediumText, core.LongText:
 		res = "CLOB"
 		res = "CLOB"
 	case core.Char, core.Varchar, core.TinyText:
 	case core.Char, core.Varchar, core.TinyText:
-		return "VARCHAR2"
+		res = "VARCHAR2"
 	default:
 	default:
 		res = t
 		res = t
 	}
 	}
@@ -536,6 +536,10 @@ func (db *oracle) SqlType(c *core.Column) string {
 	return res
 	return res
 }
 }
 
 
+func (db *oracle) AutoIncrStr() string {
+	return "AUTO_INCREMENT"
+}
+
 func (db *oracle) SupportInsertMany() bool {
 func (db *oracle) SupportInsertMany() bool {
 	return true
 	return true
 }
 }
@@ -553,10 +557,6 @@ func (db *oracle) QuoteStr() string {
 	return "\""
 	return "\""
 }
 }
 
 
-func (db *oracle) AutoIncrStr() string {
-	return ""
-}
-
 func (db *oracle) SupportEngine() bool {
 func (db *oracle) SupportEngine() bool {
 	return false
 	return false
 }
 }
@@ -565,19 +565,94 @@ func (db *oracle) SupportCharset() bool {
 	return false
 	return false
 }
 }
 
 
+func (db *oracle) SupportDropIfExists() bool {
+	return false
+}
+
 func (db *oracle) IndexOnTable() bool {
 func (db *oracle) IndexOnTable() bool {
 	return false
 	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{}) {
 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 ` +
 	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{}) {
 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{}) {
 /*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) {
 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...)
 	rows, err := db.DB().Query(query, args...)
 	if db.Logger != nil {
 	if db.Logger != nil {
 		db.Logger.Info("[sql]", query, args)
 		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) {
 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," +
 	s := "SELECT column_name,data_default,data_type,data_length,data_precision,data_scale," +
 		"nullable FROM USER_TAB_COLUMNS WHERE table_name = :1"
 		"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 := new(core.Column)
 		col.Indexes = make(map[string]bool)
 		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
 		var dataLen int
 
 
 		err = rows.Scan(&colName, &colDefault, &dataType, &dataLen, &dataPrecision,
 		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
 			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
 			col.Nullable = true
 		} else {
 		} else {
 			col.Nullable = false
 			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":
 		case "VARCHAR2":
-			col.SQLType = core.SQLType{core.Varchar, 0, 0}
+			col.SQLType = core.SQLType{core.Varchar, len1, len2}
 		case "TIMESTAMP WITH TIME ZONE":
 		case "TIMESTAMP WITH TIME ZONE":
 			col.SQLType = core.SQLType{core.TimeStampz, 0, 0}
 			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:
 		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 {
 		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
 		col.Length = dataLen
 
 
 		if col.SQLType.IsText() || col.SQLType.IsTime() {
 		if col.SQLType.IsText() || col.SQLType.IsTime() {
-			if col.Default != "" {
+			if !col.DefaultIsEmpty {
 				col.Default = "'" + col.Default + "'"
 				col.Default = "'" + col.Default + "'"
-			} else {
-				if col.DefaultIsEmpty {
-					col.Default = "''"
-				}
 			}
 			}
 		}
 		}
 		cols[col.Name] = col
 		cols[col.Name] = col

+ 2 - 7
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.session = session
 	rows.beanType = reflect.Indirect(reflect.ValueOf(bean)).Type()
 	rows.beanType = reflect.Indirect(reflect.ValueOf(bean)).Type()
 
 
-	err := rows.session.newDb()
-	if err != nil {
-		return nil, err
-	}
-
 	defer rows.session.Statement.Init()
 	defer rows.session.Statement.Init()
 
 
 	var sqlStr string
 	var sqlStr string
@@ -47,8 +42,8 @@ func newRows(session *Session, bean interface{}) (*Rows, error) {
 	}
 	}
 
 
 	rows.session.Engine.logSQL(sqlStr, args)
 	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 {
 	if err != nil {
 		rows.lastError = err
 		rows.lastError = err
 		defer rows.Close()
 		defer rows.Close()

Разлика између датотеке није приказан због своје велике величине
+ 294 - 283
Godeps/_workspace/src/github.com/go-xorm/xorm/session.go


+ 13 - 5
Godeps/_workspace/src/github.com/go-xorm/xorm/sqlite3_dialect.go

@@ -1,6 +1,7 @@
 package xorm
 package xorm
 
 
 import (
 import (
+	"database/sql"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 	"strings"
 	"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 {
 func (db *sqlite3) SqlType(c *core.Column) string {
 	switch t := c.SQLType.Name; t {
 	switch t := c.SQLType.Name; t {
 	case core.Date, core.DateTime, core.TimeStamp, core.Time:
 	case core.Date, core.DateTime, core.TimeStamp, core.Time:
-		return core.Numeric
+		return core.DateTime
 	case core.TimeStampz:
 	case core.TimeStampz:
 		return core.Text
 		return core.Text
 	case core.Char, core.Varchar, core.NVarchar, core.TinyText, core.Text, core.MediumText, core.LongText:
 	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 := new(core.Column)
 		col.Indexes = make(map[string]bool)
 		col.Indexes = make(map[string]bool)
 		col.Nullable = true
 		col.Nullable = true
+		col.DefaultIsEmpty = true
 		for idx, field := range fields {
 		for idx, field := range fields {
 			if idx == 0 {
 			if idx == 0 {
 				col.Name = strings.Trim(field, "`[] ")
 				col.Name = strings.Trim(field, "`[] ")
@@ -315,8 +317,14 @@ func (db *sqlite3) GetColumns(tableName string) ([]string, map[string]*core.Colu
 				} else {
 				} else {
 					col.Nullable = true
 					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
 		cols[col.Name] = col
 		colSeq = append(colSeq, col.Name)
 		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)
 	indexes := make(map[string]*core.Index, 0)
 	for rows.Next() {
 	for rows.Next() {
-		var sql string
-		err = rows.Scan(&sql)
+		var tmpSql sql.NullString
+		err = rows.Scan(&tmpSql)
 		if err != nil {
 		if err != nil {
 			return nil, err
 			return nil, err
 		}
 		}
 
 
-		if sql == "" {
+		if !tmpSql.Valid {
 			continue
 			continue
 		}
 		}
+		sql := tmpSql.String
 
 
 		index := new(core.Index)
 		index := new(core.Index)
 		nNStart := strings.Index(sql, "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], "` []")
 		indexName := strings.Trim(sql[nNStart+6:nNEnd], "` []")
-		//fmt.Println(indexName)
 		if strings.HasPrefix(indexName, "IDX_"+tableName) || strings.HasPrefix(indexName, "UQE_"+tableName) {
 		if strings.HasPrefix(indexName, "IDX_"+tableName) || strings.HasPrefix(indexName, "UQE_"+tableName) {
 			index.Name = indexName[5+len(tableName) : len(indexName)]
 			index.Name = indexName[5+len(tableName) : len(indexName)]
 		} else {
 		} else {

+ 78 - 36
Godeps/_workspace/src/github.com/go-xorm/xorm/statement.go

@@ -26,6 +26,11 @@ type decrParam struct {
 	arg     interface{}
 	arg     interface{}
 }
 }
 
 
+type exprParam struct {
+	colName string
+	expr    string
+}
+
 // statement save all the sql info for executing SQL
 // statement save all the sql info for executing SQL
 type Statement struct {
 type Statement struct {
 	RefTable      *core.Table
 	RefTable      *core.Table
@@ -63,6 +68,7 @@ type Statement struct {
 	inColumns     map[string]*inParam
 	inColumns     map[string]*inParam
 	incrColumns   map[string]incrParam
 	incrColumns   map[string]incrParam
 	decrColumns   map[string]decrParam
 	decrColumns   map[string]decrParam
+	exprColumns   map[string]exprParam
 }
 }
 
 
 // init
 // init
@@ -98,6 +104,7 @@ func (statement *Statement) Init() {
 	statement.inColumns = make(map[string]*inParam)
 	statement.inColumns = make(map[string]*inParam)
 	statement.incrColumns = make(map[string]incrParam)
 	statement.incrColumns = make(map[string]incrParam)
 	statement.decrColumns = make(map[string]decrParam)
 	statement.decrColumns = make(map[string]decrParam)
+	statement.exprColumns = make(map[string]exprParam)
 }
 }
 
 
 // add the raw sql statement
 // add the raw sql statement
@@ -153,9 +160,6 @@ func (statement *Statement) Table(tableNameOrBean interface{}) *Statement {
 	t := v.Type()
 	t := v.Type()
 	if t.Kind() == reflect.String {
 	if t.Kind() == reflect.String {
 		statement.AltTableName = tableNameOrBean.(string)
 		statement.AltTableName = tableNameOrBean.(string)
-		if statement.AltTableName[0] == '~' {
-			statement.AltTableName = statement.Engine.TableMapper.TableName(statement.AltTableName[1:])
-		}
 	} else if t.Kind() == reflect.Struct {
 	} else if t.Kind() == reflect.Struct {
 		statement.RefTable = statement.Engine.autoMapType(v)
 		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{},
 func buildUpdates(engine *Engine, table *core.Table, bean interface{},
 	includeVersion bool, includeUpdated bool, includeNil bool,
 	includeVersion bool, includeUpdated bool, includeNil bool,
 	includeAutoIncr bool, allUseBool bool, useAllCols 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)
 	colNames := make([]string, 0)
 	var args = make([]interface{}, 0)
 	var args = make([]interface{}, 0)
@@ -302,6 +306,9 @@ func buildUpdates(engine *Engine, table *core.Table, bean interface{},
 		if col.IsDeleted {
 		if col.IsDeleted {
 			continue
 			continue
 		}
 		}
+		if use, ok := columnMap[col.Name]; ok && !use {
+			continue
+		}
 
 
 		if engine.dialect.DBType() == core.MSSQL && col.SQLType.Name == core.Text {
 		if engine.dialect.DBType() == core.MSSQL && col.SQLType.Name == core.Text {
 			continue
 			continue
@@ -414,13 +421,16 @@ func buildUpdates(engine *Engine, table *core.Table, bean interface{},
 				if table, ok := engine.Tables[fieldValue.Type()]; ok {
 				if table, ok := engine.Tables[fieldValue.Type()]; ok {
 					if len(table.PrimaryKeys) == 1 {
 					if len(table.PrimaryKeys) == 1 {
 						pkField := reflect.Indirect(fieldValue).FieldByName(table.PKColumns()[0].FieldName)
 						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()
 							val = pkField.Interface()
 						} else {
 						} else {
 							continue
 							continue
 						}
 						}
 					} else {
 					} else {
 						//TODO: how to handler?
 						//TODO: how to handler?
+						panic("not supported")
 					}
 					}
 				} else {
 				} else {
 					val = fieldValue.Interface()
 					val = fieldValue.Interface()
@@ -579,24 +589,29 @@ func buildConditions(engine *Engine, table *core.Table, bean interface{},
 			t := int64(fieldValue.Uint())
 			t := int64(fieldValue.Uint())
 			val = reflect.ValueOf(&t).Interface()
 			val = reflect.ValueOf(&t).Interface()
 		case reflect.Struct:
 		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()) {
 				if !requiredField && (t.IsZero() || !fieldValue.IsValid()) {
 					continue
 					continue
 				}
 				}
 				val = engine.FormatTime(col.SQLType.Name, t)
 				val = engine.FormatTime(col.SQLType.Name, t)
+			} else if _, ok := reflect.New(fieldType).Interface().(core.Conversion); ok {
+				continue
 			} else {
 			} else {
 				engine.autoMapType(fieldValue)
 				engine.autoMapType(fieldValue)
 				if table, ok := engine.Tables[fieldValue.Type()]; ok {
 				if table, ok := engine.Tables[fieldValue.Type()]; ok {
 					if len(table.PrimaryKeys) == 1 {
 					if len(table.PrimaryKeys) == 1 {
 						pkField := reflect.Indirect(fieldValue).FieldByName(table.PKColumns()[0].FieldName)
 						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()
 							val = pkField.Interface()
 						} else {
 						} else {
 							continue
 							continue
 						}
 						}
 					} else {
 					} else {
 						//TODO: how to handler?
 						//TODO: how to handler?
+						panic("not supported")
 					}
 					}
 				} else {
 				} else {
 					val = fieldValue.Interface()
 					val = fieldValue.Interface()
@@ -716,6 +731,13 @@ func (statement *Statement) Decr(column string, arg ...interface{}) *Statement {
 	return 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
 // Generate  "Update ... Set column = column + arg" statment
 func (statement *Statement) getInc() map[string]incrParam {
 func (statement *Statement) getInc() map[string]incrParam {
 	return statement.incrColumns
 	return statement.incrColumns
@@ -726,6 +748,11 @@ func (statement *Statement) getDec() map[string]decrParam {
 	return statement.decrColumns
 	return statement.decrColumns
 }
 }
 
 
+// Generate  "Update ... Set column = {expression}" statment
+func (statement *Statement) getExpr() map[string]exprParam {
+	return statement.exprColumns
+}
+
 // Generate "Where column IN (?) " statment
 // Generate "Where column IN (?) " statment
 func (statement *Statement) In(column string, args ...interface{}) *Statement {
 func (statement *Statement) In(column string, args ...interface{}) *Statement {
 	k := strings.ToLower(column)
 	k := strings.ToLower(column)
@@ -941,15 +968,9 @@ func (statement *Statement) Join(join_operator string, tablename interface{}, co
 		l := len(t)
 		l := len(t)
 		if l > 1 {
 		if l > 1 {
 			table := t[0]
 			table := t[0]
-			if table[0] == '~' {
-				table = statement.Engine.TableMapper.TableName(table[1:])
-			}
 			joinTable = statement.Engine.Quote(table) + " AS " + statement.Engine.Quote(t[1])
 			joinTable = statement.Engine.Quote(table) + " AS " + statement.Engine.Quote(t[1])
 		} else if l == 1 {
 		} else if l == 1 {
 			table := t[0]
 			table := t[0]
-			if table[0] == '~' {
-				table = statement.Engine.TableMapper.TableName(table[1:])
-			}
 			joinTable = statement.Engine.Quote(table)
 			joinTable = statement.Engine.Quote(table)
 		}
 		}
 	case []interface{}:
 	case []interface{}:
@@ -962,9 +983,6 @@ func (statement *Statement) Join(join_operator string, tablename interface{}, co
 			t := v.Type()
 			t := v.Type()
 			if t.Kind() == reflect.String {
 			if t.Kind() == reflect.String {
 				table = f.(string)
 				table = f.(string)
-				if table[0] == '~' {
-					table = statement.Engine.TableMapper.TableName(table[1:])
-				}
 			} else if t.Kind() == reflect.Struct {
 			} else if t.Kind() == reflect.Struct {
 				r := statement.Engine.autoMapType(v)
 				r := statement.Engine.autoMapType(v)
 				table = r.Name
 				table = r.Name
@@ -977,9 +995,6 @@ func (statement *Statement) Join(join_operator string, tablename interface{}, co
 		}
 		}
 	default:
 	default:
 		t := fmt.Sprintf("%v", tablename)
 		t := fmt.Sprintf("%v", tablename)
-		if t[0] == '~' {
-			t = statement.Engine.TableMapper.TableName(t[1:])
-		}
 		joinTable = statement.Engine.Quote(t)
 		joinTable = statement.Engine.Quote(t)
 	}
 	}
 	if statement.JoinStr != "" {
 	if statement.JoinStr != "" {
@@ -1105,9 +1120,10 @@ func (s *Statement) genDelIndexSQL() []string {
 	return sqls
 	return sqls
 }
 }
 
 
+/*
 func (s *Statement) genDropSQL() string {
 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{}) {
 func (statement *Statement) genGetSql(bean interface{}) (string, []interface{}) {
 	var table *core.Table
 	var table *core.Table
@@ -1126,13 +1142,21 @@ func (statement *Statement) genGetSql(bean interface{}) (string, []interface{})
 	statement.BeanArgs = args
 	statement.BeanArgs = args
 
 
 	var columnStr string = statement.ColumnStr
 	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 {
 	} 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 = ""
 		id = ""
 	}
 	}
 	statement.attachInSql()
 	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) {
 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
 	var distinct string
 	if statement.IsDistinct {
 	if statement.IsDistinct {
 		distinct = "DISTINCT "
 		distinct = "DISTINCT "
@@ -1210,7 +1236,11 @@ func (statement *Statement) genSelectSql(columnStr string) (a string) {
 	}
 	}
 	var fromStr string = " FROM " + statement.Engine.Quote(statement.TableName())
 	var fromStr string = " FROM " + statement.Engine.Quote(statement.TableName())
 	if statement.TableAlias != "" {
 	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 != "" {
 	if statement.JoinStr != "" {
 		fromStr = fmt.Sprintf("%v %v", fromStr, 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]
 					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 != "" {
 	if statement.OrderStr != "" {
 		a = fmt.Sprintf("%v ORDER BY %v", a, 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 {
 		if statement.Start > 0 {
 			a = fmt.Sprintf("%v LIMIT %v OFFSET %v", a, statement.LimitN, statement.Start)
 			a = fmt.Sprintf("%v LIMIT %v OFFSET %v", a, statement.LimitN, statement.Start)
 		} else if statement.LimitN > 0 {
 		} else if statement.LimitN > 0 {
 			a = fmt.Sprintf("%v LIMIT %v", a, statement.LimitN)
 			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
 	return

+ 6 - 7
Godeps/_workspace/src/github.com/go-xorm/xorm/xorm.go

@@ -13,7 +13,7 @@ import (
 )
 )
 
 
 const (
 const (
-	Version string = "0.4.1"
+	Version string = "0.4.2.0225"
 )
 )
 
 
 func regDrvsNDialects() bool {
 func regDrvsNDialects() bool {
@@ -84,17 +84,16 @@ func NewEngine(driverName string, dataSourceName string) (*Engine, error) {
 		TZLocation:    time.Local,
 		TZLocation:    time.Local,
 	}
 	}
 
 
-	engine.SetMapper(core.NewCacheMapper(new(core.SnakeMapper)))
+	engine.dialect.SetLogger(engine.Logger)
 
 
-	//engine.Filters = dialect.Filters()
-	//engine.Cacher = NewLRUCacher()
-	//err = engine.SetPool(NewSysConnectPool())
+	engine.SetMapper(core.NewCacheMapper(new(core.SnakeMapper)))
 
 
 	runtime.SetFinalizer(engine, close)
 	runtime.SetFinalizer(engine, close)
-	return engine, err
+
+	return engine, nil
 }
 }
 
 
 // clone an engine
 // clone an engine
 func (engine *Engine) Clone() (*Engine, error) {
 func (engine *Engine) Clone() (*Engine, error) {
-	return NewEngine(engine.dialect.DriverName(), engine.dialect.DataSourceName())
+	return NewEngine(engine.DriverName(), engine.DataSourceName())
 }
 }

+ 7 - 1
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: https://github.com/mattn/go-sqlite3/issues/106
     > See also: http://www.limitlessfx.com/cross-compile-golang-app-for-windows-from-linux.html
     > 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
 License
 -------
 -------
 
 
 MIT: http://mattn.mit-license.org/2012
 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.
 In this repository, those files are amalgamation code that copied from SQLite3. The license of those codes are depend on the license of SQLite3.
 
 

+ 1 - 1
Godeps/_workspace/src/github.com/mattn/go-sqlite3/backup.go

@@ -6,7 +6,7 @@
 package sqlite3
 package sqlite3
 
 
 /*
 /*
-#include <sqlite3.h>
+#include <sqlite3-binding.h>
 #include <stdlib.h>
 #include <stdlib.h>
 */
 */
 import "C"
 import "C"

+ 6 - 0
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",
 			t.Errorf("Wrong extended error code: %d != %d",
 				sqliteErr.ExtendedCode, ErrConstraintUnique)
 				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)
+		}
 	}
 	}
 
 
 }
 }

+ 0 - 0
Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3.c → Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3-binding.c


+ 0 - 0
Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3.h → Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3-binding.h


+ 145 - 43
Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3.go

@@ -6,7 +6,10 @@
 package sqlite3
 package sqlite3
 
 
 /*
 /*
-#include <sqlite3.h>
+#cgo CFLAGS: -std=gnu99
+#cgo CFLAGS: -DSQLITE_ENABLE_RTREE -DSQLITE_THREADSAFE
+#cgo CFLAGS: -DSQLITE_ENABLE_FTS3 -DSQLITE_ENABLE_FTS3_PARENTHESIS
+#include <sqlite3-binding.h>
 #include <stdlib.h>
 #include <stdlib.h>
 #include <string.h>
 #include <string.h>
 
 
@@ -44,14 +47,23 @@ _sqlite3_bind_blob(sqlite3_stmt *stmt, int n, void *p, int np) {
 #include <stdio.h>
 #include <stdio.h>
 #include <stdint.h>
 #include <stdint.h>
 
 
-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"
 	"database/sql/driver"
 	"database/sql/driver"
 	"errors"
 	"errors"
+	"fmt"
 	"io"
 	"io"
+	"net/url"
 	"runtime"
 	"runtime"
+	"strconv"
 	"strings"
 	"strings"
 	"time"
 	"time"
 	"unsafe"
 	"unsafe"
@@ -102,7 +117,8 @@ type SQLiteDriver struct {
 
 
 // Conn struct.
 // Conn struct.
 type SQLiteConn struct {
 type SQLiteConn struct {
-	db *C.sqlite3
+	db  *C.sqlite3
+	loc *time.Location
 }
 }
 
 
 // Tx struct.
 // Tx struct.
@@ -114,6 +130,8 @@ type SQLiteTx struct {
 type SQLiteStmt struct {
 type SQLiteStmt struct {
 	c      *SQLiteConn
 	c      *SQLiteConn
 	s      *C.sqlite3_stmt
 	s      *C.sqlite3_stmt
+	nv     int
+	nn     []string
 	t      string
 	t      string
 	closed bool
 	closed bool
 	cls    bool
 	cls    bool
@@ -174,7 +192,7 @@ func (c *SQLiteConn) Exec(query string, args []driver.Value) (driver.Result, err
 		if s.(*SQLiteStmt).s != nil {
 		if s.(*SQLiteStmt).s != nil {
 			na := s.NumInput()
 			na := s.NumInput()
 			if len(args) < na {
 			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])
 			res, err = s.Exec(args[:na])
 			if err != nil && err != driver.ErrSkip {
 			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
 		s.(*SQLiteStmt).cls = true
 		na := s.NumInput()
 		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])
 		rows, err := s.Query(args[:na])
 		if err != nil && err != driver.ErrSkip {
 		if err != nil && err != driver.ErrSkip {
 			s.Close()
 			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) {
 func (c *SQLiteConn) exec(cmd string) (driver.Result, error) {
 	pcmd := C.CString(cmd)
 	pcmd := C.CString(cmd)
 	defer C.free(unsafe.Pointer(pcmd))
 	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 {
 	if rv != C.SQLITE_OK {
 		return nil, c.lastError()
 		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.
 // Begin transaction.
@@ -248,11 +268,51 @@ func errorString(err Error) string {
 //   file:test.db?cache=shared&mode=memory
 //   file:test.db?cache=shared&mode=memory
 //   :memory:
 //   :memory:
 //   file::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) {
 func (d *SQLiteDriver) Open(dsn string) (driver.Conn, error) {
 	if C.sqlite3_threadsafe() == 0 {
 	if C.sqlite3_threadsafe() == 0 {
 		return nil, errors.New("sqlite library was not compiled for thread-safe operation")
 		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
 	var db *C.sqlite3
 	name := C.CString(dsn)
 	name := C.CString(dsn)
 	defer C.free(unsafe.Pointer(name))
 	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")
 		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 {
 	if rv != C.SQLITE_OK {
 		return nil, Error{Code: ErrNo(rv)}
 		return nil, Error{Code: ErrNo(rv)}
 	}
 	}
 
 
-	conn := &SQLiteConn{db}
+	conn := &SQLiteConn{db: db, loc: loc}
 
 
 	if len(d.Extensions) > 0 {
 	if len(d.Extensions) > 0 {
 		rv = C.sqlite3_enable_load_extension(db, 1)
 		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)))
 			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 {
 		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)
 		rv = C.sqlite3_enable_load_extension(db, 0)
 		if rv != C.SQLITE_OK {
 		if rv != C.SQLITE_OK {
 			return nil, errors.New(C.GoString(C.sqlite3_errmsg(db)))
 			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()
 		return nil, c.lastError()
 	}
 	}
 	var t string
 	var t string
-	if tail != nil && C.strlen(tail) > 0 {
+	if tail != nil && *tail != '\000' {
 		t = strings.TrimSpace(C.GoString(tail))
 		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)
 	runtime.SetFinalizer(ss, (*SQLiteStmt).Close)
 	return ss, nil
 	return ss, nil
 }
 }
@@ -360,7 +422,12 @@ func (s *SQLiteStmt) Close() error {
 
 
 // Return a number of parameters.
 // Return a number of parameters.
 func (s *SQLiteStmt) NumInput() int {
 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 {
 func (s *SQLiteStmt) bind(args []driver.Value) error {
@@ -369,8 +436,24 @@ func (s *SQLiteStmt) bind(args []driver.Value) error {
 		return s.c.lastError()
 		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) {
 		switch v := v.(type) {
 		case nil:
 		case nil:
 			rv = C.sqlite3_bind_null(s.s, n)
 			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) {
 func (s *SQLiteStmt) Exec(args []driver.Value) (driver.Result, error) {
 	if err := s.bind(args); err != nil {
 	if err := s.bind(args); err != nil {
 		C.sqlite3_reset(s.s)
 		C.sqlite3_reset(s.s)
+		C.sqlite3_clear_bindings(s.s)
 		return nil, err
 		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 {
 	if rv != C.SQLITE_ROW && rv != C.SQLITE_OK && rv != C.SQLITE_DONE {
+		err := s.c.lastError()
 		C.sqlite3_reset(s.s)
 		C.sqlite3_reset(s.s)
-		return nil, s.c.lastError()
-	}
-
-	res := &SQLiteResult{
-		int64(C._sqlite3_last_insert_rowid(s.c.db)),
-		int64(C._sqlite3_changes(s.c.db)),
+		C.sqlite3_clear_bindings(s.s)
+		return nil, err
 	}
 	}
-	return res, nil
+	return &SQLiteResult{int64(rowid), int64(changes)}, nil
 }
 }
 
 
 // Close the rows.
 // 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)))
 			val := int64(C.sqlite3_column_int64(rc.s.s, C.int(i)))
 			switch rc.decltype[i] {
 			switch rc.decltype[i] {
 			case "timestamp", "datetime", "date":
 			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":
 			case "boolean":
 				dest[i] = val > 0
 				dest[i] = val > 0
 			default:
 			default:
@@ -531,16 +628,21 @@ func (rc *SQLiteRows) Next(dest []driver.Value) error {
 
 
 			switch rc.decltype[i] {
 			switch rc.decltype[i] {
 			case "timestamp", "datetime", "date":
 			case "timestamp", "datetime", "date":
+				var t time.Time
 				for _, format := range SQLiteTimestampFormats {
 				for _, format := range SQLiteTimestampFormats {
 					if timeVal, err = time.ParseInLocation(format, s, time.UTC); err == nil {
 					if timeVal, err = time.ParseInLocation(format, s, time.UTC); err == nil {
-						dest[i] = timeVal.Local()
+						t = timeVal
 						break
 						break
 					}
 					}
 				}
 				}
 				if err != nil {
 				if err != nil {
 					// The column is a time value, so return the zero time on parse failure.
 					// 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:
 			default:
 				dest[i] = []byte(s)
 				dest[i] = []byte(s)
 			}
 			}

+ 83 - 0
Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3_fts3_test.go

@@ -0,0 +1,83 @@
+// Copyright (C) 2015 Yasuhiro Matsumoto <mattn.jp@gmail.com>.
+//
+// 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")
+	}
+}

+ 1 - 1
Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3_other.go

@@ -9,6 +9,6 @@ package sqlite3
 /*
 /*
 #cgo CFLAGS: -I.
 #cgo CFLAGS: -I.
 #cgo linux LDFLAGS: -ldl
 #cgo linux LDFLAGS: -ldl
-#cgo CFLAGS: -DSQLITE_ENABLE_RTREE -DSQLITE_THREADSAFE
+#cgo LDFLAGS: -lpthread
 */
 */
 import "C"
 import "C"

+ 203 - 0
Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3_test.go

@@ -9,8 +9,10 @@ import (
 	"crypto/rand"
 	"crypto/rand"
 	"database/sql"
 	"database/sql"
 	"encoding/hex"
 	"encoding/hex"
+	"net/url"
 	"os"
 	"os"
 	"path/filepath"
 	"path/filepath"
+	"strings"
 	"testing"
 	"testing"
 	"time"
 	"time"
 
 
@@ -309,6 +311,7 @@ func TestTimestamp(t *testing.T) {
 		{"0000-00-00 00:00:00", time.Time{}},
 		{"0000-00-00 00:00:00", time.Time{}},
 		{timestamp1, timestamp1},
 		{timestamp1, timestamp1},
 		{timestamp1.Unix(), timestamp1},
 		{timestamp1.Unix(), timestamp1},
+		{timestamp1.UnixNano() / int64(time.Millisecond), timestamp1},
 		{timestamp1.In(time.FixedZone("TEST", -7*3600)), timestamp1},
 		{timestamp1.In(time.FixedZone("TEST", -7*3600)), timestamp1},
 		{timestamp1.Format("2006-01-02 15:04:05.000"), timestamp1},
 		{timestamp1.Format("2006-01-02 15:04:05.000"), timestamp1},
 		{timestamp1.Format("2006-01-02T15: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) {
 func TestSuite(t *testing.T) {
 	db, err := sql.Open("sqlite3", ":memory:")
 	db, err := sql.Open("sqlite3", ":memory:")
 	if err != nil {
 	if err != nil {
@@ -742,3 +841,107 @@ func TestStress(t *testing.T) {
 		db.Close()
 		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")
+	}
+}

+ 1 - 1
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
 // Use of this source code is governed by an MIT-style
 // license that can be found in the LICENSE file.
 // license that can be found in the LICENSE file.
+// +build windows
 
 
 package sqlite3
 package sqlite3
 
 
@@ -9,6 +10,5 @@ package sqlite3
 #cgo CFLAGS: -I. -fno-stack-check -fno-stack-protector -mno-stack-arg-probe
 #cgo CFLAGS: -I. -fno-stack-check -fno-stack-protector -mno-stack-arg-probe
 #cgo windows,386 CFLAGS: -D_localtime32=localtime
 #cgo windows,386 CFLAGS: -D_localtime32=localtime
 #cgo LDFLAGS: -lmingwex -lmingw32
 #cgo LDFLAGS: -lmingwex -lmingw32
-#cgo CFLAGS: -DSQLITE_ENABLE_RTREE -DSQLITE_THREADSAFE
 */
 */
 import "C"
 import "C"

+ 1 - 1
Godeps/_workspace/src/github.com/mattn/go-sqlite3/sqlite3ext.h

@@ -17,7 +17,7 @@
 */
 */
 #ifndef _SQLITE3EXT_H_
 #ifndef _SQLITE3EXT_H_
 #define _SQLITE3EXT_H_
 #define _SQLITE3EXT_H_
-#include "sqlite3.h"
+#include "sqlite3-binding.h"
 
 
 typedef struct sqlite3_api_routines sqlite3_api_routines;
 typedef struct sqlite3_api_routines sqlite3_api_routines;
 
 

+ 12 - 12
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)
 [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) |
 [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)
 [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.
 Graphite, InfluxDB & OpenTSDB.
 
 
 ![](http://grafana.org/assets/img/start_page_bg.png)
 ![](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)
 - [See it in action](http://grafana.org/docs/features/graphite)
 
 
 ### Graphing
 ### 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
 - Smart Y-axis formating
 - Series toggles & color selector
 - Series toggles & color selector
-- Legend values, and formating options
+- Legend values, and formatting options
 - Grid thresholds, axis labels
 - Grid thresholds, axis labels
 - [Annotations](http://grafana.org/docs/features/annotations)
 - [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)
 - [Time range controls](http://grafana.org/docs/features/time_range)
 
 
 ### InfluxDB
 ### 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
 - Query editor with series and column typeahead, easy group by and function selection
 
 
 ### OpenTSDB
 ### 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/)
 Head to [grafana.org](http://grafana.org) and [download](http://grafana.org/download/)
 the latest release.
 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).
 read the [troubleshooting guide](http://grafana.org/docs/troubleshooting).
 
 
 ## Documentation & Support
 ## Documentation & Support
@@ -129,12 +129,12 @@ Be sure to read the [getting started guide](http://grafana.org/docs/features/int
 feature guides.
 feature guides.
 
 
 ## Run from master
 ## 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
 - clone repository
 - install nodejs
 - install nodejs
 - npm install (in project root)
 - npm install (in project root)
 - npm install -g grunt-cli
 - 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 build (creates optimized & minified release)
 - grunt release (same as grunt build but will also create tar & zip package)
 - grunt release (same as grunt build but will also create tar & zip package)
 - grunt test (executes jshint and unit tests)
 - grunt test (executes jshint and unit tests)

+ 2 - 2
build.go

@@ -90,8 +90,8 @@ func main() {
 
 
 func makeLatestDistCopies() {
 func makeLatestDistCopies() {
 	runError("cp", "dist/grafana_"+version+"_amd64.deb", "dist/grafana_latest_amd64.deb")
 	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() {
 func readVersionFromPackageJson() {

+ 7 - 1
conf/defaults.ini

@@ -1,6 +1,12 @@
 app_name = Grafana
 app_name = Grafana
 app_mode = production
 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]
 [server]
 ; protocol (http or https)
 ; protocol (http or https)
 protocol = http
 protocol = http
@@ -39,7 +45,7 @@ provider = file
 ; memory: not have any config yet
 ; memory: not have any config yet
 ; file: session file path, e.g. `data/sessions`
 ; 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`
 ; 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
 provider_config = data/sessions
 ; Session cookie name
 ; Session cookie name
 cookie_name = grafana_sess
 cookie_name = grafana_sess

+ 7 - 0
conf/sample.ini

@@ -5,6 +5,13 @@
 
 
 app_mode = production
 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]
 [server]
 ; protocol (http or https)
 ; protocol (http or https)
 protocol = http
 protocol = http

+ 2 - 1
docs/mkdocs.yml

@@ -34,12 +34,13 @@ pages:
 - ['installation/migrating_to2.md', 'Installation', 'Migrating from v1.x to v2.x']
 - ['installation/migrating_to2.md', 'Installation', 'Migrating from v1.x to v2.x']
 
 
 - ['guides/gettingstarted.md', 'User Guides', 'Getting started']
 - ['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']
 - ['guides/screencasts.md', 'User Guides', 'Screencasts']
 
 
 - ['reference/graph.md', 'Reference', 'Graph Panel']
 - ['reference/graph.md', 'Reference', 'Graph Panel']
 - ['reference/singlestat.md', 'Reference', 'Singlestat Panel']
 - ['reference/singlestat.md', 'Reference', 'Singlestat Panel']
 - ['reference/dashlist.md', 'Reference', 'Dashlist Panel']
 - ['reference/dashlist.md', 'Reference', 'Dashlist Panel']
+- ['reference/sharing.md', 'Reference', 'Sharing']
 - ['reference/annotations.md', 'Reference', 'Annotations']
 - ['reference/annotations.md', 'Reference', 'Annotations']
 - ['reference/timerange.md', 'Reference', 'Time range controls']
 - ['reference/timerange.md', 'Reference', 'Time range controls']
 - ['reference/search.md', 'Reference', 'Dashboard Search']
 - ['reference/search.md', 'Reference', 'Dashboard Search']

+ 12 - 1
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.
 This guide will help you get started and acquainted with the Grafana user interface.
 
 
 ## Interface overview
 ## Interface overview
-<img src="/img/v1/interface_guide1.png" class="no-shadow">
+
+### Dashboard header
+<img class="no-shadow" src="/img/v2/v2_top_nav_annotated.png">
+
+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)
+
+<img src="/img/v1/interface_guide1.png" class="no-sthadow">
 
 
 ## New dashboard
 ## New dashboard
 ![](/img/animated_gifs/new_dashboard.gif)
 ![](/img/animated_gifs/new_dashboard.gif)

+ 38 - 19
docs/sources/guides/changes_in_v2.md → 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
 ## New dashboard top header
 
 
 <img class="no-shadow" src="/img/v2/v2_top_nav_annotated.png">
 <img class="no-shadow" src="/img/v2/v2_top_nav_annotated.png">
 
 
 1. Side menu toggle
 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
 > **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.
 > 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
 > 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 <strong>strip sensitive data</strong> 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
 ## Panel time overrides & timeshift
 
 
 In Grafana v2.x you can now override the relative time range for individual panels. You can also add a
 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
 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.
 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
 ## Dashlist panel
 
 
 ![](/img/v2/dashlist_starred.png)
 ![](/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.
 > per series permissions in Graphite, InfluxDB or OpenTSDB.
 
 
 There are currently no permissions on individual dashboards.
 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).
+
+<iframe src="http://snapshot.raintank.io/dashboard/solo/snapshot/UtvRYDv650fHOV2jV5QlAQhLnNOhB5ZN?panelId=4&fullscreen&from=1427385145990&to=1427388745990" width="650" height="300" frameborder="0"></iframe>

+ 1 - 1
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.
 This option should be configured differently depending on what type of session provider you have configured.
 
 
 - **file:** session file path, e.g. `data/sessions`
 - **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.
 if you use mysql or postgres as session store you need to create the session table manually.
 Mysql Example:
 Mysql Example:

+ 1 - 1
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
 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
 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
 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).
 
 
 
 

+ 44 - 0
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 <strong>strip sensitive data</strong> 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
+<iframe src="http://snapshot.raintank.io/dashboard/solo/snapshot/UtvRYDv650fHOV2jV5QlAQhLnNOhB5ZN?panelId=4&fullscreen&from=1427385145990&to=1427388745990" width="650" height="300" frameborder="0"></iframe>
+```
+Below there should be an interactive Grafana graph embedded in an iframe:
+<iframe src="http://snapshot.raintank.io/dashboard/solo/snapshot/UtvRYDv650fHOV2jV5QlAQhLnNOhB5ZN?panelId=4&fullscreen&from=1427385145990&to=1427388745990" width="650" height="300" frameborder="0"></iframe>

+ 1 - 1
latest.json

@@ -1,4 +1,4 @@
 {
 {
 	"version": "1.9.1",
 	"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"
 }
 }

+ 1 - 11
main.go

@@ -39,17 +39,7 @@ func main() {
 	app.Name = "Grafana Backend"
 	app.Name = "Grafana Backend"
 	app.Usage = "grafana web"
 	app.Usage = "grafana web"
 	app.Version = version
 	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{
 	app.Flags = append(app.Flags, []cli.Flag{
 		cli.StringFlag{
 		cli.StringFlag{
 			Name:  "config",
 			Name:  "config",

+ 1 - 1
package.json

@@ -62,7 +62,7 @@
   },
   },
   "license": "Apache License",
   "license": "Apache License",
   "dependencies": {
   "dependencies": {
-    "grunt-jscs": "^0.8.1",
+    "grunt-jscs": "~1.5.x",
     "karma-sinon": "^1.0.3",
     "karma-sinon": "^1.0.3",
     "lodash": "^2.4.1",
     "lodash": "^2.4.1",
     "sinon": "^1.10.3"
     "sinon": "^1.10.3"

+ 3 - 0
pkg/api/admin_users.go

@@ -3,6 +3,7 @@ package api
 import (
 import (
 	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/middleware"
 	"github.com/grafana/grafana/pkg/middleware"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/util"
 	"github.com/grafana/grafana/pkg/util"
@@ -64,6 +65,8 @@ func AdminCreateUser(c *middleware.Context, form dtos.AdminCreateUserForm) {
 		return
 		return
 	}
 	}
 
 
+	metrics.M_Api_Admin_User_Create.Inc(1)
+
 	c.JsonOK("User created")
 	c.JsonOK("User created")
 }
 }
 
 

+ 7 - 0
pkg/api/api.go

@@ -41,6 +41,13 @@ func Register(r *macaron.Macaron) {
 	r.Get("/signup", Index)
 	r.Get("/signup", Index)
 	r.Post("/api/user/signup", bind(m.CreateUserCommand{}), SignUp)
 	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
 	// authed api
 	r.Group("/api", func() {
 	r.Group("/api", func() {
 		// user
 		// user

+ 5 - 0
pkg/api/dashboard.go

@@ -7,6 +7,7 @@ import (
 
 
 	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/middleware"
 	"github.com/grafana/grafana/pkg/middleware"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
@@ -27,6 +28,8 @@ func isDasboardStarredByUser(c *middleware.Context, dashId int64) (bool, error)
 }
 }
 
 
 func GetDashboard(c *middleware.Context) {
 func GetDashboard(c *middleware.Context) {
+	metrics.M_Api_Dashboard_Get.Inc(1)
+
 	slug := c.Params(":slug")
 	slug := c.Params(":slug")
 
 
 	query := m.GetDashboardQuery{Slug: slug, OrgId: c.OrgId}
 	query := m.GetDashboardQuery{Slug: slug, OrgId: c.OrgId}
@@ -88,6 +91,8 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) {
 		return
 		return
 	}
 	}
 
 
+	metrics.M_Api_Dashboard_Post.Inc(1)
+
 	c.JSON(200, util.DynMap{"status": "success", "slug": cmd.Result.Slug, "version": cmd.Result.Version})
 	c.JSON(200, util.DynMap{"status": "success", "slug": cmd.Result.Slug, "version": cmd.Result.Version})
 }
 }
 
 

+ 87 - 0
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."})
+}

+ 4 - 3
pkg/api/dtos/models.go

@@ -27,9 +27,10 @@ type CurrentUser struct {
 }
 }
 
 
 type DashboardMeta 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 {
 type Dashboard struct {

+ 1 - 1
pkg/api/index.go

@@ -47,7 +47,7 @@ func Index(c *middleware.Context) {
 
 
 func NotFound(c *middleware.Context) {
 func NotFound(c *middleware.Context) {
 	if c.IsApiRequest() {
 	if c.IsApiRequest() {
-		c.JsonApiErr(200, "Not found", nil)
+		c.JsonApiErr(404, "Not found", nil)
 		return
 		return
 	}
 	}
 
 

+ 3 - 1
pkg/api/login.go

@@ -6,6 +6,7 @@ import (
 	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/middleware"
 	"github.com/grafana/grafana/pkg/middleware"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
@@ -75,7 +76,6 @@ func LoginView(c *middleware.Context) {
 }
 }
 
 
 func LoginPost(c *middleware.Context, cmd dtos.LoginCommand) {
 func LoginPost(c *middleware.Context, cmd dtos.LoginCommand) {
-
 	userQuery := m.GetUserByLoginQuery{LoginOrEmail: cmd.User}
 	userQuery := m.GetUserByLoginQuery{LoginOrEmail: cmd.User}
 	err := bus.Dispatch(&userQuery)
 	err := bus.Dispatch(&userQuery)
 
 
@@ -112,6 +112,8 @@ func LoginPost(c *middleware.Context, cmd dtos.LoginCommand) {
 		c.SetCookie("redirect_to", "", -1, setting.AppSubUrl+"/")
 		c.SetCookie("redirect_to", "", -1, setting.AppSubUrl+"/")
 	}
 	}
 
 
+	metrics.M_Api_Login_Post.Inc(1)
+
 	c.JSON(200, result)
 	c.JSON(200, result)
 }
 }
 
 

+ 3 - 0
pkg/api/login_oauth.go

@@ -8,6 +8,7 @@ import (
 
 
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/middleware"
 	"github.com/grafana/grafana/pkg/middleware"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
@@ -81,5 +82,7 @@ func OAuthLogin(ctx *middleware.Context) {
 	// login
 	// login
 	loginUserWithUser(userQuery.Result, ctx)
 	loginUserWithUser(userQuery.Result, ctx)
 
 
+	metrics.M_Api_Login_OAuth.Inc(1)
+
 	ctx.Redirect(setting.AppSubUrl + "/")
 	ctx.Redirect(setting.AppSubUrl + "/")
 }
 }

+ 3 - 0
pkg/api/org.go

@@ -2,6 +2,7 @@ package api
 
 
 import (
 import (
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/middleware"
 	"github.com/grafana/grafana/pkg/middleware"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
 )
 )
@@ -35,6 +36,8 @@ func CreateOrg(c *middleware.Context, cmd m.CreateOrgCommand) {
 		return
 		return
 	}
 	}
 
 
+	metrics.M_Api_Org_Create.Inc(1)
+
 	c.JsonOK("Organization created")
 	c.JsonOK("Organization created")
 }
 }
 
 

+ 3 - 0
pkg/api/signup.go

@@ -2,6 +2,7 @@ package api
 
 
 import (
 import (
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/middleware"
 	"github.com/grafana/grafana/pkg/middleware"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
@@ -26,4 +27,6 @@ func SignUp(c *middleware.Context, cmd m.CreateUserCommand) {
 	loginUserWithUser(&user, c)
 	loginUserWithUser(&user, c)
 
 
 	c.JsonOK("User created and logged in")
 	c.JsonOK("User created and logged in")
+
+	metrics.M_Api_User_SignUp.Inc(1)
 }
 }

+ 218 - 0
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
+			}
+		}
+	}
+}

+ 19 - 6
pkg/cmd/web.go

@@ -11,7 +11,6 @@ import (
 	"path"
 	"path"
 	"path/filepath"
 	"path/filepath"
 	"strconv"
 	"strconv"
-	"time"
 
 
 	"github.com/Unknwon/macaron"
 	"github.com/Unknwon/macaron"
 	"github.com/codegangsta/cli"
 	"github.com/codegangsta/cli"
@@ -20,7 +19,9 @@ import (
 	_ "github.com/macaron-contrib/session/postgres"
 	_ "github.com/macaron-contrib/session/postgres"
 
 
 	"github.com/grafana/grafana/pkg/api"
 	"github.com/grafana/grafana/pkg/api"
+	"github.com/grafana/grafana/pkg/api/static"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/middleware"
 	"github.com/grafana/grafana/pkg/middleware"
 	"github.com/grafana/grafana/pkg/plugins"
 	"github.com/grafana/grafana/pkg/plugins"
 	"github.com/grafana/grafana/pkg/services/eventpublisher"
 	"github.com/grafana/grafana/pkg/services/eventpublisher"
@@ -64,14 +65,22 @@ func newMacaron() *macaron.Macaron {
 }
 }
 
 
 func mapStatic(m *macaron.Macaron, dir string, prefix string) {
 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),
 		path.Join(setting.StaticRootPath, dir),
-		macaron.StaticOptions{
+		httpstatic.StaticOptions{
 			SkipLogging: true,
 			SkipLogging: true,
 			Prefix:      prefix,
 			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()
 	m := newMacaron()
 	api.Register(m)
 	api.Register(m)
 
 
+	if setting.ReportingEnabled {
+		go metrics.StartUsageReportLoop()
+	}
+
 	listenAddr := fmt.Sprintf("%s:%s", setting.HttpAddr, setting.HttpPort)
 	listenAddr := fmt.Sprintf("%s:%s", setting.HttpAddr, setting.HttpPort)
 	log.Info("Listen: %v://%s%s", setting.Protocol, listenAddr, setting.AppSubUrl)
 	log.Info("Listen: %v://%s%s", setting.Protocol, listenAddr, setting.AppSubUrl)
 	switch setting.Protocol {
 	switch setting.Protocol {

+ 72 - 0
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())
+}

+ 39 - 0
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")
+}

+ 29 - 0
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")
+)

+ 102 - 0
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
+}

+ 64 - 0
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)
+}

+ 12 - 0
pkg/middleware/middleware.go

@@ -10,6 +10,7 @@ import (
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/components/apikeygen"
 	"github.com/grafana/grafana/pkg/components/apikeygen"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/metrics"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/setting"
 	"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.Data["Title"] = title
 	ctx.HTML(status, strconv.Itoa(status))
 	ctx.HTML(status, strconv.Itoa(status))
 }
 }
@@ -128,7 +138,9 @@ func (ctx *Context) JsonApiErr(status int, message string, err error) {
 	switch status {
 	switch status {
 	case 404:
 	case 404:
 		resp["message"] = "Not Found"
 		resp["message"] = "Not Found"
+		metrics.M_Api_Status_500.Inc(1)
 	case 500:
 	case 500:
+		metrics.M_Api_Status_404.Inc(1)
 		resp["message"] = "Internal Server Error"
 		resp["message"] = "Internal Server Error"
 	}
 	}
 
 

+ 49 - 0
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
+}

+ 4 - 0
pkg/models/models.go

@@ -1,5 +1,7 @@
 package models
 package models
 
 
+import "errors"
+
 type OAuthType int
 type OAuthType int
 
 
 const (
 const (
@@ -7,3 +9,5 @@ const (
 	GOOGLE
 	GOOGLE
 	TWITTER
 	TWITTER
 )
 )
+
+var ErrNotFound = errors.New("Not found")

+ 11 - 0
pkg/models/stats.go

@@ -0,0 +1,11 @@
+package models
+
+type SystemStats struct {
+	DashboardCount int
+	UserCount      int
+	OrgCount       int
+}
+
+type GetSystemStatsQuery struct {
+	Result *SystemStats
+}

+ 1 - 1
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() {
 			Convey("Should be able to get key by name", func() {
 				query := m.GetApiKeyByNameQuery{KeyName: "hello", OrgId: 1}
 				query := m.GetApiKeyByNameQuery{KeyName: "hello", OrgId: 1}
 				err = GetApiKeyByName(&query)
 				err = GetApiKeyByName(&query)
-				So(err, ShouldBeNil)
 
 
+				So(err, ShouldBeNil)
 				So(query.Result, ShouldNotBeNil)
 				So(query.Result, ShouldNotBeNil)
 			})
 			})
 
 

+ 2 - 0
pkg/services/sqlstore/dashboard.go

@@ -6,6 +6,7 @@ import (
 
 
 	"github.com/go-xorm/xorm"
 	"github.com/go-xorm/xorm"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/metrics"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
 )
 )
 
 
@@ -48,6 +49,7 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
 		}
 		}
 
 
 		if dash.Id == 0 {
 		if dash.Id == 0 {
+			metrics.M_Models_Dashboard_Insert.Inc(1)
 			_, err = sess.Insert(dash)
 			_, err = sess.Insert(dash)
 		} else {
 		} else {
 			dash.Version += 1
 			dash.Version += 1

+ 65 - 0
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
+}

+ 37 - 0
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")
+			})
+
+		})
+	})
+}

+ 51 - 0
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)
+}

+ 1 - 1
pkg/services/sqlstore/migrations/datasource_mig.go

@@ -95,5 +95,5 @@ func addDataSourceMigration(mg *Migrator) {
 		"updated":             "updated",
 		"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"))
 }
 }

+ 1 - 0
pkg/services/sqlstore/migrations/migrations.go

@@ -15,6 +15,7 @@ func AddMigrations(mg *Migrator) {
 	addDashboardMigration(mg)
 	addDashboardMigration(mg)
 	addDataSourceMigration(mg)
 	addDataSourceMigration(mg)
 	addApiKeyMigrations(mg)
 	addApiKeyMigrations(mg)
+	addDashboardSnapshotMigrations(mg)
 }
 }
 
 
 func addMigrationLogMigrations(mg *Migrator) {
 func addMigrationLogMigrations(mg *Migrator) {

+ 8 - 11
pkg/services/sqlstore/sqlstore.go

@@ -22,7 +22,6 @@ import (
 var (
 var (
 	x       *xorm.Engine
 	x       *xorm.Engine
 	dialect migrator.Dialect
 	dialect migrator.Dialect
-	tables  []interface{}
 
 
 	HasEngine bool
 	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)
 		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 {
 	if enableLog {
 		logPath := path.Join(setting.LogRootPath, "xorm.log")
 		logPath := path.Join(setting.LogRootPath, "xorm.log")
 		os.MkdirAll(path.Dir(logPath), os.ModePerm)
 		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.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
 	return nil
@@ -125,7 +122,7 @@ func getEngine() (*xorm.Engine, error) {
 			DbCfg.User, DbCfg.Pwd, host, port, DbCfg.Name, DbCfg.SslMode)
 			DbCfg.User, DbCfg.Pwd, host, port, DbCfg.Name, DbCfg.SslMode)
 	case "sqlite3":
 	case "sqlite3":
 		os.MkdirAll(path.Dir(DbCfg.Path), os.ModePerm)
 		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:
 	default:
 		return nil, fmt.Errorf("Unknown database type: %s", DbCfg.Type)
 		return nil, fmt.Errorf("Unknown database type: %s", DbCfg.Type)
 	}
 	}

+ 1 - 0
pkg/services/sqlstore/sqlstore.goconvey

@@ -0,0 +1 @@
+-timeout=10s

+ 1 - 1
pkg/services/sqlstore/sqlutil/sqlutil.go

@@ -11,7 +11,7 @@ type TestDB struct {
 	ConnStr    string
 	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_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"}
 var TestDB_Postgres = TestDB{DriverName: "postgres", ConnStr: "user=grafanatest password=grafanatest host=localhost port=5432 dbname=grafanatest sslmode=disable"}
 
 

+ 36 - 0
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
+}

+ 0 - 0
pkg/services/sqlstore/xorm.log


+ 4 - 0
pkg/setting/setting.go

@@ -96,6 +96,8 @@ var (
 	PhantomDir string
 	PhantomDir string
 
 
 	configFiles []string
 	configFiles []string
+
+	ReportingEnabled bool
 )
 )
 
 
 func init() {
 func init() {
@@ -233,6 +235,8 @@ func NewConfigContext(config string) {
 	ImagesDir = "data/png"
 	ImagesDir = "data/png"
 	PhantomDir = "vendor/phantomjs"
 	PhantomDir = "vendor/phantomjs"
 
 
+	ReportingEnabled = Cfg.Section("").Key("reporting-enabled").MustBool(true)
+
 	readSessionConfig()
 	readSessionConfig()
 }
 }
 
 

+ 3 - 1
src/app/directives/graphiteSegment.js

@@ -37,12 +37,14 @@ function (angular, app, _, $) {
               if (selected) {
               if (selected) {
                 segment.value = selected.value;
                 segment.value = selected.value;
                 segment.html = selected.html;
                 segment.html = selected.html;
+                segment.fake = false;
                 segment.expandable = selected.expandable;
                 segment.expandable = selected.expandable;
               }
               }
               else {
               else {
                 segment.value = value;
                 segment.value = value;
                 segment.html = $sce.trustAsHtml(value);
                 segment.html = $sce.trustAsHtml(value);
                 segment.expandable = true;
                 segment.expandable = true;
+                segment.fake = false;
               }
               }
               $scope.segmentValueChanged(segment, $scope.$index);
               $scope.segmentValueChanged(segment, $scope.$index);
             });
             });
@@ -71,7 +73,7 @@ function (angular, app, _, $) {
                 options = _.map($scope.altSegments, function(alt) { return alt.value; });
                 options = _.map($scope.altSegments, function(alt) { return alt.value; });
 
 
                 // add custom values
                 // 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);
                   options.unshift(segment.value);
                 }
                 }
 
 

+ 4 - 82
src/app/directives/templateParamSelector.js

@@ -4,87 +4,9 @@ define([
   'lodash',
   'lodash',
   'jquery',
   'jquery',
 ],
 ],
-function (angular, app, _, $) {
+function (angular, app, _) {
   'use strict';
   'use strict';
 
 
-  angular
-    .module('grafana.directives')
-    .directive('templateParamSelector', function($compile) {
-      var inputTemplate = '<input type="text" data-provide="typeahead" ' +
-                            ' class="tight-form-clear-input input-medium"' +
-                            ' spellcheck="false" style="display:none"></input>';
-
-      var buttonTemplate = '<a  class="tight-form-item tabindex="1">{{variable.current.text}} <i class="fa fa-caret-down"></i></a>';
-
-      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
   angular
     .module('grafana.directives')
     .module('grafana.directives')
     .directive('variableValueSelect', function($compile, $window, $timeout) {
     .directive('variableValueSelect', function($compile, $window, $timeout) {
@@ -162,9 +84,9 @@ function (angular, app, _, $) {
 
 
           scope.hide = function() {
           scope.hide = function() {
             scope.selectorOpen = false;
             scope.selectorOpen = false;
-            if (scope.oldCurrentText !== variable.current.text) {
-              scope.onUpdated();
-            }
+            // if (scope.oldCurrentText !== variable.current.text) {
+            //   scope.onUpdated();
+            // }
 
 
             bodyEl.off('click', scope.bodyOnClick);
             bodyEl.off('click', scope.bodyOnClick);
           };
           };

+ 1 - 0
src/app/features/dashboard/all.js

@@ -5,6 +5,7 @@ define([
   './playlistCtrl',
   './playlistCtrl',
   './rowCtrl',
   './rowCtrl',
   './sharePanelCtrl',
   './sharePanelCtrl',
+  './shareSnapshotCtrl',
   './submenuCtrl',
   './submenuCtrl',
   './dashboardSrv',
   './dashboardSrv',
   './keybindings',
   './keybindings',

+ 30 - 1
src/app/features/dashboard/dashboardCtrl.js

@@ -18,6 +18,7 @@ function (angular, $, config) {
       dynamicDashboardSrv,
       dynamicDashboardSrv,
       dashboardSrv,
       dashboardSrv,
       dashboardViewStateSrv,
       dashboardViewStateSrv,
+      contextSrv,
       $timeout) {
       $timeout) {
 
 
     $scope.editor = { index: 0 };
     $scope.editor = { index: 0 };
@@ -49,7 +50,7 @@ function (angular, $, config) {
 
 
         $scope.dashboard = dashboard;
         $scope.dashboard = dashboard;
         $scope.dashboardViewState = dashboardViewStateSrv.create($scope);
         $scope.dashboardViewState = dashboardViewStateSrv.create($scope);
-        $scope.dashboardMeta = data.meta;
+        $scope.initDashboardMeta(data.meta, $scope.dashboard);
 
 
         dashboardKeybindings.shortcuts($scope);
         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.updateSubmenuVisibility = function() {
       $scope.submenuEnabled = $scope.dashboard.hasTemplateVarsOrAnnotations();
       $scope.submenuEnabled = $scope.dashboard.hasTemplateVarsOrAnnotations();
     };
     };
@@ -135,4 +163,5 @@ function (angular, $, config) {
     };
     };
 
 
   });
   });
+
 });
 });

+ 13 - 1
src/app/features/dashboard/dashboardNavCtrl.js

@@ -11,7 +11,7 @@ function (angular, _, moment) {
 
 
   var module = angular.module('grafana.controllers');
   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.init = function() {
       $scope.onAppEvent('save-dashboard', $scope.saveDashboard);
       $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.editJson = function() {
       $scope.appEvent('show-json-editor', { object: $scope.dashboard });
       $scope.appEvent('show-json-editor', { object: $scope.dashboard });
     };
     };

+ 11 - 0
src/app/features/dashboard/dashboardSrv.js

@@ -37,6 +37,7 @@ function (angular, $, kbn, _, moment) {
       this.templating = this._ensureListExist(data.templating);
       this.templating = this._ensureListExist(data.templating);
       this.annotations = this._ensureListExist(data.annotations);
       this.annotations = this._ensureListExist(data.annotations);
       this.refresh = data.refresh;
       this.refresh = data.refresh;
+      this.snapshot = data.snapshot;
       this.schemaVersion = data.schemaVersion || 0;
       this.schemaVersion = data.schemaVersion || 0;
       this.version = data.version || 0;
       this.version = data.version || 0;
 
 
@@ -67,6 +68,16 @@ function (angular, $, kbn, _, moment) {
       return max + 1;
       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) {
     p.rowSpan = function(row) {
       return _.reduce(row.panels, function(p,v) {
       return _.reduce(row.panels, function(p,v) {
         return p + v.span;
         return p + v.span;

+ 2 - 1
src/app/features/dashboard/dynamicDashboardSrv.js

@@ -103,11 +103,12 @@ function (angular, _) {
         }
         }
 
 
         for (i = 0; i < copy.panels.length; i++) {
         for (i = 0; i < copy.panels.length; i++) {
-          panel = row.panels[i];
+          panel = copy.panels[i];
           panel.scopedVars = panel.scopedVars || {};
           panel.scopedVars = panel.scopedVars || {};
           panel.scopedVars[variable.name] = option;
           panel.scopedVars[variable.name] = option;
         }
         }
       });
       });
+
     };
     };
 
 
     this.repeatPanel = function(panel, row, dashboard) {
     this.repeatPanel = function(panel, row, dashboard) {

+ 73 - 3
src/app/features/dashboard/partials/shareDashboard.html

@@ -1,12 +1,12 @@
 <div class="modal-body gf-box gf-box-no-margin" ng-controller="SharePanelCtrl">
 <div class="modal-body gf-box gf-box-no-margin" ng-controller="SharePanelCtrl">
 	<div class="gf-box-header">
 	<div class="gf-box-header">
 		<div class="gf-box-title">
 		<div class="gf-box-title">
-			<i class="fa fa-share"></i>
-			Share
+			<i class="fa fa-share-square-o"></i>
+			Share Dashboard
 		</div>
 		</div>
 
 
 		<div ng-model="editor.index" bs-tabs style="text-transform:capitalize;">
 		<div ng-model="editor.index" bs-tabs style="text-transform:capitalize;">
-			<div ng-repeat="tab in ['Link']" data-title="{{tab}}">
+			<div ng-repeat="tab in ['Link', 'Snapshot sharing']" data-title="{{tab}}">
 			</div>
 			</div>
 		</div>
 		</div>
 
 
@@ -45,4 +45,74 @@
 		</div>
 		</div>
 	</div>
 	</div>
 
 
+	<div class="gf-box-body share-snapshot ng-cloak" ng-cloak ng-if="editor.index === 1" ng-controller="ShareSnapshotCtrl">
+
+		<div class="share-snapshot-header" ng-if="step === 1">
+			<i ng-if="loading" class="fa fa-spinner fa-spin"></i>
+			<p class="share-snapshot-info-text">
+				A snapshot is an instant way to share an interactive dashboard publicly.
+				When created, we <strong>strip sensitive data</strong> like queries (metric, template and annotation) and panel links,
+				leaving only the visible metric data and series names embedded into your dashboard.
+			</p>
+			<p class="share-snapshot-info-text">
+				Keep in mind, your <strong>snapshot can be viewed by anyone</strong> that has the link and can reach the URL.
+				Share wisely.
+			</p>
+		</div>
+
+		<div class="editor-row" style="margin: 11px 20px 33px 20px">
+			<div class="section" ng-if="step === 1">
+				<div class="tight-form">
+					<ul class="tight-form-list">
+						<li class="tight-form-item" style="width: 110px;">
+							Snapshot name
+						</li>
+						<li>
+							<input type="text" ng-model="snapshot.name" class="input-large tight-form-input last" >
+						</li>
+					</ul>
+					<div class="clearfix"></div>
+				</div>
+				<div class="tight-form">
+					<ul class="tight-form-list">
+						<li class="tight-form-item" style="width: 110px">
+							Expire
+						</li>
+						<li>
+							<select class="input-small tight-form-input last" style="width: 211px" ng-model="snapshot.expires" ng-options="f.value as f.text for f in expireOptions"></select>
+						</li>
+					</ul>
+					<div class="clearfix"></div>
+				</div>
+			</div>
+
+			<div class="gf-form" ng-if="step === 2" style="margin-top: 55px">
+				<div class="gf-form-row">
+					<a href="{{snapshotUrl}}" class="large share-snapshot-link" target="_blank">
+						<i class="fa fa-external-link-square"></i>
+						{{snapshotUrl}}
+					</a>
+					<br>
+					<button class="btn btn-inverse btn-large" data-clipboard-text="{{snapshotUrl}}" clipboard-button><i class="fa fa-clipboard"></i> Copy Link</button>
+				</div>
+			</div>
+		</div>
+
+		<div ng-if="step === 1">
+			<button class="btn btn-success btn-large" ng-click="createSnapshot()" ng-disabled="loading">
+				<i class="fa fa-save"></i>
+				Local Snapshot
+			</button>
+
+			<button class="btn btn-primary btn-large" ng-click="createSnapshot(true)" ng-disabled="loading">
+				<i class="fa fa-cloud-upload"></i>
+				Publish to snapshot.raintank.io
+			</button>
+		</div>
+
+		<div class="pull-right" ng-if="step === 2" style="padding: 5px">
+			Did you make a mistake? <a href="{{deleteUrl}}" target="_blank">delete snapshot.</a>
+		</div>
+	</div>
 </div>
 </div>
+

+ 10 - 9
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.add_panel = function(panel) {
       $scope.dashboard.add_panel(panel, $scope.row);
       $scope.dashboard.add_panel(panel, $scope.row);
     };
     };
@@ -81,17 +76,21 @@ function (angular, app, _, config) {
       $scope.$broadcast('render');
       $scope.$broadcast('render');
     };
     };
 
 
-    $scope.remove_panel_from_row = function(row, panel) {
+    $scope.removePanel = function(panel) {
       $scope.appEvent('confirm-modal', {
       $scope.appEvent('confirm-modal', {
         title: 'Are you sure you want to remove this panel?',
         title: 'Are you sure you want to remove this panel?',
         icon: 'fa-trash',
         icon: 'fa-trash',
         yesText: 'Delete',
         yesText: 'Delete',
         onConfirm: function() {
         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) {
     $scope.replacePanel = function(newPanel, oldPanel) {
       var row = $scope.row;
       var row = $scope.row;
       var index = _.indexOf(row.panels, oldPanel);
       var index = _.indexOf(row.panels, oldPanel);
@@ -144,9 +143,11 @@ function (angular, app, _, config) {
 
 
   module.directive('panelWidth', function() {
   module.directive('panelWidth', function() {
     return function(scope, element) {
     return function(scope, element) {
-      scope.$watch('panel.span', function() {
+      function updateWidth() {
         element[0].style.width = ((scope.panel.span / 1.2) * 10) + '%';
         element[0].style.width = ((scope.panel.span / 1.2) * 10) + '%';
-      });
+      }
+
+      scope.$watch('panel.span', updateWidth);
     };
     };
   });
   });
 
 

+ 9 - 5
src/app/features/dashboard/sharePanelCtrl.js

@@ -9,7 +9,7 @@ function (angular, _, require, config) {
 
 
   var module = angular.module('grafana.controllers');
   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.init = function() {
       $scope.editor = { index: 0 };
       $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 = '<iframe src="' + $scope.soloUrl + '" width="450" height="200" frameborder="0"></iframe>';
+      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 = '<iframe src="' + soloUrl + '" width="450" height="200" frameborder="0"></iframe>';
+
+      $scope.imageUrl = soloUrl.replace('/dashboard/', '/render/dashboard/');
       $scope.imageUrl += '&width=1000';
       $scope.imageUrl += '&width=1000';
       $scope.imageUrl += '&height=500';
       $scope.imageUrl += '&height=500';
     };
     };

+ 111 - 0
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);
+    };
+
+  });
+
+});

+ 7 - 1
src/app/features/panel/panelHelper.js

@@ -72,7 +72,13 @@ function (angular, _, kbn, $) {
         cacheTimeout: scope.panel.cacheTimeout
         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;
+      });
     };
     };
 
 
   });
   });

+ 1 - 1
src/app/features/panel/panelMenu.js

@@ -22,7 +22,7 @@ function (angular, $, _) {
         template += '<div class="panel-menu-row">';
         template += '<div class="panel-menu-row">';
         template += '<a class="panel-menu-icon pull-left" ng-click="updateColumnSpan(-1)"><i class="fa fa-minus"></i></a>';
         template += '<a class="panel-menu-icon pull-left" ng-click="updateColumnSpan(-1)"><i class="fa fa-minus"></i></a>';
         template += '<a class="panel-menu-icon pull-left" ng-click="updateColumnSpan(1)"><i class="fa fa-plus"></i></a>';
         template += '<a class="panel-menu-icon pull-left" ng-click="updateColumnSpan(1)"><i class="fa fa-plus"></i></a>';
-        template += '<a class="panel-menu-icon pull-right" ng-click="remove_panel_from_row(row, panel)"><i class="fa fa-remove"></i></a>';
+        template += '<a class="panel-menu-icon pull-right" ng-click="removePanel(panel)"><i class="fa fa-remove"></i></a>';
         template += '<div class="clearfix"></div>';
         template += '<div class="clearfix"></div>';
         template += '</div>';
         template += '</div>';
 
 

+ 8 - 1
src/app/features/panel/panelSrv.js

@@ -41,7 +41,7 @@ function (angular, _, config) {
       };
       };
 
 
       $scope.updateColumnSpan = function(span) {
       $scope.updateColumnSpan = function(span) {
-        $scope.panel.span = Math.min(Math.max($scope.panel.span + span, 1), 12);
+        $scope.updatePanelSpan($scope.panel, span);
 
 
         $timeout(function() {
         $timeout(function() {
           $scope.$broadcast('render');
           $scope.$broadcast('render');
@@ -94,6 +94,13 @@ function (angular, _, config) {
       $scope.get_data = function() {
       $scope.get_data = function() {
         if ($scope.otherPanelInFullscreenMode()) { return; }
         if ($scope.otherPanelInFullscreenMode()) { return; }
 
 
+        if ($scope.panel.snapshotData) {
+          if ($scope.loadSnapshot) {
+            $scope.loadSnapshot($scope.panel.snapshotData);
+          }
+          return;
+        }
+
         delete $scope.panelMeta.error;
         delete $scope.panelMeta.error;
         $scope.panelMeta.loading = true;
         $scope.panelMeta.loading = true;
 
 

+ 22 - 16
src/app/features/panel/soloPanelCtrl.js

@@ -7,16 +7,15 @@ function (angular, $) {
 
 
   var module = angular.module('grafana.routes');
   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;
     var panelId;
 
 
@@ -26,12 +25,19 @@ function (angular, $) {
       var params = $location.search();
       var params = $location.search();
       panelId = parseInt(params.panelId);
       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) {
     $scope.initPanelScope = function(dashboard) {

+ 9 - 2
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({
     $scope.panelMeta = new PanelMeta({
       panelName: 'Graph',
       panelName: 'Graph',
@@ -116,7 +116,7 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) {
     _.defaults($scope.panel.grid, _d.grid);
     _.defaults($scope.panel.grid, _d.grid);
     _.defaults($scope.panel.legend, _d.legend);
     _.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.hiddenSeries = {};
     $scope.seriesList = [];
     $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) {
     $scope.dataHandler = function(results) {
       // png renderer returns just a url
       // png renderer returns just a url
       if (_.isString(results)) {
       if (_.isString(results)) {
@@ -285,6 +291,7 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) {
     };
     };
 
 
     panelSrv.init($scope);
     panelSrv.init($scope);
+
   });
   });
 
 
 });
 });

Неке датотеке нису приказане због велике количине промена