1 Star 0 Fork 0

DaMeng/Atlas

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
文件
克隆/下载
inspect.go 29.93 KB
一键复制 编辑 原始数据 按行查看 历史
DaMeng 提交于 2024-10-24 15:32 +08:00 . :art:修改mod名称
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004
// Copyright 2021-present The Atlas Authors. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package mysql
import (
"context"
"database/sql"
"fmt"
"regexp"
"strconv"
"strings"
"gitee.com/damengde/atlas/sql/internal/sqlx"
"gitee.com/damengde/atlas/sql/schema"
)
// A diff provides a MySQL implementation for schema.Inspector.
type inspect struct{ *conn }
var _ schema.Inspector = (*inspect)(nil)
// InspectRealm returns schema descriptions of all resources in the given realm.
func (i *inspect) InspectRealm(ctx context.Context, opts *schema.InspectRealmOption) (*schema.Realm, error) {
schemas, err := i.schemas(ctx, opts)
if err != nil {
return nil, err
}
if opts == nil {
opts = &schema.InspectRealmOption{}
}
var (
mode = sqlx.ModeInspectRealm(opts)
r = schema.NewRealm(schemas...).SetCharset(i.charset).SetCollation(i.collate)
)
if len(schemas) > 0 {
if mode.Is(schema.InspectTables) {
if err := i.inspectTables(ctx, r, nil); err != nil {
return nil, err
}
sqlx.LinkSchemaTables(schemas)
}
if mode.Is(schema.InspectViews) {
if err := i.inspectViews(ctx, r, nil); err != nil {
return nil, err
}
}
if mode.Is(schema.InspectFuncs) {
if err := i.inspectFuncs(ctx, r, nil); err != nil {
return nil, err
}
}
if mode.Is(schema.InspectTriggers) {
if err := i.inspectTriggers(ctx, r, nil); err != nil {
return nil, err
}
}
}
return schema.ExcludeRealm(r, opts.Exclude)
}
// InspectSchema returns schema descriptions of the tables in the given schema.
// If the schema name is empty, the result will be the attached schema.
func (i *inspect) InspectSchema(ctx context.Context, name string, opts *schema.InspectOptions) (*schema.Schema, error) {
schemas, err := i.schemas(ctx, &schema.InspectRealmOption{Schemas: []string{name}})
if err != nil {
return nil, err
}
switch n := len(schemas); {
case n == 0:
return nil, &schema.NotExistError{Err: fmt.Errorf("mysql: schema %q was not found", name)}
case n > 1:
return nil, fmt.Errorf("mysql: %d schemas were found for %q", n, name)
}
if opts == nil {
opts = &schema.InspectOptions{}
}
var (
mode = sqlx.ModeInspectSchema(opts)
r = schema.NewRealm(schemas...).SetCharset(i.charset).SetCollation(i.collate)
)
if mode.Is(schema.InspectTables) {
if err := i.inspectTables(ctx, r, opts); err != nil {
return nil, err
}
sqlx.LinkSchemaTables(schemas)
}
if mode.Is(schema.InspectViews) {
if err := i.inspectViews(ctx, r, opts); err != nil {
return nil, err
}
}
if mode.Is(schema.InspectFuncs) {
if err := i.inspectFuncs(ctx, r, opts); err != nil {
return nil, err
}
}
if mode.Is(schema.InspectTriggers) {
if err := i.inspectTriggers(ctx, r, nil); err != nil {
return nil, err
}
}
return schema.ExcludeSchema(r.Schemas[0], opts.Exclude)
}
func (i *inspect) inspectTables(ctx context.Context, r *schema.Realm, opts *schema.InspectOptions) error {
if err := i.tables(ctx, r, opts); err != nil {
return err
}
for _, s := range r.Schemas {
if len(s.Tables) == 0 {
continue
}
if err := i.columns(ctx, s); err != nil {
return err
}
if err := i.indexes(ctx, s); err != nil {
return err
}
if err := i.fks(ctx, s); err != nil {
return err
}
if err := i.checks(ctx, s); err != nil {
return err
}
if err := i.showCreate(ctx, s); err != nil {
return err
}
}
return nil
}
// schemas returns the list of the schemas in the database.
func (i *inspect) schemas(ctx context.Context, opts *schema.InspectRealmOption) ([]*schema.Schema, error) {
var (
args []any
query = schemasQuery
)
if opts != nil {
switch n := len(opts.Schemas); {
case n == 1 && opts.Schemas[0] == "":
query = fmt.Sprintf(schemasQueryArgs, "= SCHEMA()")
case n == 1 && opts.Schemas[0] != "":
query = fmt.Sprintf(schemasQueryArgs, "= ?")
args = append(args, opts.Schemas[0])
case n > 0:
query = fmt.Sprintf(schemasQueryArgs, "IN ("+nArgs(len(opts.Schemas))+")")
for _, s := range opts.Schemas {
args = append(args, s)
}
}
}
rows, err := i.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("mysql: querying schemas: %w", err)
}
defer rows.Close()
var schemas []*schema.Schema
for rows.Next() {
var name, charset, collation string
if err := rows.Scan(&name, &charset, &collation); err != nil {
return nil, err
}
schemas = append(schemas, &schema.Schema{
Name: name,
Attrs: []schema.Attr{
&schema.Charset{
V: charset,
},
&schema.Collation{
V: collation,
},
},
})
}
return schemas, nil
}
func (i *inspect) tables(ctx context.Context, realm *schema.Realm, opts *schema.InspectOptions) error {
var (
args []any
query = fmt.Sprintf(tablesQuery, nArgs(len(realm.Schemas)))
)
for _, s := range realm.Schemas {
args = append(args, s.Name)
}
if opts != nil && len(opts.Tables) > 0 {
for _, t := range opts.Tables {
args = append(args, t)
}
query = fmt.Sprintf(tablesQueryArgs, nArgs(len(realm.Schemas)), nArgs(len(opts.Tables)))
}
rows, err := i.QueryContext(ctx, query, args...)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var (
defaultE sql.NullBool
autoinc sql.NullInt64
tSchema, name, charset, collation, comment, options, engine sql.NullString
)
if err := rows.Scan(&tSchema, &name, &charset, &collation, &autoinc, &comment, &options, &engine, &defaultE); err != nil {
return fmt.Errorf("scan table information: %w", err)
}
if !sqlx.ValidString(tSchema) || !sqlx.ValidString(name) {
return fmt.Errorf("invalid schema or table name: %q.%q", tSchema.String, name.String)
}
s, ok := realm.Schema(tSchema.String)
if !ok {
return fmt.Errorf("schema %q was not found in realm", tSchema.String)
}
t := &schema.Table{Name: name.String}
s.AddTables(t)
if sqlx.ValidString(charset) {
t.Attrs = append(t.Attrs, &schema.Charset{
V: charset.String,
})
}
if sqlx.ValidString(collation) {
t.Attrs = append(t.Attrs, &schema.Collation{
V: collation.String,
})
}
if sqlx.ValidString(comment) {
t.Attrs = append(t.Attrs, &schema.Comment{
Text: comment.String,
})
}
if sqlx.ValidString(options) {
t.Attrs = append(t.Attrs, &CreateOptions{
V: options.String,
})
}
if sqlx.ValidString(engine) && defaultE.Valid {
t.Attrs = append(t.Attrs, &Engine{
V: engine.String,
Default: defaultE.Bool,
})
}
if autoinc.Valid {
t.Attrs = append(t.Attrs, &AutoIncrement{
V: autoinc.Int64,
})
}
}
return rows.Close()
}
// columns queries and appends the columns of the given table.
func (i *inspect) columns(ctx context.Context, s *schema.Schema) error {
query := columnsQuery
if i.SupportsGeneratedColumns() {
query = columnsExprQuery
}
rows, err := i.querySchema(ctx, query, s)
if err != nil {
return fmt.Errorf("mysql: query schema %q columns: %w", s.Name, err)
}
defer rows.Close()
for rows.Next() {
if err := i.addColumn(s, rows); err != nil {
return fmt.Errorf("mysql: %w", err)
}
}
return rows.Err()
}
// addColumn scans the current row and adds a new column from it to the table.
func (i *inspect) addColumn(s *schema.Schema, rows *sql.Rows) error {
var table, name, typ, comment, nullable, key, defaults, extra, charset, collation, expr sql.NullString
if err := rows.Scan(&table, &name, &typ, &comment, &nullable, &key, &defaults, &extra, &charset, &collation, &expr); err != nil {
return err
}
t, ok := s.Table(table.String)
if !ok {
return fmt.Errorf("table %q was not found in schema", table.String)
}
c := &schema.Column{
Name: name.String,
Type: &schema.ColumnType{
Raw: typ.String,
Null: nullable.String == "YES",
},
}
ct, err := ParseType(c.Type.Raw)
if err != nil {
return fmt.Errorf("parse %q.%q type %q: %w", t.Name, c.Name, c.Type.Raw, err)
}
c.Type.Type = ct
attr, err := parseExtra(extra.String)
if err != nil {
return err
}
if attr.autoinc {
a := &AutoIncrement{}
if !sqlx.Has(t.Attrs, a) {
// A table can have only one AUTO_INCREMENT column. If it was returned as NULL
// from INFORMATION_SCHEMA, it is due to information_schema_stats_expiry, and
// we need to extract it from the 'CREATE TABLE' command.
putShow(t).auto = a
}
c.Attrs = append(c.Attrs, a)
}
if attr.onUpdate != "" {
c.Attrs = append(c.Attrs, &OnUpdate{A: attr.onUpdate})
}
if x := expr.String; x != "" {
if !i.Maria() {
x = unescape(x)
}
c.SetGeneratedExpr(&schema.GeneratedExpr{Expr: x, Type: attr.generatedType})
}
if defaults.Valid {
if i.Maria() {
c.Default = i.marDefaultExpr(c, defaults.String)
} else {
c.Default = i.myDefaultExpr(c, defaults.String, attr)
}
}
if sqlx.ValidString(comment) {
c.SetComment(comment.String)
}
if sqlx.ValidString(charset) {
c.SetCharset(charset.String)
}
if sqlx.ValidString(collation) {
c.SetCollation(collation.String)
}
t.AddColumns(c)
// From MySQL doc: A UNIQUE index may be displayed as "PRI" if it is NOT NULL
// and there is no PRIMARY KEY in the table. We detect this in `addIndexes`.
if key.String == "PRI" {
if t.PrimaryKey == nil {
t.PrimaryKey = &schema.Index{Table: t, Name: key.String}
}
t.PrimaryKey.Parts = append(t.PrimaryKey.Parts, &schema.IndexPart{
C: c,
SeqNo: len(t.PrimaryKey.Parts),
})
}
return nil
}
// indexes queries and appends the indexes of the given table.
func (i *inspect) indexes(ctx context.Context, s *schema.Schema) error {
query := i.indexQuery()
rows, err := i.querySchema(ctx, query, s)
if err != nil {
return fmt.Errorf("mysql: query schema %q indexes: %w", s.Name, err)
}
defer rows.Close()
if err := i.addIndexes(s, rows); err != nil {
return err
}
return rows.Err()
}
// addIndexes scans the rows and adds the indexes to the table.
func (i *inspect) addIndexes(s *schema.Schema, rows *sql.Rows) error {
hasPK := make(map[*schema.Table]bool)
for rows.Next() {
var (
seqno int
table, name, indexType string
nonuniq, desc sql.NullBool
column, subPart, expr, comment sql.NullString
)
if err := rows.Scan(&table, &name, &column, &nonuniq, &seqno, &indexType, &desc, &comment, &subPart, &expr); err != nil {
return fmt.Errorf("mysql: scanning indexes for schema %q: %w", s.Name, err)
}
t, ok := s.Table(table)
if !ok {
return fmt.Errorf("table %q was not found in schema", table)
}
// Ignore primary keys.
if name == "PRIMARY" {
hasPK[t] = true
continue
}
idx, ok := t.Index(name)
if !ok {
idx = schema.NewIndex(name).
SetUnique(!nonuniq.Bool).
AddAttrs(&IndexType{T: indexType})
if indexType == IndexTypeFullText {
putShow(t).addFullText(idx)
}
if sqlx.ValidString(comment) {
idx.SetComment(comment.String)
}
t.AddIndexes(idx)
}
// Rows are ordered by SEQ_IN_INDEX that specifies the
// position of the column in the index definition.
part := &schema.IndexPart{SeqNo: seqno, Desc: desc.Bool}
switch {
case sqlx.ValidString(expr):
part.X = &schema.RawExpr{X: unescape(expr.String)}
case sqlx.ValidString(column):
part.C, ok = t.Column(column.String)
if !ok {
return fmt.Errorf("mysql: column %q was not found for index %q", column.String, idx.Name)
}
if sqlx.ValidString(subPart) {
n, err := strconv.Atoi(subPart.String)
if err != nil {
return fmt.Errorf("mysql: parse index prefix size %q: %w", subPart.String, err)
}
part.Attrs = append(part.Attrs, &SubPart{
Len: n,
})
}
part.C.Indexes = append(part.C.Indexes, idx)
default:
return fmt.Errorf("mysql: invalid part for index %q", idx.Name)
}
idx.Parts = append(idx.Parts, part)
}
for _, t := range s.Tables {
if !hasPK[t] && t.PrimaryKey != nil {
t.PrimaryKey = nil
}
}
return nil
}
// fks queries and appends the foreign keys of the given table.
func (i *inspect) fks(ctx context.Context, s *schema.Schema) error {
rows, err := i.querySchema(ctx, fksQuery, s)
if err != nil {
return fmt.Errorf("mysql: querying %q foreign keys: %w", s.Name, err)
}
defer rows.Close()
if err := sqlx.SchemaFKs(s, rows); err != nil {
return fmt.Errorf("mysql: %w", err)
}
return rows.Err()
}
// checks queries and appends the check constraints of the given table.
func (i *inspect) checks(ctx context.Context, s *schema.Schema) error {
query, ok := i.supportsCheck()
if !ok {
return nil
}
rows, err := i.querySchema(ctx, query, s)
if err != nil {
return fmt.Errorf("mysql: querying %q check constraints: %w", s.Name, err)
}
defer rows.Close()
for rows.Next() {
var table, name, clause, enforced sql.NullString
if err := rows.Scan(&table, &name, &clause, &enforced); err != nil {
return fmt.Errorf("mysql: %w", err)
}
t, ok := s.Table(table.String)
if !ok {
return fmt.Errorf("table %q was not found in schema", table.String)
}
check := &schema.Check{
Name: name.String,
Expr: unescape(clause.String),
}
if i.Maria() {
check.Expr = clause.String
// In MariaDB, JSON is an alias to LONGTEXT. For versions >= 10.4.3, the CHARSET and COLLATE set to utf8mb4
// and a CHECK constraint is automatically created for the column as well (i.e. JSON_VALID(`<C>`)). However,
// we expect tools like Atlas and Ent to manually add this CHECK for older versions of MariaDB.
c, ok := t.Column(check.Name)
if ok && c.Type.Raw == TypeLongText && check.Expr == fmt.Sprintf("json_valid(`%s`)", c.Name) {
c.Type.Raw = TypeJSON
c.Type.Type = &schema.JSONType{T: TypeJSON}
// Unset the inspected CHARSET/COLLATE attributes
// as they are valid only for character types.
c.UnsetCharset().UnsetCollation()
// Skip adding this CHECK constraint to the table definition
// as it is implicitly created by MariaDB for this JSON column.
continue
}
} else if enforced.String == "NO" {
// The ENFORCED attribute is not supported by MariaDB.
// Also, skip adding it in case the CHECK is ENFORCED,
// as the default is ENFORCED if not state otherwise.
check.Attrs = append(check.Attrs, &Enforced{V: false})
}
t.Attrs = append(t.Attrs, check)
}
return rows.Err()
}
// supportsCheck reports if the connected database supports
// the CHECK clause, and return the querying for getting them.
func (i *inspect) supportsCheck() (string, bool) {
q := myChecksQuery
if i.Maria() {
q = marChecksQuery
}
return q, i.SupportsCheck()
}
// indexQuery returns the query to retrieve the indexes of the given table.
func (i *inspect) indexQuery() string {
query := indexesNoCommentQuery
if i.SupportsIndexComment() {
query = indexesQuery
}
if i.SupportsIndexExpr() {
query = indexesExprQuery
}
return query
}
// extraAttr is a parsed version of the information_schema EXTRA column.
type extraAttr struct {
autoinc bool
onUpdate string
generatedType string
defaultGenerated bool
}
var (
reGenerateType = regexp.MustCompile(`(?i)^(stored|persistent|virtual) generated$`)
reTimeOnUpdate = regexp.MustCompile(`(?i)^(?:default_generated )?on update (current_timestamp(?:\(\d?\))?)$`)
)
// parseExtra returns a parsed version of the EXTRA column
// from the INFORMATION_SCHEMA.COLUMNS table.
func parseExtra(extra string) (*extraAttr, error) {
attr := &extraAttr{}
switch el := strings.ToLower(extra); {
case el == "", el == "null":
case el == defaultGen:
attr.defaultGenerated = true
// The column has an expression default value,
// and it is handled in Driver.addColumn.
case el == autoIncrement:
attr.autoinc = true
case reTimeOnUpdate.MatchString(extra):
attr.onUpdate = reTimeOnUpdate.FindStringSubmatch(extra)[1]
case reGenerateType.MatchString(extra):
attr.generatedType = reGenerateType.FindStringSubmatch(extra)[1]
default:
return nil, fmt.Errorf("unknown extra column attribute %q", extra)
}
return attr, nil
}
// showCreate sets and fixes schema elements that require information from
// the 'SHOW CREATE' command.
func (i *inspect) showCreate(ctx context.Context, s *schema.Schema) error {
for _, t := range s.Tables {
st, ok := popShow(t)
if !ok {
continue
}
c, err := i.createStmt(ctx, t)
if err != nil {
return err
}
st.setIndexParser(c)
if err := st.setAutoInc(t, c); err != nil {
return err
}
}
return nil
}
var reAutoinc = regexp.MustCompile(`(?i)\s*AUTO_INCREMENT\s*=\s*(\d+)\s*`)
// createStmt loads the CREATE TABLE statement for the table.
func (i *inspect) createStmt(ctx context.Context, t *schema.Table) (*CreateStmt, error) {
c := &CreateStmt{}
b := &sqlx.Builder{QuoteOpening: '`', QuoteClosing: '`'}
rows, err := i.QueryContext(ctx, b.P("SHOW CREATE TABLE").Table(t).String())
if err != nil {
return nil, fmt.Errorf("query CREATE TABLE %q: %w", t.Name, err)
}
if err := sqlx.ScanOne(rows, &sql.NullString{}, &c.S); err != nil {
return nil, fmt.Errorf("scan CREATE TABLE %q: %w", t.Name, err)
}
t.Attrs = append(t.Attrs, c)
return c, nil
}
var reCurrTimestamp = regexp.MustCompile(`(?i)^current_timestamp(?:\(\d?\))?$`)
// myDefaultExpr returns the correct schema.Expr based on the column attributes for MySQL.
func (i *inspect) myDefaultExpr(c *schema.Column, x string, attr *extraAttr) schema.Expr {
// In MySQL, the DEFAULT_GENERATED indicates the column has an expression default value.
if i.SupportsExprDefault() && attr.defaultGenerated {
// Skip CURRENT_TIMESTAMP, because wrapping it with parens will translate it to now().
if _, ok := c.Type.Type.(*schema.TimeType); ok && reCurrTimestamp.MatchString(x) {
return &schema.RawExpr{X: x}
}
return &schema.RawExpr{X: sqlx.MayWrap(unescape(x))}
}
switch c.Type.Type.(type) {
case *schema.BinaryType:
// MySQL v8 uses Hexadecimal representation.
if isHex(x) {
return &schema.Literal{V: x}
}
case *BitType, *schema.BoolType, *schema.IntegerType, *schema.DecimalType, *schema.FloatType:
return &schema.Literal{V: x}
case *schema.TimeType:
// "current_timestamp" is exceptional in old versions
// of MySQL for timestamp and datetime data types.
if reCurrTimestamp.MatchString(x) {
return &schema.RawExpr{X: x}
}
}
return &schema.Literal{V: quote(x)}
}
// parseColumn returns column parts, size and signed-info from a MySQL type.
func parseColumn(typ string) (parts []string, size int, unsigned bool, err error) {
// Remove MariaDB like comments embedded in the type
// for compatibility. For example: /* mariadb-5.3 */.
if i := strings.Index(typ, "/*"); i > 0 && strings.HasSuffix(strings.TrimSpace(typ), "*/") {
typ = strings.TrimSpace(typ[:i])
}
switch parts = strings.FieldsFunc(typ, func(r rune) bool {
return r == '(' || r == ')' || r == ' ' || r == ','
}); parts[0] {
case TypeBit, TypeBinary, TypeVarBinary, TypeChar, TypeVarchar:
case TypeTinyInt, TypeSmallInt, TypeMediumInt, TypeInt, TypeBigInt,
TypeDecimal, TypeNumeric, TypeFloat, TypeDouble, TypeReal:
if attr := parts[len(parts)-1]; attr == "unsigned" || attr == "zerofill" {
unsigned = true
}
}
if len(parts) > 1 && sqlx.IsUint(parts[1]) {
size, err = strconv.Atoi(parts[1])
}
if err != nil {
return nil, 0, false, fmt.Errorf("parse %q to int: %w", parts[1], err)
}
return parts, size, unsigned, nil
}
// hasNumericDefault reports if the given type has a numeric default value.
func hasNumericDefault(t schema.Type) bool {
switch t.(type) {
case *BitType, *schema.BoolType, *schema.IntegerType, *schema.DecimalType, *schema.FloatType:
return true
}
return false
}
func isHex(x string) bool { return len(x) > 2 && strings.ToLower(x[:2]) == "0x" }
// marDefaultExpr returns the correct schema.Expr based on the column attributes for MariaDB.
func (i *inspect) marDefaultExpr(c *schema.Column, x string) schema.Expr {
// Unlike MySQL, NULL means default to NULL or no default.
if x == "NULL" {
return nil
}
// From MariaDB 10.2.7, string-based literals are quoted to distinguish them from expressions.
if i.GTE("10.2.7") && sqlx.IsQuoted(x, '\'') {
return &schema.Literal{V: x}
}
// In this case, we need to manually check if the expression is literal, or fallback to raw expression.
switch c.Type.Type.(type) {
case *BitType:
// Bit literal values. See https://mariadb.com/kb/en/binary-literals.
if strings.HasPrefix(x, "b'") && strings.HasSuffix(x, "'") {
return &schema.Literal{V: x}
}
case *schema.BoolType, *schema.IntegerType, *schema.DecimalType, *schema.FloatType:
if _, err := strconv.ParseFloat(x, 64); err == nil {
return &schema.Literal{V: x}
}
case *schema.TimeType:
// "current_timestamp" is exceptional in old versions
// of MySQL (i.e. MariaDB in this case).
if strings.ToLower(x) == currentTS {
return &schema.RawExpr{X: x}
}
}
if !i.SupportsExprDefault() {
return &schema.Literal{V: quote(x)}
}
return &schema.RawExpr{X: sqlx.MayWrap(x)}
}
func (i *inspect) querySchema(ctx context.Context, query string, s *schema.Schema) (*sql.Rows, error) {
// Number of times the schema name is parameterized.
args := make([]any, strings.Count(query, "?"))
for i := range args {
args[i] = s.Name
}
for _, t := range s.Tables {
args = append(args, t.Name)
}
return i.QueryContext(ctx, fmt.Sprintf(query, nArgs(len(s.Tables))), args...)
}
func nArgs(n int) string { return strings.Repeat("?, ", n-1) + "?" }
const (
// Query to list system variables.
variablesQuery = "SELECT @@version, @@collation_server, @@character_set_server, @@lower_case_table_names"
// Query to list database schemas.
schemasQuery = "SELECT `SCHEMA_NAME`, `DEFAULT_CHARACTER_SET_NAME`, `DEFAULT_COLLATION_NAME` from `INFORMATION_SCHEMA`.`SCHEMATA` WHERE `SCHEMA_NAME` NOT IN ('information_schema','innodb','mysql','performance_schema','sys') ORDER BY `SCHEMA_NAME`"
// Query to list specific database schemas.
schemasQueryArgs = "SELECT `SCHEMA_NAME`, `DEFAULT_CHARACTER_SET_NAME`, `DEFAULT_COLLATION_NAME` from `INFORMATION_SCHEMA`.`SCHEMATA` WHERE `SCHEMA_NAME` %s ORDER BY `SCHEMA_NAME`"
// Query to list table columns.
columnsQuery = "SELECT `TABLE_NAME`, `COLUMN_NAME`, `COLUMN_TYPE`, `COLUMN_COMMENT`, `IS_NULLABLE`, `COLUMN_KEY`, `COLUMN_DEFAULT`, `EXTRA`, `CHARACTER_SET_NAME`, `COLLATION_NAME`, NULL AS `GENERATION_EXPRESSION` FROM `INFORMATION_SCHEMA`.`COLUMNS` WHERE `TABLE_SCHEMA` = ? AND `TABLE_NAME` IN (%s) ORDER BY `ORDINAL_POSITION`"
columnsExprQuery = "SELECT `TABLE_NAME`, `COLUMN_NAME`, `COLUMN_TYPE`, `COLUMN_COMMENT`, `IS_NULLABLE`, `COLUMN_KEY`, `COLUMN_DEFAULT`, `EXTRA`, `CHARACTER_SET_NAME`, `COLLATION_NAME`, `GENERATION_EXPRESSION` FROM `INFORMATION_SCHEMA`.`COLUMNS` WHERE `TABLE_SCHEMA` = ? AND `TABLE_NAME` IN (%s) ORDER BY `ORDINAL_POSITION`"
// Query to list table indexes.
indexesQuery = "SELECT `TABLE_NAME`, `INDEX_NAME`, `COLUMN_NAME`, `NON_UNIQUE`, `SEQ_IN_INDEX`, `INDEX_TYPE`, UPPER(`COLLATION`) = 'D' AS `DESC`, `INDEX_COMMENT`, `SUB_PART`, NULL AS `EXPRESSION` FROM `INFORMATION_SCHEMA`.`STATISTICS` WHERE `TABLE_SCHEMA` = ? AND `TABLE_NAME` IN (%s) ORDER BY `index_name`, `seq_in_index`"
indexesExprQuery = "SELECT `TABLE_NAME`, `INDEX_NAME`, `COLUMN_NAME`, `NON_UNIQUE`, `SEQ_IN_INDEX`, `INDEX_TYPE`, UPPER(`COLLATION`) = 'D' AS `DESC`, `INDEX_COMMENT`, `SUB_PART`, `EXPRESSION` FROM `INFORMATION_SCHEMA`.`STATISTICS` WHERE `TABLE_SCHEMA` = ? AND `TABLE_NAME` IN (%s) ORDER BY `index_name`, `seq_in_index`"
indexesNoCommentQuery = "SELECT `TABLE_NAME`, `INDEX_NAME`, `COLUMN_NAME`, `NON_UNIQUE`, `SEQ_IN_INDEX`, `INDEX_TYPE`, UPPER(`COLLATION`) = 'D' AS `DESC`, NULL AS `INDEX_COMMENT`, `SUB_PART`, NULL AS `EXPRESSION` FROM `INFORMATION_SCHEMA`.`STATISTICS` WHERE `TABLE_SCHEMA` = ? AND `TABLE_NAME` IN (%s) ORDER BY `index_name`, `seq_in_index`"
tablesQuery = `
SELECT
t1.TABLE_SCHEMA,
t1.TABLE_NAME,
t2.CHARACTER_SET_NAME,
t1.TABLE_COLLATION,
t1.AUTO_INCREMENT,
t1.TABLE_COMMENT,
t1.CREATE_OPTIONS,
t1.ENGINE,
t3.SUPPORT = 'DEFAULT' AS DEFAULT_ENGINE
FROM
INFORMATION_SCHEMA.TABLES AS t1
LEFT JOIN INFORMATION_SCHEMA.COLLATIONS AS t2
ON t1.TABLE_COLLATION = t2.COLLATION_NAME
LEFT JOIN INFORMATION_SCHEMA.ENGINES AS t3
ON t1.ENGINE = t3.ENGINE
WHERE
TABLE_SCHEMA IN (%s)
AND TABLE_TYPE = 'BASE TABLE'
ORDER BY
TABLE_SCHEMA, TABLE_NAME`
tablesQueryArgs = `
SELECT
t1.TABLE_SCHEMA,
t1.TABLE_NAME,
t2.CHARACTER_SET_NAME,
t1.TABLE_COLLATION,
t1.AUTO_INCREMENT,
t1.TABLE_COMMENT,
t1.CREATE_OPTIONS,
t1.ENGINE,
t3.SUPPORT = 'DEFAULT' AS DEFAULT_ENGINE
FROM
INFORMATION_SCHEMA.TABLES AS t1
JOIN INFORMATION_SCHEMA.COLLATIONS AS t2
ON t1.TABLE_COLLATION = t2.COLLATION_NAME
LEFT JOIN INFORMATION_SCHEMA.ENGINES AS t3
ON t1.ENGINE = t3.ENGINE
WHERE
TABLE_SCHEMA IN (%s)
AND TABLE_NAME IN (%s)
AND TABLE_TYPE = 'BASE TABLE'
ORDER BY
TABLE_SCHEMA, TABLE_NAME`
// Query to list table check constraints.
myChecksQuery = `
SELECT
t1.TABLE_NAME,
t1.CONSTRAINT_NAME,
t2.CHECK_CLAUSE,
t1.ENFORCED
FROM
INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS t1
JOIN INFORMATION_SCHEMA.CHECK_CONSTRAINTS AS t2
ON t1.CONSTRAINT_NAME = t2.CONSTRAINT_NAME
AND t1.CONSTRAINT_SCHEMA = t2.CONSTRAINT_SCHEMA
WHERE
t1.CONSTRAINT_TYPE = 'CHECK'
AND t1.TABLE_SCHEMA = ?
AND t1.TABLE_NAME IN (%s)
ORDER BY
t1.TABLE_NAME, t1.CONSTRAINT_NAME
`
marChecksQuery = `
SELECT
TABLE_NAME,
CONSTRAINT_NAME,
CHECK_CLAUSE,
"YES" AS ENFORCED
FROM
INFORMATION_SCHEMA.CHECK_CONSTRAINTS
WHERE
CONSTRAINT_SCHEMA = ?
AND TABLE_NAME IN (%s)
ORDER BY
TABLE_NAME, CONSTRAINT_NAME
`
// Query to list table foreign keys.
fksQuery = `
SELECT
t1.CONSTRAINT_NAME,
t1.TABLE_NAME,
t1.COLUMN_NAME,
t1.TABLE_SCHEMA,
t1.REFERENCED_TABLE_NAME,
t1.REFERENCED_COLUMN_NAME,
t1.REFERENCED_TABLE_SCHEMA,
t2.UPDATE_RULE,
t2.DELETE_RULE
FROM
INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS t1
JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS t2
ON t1.CONSTRAINT_NAME = t2.CONSTRAINT_NAME
WHERE
t1.REFERENCED_COLUMN_NAME IS NOT NULL
AND BINARY t1.TABLE_SCHEMA = ?
AND BINARY t2.CONSTRAINT_SCHEMA = ?
AND t1.TABLE_NAME IN (%s)
ORDER BY
BINARY t1.TABLE_NAME,
BINARY t1.CONSTRAINT_NAME,
t1.ORDINAL_POSITION`
)
type (
// AutoIncrement attribute for columns with "AUTO_INCREMENT" as a default.
// V represent an optional start value for the counter.
AutoIncrement struct {
schema.Attr
V int64
}
// CreateOptions attribute for describing extra options used with CREATE TABLE.
CreateOptions struct {
schema.Attr
V string
}
// CreateStmt describes the SQL statement used to create a table.
CreateStmt struct {
schema.Attr
S string
}
// Engine attribute describes the storage engine used to create a table.
Engine struct {
schema.Attr
V string // InnoDB, MyISAM, etc.
Default bool // The default engine used by the server.
}
// OnUpdate attribute for columns with "ON UPDATE CURRENT_TIMESTAMP" as a default.
OnUpdate struct {
schema.Attr
A string
}
// SubPart attribute defines an option index prefix length for columns.
SubPart struct {
schema.Attr
Len int
}
// Enforced attribute defines the ENFORCED flag for CHECK constraint.
Enforced struct {
schema.Attr
V bool // V indicates if the CHECK is enforced or not.
}
// The DisplayWidth represents a display width of an integer type.
DisplayWidth struct {
schema.Attr
N int
}
// The ZeroFill represents the ZEROFILL attribute which is
// deprecated for MySQL version >= 8.0.17.
ZeroFill struct {
schema.Attr
A string
}
// IndexType represents an index type.
IndexType struct {
schema.Attr
T string // BTREE, HASH, FULLTEXT, SPATIAL, RTREE
}
// IndexParser defines the parser plugin used
// by a FULLTEXT index.
IndexParser struct {
schema.Attr
P string // Name of the parser plugin. e.g., ngram or mecab.
}
// BitType represents the type bit.
BitType struct {
schema.Type
T string
Size int
}
// SetType represents a set type.
SetType struct {
schema.Type
Values []string
}
// putShow is an intermediate table attribute used
// on inspection to indicate if the 'SHOW TABLE' is
// required and for what.
showTable struct {
schema.Attr
// AUTO_INCREMENT value due to missing value in information_schema.
auto *AutoIncrement
// FULLTEXT indexes that might have custom parser.
idxs []*schema.Index
}
)
// addIndex adds an index to the list of indexes
// that needs further processing.
func (s *showTable) addFullText(idx *schema.Index) {
s.idxs = append(s.idxs, idx)
}
// setAutoInc extracts the updated AUTO_INCREMENT from CREATE TABLE.
func (s *showTable) setAutoInc(t *schema.Table, c *CreateStmt) error {
if s.auto == nil {
return nil
}
if sqlx.Has(t.Attrs, &AutoIncrement{}) {
return fmt.Errorf("unexpected AUTO_INCREMENT attributes for table: %q", t.Name)
}
matches := reAutoinc.FindStringSubmatch(c.S)
if len(matches) != 2 {
return nil
}
v, err := strconv.ParseInt(matches[1], 10, 64)
if err != nil {
return err
}
s.auto.V = v
t.Attrs = append(t.Attrs, s.auto)
return nil
}
// reIndexParser matches the parser name from the index definition.
var reIndexParser = regexp.MustCompile("/\\*!50100 WITH PARSER `([^`]+)` \\*/")
// setIndexParser updates the FULLTEXT parser from CREATE TABLE statement.
func (s *showTable) setIndexParser(c *CreateStmt) {
b := (&sqlx.Builder{QuoteOpening: '`', QuoteClosing: '`'}).P("FULLTEXT KEY")
for _, idx := range s.idxs {
bi := b.Clone().Ident(idx.Name).Wrap(func(b *sqlx.Builder) {
b.MapComma(idx.Parts, func(i int, b *sqlx.Builder) {
// We expect column names only, as functional
// fulltext indexes are not supported by MySQL.
if idx.Parts[i].C != nil {
b.Ident(idx.Parts[i].C.Name)
}
})
})
i := strings.Index(c.S, bi.String())
if i == -1 || i+bi.Len() >= len(c.S) {
continue
}
i += bi.Len()
j := strings.Index(c.S[i:], "\n")
if j == -1 {
continue
}
// The rest of the line holds index, algorithm and lock options.
if matches := reIndexParser.FindStringSubmatch(c.S[i : i+j]); len(matches) == 2 {
idx.AddAttrs(&IndexParser{P: matches[1]})
}
}
}
func putShow(t *schema.Table) *showTable {
for i := range t.Attrs {
if s, ok := t.Attrs[i].(*showTable); ok {
return s
}
}
s := &showTable{}
t.Attrs = append(t.Attrs, s)
return s
}
func popShow(t *schema.Table) (*showTable, bool) {
for i := range t.Attrs {
if s, ok := t.Attrs[i].(*showTable); ok {
t.Attrs = append(t.Attrs[:i], t.Attrs[i+1:]...)
return s, true
}
}
return nil, false
}
Loading...
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
1
https://gitee.com/damengde/atlas.git
git@gitee.com:damengde/atlas.git
damengde
atlas
Atlas
v0.0.3

搜索帮助