mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-06-02 12:52:12 +00:00
Integrate public as bindata optionally (#293)
* Dropped unused codekit config * Integrated dynamic and static bindata for public * Ignore public bindata * Add a general generate make task * Integrated flexible public assets into web command * Updated vendoring, added all missiong govendor deps * Made the linter happy with the bindata and dynamic code * Moved public bindata definition to modules directory * Ignoring the new bindata path now * Updated to the new public modules import path * Updated public bindata command and drop the new prefix
This commit is contained in:
parent
4680c349dd
commit
b6a95a8cb3
691 changed files with 305318 additions and 1272 deletions
499
vendor/github.com/pingcap/tidb/infoschema/infoschema.go
generated
vendored
Normal file
499
vendor/github.com/pingcap/tidb/infoschema/infoschema.go
generated
vendored
Normal file
|
@ -0,0 +1,499 @@
|
|||
// Copyright 2015 PingCAP, Inc.
|
||||
//
|
||||
// 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,
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package infoschema
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/juju/errors"
|
||||
"github.com/pingcap/tidb/kv"
|
||||
"github.com/pingcap/tidb/meta"
|
||||
"github.com/pingcap/tidb/meta/autoid"
|
||||
"github.com/pingcap/tidb/model"
|
||||
"github.com/pingcap/tidb/mysql"
|
||||
"github.com/pingcap/tidb/perfschema"
|
||||
"github.com/pingcap/tidb/table"
|
||||
"github.com/pingcap/tidb/terror"
|
||||
// import table implementation to init table.TableFromMeta
|
||||
_ "github.com/pingcap/tidb/table/tables"
|
||||
"github.com/pingcap/tidb/util/types"
|
||||
)
|
||||
|
||||
// InfoSchema is the interface used to retrieve the schema information.
|
||||
// It works as a in memory cache and doesn't handle any schema change.
|
||||
// InfoSchema is read-only, and the returned value is a copy.
|
||||
// TODO: add more methods to retrieve tables and columns.
|
||||
type InfoSchema interface {
|
||||
SchemaByName(schema model.CIStr) (*model.DBInfo, bool)
|
||||
SchemaExists(schema model.CIStr) bool
|
||||
TableByName(schema, table model.CIStr) (table.Table, error)
|
||||
TableExists(schema, table model.CIStr) bool
|
||||
ColumnByName(schema, table, column model.CIStr) (*model.ColumnInfo, bool)
|
||||
ColumnExists(schema, table, column model.CIStr) bool
|
||||
IndexByName(schema, table, index model.CIStr) (*model.IndexInfo, bool)
|
||||
SchemaByID(id int64) (*model.DBInfo, bool)
|
||||
TableByID(id int64) (table.Table, bool)
|
||||
AllocByID(id int64) (autoid.Allocator, bool)
|
||||
ColumnByID(id int64) (*model.ColumnInfo, bool)
|
||||
ColumnIndicesByID(id int64) ([]*model.IndexInfo, bool)
|
||||
AllSchemaNames() []string
|
||||
AllSchemas() []*model.DBInfo
|
||||
Clone() (result []*model.DBInfo)
|
||||
SchemaTables(schema model.CIStr) []table.Table
|
||||
SchemaMetaVersion() int64
|
||||
}
|
||||
|
||||
// Infomation Schema Name.
|
||||
const (
|
||||
Name = "INFORMATION_SCHEMA"
|
||||
)
|
||||
|
||||
type infoSchema struct {
|
||||
schemaNameToID map[string]int64
|
||||
tableNameToID map[tableName]int64
|
||||
columnNameToID map[columnName]int64
|
||||
schemas map[int64]*model.DBInfo
|
||||
tables map[int64]table.Table
|
||||
tableAllocators map[int64]autoid.Allocator
|
||||
columns map[int64]*model.ColumnInfo
|
||||
indices map[indexName]*model.IndexInfo
|
||||
columnIndices map[int64][]*model.IndexInfo
|
||||
|
||||
// We should check version when change schema.
|
||||
schemaMetaVersion int64
|
||||
}
|
||||
|
||||
var _ InfoSchema = (*infoSchema)(nil)
|
||||
|
||||
type tableName struct {
|
||||
schema string
|
||||
table string
|
||||
}
|
||||
|
||||
type columnName struct {
|
||||
tableName
|
||||
name string
|
||||
}
|
||||
|
||||
type indexName struct {
|
||||
tableName
|
||||
name string
|
||||
}
|
||||
|
||||
func (is *infoSchema) SchemaByName(schema model.CIStr) (val *model.DBInfo, ok bool) {
|
||||
id, ok := is.schemaNameToID[schema.L]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
val, ok = is.schemas[id]
|
||||
return
|
||||
}
|
||||
|
||||
func (is *infoSchema) SchemaMetaVersion() int64 {
|
||||
return is.schemaMetaVersion
|
||||
}
|
||||
|
||||
func (is *infoSchema) SchemaExists(schema model.CIStr) bool {
|
||||
_, ok := is.schemaNameToID[schema.L]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (is *infoSchema) TableByName(schema, table model.CIStr) (t table.Table, err error) {
|
||||
id, ok := is.tableNameToID[tableName{schema: schema.L, table: table.L}]
|
||||
if !ok {
|
||||
return nil, TableNotExists.Gen("table %s.%s does not exist", schema, table)
|
||||
}
|
||||
t = is.tables[id]
|
||||
return
|
||||
}
|
||||
|
||||
func (is *infoSchema) TableExists(schema, table model.CIStr) bool {
|
||||
_, ok := is.tableNameToID[tableName{schema: schema.L, table: table.L}]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (is *infoSchema) ColumnByName(schema, table, column model.CIStr) (val *model.ColumnInfo, ok bool) {
|
||||
id, ok := is.columnNameToID[columnName{tableName: tableName{schema: schema.L, table: table.L}, name: column.L}]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
val, ok = is.columns[id]
|
||||
return
|
||||
}
|
||||
|
||||
func (is *infoSchema) ColumnExists(schema, table, column model.CIStr) bool {
|
||||
_, ok := is.columnNameToID[columnName{tableName: tableName{schema: schema.L, table: table.L}, name: column.L}]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (is *infoSchema) IndexByName(schema, table, index model.CIStr) (val *model.IndexInfo, ok bool) {
|
||||
val, ok = is.indices[indexName{tableName: tableName{schema: schema.L, table: table.L}, name: index.L}]
|
||||
return
|
||||
}
|
||||
|
||||
func (is *infoSchema) SchemaByID(id int64) (val *model.DBInfo, ok bool) {
|
||||
val, ok = is.schemas[id]
|
||||
return
|
||||
}
|
||||
|
||||
func (is *infoSchema) TableByID(id int64) (val table.Table, ok bool) {
|
||||
val, ok = is.tables[id]
|
||||
return
|
||||
}
|
||||
|
||||
func (is *infoSchema) AllocByID(id int64) (val autoid.Allocator, ok bool) {
|
||||
val, ok = is.tableAllocators[id]
|
||||
return
|
||||
}
|
||||
|
||||
func (is *infoSchema) ColumnByID(id int64) (val *model.ColumnInfo, ok bool) {
|
||||
val, ok = is.columns[id]
|
||||
return
|
||||
}
|
||||
|
||||
func (is *infoSchema) ColumnIndicesByID(id int64) (indices []*model.IndexInfo, ok bool) {
|
||||
indices, ok = is.columnIndices[id]
|
||||
return
|
||||
}
|
||||
|
||||
func (is *infoSchema) AllSchemaNames() (names []string) {
|
||||
for _, v := range is.schemas {
|
||||
names = append(names, v.Name.O)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (is *infoSchema) AllSchemas() (schemas []*model.DBInfo) {
|
||||
for _, v := range is.schemas {
|
||||
schemas = append(schemas, v)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (is *infoSchema) SchemaTables(schema model.CIStr) (tables []table.Table) {
|
||||
di, ok := is.SchemaByName(schema)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
for _, ti := range di.Tables {
|
||||
tables = append(tables, is.tables[ti.ID])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (is *infoSchema) Clone() (result []*model.DBInfo) {
|
||||
for _, v := range is.schemas {
|
||||
result = append(result, v.Clone())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Handle handles information schema, including getting and setting.
|
||||
type Handle struct {
|
||||
value atomic.Value
|
||||
store kv.Storage
|
||||
}
|
||||
|
||||
// NewHandle creates a new Handle.
|
||||
func NewHandle(store kv.Storage) *Handle {
|
||||
h := &Handle{
|
||||
store: store,
|
||||
}
|
||||
// init memory tables
|
||||
initMemoryTables(store)
|
||||
initPerfSchema(store)
|
||||
return h
|
||||
}
|
||||
|
||||
func initPerfSchema(store kv.Storage) {
|
||||
perfHandle = perfschema.NewPerfHandle(store)
|
||||
}
|
||||
|
||||
func genGlobalID(store kv.Storage) (int64, error) {
|
||||
var globalID int64
|
||||
err := kv.RunInNewTxn(store, true, func(txn kv.Transaction) error {
|
||||
var err error
|
||||
globalID, err = meta.NewMeta(txn).GenGlobalID()
|
||||
return errors.Trace(err)
|
||||
})
|
||||
return globalID, errors.Trace(err)
|
||||
}
|
||||
|
||||
var (
|
||||
// Information_Schema
|
||||
isDB *model.DBInfo
|
||||
schemataTbl table.Table
|
||||
tablesTbl table.Table
|
||||
columnsTbl table.Table
|
||||
statisticsTbl table.Table
|
||||
charsetTbl table.Table
|
||||
collationsTbl table.Table
|
||||
filesTbl table.Table
|
||||
defTbl table.Table
|
||||
profilingTbl table.Table
|
||||
nameToTable map[string]table.Table
|
||||
|
||||
perfHandle perfschema.PerfSchema
|
||||
)
|
||||
|
||||
func setColumnID(meta *model.TableInfo, store kv.Storage) error {
|
||||
var err error
|
||||
for _, c := range meta.Columns {
|
||||
c.ID, err = genGlobalID(store)
|
||||
if err != nil {
|
||||
return errors.Trace(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func initMemoryTables(store kv.Storage) error {
|
||||
// Init Information_Schema
|
||||
var (
|
||||
err error
|
||||
tbl table.Table
|
||||
)
|
||||
dbID, err := genGlobalID(store)
|
||||
if err != nil {
|
||||
return errors.Trace(err)
|
||||
}
|
||||
nameToTable = make(map[string]table.Table)
|
||||
isTables := make([]*model.TableInfo, 0, len(tableNameToColumns))
|
||||
for name, cols := range tableNameToColumns {
|
||||
meta := buildTableMeta(name, cols)
|
||||
isTables = append(isTables, meta)
|
||||
meta.ID, err = genGlobalID(store)
|
||||
if err != nil {
|
||||
return errors.Trace(err)
|
||||
}
|
||||
err = setColumnID(meta, store)
|
||||
if err != nil {
|
||||
return errors.Trace(err)
|
||||
}
|
||||
alloc := autoid.NewMemoryAllocator(dbID)
|
||||
tbl, err = createMemoryTable(meta, alloc)
|
||||
if err != nil {
|
||||
return errors.Trace(err)
|
||||
}
|
||||
nameToTable[meta.Name.L] = tbl
|
||||
}
|
||||
schemataTbl = nameToTable[strings.ToLower(tableSchemata)]
|
||||
tablesTbl = nameToTable[strings.ToLower(tableTables)]
|
||||
columnsTbl = nameToTable[strings.ToLower(tableColumns)]
|
||||
statisticsTbl = nameToTable[strings.ToLower(tableStatistics)]
|
||||
charsetTbl = nameToTable[strings.ToLower(tableCharacterSets)]
|
||||
collationsTbl = nameToTable[strings.ToLower(tableCollations)]
|
||||
|
||||
// CharacterSets/Collations contain static data. Init them now.
|
||||
err = insertData(charsetTbl, dataForCharacterSets())
|
||||
if err != nil {
|
||||
return errors.Trace(err)
|
||||
}
|
||||
err = insertData(collationsTbl, dataForColltions())
|
||||
if err != nil {
|
||||
return errors.Trace(err)
|
||||
}
|
||||
// create db
|
||||
isDB = &model.DBInfo{
|
||||
ID: dbID,
|
||||
Name: model.NewCIStr(Name),
|
||||
Charset: mysql.DefaultCharset,
|
||||
Collate: mysql.DefaultCollationName,
|
||||
Tables: isTables,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func insertData(tbl table.Table, rows [][]types.Datum) error {
|
||||
for _, r := range rows {
|
||||
_, err := tbl.AddRecord(nil, r)
|
||||
if err != nil {
|
||||
return errors.Trace(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func refillTable(tbl table.Table, rows [][]types.Datum) error {
|
||||
err := tbl.Truncate(nil)
|
||||
if err != nil {
|
||||
return errors.Trace(err)
|
||||
}
|
||||
return insertData(tbl, rows)
|
||||
}
|
||||
|
||||
// Set sets DBInfo to information schema.
|
||||
func (h *Handle) Set(newInfo []*model.DBInfo, schemaMetaVersion int64) error {
|
||||
info := &infoSchema{
|
||||
schemaNameToID: map[string]int64{},
|
||||
tableNameToID: map[tableName]int64{},
|
||||
columnNameToID: map[columnName]int64{},
|
||||
schemas: map[int64]*model.DBInfo{},
|
||||
tables: map[int64]table.Table{},
|
||||
tableAllocators: map[int64]autoid.Allocator{},
|
||||
columns: map[int64]*model.ColumnInfo{},
|
||||
indices: map[indexName]*model.IndexInfo{},
|
||||
columnIndices: map[int64][]*model.IndexInfo{},
|
||||
schemaMetaVersion: schemaMetaVersion,
|
||||
}
|
||||
var err error
|
||||
var hasOldInfo bool
|
||||
infoschema := h.Get()
|
||||
if infoschema != nil {
|
||||
hasOldInfo = true
|
||||
}
|
||||
for _, di := range newInfo {
|
||||
info.schemas[di.ID] = di
|
||||
info.schemaNameToID[di.Name.L] = di.ID
|
||||
for _, t := range di.Tables {
|
||||
alloc := autoid.NewAllocator(h.store, di.ID)
|
||||
if hasOldInfo {
|
||||
val, ok := infoschema.AllocByID(t.ID)
|
||||
if ok {
|
||||
alloc = val
|
||||
}
|
||||
}
|
||||
info.tableAllocators[t.ID] = alloc
|
||||
info.tables[t.ID], err = table.TableFromMeta(alloc, t)
|
||||
if err != nil {
|
||||
return errors.Trace(err)
|
||||
}
|
||||
tname := tableName{di.Name.L, t.Name.L}
|
||||
info.tableNameToID[tname] = t.ID
|
||||
for _, c := range t.Columns {
|
||||
info.columns[c.ID] = c
|
||||
info.columnNameToID[columnName{tname, c.Name.L}] = c.ID
|
||||
}
|
||||
for _, idx := range t.Indices {
|
||||
info.indices[indexName{tname, idx.Name.L}] = idx
|
||||
for _, idxCol := range idx.Columns {
|
||||
columnID := t.Columns[idxCol.Offset].ID
|
||||
columnIndices := info.columnIndices[columnID]
|
||||
info.columnIndices[columnID] = append(columnIndices, idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Build Information_Schema
|
||||
info.schemaNameToID[isDB.Name.L] = isDB.ID
|
||||
info.schemas[isDB.ID] = isDB
|
||||
for _, t := range isDB.Tables {
|
||||
tbl, ok := nameToTable[t.Name.L]
|
||||
if !ok {
|
||||
return errors.Errorf("table `%s` is missing.", t.Name)
|
||||
}
|
||||
info.tables[t.ID] = tbl
|
||||
tname := tableName{isDB.Name.L, t.Name.L}
|
||||
info.tableNameToID[tname] = t.ID
|
||||
for _, c := range t.Columns {
|
||||
info.columns[c.ID] = c
|
||||
info.columnNameToID[columnName{tname, c.Name.L}] = c.ID
|
||||
}
|
||||
}
|
||||
|
||||
// Add Performance_Schema
|
||||
psDB := perfHandle.GetDBMeta()
|
||||
info.schemaNameToID[psDB.Name.L] = psDB.ID
|
||||
info.schemas[psDB.ID] = psDB
|
||||
for _, t := range psDB.Tables {
|
||||
tbl, ok := perfHandle.GetTable(t.Name.O)
|
||||
if !ok {
|
||||
return errors.Errorf("table `%s` is missing.", t.Name)
|
||||
}
|
||||
info.tables[t.ID] = tbl
|
||||
tname := tableName{psDB.Name.L, t.Name.L}
|
||||
info.tableNameToID[tname] = t.ID
|
||||
for _, c := range t.Columns {
|
||||
info.columns[c.ID] = c
|
||||
info.columnNameToID[columnName{tname, c.Name.L}] = c.ID
|
||||
}
|
||||
}
|
||||
// Should refill some tables in Information_Schema.
|
||||
// schemata/tables/columns/statistics
|
||||
dbNames := make([]string, 0, len(info.schemas))
|
||||
dbInfos := make([]*model.DBInfo, 0, len(info.schemas))
|
||||
for _, v := range info.schemas {
|
||||
dbNames = append(dbNames, v.Name.L)
|
||||
dbInfos = append(dbInfos, v)
|
||||
}
|
||||
err = refillTable(schemataTbl, dataForSchemata(dbNames))
|
||||
if err != nil {
|
||||
return errors.Trace(err)
|
||||
}
|
||||
err = refillTable(tablesTbl, dataForTables(dbInfos))
|
||||
if err != nil {
|
||||
return errors.Trace(err)
|
||||
}
|
||||
err = refillTable(columnsTbl, dataForColumns(dbInfos))
|
||||
if err != nil {
|
||||
return errors.Trace(err)
|
||||
}
|
||||
err = refillTable(statisticsTbl, dataForStatistics(dbInfos))
|
||||
if err != nil {
|
||||
return errors.Trace(err)
|
||||
}
|
||||
h.value.Store(info)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get gets information schema from Handle.
|
||||
func (h *Handle) Get() InfoSchema {
|
||||
v := h.value.Load()
|
||||
schema, _ := v.(InfoSchema)
|
||||
return schema
|
||||
}
|
||||
|
||||
// Schema error codes.
|
||||
const (
|
||||
CodeDbDropExists terror.ErrCode = 1008
|
||||
CodeDatabaseNotExists = 1049
|
||||
CodeTableNotExists = 1146
|
||||
CodeColumnNotExists = 1054
|
||||
|
||||
CodeDatabaseExists = 1007
|
||||
CodeTableExists = 1050
|
||||
CodeBadTable = 1051
|
||||
)
|
||||
|
||||
var (
|
||||
// DatabaseDropExists returns for drop an unexist database.
|
||||
DatabaseDropExists = terror.ClassSchema.New(CodeDbDropExists, "database doesn't exist")
|
||||
// DatabaseNotExists returns for database not exists.
|
||||
DatabaseNotExists = terror.ClassSchema.New(CodeDatabaseNotExists, "database not exists")
|
||||
// TableNotExists returns for table not exists.
|
||||
TableNotExists = terror.ClassSchema.New(CodeTableNotExists, "table not exists")
|
||||
// ColumnNotExists returns for column not exists.
|
||||
ColumnNotExists = terror.ClassSchema.New(CodeColumnNotExists, "field not exists")
|
||||
|
||||
// DatabaseExists returns for database already exists.
|
||||
DatabaseExists = terror.ClassSchema.New(CodeDatabaseExists, "database already exists")
|
||||
// TableExists returns for table already exists.
|
||||
TableExists = terror.ClassSchema.New(CodeTableExists, "table already exists")
|
||||
// TableDropExists returns for drop an unexist table.
|
||||
TableDropExists = terror.ClassSchema.New(CodeBadTable, "unknown table")
|
||||
)
|
||||
|
||||
func init() {
|
||||
schemaMySQLErrCodes := map[terror.ErrCode]uint16{
|
||||
CodeDbDropExists: mysql.ErrDbDropExists,
|
||||
CodeDatabaseNotExists: mysql.ErrBadDb,
|
||||
CodeTableNotExists: mysql.ErrNoSuchTable,
|
||||
CodeColumnNotExists: mysql.ErrBadField,
|
||||
CodeDatabaseExists: mysql.ErrDbCreateExists,
|
||||
CodeTableExists: mysql.ErrTableExists,
|
||||
CodeBadTable: mysql.ErrBadTable,
|
||||
}
|
||||
terror.ErrClassToMySQLCodes[terror.ClassSchema] = schemaMySQLErrCodes
|
||||
}
|
458
vendor/github.com/pingcap/tidb/infoschema/tables.go
generated
vendored
Normal file
458
vendor/github.com/pingcap/tidb/infoschema/tables.go
generated
vendored
Normal file
|
@ -0,0 +1,458 @@
|
|||
// Copyright 2016 PingCAP, Inc.
|
||||
//
|
||||
// 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,
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package infoschema
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/pingcap/tidb/column"
|
||||
"github.com/pingcap/tidb/meta/autoid"
|
||||
"github.com/pingcap/tidb/model"
|
||||
"github.com/pingcap/tidb/mysql"
|
||||
"github.com/pingcap/tidb/table"
|
||||
"github.com/pingcap/tidb/table/tables"
|
||||
"github.com/pingcap/tidb/util/charset"
|
||||
"github.com/pingcap/tidb/util/types"
|
||||
)
|
||||
|
||||
const (
|
||||
tableSchemata = "SCHEMATA"
|
||||
tableTables = "TABLES"
|
||||
tableColumns = "COLUMNS"
|
||||
tableStatistics = "STATISTICS"
|
||||
tableCharacterSets = "CHARACTER_SETS"
|
||||
tableCollations = "COLLATIONS"
|
||||
tableFiles = "FILES"
|
||||
catalogVal = "def"
|
||||
tableProfiling = "PROFILING"
|
||||
)
|
||||
|
||||
type columnInfo struct {
|
||||
name string
|
||||
tp byte
|
||||
size int
|
||||
flag uint
|
||||
deflt interface{}
|
||||
elems []string
|
||||
}
|
||||
|
||||
func buildColumnInfo(tableName string, col columnInfo) *model.ColumnInfo {
|
||||
mCharset := charset.CharsetBin
|
||||
mCollation := charset.CharsetBin
|
||||
mFlag := mysql.UnsignedFlag
|
||||
if col.tp == mysql.TypeVarchar || col.tp == mysql.TypeBlob {
|
||||
mCharset = mysql.DefaultCharset
|
||||
mCollation = mysql.DefaultCollationName
|
||||
mFlag = 0
|
||||
}
|
||||
fieldType := types.FieldType{
|
||||
Charset: mCharset,
|
||||
Collate: mCollation,
|
||||
Tp: col.tp,
|
||||
Flen: col.size,
|
||||
Flag: uint(mFlag),
|
||||
}
|
||||
return &model.ColumnInfo{
|
||||
Name: model.NewCIStr(col.name),
|
||||
FieldType: fieldType,
|
||||
State: model.StatePublic,
|
||||
}
|
||||
}
|
||||
|
||||
func buildTableMeta(tableName string, cs []columnInfo) *model.TableInfo {
|
||||
cols := make([]*model.ColumnInfo, 0, len(cs))
|
||||
for _, c := range cs {
|
||||
cols = append(cols, buildColumnInfo(tableName, c))
|
||||
}
|
||||
for i, col := range cols {
|
||||
col.Offset = i
|
||||
}
|
||||
return &model.TableInfo{
|
||||
Name: model.NewCIStr(tableName),
|
||||
Columns: cols,
|
||||
State: model.StatePublic,
|
||||
}
|
||||
}
|
||||
|
||||
var schemataCols = []columnInfo{
|
||||
{"CATALOG_NAME", mysql.TypeVarchar, 512, 0, nil, nil},
|
||||
{"SCHEMA_NAME", mysql.TypeVarchar, 64, 0, nil, nil},
|
||||
{"DEFAULT_CHARACTER_SET_NAME", mysql.TypeVarchar, 64, 0, nil, nil},
|
||||
{"DEFAULT_COLLATION_NAME", mysql.TypeVarchar, 32, 0, nil, nil},
|
||||
{"SQL_PATH", mysql.TypeVarchar, 512, 0, nil, nil},
|
||||
}
|
||||
|
||||
var tablesCols = []columnInfo{
|
||||
{"TABLE_CATALOG", mysql.TypeVarchar, 512, 0, nil, nil},
|
||||
{"TABLE_SCHEMA", mysql.TypeVarchar, 64, 0, nil, nil},
|
||||
{"TABLE_NAME", mysql.TypeVarchar, 64, 0, nil, nil},
|
||||
{"TABLE_TYPE", mysql.TypeVarchar, 64, 0, nil, nil},
|
||||
{"ENGINE", mysql.TypeVarchar, 64, 0, nil, nil},
|
||||
{"VERSION", mysql.TypeLonglong, 21, 0, nil, nil},
|
||||
{"ROW_FORMAT", mysql.TypeVarchar, 10, 0, nil, nil},
|
||||
{"TABLE_ROWS", mysql.TypeLonglong, 21, 0, nil, nil},
|
||||
{"AVG_ROW_LENGTH", mysql.TypeLonglong, 21, 0, nil, nil},
|
||||
{"DATA_LENGTH", mysql.TypeLonglong, 21, 0, nil, nil},
|
||||
{"MAX_DATA_LENGTH", mysql.TypeLonglong, 21, 0, nil, nil},
|
||||
{"INDEX_LENGTH", mysql.TypeLonglong, 21, 0, nil, nil},
|
||||
{"DATA_FREE", mysql.TypeLonglong, 21, 0, nil, nil},
|
||||
{"AUTO_INCREMENT", mysql.TypeLonglong, 21, 0, nil, nil},
|
||||
{"CREATE_TIME", mysql.TypeDatetime, 19, 0, nil, nil},
|
||||
{"UPDATE_TIME", mysql.TypeDatetime, 19, 0, nil, nil},
|
||||
{"CHECK_TIME", mysql.TypeDatetime, 19, 0, nil, nil},
|
||||
{"TABLE_COLLATION", mysql.TypeVarchar, 32, 0, nil, nil},
|
||||
{"CHECK_SUM", mysql.TypeLonglong, 21, 0, nil, nil},
|
||||
{"CREATE_OPTIONS", mysql.TypeVarchar, 255, 0, nil, nil},
|
||||
{"TABLE_COMMENT", mysql.TypeVarchar, 2048, 0, nil, nil},
|
||||
}
|
||||
|
||||
var columnsCols = []columnInfo{
|
||||
{"TABLE_CATALOG", mysql.TypeVarchar, 512, 0, nil, nil},
|
||||
{"TABLE_SCHEMA", mysql.TypeVarchar, 64, 0, nil, nil},
|
||||
{"TABLE_NAME", mysql.TypeVarchar, 64, 0, nil, nil},
|
||||
{"COLUMN_NAME", mysql.TypeVarchar, 64, 0, nil, nil},
|
||||
{"ORIGINAL_POSITION", mysql.TypeLonglong, 64, 0, nil, nil},
|
||||
{"COLUMN_DEFAULT", mysql.TypeBlob, 196606, 0, nil, nil},
|
||||
{"IS_NULLABLE", mysql.TypeVarchar, 3, 0, nil, nil},
|
||||
{"DATA_TYPE", mysql.TypeVarchar, 64, 0, nil, nil},
|
||||
{"CHARACTER_MAXIMUM_LENGTH", mysql.TypeLonglong, 21, 0, nil, nil},
|
||||
{"CHARACTOR_OCTET_LENGTH", mysql.TypeLonglong, 21, 0, nil, nil},
|
||||
{"NUMERIC_PRECISION", mysql.TypeLonglong, 21, 0, nil, nil},
|
||||
{"NUMERIC_SCALE", mysql.TypeLonglong, 21, 0, nil, nil},
|
||||
{"DATETIME_PRECISION", mysql.TypeLonglong, 21, 0, nil, nil},
|
||||
{"CHARACTER_SET_NAME", mysql.TypeVarchar, 32, 0, nil, nil},
|
||||
{"COLLATION_NAME", mysql.TypeVarchar, 32, 0, nil, nil},
|
||||
{"COLUMN_TYPE", mysql.TypeBlob, 196606, 0, nil, nil},
|
||||
{"COLUMN_KEY", mysql.TypeVarchar, 3, 0, nil, nil},
|
||||
{"EXTRA", mysql.TypeVarchar, 30, 0, nil, nil},
|
||||
{"PRIVILEGES", mysql.TypeVarchar, 80, 0, nil, nil},
|
||||
{"COLUMN_COMMENT", mysql.TypeVarchar, 1024, 0, nil, nil},
|
||||
}
|
||||
|
||||
var statisticsCols = []columnInfo{
|
||||
{"TABLE_CATALOG", mysql.TypeVarchar, 512, 0, nil, nil},
|
||||
{"TABLE_SCHEMA", mysql.TypeVarchar, 64, 0, nil, nil},
|
||||
{"TABLE_NAME", mysql.TypeVarchar, 64, 0, nil, nil},
|
||||
{"NON_UNIQUE", mysql.TypeVarchar, 1, 0, nil, nil},
|
||||
{"INDEX_SCHEMA", mysql.TypeVarchar, 64, 0, nil, nil},
|
||||
{"INDEX_NAME", mysql.TypeVarchar, 64, 0, nil, nil},
|
||||
{"SEQ_IN_INDEX", mysql.TypeLonglong, 2, 0, nil, nil},
|
||||
{"COLUMN_NAME", mysql.TypeVarchar, 21, 0, nil, nil},
|
||||
{"COLLATION", mysql.TypeVarchar, 1, 0, nil, nil},
|
||||
{"CARDINALITY", mysql.TypeLonglong, 21, 0, nil, nil},
|
||||
{"SUB_PART", mysql.TypeLonglong, 3, 0, nil, nil},
|
||||
{"PACKED", mysql.TypeVarchar, 10, 0, nil, nil},
|
||||
{"NULLABLE", mysql.TypeVarchar, 3, 0, nil, nil},
|
||||
{"INDEX_TYPE", mysql.TypeVarchar, 16, 0, nil, nil},
|
||||
{"COMMENT", mysql.TypeVarchar, 16, 0, nil, nil},
|
||||
{"INDEX_COMMENT", mysql.TypeVarchar, 1024, 0, nil, nil},
|
||||
}
|
||||
|
||||
var profilingCols = []columnInfo{
|
||||
{"QUERY_ID", mysql.TypeLong, 20, 0, nil, nil},
|
||||
{"SEQ", mysql.TypeLong, 20, 0, nil, nil},
|
||||
{"STATE", mysql.TypeVarchar, 30, 0, nil, nil},
|
||||
{"DURATION", mysql.TypeNewDecimal, 9, 0, nil, nil},
|
||||
{"CPU_USER", mysql.TypeNewDecimal, 9, 0, nil, nil},
|
||||
{"CPU_SYSTEM", mysql.TypeNewDecimal, 9, 0, nil, nil},
|
||||
{"CONTEXT_VOLUNTARY", mysql.TypeLong, 20, 0, nil, nil},
|
||||
{"CONTEXT_INVOLUNTARY", mysql.TypeLong, 20, 0, nil, nil},
|
||||
{"BLOCK_OPS_IN", mysql.TypeLong, 20, 0, nil, nil},
|
||||
{"BLOCK_OPS_OUT", mysql.TypeLong, 20, 0, nil, nil},
|
||||
{"MESSAGES_SENT", mysql.TypeLong, 20, 0, nil, nil},
|
||||
{"MESSAGES_RECEIVED", mysql.TypeLong, 20, 0, nil, nil},
|
||||
{"PAGE_FAULTS_MAJOR", mysql.TypeLong, 20, 0, nil, nil},
|
||||
{"PAGE_FAULTS_MINOR", mysql.TypeLong, 20, 0, nil, nil},
|
||||
{"SWAPS", mysql.TypeLong, 20, 0, nil, nil},
|
||||
{"SOURCE_FUNCTION", mysql.TypeVarchar, 30, 0, nil, nil},
|
||||
{"SOURCE_FILE", mysql.TypeVarchar, 20, 0, nil, nil},
|
||||
{"SOURCE_LINE", mysql.TypeLong, 20, 0, nil, nil},
|
||||
}
|
||||
|
||||
var charsetCols = []columnInfo{
|
||||
{"CHARACTER_SET_NAME", mysql.TypeVarchar, 32, 0, nil, nil},
|
||||
{"DEFAULT_COLLATE_NAME", mysql.TypeVarchar, 32, 0, nil, nil},
|
||||
{"DESCRIPTION", mysql.TypeVarchar, 60, 0, nil, nil},
|
||||
{"MAXLEN", mysql.TypeLonglong, 3, 0, nil, nil},
|
||||
}
|
||||
|
||||
var collationsCols = []columnInfo{
|
||||
{"COLLATION_NAME", mysql.TypeVarchar, 32, 0, nil, nil},
|
||||
{"CHARACTER_SET_NAME", mysql.TypeVarchar, 32, 0, nil, nil},
|
||||
{"ID", mysql.TypeLonglong, 11, 0, nil, nil},
|
||||
{"IS_DEFAULT", mysql.TypeVarchar, 3, 0, nil, nil},
|
||||
{"IS_COMPILED", mysql.TypeVarchar, 3, 0, nil, nil},
|
||||
{"SORTLEN", mysql.TypeLonglong, 3, 0, nil, nil},
|
||||
}
|
||||
|
||||
func dataForCharacterSets() (records [][]types.Datum) {
|
||||
records = append(records,
|
||||
types.MakeDatums("ascii", "ascii_general_ci", "US ASCII", 1),
|
||||
types.MakeDatums("binary", "binary", "Binary pseudo charset", 1),
|
||||
types.MakeDatums("latin1", "latin1_swedish_ci", "cp1252 West European", 1),
|
||||
types.MakeDatums("utf8", "utf8_general_ci", "UTF-8 Unicode", 3),
|
||||
types.MakeDatums("utf8mb4", "utf8mb4_general_ci", "UTF-8 Unicode", 4),
|
||||
)
|
||||
return records
|
||||
}
|
||||
|
||||
func dataForColltions() (records [][]types.Datum) {
|
||||
records = append(records,
|
||||
types.MakeDatums("ascii_general_ci", "ascii", 1, "Yes", "Yes", 1),
|
||||
types.MakeDatums("binary", "binary", 2, "Yes", "Yes", 1),
|
||||
types.MakeDatums("latin1_swedish_ci", "latin1", 3, "Yes", "Yes", 1),
|
||||
types.MakeDatums("utf8_general_ci", "utf8", 4, "Yes", "Yes", 1),
|
||||
types.MakeDatums("utf8mb4_general_ci", "utf8mb4", 5, "Yes", "Yes", 1),
|
||||
)
|
||||
return records
|
||||
}
|
||||
|
||||
var filesCols = []columnInfo{
|
||||
{"FILE_ID", mysql.TypeLonglong, 4, 0, nil, nil},
|
||||
{"FILE_NAME", mysql.TypeVarchar, 64, 0, nil, nil},
|
||||
{"TABLESPACE_NAME", mysql.TypeVarchar, 20, 0, nil, nil},
|
||||
{"TABLE_CATALOG", mysql.TypeVarchar, 64, 0, nil, nil},
|
||||
{"TABLE_SCHEMA", mysql.TypeVarchar, 64, 0, nil, nil},
|
||||
{"TABLE_NAME", mysql.TypeVarchar, 64, 0, nil, nil},
|
||||
{"LOGFILE_GROUP_NAME", mysql.TypeVarchar, 64, 0, nil, nil},
|
||||
{"LOGFILE_GROUP_NUMBER", mysql.TypeLonglong, 32, 0, nil, nil},
|
||||
{"ENGINE", mysql.TypeVarchar, 64, 0, nil, nil},
|
||||
{"FULLTEXT_KEYS", mysql.TypeVarchar, 64, 0, nil, nil},
|
||||
{"DELETED_ROWS", mysql.TypeLonglong, 4, 0, nil, nil},
|
||||
{"UPDATE_COUNT", mysql.TypeLonglong, 4, 0, nil, nil},
|
||||
{"FREE_EXTENTS", mysql.TypeLonglong, 4, 0, nil, nil},
|
||||
{"TOTAL_EXTENTS", mysql.TypeLonglong, 4, 0, nil, nil},
|
||||
{"EXTENT_SIZE", mysql.TypeLonglong, 4, 0, nil, nil},
|
||||
{"INITIAL_SIZE", mysql.TypeLonglong, 21, 0, nil, nil},
|
||||
{"MAXIMUM_SIZE", mysql.TypeLonglong, 21, 0, nil, nil},
|
||||
{"AUTOEXTEND_SIZE", mysql.TypeLonglong, 21, 0, nil, nil},
|
||||
{"CREATION_TIME", mysql.TypeDatetime, -1, 0, nil, nil},
|
||||
{"LAST_UPDATE_TIME", mysql.TypeDatetime, -1, 0, nil, nil},
|
||||
{"LAST_ACCESS_TIME", mysql.TypeDatetime, -1, 0, nil, nil},
|
||||
{"RECOVER_TIME", mysql.TypeLonglong, 4, 0, nil, nil},
|
||||
{"TRANSACTION_COUNTER", mysql.TypeLonglong, 4, 0, nil, nil},
|
||||
{"VERSION", mysql.TypeLonglong, 21, 0, nil, nil},
|
||||
{"ROW_FORMAT", mysql.TypeVarchar, 21, 0, nil, nil},
|
||||
{"TABLE_ROWS", mysql.TypeLonglong, 21, 0, nil, nil},
|
||||
{"AVG_ROW_LENGTH", mysql.TypeLonglong, 21, 0, nil, nil},
|
||||
{"DATA_FREE", mysql.TypeLonglong, 21, 0, nil, nil},
|
||||
{"CREATE_TIME", mysql.TypeDatetime, -1, 0, nil, nil},
|
||||
{"UPDATE_TIME", mysql.TypeDatetime, -1, 0, nil, nil},
|
||||
{"CHECK_TIME", mysql.TypeDatetime, -1, 0, nil, nil},
|
||||
{"CHECKSUM", mysql.TypeLonglong, 21, 0, nil, nil},
|
||||
{"STATUS", mysql.TypeVarchar, 20, 0, nil, nil},
|
||||
{"EXTRA", mysql.TypeVarchar, 255, 0, nil, nil},
|
||||
}
|
||||
|
||||
func dataForSchemata(schemas []string) [][]types.Datum {
|
||||
sort.Strings(schemas)
|
||||
rows := [][]types.Datum{}
|
||||
for _, schema := range schemas {
|
||||
record := types.MakeDatums(
|
||||
catalogVal, // CATALOG_NAME
|
||||
schema, // SCHEMA_NAME
|
||||
mysql.DefaultCharset, // DEFAULT_CHARACTER_SET_NAME
|
||||
mysql.DefaultCollationName, // DEFAULT_COLLATION_NAME
|
||||
nil,
|
||||
)
|
||||
rows = append(rows, record)
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
func dataForTables(schemas []*model.DBInfo) [][]types.Datum {
|
||||
rows := [][]types.Datum{}
|
||||
for _, schema := range schemas {
|
||||
for _, table := range schema.Tables {
|
||||
record := types.MakeDatums(
|
||||
catalogVal, // TABLE_CATALOG
|
||||
schema.Name.O, // TABLE_SCHEMA
|
||||
table.Name.O, // TABLE_NAME
|
||||
"BASE_TABLE", // TABLE_TYPE
|
||||
"InnoDB", // ENGINE
|
||||
uint64(10), // VERSION
|
||||
"Compact", // ROW_FORMAT
|
||||
uint64(0), // TABLE_ROWS
|
||||
uint64(0), // AVG_ROW_LENGTH
|
||||
uint64(16384), // DATA_LENGTH
|
||||
uint64(0), // MAX_DATA_LENGTH
|
||||
uint64(0), // INDEX_LENGTH
|
||||
uint64(0), // DATA_FREE
|
||||
nil, // AUTO_INCREMENT
|
||||
nil, // CREATE_TIME
|
||||
nil, // UPDATE_TIME
|
||||
nil, // CHECK_TIME
|
||||
"latin1_swedish_ci", // TABLE_COLLATION
|
||||
nil, // CHECKSUM
|
||||
"", // CREATE_OPTIONS
|
||||
"", // TABLE_COMMENT
|
||||
)
|
||||
rows = append(rows, record)
|
||||
}
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
func dataForColumns(schemas []*model.DBInfo) [][]types.Datum {
|
||||
rows := [][]types.Datum{}
|
||||
for _, schema := range schemas {
|
||||
for _, table := range schema.Tables {
|
||||
rs := dataForColumnsInTable(schema, table)
|
||||
for _, r := range rs {
|
||||
rows = append(rows, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
func dataForColumnsInTable(schema *model.DBInfo, table *model.TableInfo) [][]types.Datum {
|
||||
rows := [][]types.Datum{}
|
||||
for i, col := range table.Columns {
|
||||
colLen := col.Flen
|
||||
if colLen == types.UnspecifiedLength {
|
||||
colLen = mysql.GetDefaultFieldLength(col.Tp)
|
||||
}
|
||||
decimal := col.Decimal
|
||||
if decimal == types.UnspecifiedLength {
|
||||
decimal = 0
|
||||
}
|
||||
columnType := col.FieldType.CompactStr()
|
||||
columnDesc := column.NewColDesc(&column.Col{ColumnInfo: *col})
|
||||
var columnDefault interface{}
|
||||
if columnDesc.DefaultValue != nil {
|
||||
columnDefault = fmt.Sprintf("%v", columnDesc.DefaultValue)
|
||||
}
|
||||
record := types.MakeDatums(
|
||||
catalogVal, // TABLE_CATALOG
|
||||
schema.Name.O, // TABLE_SCHEMA
|
||||
table.Name.O, // TABLE_NAME
|
||||
col.Name.O, // COLUMN_NAME
|
||||
i+1, // ORIGINAL_POSITION
|
||||
columnDefault, // COLUMN_DEFAULT
|
||||
columnDesc.Null, // IS_NULLABLE
|
||||
types.TypeToStr(col.Tp, col.Charset), // DATA_TYPE
|
||||
colLen, // CHARACTER_MAXIMUM_LENGTH
|
||||
colLen, // CHARACTOR_OCTET_LENGTH
|
||||
decimal, // NUMERIC_PRECISION
|
||||
0, // NUMERIC_SCALE
|
||||
0, // DATETIME_PRECISION
|
||||
col.Charset, // CHARACTER_SET_NAME
|
||||
col.Collate, // COLLATION_NAME
|
||||
columnType, // COLUMN_TYPE
|
||||
columnDesc.Key, // COLUMN_KEY
|
||||
columnDesc.Extra, // EXTRA
|
||||
"select,insert,update,references", // PRIVILEGES
|
||||
"", // COLUMN_COMMENT
|
||||
)
|
||||
rows = append(rows, record)
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
func dataForStatistics(schemas []*model.DBInfo) [][]types.Datum {
|
||||
rows := [][]types.Datum{}
|
||||
for _, schema := range schemas {
|
||||
for _, table := range schema.Tables {
|
||||
rs := dataForStatisticsInTable(schema, table)
|
||||
for _, r := range rs {
|
||||
rows = append(rows, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
func dataForStatisticsInTable(schema *model.DBInfo, table *model.TableInfo) [][]types.Datum {
|
||||
rows := [][]types.Datum{}
|
||||
if table.PKIsHandle {
|
||||
for _, col := range table.Columns {
|
||||
if mysql.HasPriKeyFlag(col.Flag) {
|
||||
record := types.MakeDatums(
|
||||
catalogVal, // TABLE_CATALOG
|
||||
schema.Name.O, // TABLE_SCHEMA
|
||||
table.Name.O, // TABLE_NAME
|
||||
"0", // NON_UNIQUE
|
||||
schema.Name.O, // INDEX_SCHEMA
|
||||
"PRIMARY", // INDEX_NAME
|
||||
1, // SEQ_IN_INDEX
|
||||
col.Name.O, // COLUMN_NAME
|
||||
"A", // COLLATION
|
||||
0, // CARDINALITY
|
||||
nil, // SUB_PART
|
||||
nil, // PACKED
|
||||
"", // NULLABLE
|
||||
"BTREE", // INDEX_TYPE
|
||||
"", // COMMENT
|
||||
"", // INDEX_COMMENT
|
||||
)
|
||||
rows = append(rows, record)
|
||||
}
|
||||
}
|
||||
}
|
||||
nameToCol := make(map[string]*model.ColumnInfo, len(table.Columns))
|
||||
for _, c := range table.Columns {
|
||||
nameToCol[c.Name.L] = c
|
||||
}
|
||||
for _, index := range table.Indices {
|
||||
nonUnique := "1"
|
||||
if index.Unique {
|
||||
nonUnique = "0"
|
||||
}
|
||||
for i, key := range index.Columns {
|
||||
col := nameToCol[key.Name.L]
|
||||
nullable := "YES"
|
||||
if mysql.HasNotNullFlag(col.Flag) {
|
||||
nullable = ""
|
||||
}
|
||||
record := types.MakeDatums(
|
||||
catalogVal, // TABLE_CATALOG
|
||||
schema.Name.O, // TABLE_SCHEMA
|
||||
table.Name.O, // TABLE_NAME
|
||||
nonUnique, // NON_UNIQUE
|
||||
schema.Name.O, // INDEX_SCHEMA
|
||||
index.Name.O, // INDEX_NAME
|
||||
i+1, // SEQ_IN_INDEX
|
||||
key.Name.O, // COLUMN_NAME
|
||||
"A", // COLLATION
|
||||
0, // CARDINALITY
|
||||
nil, // SUB_PART
|
||||
nil, // PACKED
|
||||
nullable, // NULLABLE
|
||||
"BTREE", // INDEX_TYPE
|
||||
"", // COMMENT
|
||||
"", // INDEX_COMMENT
|
||||
)
|
||||
rows = append(rows, record)
|
||||
}
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
var tableNameToColumns = map[string]([]columnInfo){
|
||||
tableSchemata: schemataCols,
|
||||
tableTables: tablesCols,
|
||||
tableColumns: columnsCols,
|
||||
tableStatistics: statisticsCols,
|
||||
tableCharacterSets: charsetCols,
|
||||
tableCollations: collationsCols,
|
||||
tableFiles: filesCols,
|
||||
tableProfiling: profilingCols,
|
||||
}
|
||||
|
||||
func createMemoryTable(meta *model.TableInfo, alloc autoid.Allocator) (table.Table, error) {
|
||||
tbl, _ := tables.MemoryTableFromMeta(alloc, meta)
|
||||
return tbl, nil
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue